@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
267 lines (224 loc) • 10.3 kB
JavaScript
const path = require('node:path')
const { sep } = path
const fs = require('node:fs')
const cli = {
Shortcuts: {
i: 'init',
a: 'add',
y: 'bind',
m: 'import',
c: 'compile',
p: 'parse',
s: 'serve',
w: 'watch',
e: 'env',
b: 'build',
d: 'deploy',
l: 'login',
o: 'logout',
t: 'test',
v: 'version', '-v':'version', '--version':'version',
h: 'help', '?':'help', '-?':'help', '--help':'help', '-h':'help',
r: 'repl', '-r':'repl',
'-e':'eval'
},
async exec (cmd = process.argv[2], ...argv) {
if (!argv.length) argv = process.argv.slice(3)
if (!cmd) cmd = process.stdin.isTTY ? 'help' : 'compile'
if (cmd in this.Shortcuts) cmd = process.argv[2] = this.Shortcuts[cmd]
if (argv.some(a => this.Shortcuts[a]==='help')) return this.help (cmd)
// normalize command case (but not the shortcuts above) so that checks against `cds.cli.command` work
if (cmd === process.argv[2]) cmd = process.argv[2] = cmd.toLowerCase()
else if (cmd) cmd = cmd.toLowerCase()
const cds = require('../lib') // make sure to load cds-dk API for plugins
DEBUG?.(`[cds] - @sap/cds ${cds.version} loaded: ${cds.home}`)
try {
Object.assign(module.exports, { cmds: getCommands() })
const task = this.load(cmd, /*else*/(f) => argv.unshift(cmd) && (cmd = 'compile') && _isDefaultCompile(argv, f))
if (task) {
if (task._is_new_cmd) return await task.exec (this)
// '--' handling for cds bind --exec
const endOfOptions = argv.indexOf('--')
let appendArgs = []
if (endOfOptions >= 0) {
appendArgs = argv.slice(endOfOptions + 1)
argv = argv.slice(0, endOfOptions)
}
// Have to load plugins to allow for cds add plugin-contributed flags!
if (cmd === 'add') {
cds.cli = { command: 'add' }
if (argv.includes('-p') || argv.includes('--package')) {
const pIndex = argv.indexOf('-p') + 1 || argv.indexOf('--package') + 1
const p = argv[pIndex]
console.log(`running with --package, adding dependency ${p}`)
try {
const { execSync } = require('child_process')
execSync(`npm add ${p}`, { stdio: 'inherit', cwd: cds.root })
argv.splice(pIndex - 1, 2)
return execSync(`cds add ${argv.join(' ')}`, { stdio: 'inherit', cwd: cds.root })
} catch (e) {
throw e.message
}
}
await cds.plugins
for (const [, Plugin] of cds.add.registered) {
const all = Object.entries((new Plugin).options())
const flags = all.filter(([,f]) => f.type === 'boolean')
const options = all.filter(([,o]) => o.type !== 'boolean')
task.options.push(...options.map(([key]) => '--'+key))
task.shortcuts.push(...options.map(([, { short }]) => short ? '-'+short : undefined).filter(Boolean))
const byShortcut = (a, b) => (b.short ? 1 : 0) - (a.short ? 1 : 0)
task.options.sort(byShortcut)
task.shortcuts.sort(byShortcut)
task.flags.push(...flags.map(([key]) => '--'+key))
}
}
const args = this.args(task, argv)
args[0].push(...appendArgs)
cds.cli = { command: cmd, argv: args[0], options: args[1] }
if (args[1]?.['resolve-bindings']) await _resolveBindings({ silent: cmd === 'env' })
_prepareTsIfNeeded(cds)
return await task.apply(this, args)
}
} catch (e) {
process.exitCode ??= 1
if (cds.watched) throw e // bin/watched.js handles errors
if (e.messages?.length)
cds._log(e.messages, { 'log-level': cds.env.log.levels.cli }) // REVISIT: no special handling for compiler messages
else return console.error(e)
}
},
load (cmd,_default) {
return /[\\/.\-@]/.test(cmd) && _default && _default(true) // like `cds ./srv` or `cds @capire/bookshop`
|| /cds/i.test(cmd) && _dieForUnknownCommand (cmd) // cds cds
|| _require ('./'+cmd) // cds-dk commands
|| _default && _default()
|| _dieForUnknownCommand (cmd)
},
args (task, argv) {
const { options:o=[], flags:f=[], shortcuts:s=[] } = task
const _global = /^--(profile|production|sql|odata|add-.*|build-.*|cdsc-.*|odata-.*|folders-.*)$/
const _flags = { '--production':true }
const options = {}, args = []
let k,a, env = null
if (argv.length) for (let i=0; i < argv.length; ++i) {
if ((a = argv[i])[0] !== '-') args.push(a)
else if ((k = s.indexOf(a)) >= 0) k < o.length ? add(o[k],argv[++i]) : add(f[k-o.length])
else if ((k = o.indexOf(a)) >= 0) add(o[k],argv[++i])
else if ((k = f.indexOf(a)) >= 0) add(f[k])
else if (_global.test(a)) add_global(a, _flags[a] || argv[++i])
else throw 'Invalid option: '+ a
}
// consistent production setting for NODE_ENV and CDS_ENV
// if (process.env.NODE_ENV !== 'production') process.env.NODE_ENV = process.env.CDS_ENV?.split(',').find(p => p === 'production') || process.env.NODE_ENV
if (process.env.NODE_ENV !== 'production') { if (process.env.CDS_ENV?.split(',').includes('production')) process.env.NODE_ENV = 'production' }
else process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
function add (k,v) { options[k.slice(2)] = v || true }
function add_global (k,v='') {
if (k === '--production') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
if (k === '--profile') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], ...v.split(',')]))
if (k === '--odata') v = { flavor:v }
let e=env || (env={}), path = k.slice(2).split('-')
while (path.length > 1) { let p = path.shift(); e = e[p]||(e[p]={}) }
add (k, e[path[0]] = v)
}
// always use 'plain' log format for cds-dk logging
// by default 'plain' log format is used for development while 'json' format is used for production
// the json formatter logs stringified JSON which causing any line breaks to get replaced by '\n'
// using the env variable ensures that cds.env isn't loaded too early
env ??= {}
env.log ??= {}
env.log.format = 'plain'
if (env) process.env.CDS_CONFIG ??= JSON.stringify(env)
return [ args, options ]
},
help (cmd) { return this.exec ('help', cmd) }
}
const DEBUG = /\b(y|all|cli)\b/.test(process.env.DEBUG) ? console.debug : undefined
async function _resolveBindings(options) {
const cds = require('../lib/cds')
const { env: bindingEnv } = require('../lib/bind/shared')
Object.assign(process.env, await bindingEnv(options))
cds.env = cds.env.for('cds') // reload env
cds.requires = cds.env.requires
}
function _isDefaultCompile (argv, _force) {
const _is_model = a => /\*|\.(cds|json|csn)$/.test(a)
const compile = _require('./compile')
if (_force) return compile
// first argument must be a model file or an option
if (!_is_model(argv[0]) && !argv[0].startsWith('-')) return false
// at least one model file must in the arguments
if (argv.some(_is_model)) return compile
}
function _dieForUnknownCommand(cmd) {
const term = require('../lib/util/term');
const fuzzySearch = _require('./util/fuzzySearch');
let bestMatchText = '';
const commands = getCommands();
const bestMatches = fuzzySearch(cmd, commands, DEBUG, { maxListLength: commands.length });
switch (bestMatches.length) {
case 0:
bestMatchText = '';
break;
case 1:
bestMatchText = `A similar command is \n\n ${term.bold("cds " + bestMatches[0])}`;
break;
default:
bestMatchText = `Similar commands are \n\n ${term.bold("cds " + bestMatches.join('\n cds '))}`;
}
const msg = `
Unknown command '${term.bold(cmd)}'. ${bestMatchText}
Haven't found the proper command yet? Here are all supported commands:
${term.bold(commands.join('\n '))}
You can use ${term.bold("cds help [command]")} to get more information on the command.
`
throw msg;
}
function getCommands() {
const excludeList = new Set(['cds.js', 'cds-ts.js', 'fix-redirects.js']);
const files = fs.readdirSync(__dirname, { withFileTypes: true, encoding: 'utf8' })
const result = new Set()
for (const f of files) {
if (excludeList.has(f.name)) continue;
if ((f.isDirectory() && fs.existsSync(path.join(__dirname, f.name, 'index.js'))) || (f.isFile() && f.name.endsWith('.js'))) {
result.add(path.parse(f.name).name)
}
}
return Array.from(result).sort()
}
function _require (id,o) {
try { var resolved = require.resolve(id,o) } catch { DEBUG?.(`[cds] - Command not found: ${id}`); return }
DEBUG?.(`[cds] - Command resolved: ${resolved}`)
return require (resolved)
}
module.exports = Object.assign ((..._) => cli.exec(..._), cli)
if (!module.parent) module.exports()
// check for an already loaded TS runner
if (process[Symbol.for('ts-node.register.instance')])
process.env.CDS_TYPESCRIPT = 'ts-node'
else if (process._preload_modules?.some(m => m?.includes(`${sep}tsx${sep}`)))
process.env.CDS_TYPESCRIPT = 'tsx'
/**
* Load tsx if needed in a TS project. Warn users if this fails.
*/
function _prepareTsIfNeeded(cds) {
const tsconfig = path.join(cds.root ?? '.', 'tsconfig.json')
if (cds.cli?.command === 'serve' && fs.existsSync(tsconfig)) { // for `cds serve` started directly or through watch
if (/\b(0|no|false)\b/.test(process.env.CDS_TYPESCRIPT)) // opt-out switch to run JS code in a TS project
return delete process.env.CDS_TYPESCRIPT
if (!process.env.CDS_TYPESCRIPT ) {
try {
cds._localOrGlobal('tsx/cjs') // cjs is needed as we are in CJS code
DEBUG?.(`[cds] - Detected ${path.relative(cds.root, tsconfig)}. Running with tsx.\n`)
process.env.CDS_TYPESCRIPT = 'tsx'
}
catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') throw e
console.error(`Error: detected ${tsconfig}, but could not run with 'tsx'. Enable Typescript with\n npm install -D tsx typescript\n`)
DEBUG && console.error(e)
}
}
}
}