UNPKG

@sap/cds-dk

Version:

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

267 lines (224 loc) 10.3 kB
#!/usr/bin/env node 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) } } } }