@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
386 lines (306 loc) • 12.3 kB
JavaScript
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 */