@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
276 lines (240 loc) • 10.6 kB
JavaScript
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);
}
}
}