@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
185 lines (163 loc) • 8.8 kB
JavaScript
const cds = require('../../../cds')
const { exists, path } = cds.utils
const { SEVERITY_ERROR: ERROR, FOLDER_GEN, MTX_SIDECAR_DB_VALIDATION, CONTENT_EDMX, FLAVOR_LOCALIZED_EDMX } = require('../../constants')
const ResourcesTarBuilder = require('../mtx/resourcesTarBuilder')
const { relativePaths, BuildError, resolveRequiredSapModels, getI18nDefaultFolder } = require('../../util')
const DEBUG = cds.debug('cli|build')
const DEFAULT_MAIN_FOLDER = "_main"
const [major, minor] = cds.version.split('.').map(Number)
const COMPAT = major === 9 && minor < 7 // cds10: remove
module.exports = class MtxSidecarBuildPlugin extends require('../nodejs') {
static get taskDefaults() { return { src: 'mtx/sidecar' } }
static hasTask() {
if (cds.env.extends) return
const mps = cds.env.requires['cds.xt.ModelProviderService']
const isMtxs = cds.env.requires.multitenancy || cds.env.requires.toggles || cds.env.requires['cds.xt.ModelProviderService']
const src = path.join(cds.root, this.taskDefaults?.src ?? 'mtx/sidecar')
if (mps?._in_sidecar) {
throw new BuildError(`Seems that you are executing 'cds build' in the 'mtx/sidecar' folder. Execute 'cds build' in the project root folder instead.`)
}
return isMtxs && exists(src)
}
get requires() {
return ['hana']
}
init() {
super.init()
if (cds.env.build.target === '.') {
this.task.dest = path.join(this.task.dest, FOLDER_GEN)
}
}
/**
* Builds the MTX sidecar app consisting of:
* - Node.js app model defined by the required sidecar services
* - Main app model defined by the build task's model options, including feature models and resources TAR
*
* build.target=".": 'dest' -> 'model-provider/gen'
* build.target="gen": 'dest' -> 'gen/model-provider'
*/
async build() {
// REVISIT: required to load plugins from mtx/sidecar, not the main app for the sidecar CSN
const oldRoot = cds.root
cds.root = this.task.src
const sidecarEnv = cds.env.for('cds', this.task.src)
cds.root = oldRoot
const [rMain, rNode] = await Promise.allSettled([
this._buildMainApp(sidecarEnv),
this._buildSidecarApp(sidecarEnv)
])
if (rMain.status === 'rejected') throw rMain.reason
if (rNode.status === 'rejected') throw rNode.reason
}
/**
* Builds the MTX sidecar Node.js app parts.
* @param {import('../../../cds').env} sidecarEnv `cds.env` based on the MTX sidecar directory.
*/
async _buildSidecarApp(sidecarEnv) {
const { dest, src } = this.task
const model = COMPAT ? this._compatCompileSidecarSync(sidecarEnv) : this._compileSidecarSync(sidecarEnv)
const srv = path.join(dest, sidecarEnv.folders.srv)
const promises = [
this.compileToJson(model, path.join(srv, 'csn.json')),
this.collectLanguageBundles(model, path.join(srv, getI18nDefaultFolder())),
this.copySrvContent(src, dest, dest)
]
// copy .npmrc from project root, if it exists and no .npmrc exists in the sidecar
if (!exists(path.join(dest, '.npmrc')) && exists('.npmrc')) {
promises.push(this.copy(path.join(cds.root, '.npmrc')).to(path.join(dest, '.npmrc')))
}
await Promise.all(promises)
}
/**
* Builds the main app parts containing base model CSN with feature CSNs and resources TAR.
* @param {import('../../../cds').env} sidecarEnv `cds.env` based on the MTX sidecar directory.
*/
async _buildMainApp(sidecarEnv) {
let main = sidecarEnv.requires['cds.xt.ModelProviderService']?.root
if (!main) {
throw new BuildError("Invalid MTX sidecar configuration. Make sure that the profile 'mtx-sidecar' is configured. Use 'npm install @sap/cds-mtxs' to install an up-to-date version.")
}
if (!this.hasBuildOption(MTX_SIDECAR_DB_VALIDATION, false)
&& !cds.env.build.tasks?.length // disable if custom build tasks exist
&& sidecarEnv.requires.db && cds.env.requires.db // ensure database is configured
&& sidecarEnv.requires.db.kind !== cds.env.requires.db.kind) {
throw new BuildError(`Inconsistent database configuration found - application db '${cds.env.requires.db?.kind}', sidecar db '${sidecarEnv.requires.db?.kind}'`)
}
const profiles = cds.env.profiles ?? []
if (!profiles.includes("production") && !profiles.includes("prod")) {
// need to overwrite 'development' profile setting '../..' with '_main', although it is questionable why cds build is executed with development profile
main = DEFAULT_MAIN_FOLDER
}
const destMain = path.join(this.task.dest, main)
const destMainSrv = path.join(destMain, cds.env.folders.srv)
const model = await this.model()
if (!model) {
return
}
const { dictionary, sources } = await this.compileAll(model, destMainSrv, destMain)
const promises = []
promises.push(this.collectAllLanguageBundles(dictionary, sources, destMainSrv, destMain))
// promises.push(this.collectAllLanguageBundles(dictionary, sources, destMain, destMain))
if (!this.hasBuildOption(CONTENT_EDMX, false)) {
// inferred model expected by cds.compile.to.edmx()
const compileOptions = { [FLAVOR_LOCALIZED_EDMX]: this.hasBuildOption(FLAVOR_LOCALIZED_EDMX, true) } // disabled by default
const baseModel = cds.compiler.compileSources({ 'base.json': dictionary.base }, { messages: this.messages })
promises.push(this.compileToEdmx(baseModel, path.join(destMainSrv, 'odata', cds.env.odata.version), compileOptions))
}
// create resources TAR
// resources are determined based on available database build task, SQLite as fallback
promises.push(new ResourcesTarBuilder(this).createTar(destMain, model))
// copy package.json and .cdsrc.json from project root
const packageJson = path.join(cds.root, 'package.json')
const cdsrcJson = path.join(cds.root, '.cdsrc.json')
if (exists('package.json')) {
promises.push(this.copy(packageJson).to(path.join(destMain, 'package.json')))
}
if (exists('.cdsrc.json')) {
promises.push(this.copy(cdsrcJson).to(path.join(destMain, '.cdsrc.json')))
}
return Promise.all(promises)
}
/**
* Synchronous compilation using the MTX sidecar context.
* @param {import('../../../cds').env} env `cds.env` based on the MTX sidecar directory.
* @returns the compiled MTX sidecar CSN
*/
_compileSidecarSync(env) {
const options = { env, root: this.task.src }
const modelPaths = COMPAT ? cds.resolve('*', false) : cds.resolve('*', { ...options, dry: true })
const modelFilePaths = COMPAT ? cds.resolve(modelPaths) : cds.resolve(modelPaths, options)
if (!modelFilePaths || modelFilePaths.length === 0) {
throw new BuildError("No CDS models found in MTX sidecar. Run the 'npm install' command to install up-to-date versions of the required packages.")
}
DEBUG?.(`sidecar model: ${relativePaths(cds.root, modelFilePaths).join(", ")}`)
// check whether all models belonging to the @sap namespace can be resolved
const unresolved = resolveRequiredSapModels(modelPaths)
if (unresolved.length > 0) {
// log error, but don't fail
this.pushMessage(`CDS models [${unresolved.join(', ')}] required by MTX sidecar cannot be resolved. Run the 'npm install' command to install up-to-date versions of the required packages.`, ERROR)
}
// using synchronous compilation as cds.root and cds.env have been switched to mtx/sidecar
// using different messages array ensures that cds compilation doesn't fail because of
// existing compilation error messages that have been downgraded to warnings
// see cap/issues/issues/16401#issuecomment-7205397
const messages = []
const model = cds.load(modelFilePaths, { sync: true, messages, cwd: this.task.src })
messages.forEach(msg => this.messages.push(msg))
return model
}
// cds10: remove
_compatCompileSidecarSync(sidecarEnv) {
const env = cds.env
const root = cds.root
try {
cds.root = this.task.src
cds.env = sidecarEnv
return this._compileSidecarSync(sidecarEnv)
} finally {
// restore project scope
cds.root = root
cds.env = env
}
}
}