UNPKG

@sap/cds-dk

Version:

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

335 lines (297 loc) 14.1 kB
const { readdirSync } = require('node:fs') const { basename, join, relative, resolve } = require('node:path') const { OPTIONS, COMMAND_INIT } = require('./constants'), { JAVA, COMPLETION } = OPTIONS const validate = require('./validate') const cmd = require('../util/command') const cds = require('..'), { exists } = cds.utils const { BOLD, DIMMED, RESET } = cds.utils.colors const DEBUG = /\b(y|all|cli|init|add)\b/.test(process.env.DEBUG) ? console.debug : undefined module.exports = class CDSGenerator { constructor() { this.cwd = process.cwd() this.uiConfig = require('./bas') // Used by CAP Generator } static help({ exclude = [] } = {}) { if (!cds.add) return // shell completion: cds.add is undefined and not needed const plugins = this.readPlugins(exclude) const nameFixedLength = Math.max(...plugins.map(plugin => plugin.name.length)) return plugins .filter(({module}) => module.help()) .map(({name, module}) => { return ` *${name}*${' '.repeat(nameFixedLength - name.length)} - ${module.help()}` }) .join('\n') } /** * @param excluded {string[]} * @returns {{name: string, module: import('module')}[]} */ static readPlugins(excluded = []) { const fromDk = readdirSync(join(__dirname, 'template')) const fromPlugins = Object.keys(Object.fromEntries(require('./add').registered)) const all = [...fromDk, ...fromPlugins] .filter(plugin => !excluded?.includes(plugin)) .map(plugin => ({ name: plugin, module: cds.add.registered.get(plugin) ?? require('./template/' + plugin) })) const cmds = Object.values(OPTIONS) const byPriority = (lhs, rhs) => { const a = cmds.indexOf(lhs.name), b = cmds.indexOf(rhs.name) if (a === -1 && b === -1) return 0 if (a === -1) return 1 if (b === -1) return -1 return a - b } return all.sort(byPriority) } /** * @param {string} facets, comma separated list of facets * @param {any} options, additional options */ async add(facets) { if (!facets?.length) { const cli = require('../../bin/cds'); const help = await cli?.load('add', () => ({ help: '' }))?.help ?? ''; console.log (require('../../bin/help').formatHelp(help)) process.exitCode=0 return; } this._initialize(null); this._greetings(); await this._process(facets); await this.stepEnd(); } /** * @param {string} projectName, the project name */ async init(projectName) { await this.stepInit(projectName); await this.stepEnd(); } async stepInit(projectName, options) { // Also used by CAP Generator projectName = projectName?.trim(); if (options) { // called from CAP generator with options // cds.cli is not set correctly when called from CAP generator // so set defaults here cds.cli = { command: COMMAND_INIT, // keep caller independent from internal names options: { ...options, add: new Set(options.add) }, argv:[] } // must be called after above code to ensure cds.cli.options.add is set cds.add = require('./add') } this._initialize(projectName); this._greetings(); await this._process(); } _initialize(projectName) { // avoid config files to be created in the home dir if (cds.cli.command === COMMAND_INIT && !projectName && process.cwd() === require('os').homedir()) { throw `in your home directory, use 'cds init <project>'` } const { options } = cds.cli this.cwd = options.cwd || process.cwd(); cds.root = resolve(this.cwd, projectName || '.'); this.projectName = basename(cds.root); this._cleanupOptions(); } _greetings() { if (cds.cli.options.for && typeof cds.cli.options.for !== 'string') { throw 'the --for argument must not be empty' } if (cds.cli.options.force && !cds.cli.options.dry) { console.log(`using '--force' ... existing files will be overwritten\n`) } } async _process(facets) { DEBUG?.(`project path: ${cds.root}`); const { options, command } = cds.cli if (command === COMMAND_INIT) { validate.projectName(this.projectName) validate.projectFolder(this.cwd) } const plugins = command === COMMAND_INIT ? options.add : new Set(facets) await this._fillTemplateList(plugins) const cmds = Object.values(OPTIONS) const byPriority = (a, b) => cmds.indexOf(a) - cmds.indexOf(b) const sorted = Array.from(plugins).sort(byPriority) const skipLogFacets = ['initial', COMPLETION] for (const [i, facet] of Object.entries(sorted)) { if (!cds.cli.options.dry && !skipLogFacets.includes(facet)) console.log(`Adding facet: ${BOLD + facet + RESET}`) const template = this.templateList[i] await template.run() } if ((cds.cli.command !== 'init' || cds.cli.options.add.size !== 1 || !cds.cli.options.add.has('initial')) && !cds.cli.options.dry) console.log() DEBUG && console.log('plugins active for production:') const fromDk = readdirSync(join(__dirname, './template')) const { registered } = require('./add') const { env4 } = require('./projectReader') const processPlugins = async (plugins) => { const cmds = Object.values(OPTIONS) const byPriority = (a, b) => cmds.indexOf(a) - cmds.indexOf(b) const sorted = Array.from(plugins).sort(byPriority) for (const plugin of sorted) { const Plugin = registered.get(plugin) ?? require('./template/' + plugin) const template = new Plugin() const added = cds.cli.command === 'add' && cds.cli.argv[0]?.split(',').includes(plugin) || cds.cli.options.add?.has(plugin) const inProduction = Plugin.hasInProduction(env4('production')) || added DEBUG && console.log(inProduction ? '✅' : '❌', plugin) if (inProduction) await template.combine() } } if (DEBUG) console.log(`\n ${fromDk.length} from @sap/cds-dk:`) await processPlugins(fromDk) if (DEBUG) console.log(`\n ${registered.size} from plugins:`) await processPlugins(registered.keys()) if (cds.env['project-nature'] === 'java') { console.log('updating frozen dependencies in package-lock.json…') await cmd.spawnCommand('npm' , ['i', '--package-lock-only'], { cwd:cds.root }) if (exists('mtx/sidecar/package.json') && !exists('mtx/sidecar/package-lock.json')) { console.log('updating frozen dependencies in mtx/sidecar/package-lock.json…') await cmd.spawnCommand('npm', ['i', '--package-lock-only', '--prefix', 'mtx/sidecar'], { cwd:cds.root }) } } if (cds.cli.command === COMMAND_INIT) { console.log ('Successfully initialized CAP project') let relativeProjectPath = relative(this.cwd, cds.root) if (relativeProjectPath) console.log ('Continue with:', BOLD +'code', relativeProjectPath, RESET) } else if (!cds.cli.options.dry) { console.log(`Successfully added features to your project`) } } async _createTemplate(plugin) { try { const Plugin = cds.add.registered.get(plugin) ?? require(`./template/${plugin}`) return new Plugin(this) } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { const entries = this.constructor.readPlugins() const fuzzySearch = require('../../bin/util/fuzzySearch') const [bestMatch] = fuzzySearch(plugin, entries.map(e => e.name), null, { maxListLength: entries.length }) const max = entries.reduce((max, {name}) => Math.max(max, name.length), 0) const allFacetsText = entries.filter(({module})=> module.help()).map(({name, module}) => { const help = module.help?.() ?? '' return `${BOLD+ name +RESET + ' '.repeat(max - name.length)} ${DIMMED+ help +RESET}` }).join('\n') DEBUG?.(err) throw `unknown facet ${BOLD+ plugin +RESET} – did you mean ${BOLD + `cds add ` + bestMatch + RESET}?\n\nall supported facets:\n\n${allFacetsText}\n` } throw err } } async _fillTemplateList(plugins) { const templates = new Map for (let plugin of plugins) { // cds10: remove if (plugin === "helm" || plugin === "helm-unified-runtime") { cds.cli.options.add?.add("kyma") cds.cli.argv[0] += ",kyma" } // Compat if (plugin === 'helm' && cds.cli.options['internal-unified-runtime-charts']) { console.warn('"--internal-unified-runtime-charts" is deprecated, please use "cds add kyma --unified-runtime" instead') plugin = 'helm-unified-runtime' cds.cli.options.add?.delete('helm') cds.cli.options.add?.add('helm-unified-runtime') } const _alias = (alias, preferred) => { if (plugin === alias) { plugin = preferred cds.cli.options.add?.delete(alias) cds.cli.options.add?.add(preferred) cds.cli.argv[0] = cds.cli.argv[0].replace(alias, preferred) } } _alias('appfront', 'app-frontend') _alias('app-front', 'app-frontend') _alias('gha', 'github-actions') _alias('postgresql', 'postgres') _alias('kibana', 'application-logging') _alias('kibana-logging', 'application-logging') const runtimeAgnostic = { initial:1, 'tiny-sample':1, data:1, completion: 1 } if (!(plugin in runtimeAgnostic)) { const creating = ['java', 'nodejs', 'typescript', 'esm', 'extension'] const { command, options, argv } = cds.cli const inOptions = option => command === 'init' && options.add.has(option) || command === 'add' && argv[0]?.split(',').includes(option) if (!exists('pom.xml') && !exists('package.json') && !creating.some(inOptions)) { if (command === 'init') { const project = argv[0] ? argv[0]+' ' : '' throw `First decide if this is a Node.js or Java project. Either run\n ${BOLD+`cds init ${project}--java`+RESET} ...\nor\n ${BOLD+`cds init ${project}--nodejs`+RESET} ...\n` } throw `First decide if this is a Node.js or Java project. Either run\n ${BOLD+'cds add nodejs'+RESET}\nor\n ${BOLD+'cds add java'+RESET}\n` } } if (!templates.has(plugin)) { const template = await this._createTemplate(plugin) if (await template.canRun()) { templates.set(plugin, template) const dependencies = await template.requires() dependencies?.forEach(d => cds.cli.options.add?.add(d)) dependencies?.forEach(d => plugins.add(d)) } else { throw 'cannot run plugin \'' + plugin + '\'' + (DEBUG ? 'Does not satisfy the canRun() function:\n ' + template.canRun : '') } } } const cmds = Object.values(OPTIONS) const priorities = new Map(cmds.map((cmd, i) => [cmd, i])) this.templateList = Array.from(templates.keys()) .sort((lhs, rhs) => { const a = priorities.get(lhs) ?? Infinity const b = priorities.get(rhs) ?? Infinity return a - b }) .map(key => templates.get(key)) } async stepEnd() { // Also used by CAP Generator for (const template of this.templateList) { await template.finalize(); } } _cleanupOptions() { let tokens = []; const { options } = cds.cli if (typeof options.add === 'string') { tokens = options.add.split(/[,\s+]/g) } else if (Array.isArray(options.add)) { tokens = options.add } else if (options.add instanceof Set) { tokens = [...options.add]; } const trimmedTokens = tokens.map((token) => { token = token.replace(/\s+/g, ''); const tokens = token.split(':'), [facet] = tokens if (tokens.length > 1) { if (options[facet]) { options[facet].add(token); } else { options[facet] = new Set([token]); } } return facet; }).filter(Boolean); options.add = new Set(trimmedTokens); if (options?.nodejs) { // --nodejs -> --add nodejs options.add.add('nodejs') } if (options?.java) { // --java -> --add java options.add.add('java') } if (cds.cli.command === COMMAND_INIT && !options.add.has(JAVA)) { options.add.add('initial') // cds10: remove const { isBas } = require('./projectReader').readProject() if (isBas) options.add.add('nodejs') } } }