UNPKG

@sap/cds-dk

Version:

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

174 lines (157 loc) 6.97 kB
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)) } }