UNPKG

@sap/cds-dk

Version:

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

212 lines (194 loc) 10.9 kB
const fs = require('fs') const path = require('path') const cds = require('../../../cds') const cmd = require('../../../util/command'); const EdmxBuildPlugin = require('../edmxBuildPlugin') const { BuildError, getWorkspaces, findWorkspaceRoot } = require('../../util') const { OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM, ODATA_VERSION_V2, FOLDER_GEN, CONTENT_EDMX, CONTENT_PACKAGELOCK_JSON, CONTENT_NPMRC, CONTENT_CDSRC_JSON, CONTENT_ENV, CONTENT_DEFAULT_ENV_JSON, FLAVOR_LOCALIZED_EDMX, OPTION_WS_PACK} = require('../../constants') const { WARNING } = EdmxBuildPlugin const LOG = cds.log('cli|build'), DEBUG = cds.debug('cli|build') class NodejsBuildPlugin extends EdmxBuildPlugin { init() { super.init() if (this.task.options.compileDest) { throw new BuildError("Option not supported - compileDest") } // fallback if src has been defined as '.' this.destSrv = this.isStagingBuild() ? path.resolve(this.task.dest, cds.env.folders.srv) : path.join(this.task.dest, FOLDER_GEN) } options() { const options = super.options() if (cds.env.requires.extensibility || cds.env.requires.toggles) { options.flavor = 'xtended' } return options } async build() { const destSrv = this.isStagingBuild() ? this.destSrv : path.resolve(this.destSrv, cds.env.folders.srv) const destRoot = this.isStagingBuild() ? this.task.dest : this.destSrv if (cds.env.odata.version === ODATA_VERSION_V2) { // log warning as nodejs is only supporting odata version V4 this.pushMessage("OData v2 is not supported by node runtime. Make sure to define OData v2 in cds configuration.", WARNING) } // by default model contains all features const model = await this.model() if (!model && !this.context.options[OPTION_WS_PACK]) { return } if (model) { const { dictionary, sources } = await this.compileAll(model, destSrv, destRoot) // collect and write language bundles into single i18n.json file await this.collectAllLanguageBundles(dictionary, sources, destSrv, destRoot) if (!this.hasBuildOption(CONTENT_EDMX, false)) { const compileOptions = { [FLAVOR_LOCALIZED_EDMX]: this.hasBuildOption(FLAVOR_LOCALIZED_EDMX, true) } // inferred flavor is required by edmx compiler backend // using cds.compile instead of cds.compiler.compileSources ensures that cds.env options are correctly read const baseModel = dictionary.base.meta.flavor !== 'inferred' ? await cds.compile(sources.base, super.options(), 'inferred') : dictionary.base await this.compileToEdmx(baseModel, path.join(this.destSrv, 'odata', cds.env.odata.version), compileOptions) } } if (this.isStagingBuild() && this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM)) { const srcSrv = this.task.src === cds.root ? path.resolve(this.task.src, cds.env.folders.srv) : this.task.src await this._copyNativeContent(cds.root, srcSrv, destRoot, destSrv) if (this.context.options[OPTION_WS_PACK]) { await this._packWorkspaceDependencies(destRoot) } } return this._result } async clean() { // staging build content is deleted by BuildTaskEngine if (cds.env.build.target === '.') { // delete the entire 'task.dest' folder otherwise, for details see #constructor // - the value of the folder 'src' has been appended to the origin 'task.dest' dir DEBUG?.(`Deleting build target folder ${this.destSrv}`) await fs.promises.rm(this.isStagingBuild() ? this.task.dest : this.destSrv, { force: true, recursive: true }) } } async _copyNativeContent(srcRoot, srcSrv, destRoot, destSrv) { // project/srv/** -> 'gen/srv/srv/**' const filesFilter = await this.copySrvContent(srcSrv, destRoot, this.hasBuildOption('flatDeployLayout', true) ? destRoot : destSrv) // project/* -> 'gen/srv/*' await this.copyProjectRootContent(srcRoot, destRoot, (entry) => !filesFilter.includes(path.basename(entry))) } /** * Copy files for nodejs staging builds from the given <em>src</em>' folder (e.g. 'project/srv') to either <em>destRoot</em> (e.g. 'project/gen/srv') * or <em>destSrv</em> (e.g. 'project/gen/srv/srv') folders according to the file semantics. * Files with project semantics like 'package.json' or '.npmrc' file are copied to <em>destRoot</em> while others like '.js' service handlers * are copied to <em>destSrv</em>. * @param {*} src * @param {*} destRoot - folder name representing the app root folder (e.g. gen/srv) * @param {*} destSrv - folder name representing the app sub-folder (e.g. gen/srv/srv) * @returns the list of files that have been copied */ async copySrvContent(src, destRoot, destSrv) { const srvRootBlockList = RegExp('package\\.json$|package-lock\\.json$|\\.npmrc$|\\.cdsrc\\.json$') const srvBlockList = RegExp('\\.cds$|csn\\.json$|\\.csn$|manifest\\.y.?ml$|\\.env($|\\..*$)|default-env\\.json$|\\.cdsrc-private.json$') const srvRootFileNames = [] // 1. copy all files to 'destSrv' except those contained in the blocklist (including node_modules) // project/srv -> 'gen/srv/srv' await super.copyNativeContent(src, destSrv, (entry) => { if (fs.statSync(entry).isDirectory()) { // TODO shall not copy language bundles - return !/(\/|\\)(node_modules|_i18n)(\/|\\)?$/.test(entry) return !/(\/|\\)node_modules(\/|\\)?$/.test(entry) } // make sure the file exists on srv root level - see https://github.tools.sap/cap/issues/issues/12077 if (srvRootBlockList.test(entry) && path.dirname(entry) === src) { srvRootFileNames.push(path.basename(entry)) return false } return !srvBlockList.test(entry) }) // 2. copy dedicated files like package.json, .npmrc to 'destRoot' // project/srv -> 'gen/srv' let srvAllowList = "package\\.json$" // always copy package.json, modify only if CONTENT_PACKAGE_JSON is true srvAllowList += !this.hasBuildOption(CONTENT_PACKAGELOCK_JSON, false) ? "|package-lock\\.json$" : "" srvAllowList += !this.hasBuildOption(CONTENT_NPMRC, false) ? "|\\.npmrc$" : "" srvAllowList += !this.hasBuildOption(CONTENT_CDSRC_JSON, false) ? "|\\.cdsrc\\.json$" : "" srvAllowList += this.hasBuildOption(CONTENT_ENV, true) ? "|\\.env($|\\..*$)" : "" srvAllowList += this.hasBuildOption(CONTENT_DEFAULT_ENV_JSON, true) ? "|default-env\\.json$" : "" srvAllowList = new RegExp(srvAllowList) await Promise.all(srvRootFileNames.map(fileName => { if (srvAllowList.test(fileName)) { return this.copy(path.join(src, fileName)).to(path.join(destRoot, fileName)) } })) return srvRootFileNames } /** * Copy dedicated files (files with project semantics like package.json, .npmrc, .cdsrc, etc.) * from the given <em>src</em> folder (e.g. 'project') into the given <em>dest</em> folder (e.g. 'project/gen/srv') * @param {*} src * @param {*} dest * @param {*} filter - copy file if filter function returns true */ async copyProjectRootContent(src, dest, filter) { let { folders = ['i18n'] } = cds.env.i18n folders.push('handlers') folders = folders.map(folder => path.join(src, folder)) let srvAllowList = "package\\.json$" // always copy package.json, modify only if CONTENT_PACKAGE_JSON is true srvAllowList += !this.hasBuildOption(CONTENT_PACKAGELOCK_JSON, false) ? "|package-lock\\.json$" : "" srvAllowList += !this.hasBuildOption(CONTENT_NPMRC, false) ? "|\\.npmrc$" : "" srvAllowList += !this.hasBuildOption(CONTENT_CDSRC_JSON, false) ? "|\\.cdsrc\\.json$" : "" srvAllowList += this.hasBuildOption(CONTENT_ENV, true) ? "|\\.env($|\\..*$)" : "" srvAllowList += this.hasBuildOption(CONTENT_DEFAULT_ENV_JSON, true) ? "|default-env\\.json$" : "" srvAllowList = new RegExp(srvAllowList) await super.copyNativeContent(src, dest, (entry) => { if (fs.statSync(entry).isDirectory()) { return folders.some(folder => entry.startsWith(folder)) } if (/\.js$|\.properties$/.test(entry)) { return true } return srvAllowList.test(entry) && (!filter || filter.call(this, entry)) }) } async _packWorkspaceDependencies(dest) { const wsRoot = findWorkspaceRoot(cds.root) const appPkg = require(path.join(dest, 'package.json')) let workspaces if (this.task.options.workspaces) { workspaces = Array.isArray(this.task.options.workspaces) ? this.task.options.workspaces : [this.task.options.workspaces] } else { // devDependencies are not considered workspaces = await getWorkspaces(wsRoot, true, false); } if (workspaces.length) { let pkgDescriptors try { pkgDescriptors = await NodejsBuildPlugin._execNpmPack(dest, wsRoot, workspaces) } catch (e) { DEBUG?.(e) throw new BuildError(`Failed to package npm workspace dependencies\n${e.message}`) } if (pkgDescriptors?.length) { let changed for (const pkgDescriptor of pkgDescriptors) { const { name, filename: fileName } = pkgDescriptor const filePath = path.join(dest, fileName) if (appPkg.dependencies?.[name]) { appPkg.dependencies[name] = `file:${fileName}` this.pushFile(filePath) changed |= true } } if (changed) { await this.write(JSON.stringify(appPkg, 0, 2)).to(path.join(dest, 'package.json')) } else { throw new BuildError('No matching workspaces found!') } } } } static async _execNpmPack(dest, wsRoot, workspaces) { const args = ['pack', '-ws', '--json', `--pack-destination=${dest}`] workspaces.forEach(w => args.push(`-w=${w}`)) DEBUG?.(`execute command: npm ${args.join(', ')}`) const result = await cmd.spawnCommand('npm', args, { cwd: wsRoot }, true, !!LOG._debug) DEBUG?.(result) return JSON.parse(result) } } module.exports = NodejsBuildPlugin