UNPKG

@sap/cds-dk

Version:

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

294 lines (246 loc) 11.2 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', 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 { const allCmds = getCommands(); //TODO: make better; only POC for cds help Object.assign(module.exports, { cmds: allCmds }) // const { handleCompletion } = require('./util/completion'); // await handleCompletion(process.argv, allCmds, this); const task = this.load(cmd, /*else*/(f) => argv.unshift(cmd) && (cmd = 'compile') && _defaultCompile(argv, f)) if (task) { // REVISIT bind: '--' handling for cds bind --exec. Should be moved to cds_cli const endOfOptions = argv.indexOf('--') let appendArgs = [] if (endOfOptions >= 0) { appendArgs = argv.slice(endOfOptions + 1) argv = argv.slice(0, endOfOptions) } // REVISIT: Have to load plugins to allow for 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 else return console.error(e) } }, load (cmd,_default) { if (cmd === 'build/all') { console.error(`'cds build/all' is no longer supported, use 'cds build' instead`) return } 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) }, // TODO replace w/ common arg parser from node 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, see cap/cds/blob/main/lib/env/defaults.js#L97 // 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) require('../lib/cds').env.add (env) //> avoid loading env // unless it has already explicitly been set by the user 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 { bindingEnv } = require('../lib/bind/shared') Object.assign(process.env, await bindingEnv(options)) cds.env = cds.env.for('cds') // reload env // FIXME: This is too early! cds.requires = cds.env.requires // REVISIT: We should clean this up! } function _local (id, _else) { try { return require.resolve (id, {paths:[process.cwd(), __dirname]}) } catch(e) { if (_else) return _else(e) else throw e } } function _defaultCompile (argv, _force) { // Try all arguments if one resolves to a model. If yes, we assume 'compile' is intended. const model = _force || _is_model(argv) if (model) return _require ('./compile') DEBUG?.(`[cds] - Command is not a model to compile: ${argv}`) } function _is_model (argv) { // IMPORTANT: have a separate env here, so that the normal one is not initialized before `cds_cli.args` above const env = require (_local('@sap/cds/lib/env/cds-env', ()=>_local('@sap/cds/lib/env'))).for('cds') const cds = require (_local('@sap/cds')) return !!cds.resolve(argv,{env,cache:{}}) } 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 fs = require('node:fs'); const path = require('node:path'); const excludeList = ['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.includes(f.name)) { 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(e){ 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 console.log(`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) } } } } /* eslint no-console:off */