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