UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

386 lines (306 loc) 12.3 kB
const path = require('path') const {colors} = require('../../lib/util/term') module.exports = Object.assign ( compile, { handleCompletion, options: [ '--from', '--service', '--lang', '--for', '--to', '--dest', '--log-level', '--flavor', '--dialect', '--sql-dialect', '--openapi:url', '--openapi:servers', '--openapi:config-file', '--odata-version' ], shortcuts: [ undefined, '-s', '-l', '-4', '-2', '-o', undefined, '-f' ], flags: [ '--parse', '--plain', '--clean', '--files', '--sources', '--resolved', '--all', '--beta', '--conceptual', '--min', '--docs', '--locations', '--openapi:diagram', '--asyncapi:merged' ], help: ` # SYNOPSIS *cds compile* <models> [<options>] Compiles the specified models to CSN format, applying processors to unfold and generate target outputs using the <options>. ${'' // If no <models> are specified *cds* reads from stdin, which allows it to // be used with unix pipes, for example: // *cat* hy | *cds* -2 sql | *sqlite3* test.db } # OPTIONS ${'' // *-f, --from* <frontend> // Use the specified frontend to parse the input. <frontend> can be one of // the built-in parsers like *cdl*, *yaml*, or // valid node module IDs of custom parsers. } *-2* | *--to* <target format> Compiles the given models to the specified <target format>. Currently supported: - json, yml - edm, edmx, edmx-v2, edmx-v4, edmx-w4, edmx-x4 - sql, hdbcds, hdbtable, hana - cdl - xsuaa - openapi - asyncapi *-4* | *--for* <target> Unfolds the compiled CSN for the specified <target> usages, or get a comma-separated list, without generating target formats. Currently supported: - odata - sql *--dialect* <dialect> Needs option *--to*. Specify the dialect in combination with *--to sql*. Currently supported: - sqlite - h2 - postgres - hana *-s* | *--service* <name> | all Chooses a specific service or _all_ to force output for all services. The service name must be fully qualified, including the namespace, if any. *-l* | *--lang* <languages> | all Localizes the output with given <languages>, a comma-separated list of language/locale codes or _all_. Localization is carried out before all other processors (-4/u) or backends (-2). *-o* | *--dest* <folder> Writes output to the given folder instead of stdout. *-f* | *--flavor* sources | files | parsed | xtended | inferred Depending on the argument, returns a model with the given level of detail: sources: paths and content of all resolved source files files: paths of all effectively referenced files parsed: the definitions and extensions, without applying the extensions or includes, and without imported definitions. xtended: the definitions with all imports and extensions resolved, but without any derived information inferred: the effective model, including imported definitions, extensions, and derived information. This is the default flavor. *--parse* Shortcut for '--flavor parsed' *--plain* Shortcut for '--flavor xtended' *--docs* Preserves /**...*/ doc comments in 'doc' properties of CSN outputs, as well as in 'Core.Description' annotations of EDMX outputs. *--locations* Preserves $location properties of CSN outputs. *--log-level* debug | info | warn | error Chooses which level of compiler messages to log. The default log-level is *warn*. *--openapi:url* <Server URL for Open API export> The server URL used in the generated OpenAPI document. The default is the service base path as declared in the CDS source. Use the \${service-path} variable to have the service path included in the URL. *--openapi:servers* <Stringified JSON Object for Open API export> The servers definition used in the generated OpenAPI document. *--openapi:url* is ignored when this option is specified. *--odata-version* 4.0|4.01 Adds the OData version's functionality of the input CDS/CSN file to the generated OpenAPI document. *--openapi:diagram* Include YUML diagram in the generated OpenAPI document, default: *false*. *--openapi:config-file* filename The passed configuration file will be read to generate the OpenAPI document, incorporating all specified options. Precedence of Options: Inline options specified in the command line will take precedence over those defined in the configuration file. *--asyncapi:merged* A single AsyncAPI document is generated using the details of all input services. Information of _title_ and _version_ should be provided as preset. # EXAMPLES *cds* compile model.cds *cds* c model.json --to sql *cds* srv -s all -l all -2 edmx -o _out *cds* compile srv -s sap.sample.TestService -2 asyncapi -o _out `}) async function handleCompletion(currentWord, previousWord, argv, util) { const allOptionsFlags = [ ...compile.options ?? [], ...compile.flags ?? [] ].filter(e => !argv.includes(e)) if (currentWord?.startsWith('-')) { return allOptionsFlags } switch (previousWord) { case '-2': case '--to': return ['json', 'yml', 'edm', 'edmx', 'edmx-v2', 'edmx-v4', 'sql', 'hdbcds', 'hdbtable', 'hana', 'cdl', 'xsuaa', 'openapi', 'asyncapi']; case '-4': case '--for': return ['odata', 'sql']; case '--dialect': return ['sqlite', 'h2', 'postgres', 'hana']; case '--log-level': return ['debug', 'info', 'warn', 'error']; case '--odata-version': return ['4.0', '4.01']; case '-f': case '--flavor': return ['sources', 'files', 'parsed', 'xtended', 'inferred']; case '-s': case '--service': case '-l': case '--lang': return ['all']; case '-o': case '--dest': return util.completionFs.readdir(currentWord, { files: false }) default: } if (!currentWord && compile.options?.includes(previousWord)) { return [] } return util.completionFs.readdir(currentWord, { fileRegex: /\.(cds|json)/ }) } async function compile_all (root='.') { const {exec} = require ('child_process') const cds = require('../../lib') // get the enhanced cds w/ additional compile processors await cds.plugins // ensure plugins that extend compile targets are loaded exec(`find ${root} -name *.cds ! -path '*/node_modules/*'`, (_,stdout)=>{ const all = stdout.split('\n').slice(0,-1) const info = `\n/ compiled ${all.length} cds models in` console.log (`Compiling ${all.length} cds models found in ${process.cwd()}...\n`) console.time (info) return Promise.all (all.map (each => cds.load(each) .then (()=> console.log (' ',each)) .catch (()=> console.log (' \x1b[91m', each, '\x1b[0m')) )).then (()=> console.timeEnd (info) ) }) } async function compile (models, options={}) { const { dest } = options if (dest) { options.service ??= 'all' const ext = path.extname(dest) if (ext) { options.file ??= path.basename(dest, ext) options.dest = path.dirname(dest) options.suffix ??= ext } } if (options.all) return compile_all (models[0]) const { inspect } = require('util') const cds = require('../../lib') // get the enhanced cds w/ additional compile processors await cds.plugins // ensure plugins that extend compile targets are loaded let model, src, _suffix; //> be filled in below if (!options.as && !/,/.test(options.to)) options.as = 'str' if (options.beta) options.betaMode = true if (options['sql-dialect']) options.dialect = options['sql-dialect'] // if (options['sql-dialect']) options.dialect = options['sql-dialect'] if (typeof models === 'string') models = [models] const messages = options.messages = [] if (Array.isArray(models) && models.length > 0) { // any arguments? if (/mta.*\.ya?ml/.test(models[0])) { model = Promise.resolve(models[0]) } else { for (const kind of ['to', 'for']) { const tail = options[kind] if (typeof tail === 'string' && !cds.compile[kind][tail]) { throw `Unknown model processor: cds.compile.${kind}.${tail}` } } model = cds.load (models, options) src = models[0] .replace (/\.[^.]+$/,'') //> excluding source file extension, e.g. .cds } } else if (!process.stdin.isTTY && process.argv[1].endsWith('cds')) { // else check for stdin model = readModelFromStdin() src = 'stdin' } else { // no args, no stdin throw `You must specify a model to compile.\nRun 'cds c -?' to learn more.` } let chain = model.then (m => model=m) // add processors for compiling if (options.for) for (let each of options.for.split(',')) { chain = chain.then (next (processor ('for', each))) } // add processors for compiler backends if (typeof options.to === 'string') for (let each of options.to.split(',')) { chain = chain.then (next (processor ('to', _suffix=each))) } else if (options.to) { throw `Specify a model processor` } // add processor for i18n if (options.lang) { if (options.lang === true) options.lang = cds.env.i18n.languages // --language w/o value let lang = options.lang if (lang.split) lang = lang.split(',') // string to array if (lang.length === 1 && options.lang !== '*' && options.lang !== 'all') lang = lang[0] // pick single const localize = each => cds.localize (model, lang, each) chain = chain.then (next (x => { if (isSingle(x)) return localize(x) else return function*(){ for (let [each,o] of x) yield [ localize(each), o ] }() })) } // add output processor const write = require ('../../lib/util/write') chain = chain.then (write.to ({ folder: options.dest, file: options.file || (options.service === 'all' ? path.basename(src) : options.service), suffix: options.suffix || suffix4(_suffix), [options.dest ? 'foreach' : 'log']: options.log || consoleLog })) // add processor for logging compiler messages and errors const log = this.log || _log chain = chain.then ((results)=>{ if (messages.length) log (messages, options) return results }, (e) => { process.exitCode = 1 if (e.code === 'MODEL_NOT_FOUND') console.error(e.message) else if (e.errors) log (e.errors, options) else throw e }) // return to run return chain function processor (kind, tail) { let p = cds.compile[kind] for (let each of tail.split('.')) { p = p[each] } return p } function next (proc) { return (prev) => function*(){ if (isSingle(prev)) yield [ proc(prev,options) ] else for (let [outer,_outer] of prev) { let next = proc (outer, options) if (isSingle(next)) yield [ next, _outer ] else for (let [inner,_inner] of next) { yield [ inner, Object.assign({},_outer,_inner) ] } } }() } function isSingle (x) { return !(x[Symbol.iterator] && x.next) } function readModelFromStdin(){ return new Promise ((_resolved, _error) => { let src=""; process.stdin .on ('data', chunk => src += chunk) .on ('end', ()=> _resolved (src[0] === '{' ? JSON.parse(src) : cds.parse(src))) .on ('error', _error) }) } function consoleLog(o) { if (typeof o === 'string') return console.log (o) if (process.stdout.isTTY) { o = inspect(o,{colors, depth: 111, compact:false}).replace(/\[Object: null prototype\] /g, '') } else o = JSON.stringify(o, null, 2) return console.log (o) } function suffix4 (x) { return x && x !== 'hdbtable' && x !== 'hana' && ({ edmx: '.xml', "edmx-v2": '.xml', "edmx-v4": '.xml', openapi: '.openapi3.json', //'.yml', cdl: '.cds', ddl: '.sql', sql: '.sql', edm: '.json', asyncapi: '.json', xsuaa: '.json', ord: '.json' }[x] || '.'+x) } } function _log (messages, options) { const cds = require('../../lib/cds') const { sortMessagesSeverityAware, deduplicateMessages } = cds.compiler deduplicateMessages(messages) messages = sortMessagesSeverityAware (messages) cds._log (messages, options) } /* eslint no-console:0 */