@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
160 lines (146 loc) • 6.52 kB
JavaScript
const fs = require('fs')
const path = require('path')
const cds = require('../../cds')
const Plugin = require('../plugin')
const { hasOptionValue, getFiles } = require('../util')
const { FOLDER_GEN, DEFAULT_BUILTIN_PRIORITY_MAX } = require('../constants')
class InternalBuildPlugin extends Plugin {
/**
* By default custom build plugins are executed before internal build plugins
* to ensure built-in plugin content isn't overwritten by mistake.
*/
get priority() {
return DEFAULT_BUILTIN_PRIORITY_MAX / 2
}
init() {
// REVISIT: no default gen folder for now
}
async clean() {
function _isSubDirectory(parent, child) {
return !path.relative(parent, child).startsWith('..')
}
// build results are deleted by the BuildTaskEngine if the build.target !== '.'
// make sure that src is not a subfolder of dest
if (cds.env.build.target === '.' && this.task.src !== this.task.dest && !_isSubDirectory(this.task.dest, this.task.src)) {
await fs.promises.rm(this.task.dest, { force: true, recursive: true })
}
}
/**
* Called by the framework after {@link #init()}, used to perform more elaborate preparation.
* E.g. caching some pre-calculated data that can be used across multiple build tasks. This kind of data
* has to be stored in the plugin type specific options section.
* @returns {Promise<boolean>} A value 'false' indicates that {@link #prepare()} will not be called for other instances
* of this plugin type.
* True indicates that {@link #prepare()} will be called for other instances of this of this plugin type.
* @deprecated
*/
async prepare() {
// cancel subsequent prepare calls for other plugins of the same type by default
return false
}
/**
* Determines whether the given build option value has been set for this build task.
* If the value is omitted, the existence of the given property name is checked.
* @param {string} qualifiedName
* @param {any=} value
*/
hasBuildOption(qualifiedName, value) {
return hasOptionValue(this._getBuildOption(qualifiedName), value)
}
/**
* Returns the value of the given build option defined for this build task.
* @param {string} qualifiedName
*/
getBuildOption(qualifiedName) {
return super._getBuildOption(qualifiedName)
}
/**
* Returns whether the build results of this build plugin are created inplace
* or in a separate staging folder which is not part of the build tasks src folder.
*/
isStagingBuild() {
return !this.task.src.startsWith(this.task.dest)
}
async copyNativeContent(srcDir, destDir, customFilter) {
function commonStagingBuildFilter(src, destDir) {
if (typeof src !== "string" || typeof destDir !== "string") {
return false
}
if (!fs.statSync(src).isDirectory()) {
return true //file
}
if (src === destDir) {
return false
}
const regex = new RegExp(FOLDER_GEN + "\\b")
if (src === path.resolve(cds.root, cds.env.build.target)) {
return false
}
return !regex.exec(path.basename(src))
}
const files = getFiles(srcDir, (src) => {
// do not copy files that:
// - from 'cds.env.build.target'
// - from 'dest'
// - from some generation folder
// - do not match filter function ,
return commonStagingBuildFilter(src, destDir) && (!customFilter || customFilter.call(this, src))
})
return Promise.all(
files.map((srcFile) => {
let relFile = path.relative(srcDir, srcFile)
let destFile = path.join(destDir, relFile)
return this.copy(srcFile).to(destFile)
})
)
}
async compileToJson(model, csnFile) {
// This will als add a @source prop containing the relative path to the origin .cds source file
// and a parsed _where clause for @restrict.{grant,where} annotations.
// The @source annotation is required for correct custom handler resolution if no @impl annotation has been defined as
// custom service handler implementations are relative to the origin .cds source files.
// For staging builds (task.src !== task.dest) the csn.json file that is served at runtime is copied into a corresponding srv subfolder.
// As a consequence the src folder name has to be included in the @source file name while for inplace builds (task.src === task.dest) this is not the case.
// This ensures that the paths are relative to the cwd when executing cds run.
const jsonOptions = {
cwd: cds.root,
src: this.hasBuildOption('flatDeployLayout', true) || this.task.src === this.task.dest ? this.task.src : cds.root
}
const csnStr = cds.compile.to.json(model, jsonOptions)
await this.write(csnStr).to(csnFile)
return csnStr
}
/**
* Collect and write language bundles into a single i18n.json file.
* @param {Object} model
* @param {string} bundleDest
*/
async collectLanguageBundles(model, bundleDest) {
// collect effective i18n properties...
let bundles = {}
const bundleGenerator = cds.localize.bundles4(model)
if (bundleGenerator && bundleGenerator[Symbol.iterator]) {
for (let [locale, bundle] of bundleGenerator) {
// fallback bundle has the name ""
if (typeof locale === 'string') {
bundles[locale] = bundle
}
}
}
// omit bundles in case the fallback bundle is the only existing entry
const keys = Object.keys(bundles)
if (keys.length === 1 && keys[0] === "" && Object.keys(bundles[keys[0]]).length === 0) {
bundles = {}
}
// copied from ../compile/i18n.js
const { file: base = 'i18n' } = cds.env.i18n
const file = path.join(bundleDest, base + '.json')
// bundleDest might be null
if (bundleDest && Object.keys(bundles).length > 0) {
await this.write(bundles).to(file)
return { file, bundles }
}
return null
}
}
module.exports = InternalBuildPlugin