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