UNPKG

@sap/cds-dk

Version:

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

276 lines (240 loc) 10.6 kB
const path = require('path') const term = require('../util/term') const validate = require('./validate') const { OPTIONS, COMMAND_INIT, PROJECT_FILES } = require('./constants') const { readdirSync } = require('fs') const { join } = require('path') const cmd = require('../util/command') const { NODEJS, JAVA } = OPTIONS const cds = require('..'), { exists } = cds.utils const DEBUG = /\b(y|all|cli)\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) throw 'you must specify a facet to add to the project' this._initialize(null); this._greetings(); // `add completion` configures global shell completion and not project settings // `add data` can live without project metadata const nonProjectFacets = { completion:1, data:1 } const needsProject = facets.some(facet => !nonProjectFacets[facet]) if (needsProject && !PROJECT_FILES.some(exists)) { throw `The current folder doesn't seem to contain a project. None of the following files found: ${PROJECT_FILES.join(', ')}.` } 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 if (options) { // called from CAP generator with options cds.cli = { command: COMMAND_INIT, // keep caller independent from internal names options: { cwd: options.cwd, add: new Set(options.add) } } // 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 = path.resolve(this.cwd, projectName || '.'); this.projectName = path.basename(cds.root); this._cleanupOptions(); } _greetings() { if (cds.cli.command === COMMAND_INIT) { const relativeProjectPath = path.relative(this.cwd, cds.root) const folderName = relativeProjectPath ? `.${path.sep}${relativeProjectPath}` : 'the current folder' console.log(`creating new CAP project in ${term.bold(folderName)}\n`) } 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`) } } 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) for (const [i, facet] of Object.entries(sorted)) { if (!cds.cli.options.dry) console.log(`adding ${term.bold(facet)}`) const template = this.templateList[i] await template.run(); await template.combine(); await template.combineSupported(); } 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 (cds.cli.command === COMMAND_INIT) { const relativeProjectPath = path.relative(this.cwd, cds.root) let message = 'successfully created project' if (relativeProjectPath) { message += ` – continue with ${term.bold('cd ' + relativeProjectPath)}` } console.log('\n' + message) } else if (!cds.cli.options.dry) { console.log(`\nsuccessfully 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 `${term.bold(name) + ' '.repeat(max - name.length)} ${term.dim(help)}` }).join('\n') DEBUG?.(err) throw `unknown facet ${term.bold(plugin)} – did you mean ${term.bold(`cds add ${bestMatch}`)}?\n\nall supported facets:\n\n${allFacetsText}\n` } throw err } } async _fillTemplateList(plugins) { const templates = new Map for (let plugin of plugins) { // Compat if (plugin === 'helm' && cds.cli.options['internal-unified-runtime-charts']) { plugin = 'helm-unified-runtime' cds.cli.options.add.delete('helm') cds.cli.options.add.add('helm-unified-runtime') } if (plugin === 'sample-tiny') plugin = 'tiny-sample' if (plugin in { 'kibana-logging': 1, 'kibana': 1 }) plugin = 'application-logging' if (plugin === 'postgresql') plugin = 'postgres' 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 => 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); // set is ordered ... always options.add = new Set(trimmedTokens); if (options?.java) { // --java -> --add java options.add.add('java') } if (cds.cli.command === COMMAND_INIT && !options.add.has(JAVA)) { options.add.add(NODEJS); } } }