@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
174 lines (157 loc) • 6.97 kB
JavaScript
const path = require('path')
const cds = require('../../cds')
const { rimraf, isdir } = cds.utils
const { find } = require('../../util/fs')
const { hasOptionValue } = require('../util')
const { FOLDER_GEN } = require('../constants')
const TRACE = cds.debug('trace')
module.exports = class InternalBuildPlugin extends require('./plugin') {
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 default 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 rimraf(this.task.dest)
}
}
/**
* 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) {
const regex = new RegExp(FOLDER_GEN + "\\b")
function commonStagingBuildFilter(src, destDir) {
if (typeof src !== "string" || typeof destDir !== "string") {
return false
}
if (!isdir(src)) {
return true //file
}
if (src === destDir) {
return false
}
if (src === path.resolve(cds.root, cds.env.build.target)) {
return false
}
return !regex.exec(path.basename(src))
}
const files = await find(srcDir, { filter: 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.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)
// also collect and write messages from plugins
await this.collectMessageBundles(bundleDest)
return { file, bundles }
}
return null
}
/**
* Collect and write message bundles from CAP plugins into messages.properties files.
* @param {string} bundleDest
*/
async collectMessageBundles(bundleDest) {
const messageBundle = cds.i18n.bundle4('messages')
const messagesData = messageBundle.translations4?.(cds.env.i18n.languages) ?? {}
// Write messages.properties files for each locale
for (const [locale, messages] of Object.entries(messagesData)) {
if (Object.keys(messages).length > 0) {
const fileName = locale ? `messages_${locale}.properties` : 'messages.properties'
const filePath = path.join(bundleDest, fileName)
const content = Object.entries(messages)
.map(([key, value]) => `${key}=${value}`)
.join('\n') + '\n'
await this.write(content).to(filePath)
}
}
}
}
// Emits the 'compile.for.runtime' event, as the produced CSN is for runtime usage.
// Plugins use it to modify the CSN before it is stringified.
function cds_compile_to_json (csn,o) {
TRACE?.time('cds.compile 2json'.padEnd(22)); try {
let result, next = ()=> result ??= cds.compile.to.json (csn,o)
cds.emit ('compile.for.runtime', csn, o, next)
return next() //> in case no handler called next
}
finally { TRACE?.timeEnd('cds.compile 2json'.padEnd(22)) }
}