UNPKG

@sap/cds-dk

Version:

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

261 lines (238 loc) 9.76 kB
const path = require('path') const fs = require('fs') const cds = require('../cds') const { OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM, SEVERITY_INFO, SEVERITY_WARNING, SEVERITY_ERROR } = require('./constants') const { hasOptionValue, getProperty, relativePaths, BuildMessage, pathExists, filterFeaturePaths, getFiles } = require('./util') const DEBUG = cds.debug('cli|build') /** * The build plugin creates the build output for a dedicated build task. It is uniquely identified * by the build task's <code>for</code> or <code>use</code>property. The <code>use</code> property represents * the fully qualified node module path of the build plugin implementation. * <p> * The build task engine defines the following protocol. The methods are invoked in descending order: * <ul> * <li>init() - optional</li> * <li>get priority() - optional</li> * <li>async clean()</li> * <li>async build()</li> * </ul> * The reflected CSN can be accessed using the async method <code>model()</code>. */ class Plugin { /** * @constructor */ constructor() { this._files = new Set() this._messages = [] } static INFO = SEVERITY_INFO static WARNING = SEVERITY_WARNING static ERROR = SEVERITY_ERROR /** * Determines whether a task of this type will be created when cds build is executed, * returns true by default. */ static hasTask() { return true } /** * Returns the build task executed by this build plugin. * @return {object} */ get task() { //injected by framework return this._task } /** * Returns a list of build and compiler messages created by this build plugin. * Supported message severities are 'Info', 'Warning', and 'Error'. * @return {Array} */ get messages() { return this._messages } /** * Returns the list of files and folders written by this build plugin. * @return {Array} */ get files() { return [...this._files] } /** * Returns the build context * @returns {object} */ get context() { return this._context } /** * Returns the priority of this plugin as number, where 1024 represents the default value * to ensure that custom plugins are by default executed before the built-in plugins. * The higher the priority value, the sooner the build plugin will be run. * The range of valid priority values is from -1024 to +1024. * Built-in plugins have a priority value range from 0 - 524. * @return {number} the priority for this plugin as integer. */ get priority() { return 1024 } /** * Called by the framework immediately after this instance has been created. * The instance has already been fully initialized. */ init() { if (cds.env.build.target === '.') { this.task.dest = path.join(this.task.dest, 'gen') } } /** * @abstract * Called by the framework to create the artifacts of this build plugin. */ async build() { } /** * Called by the framework immediately before 'build' to delete any output created by this build plugin. * <p> * Note: The <code>BuildTaskEngine</code> is cleaning the common generation target folder if the build is * executed in staging mode, e.g. build.target: "gen". */ async clean() { function isSubDirectory(parent, child) { return !path.relative(parent, child).startsWith('..') } if (cds.env.build.target === '.' && this.task.src !== this.task.dest && !isSubDirectory(this.task.dest, this.task.src) && await pathExists(this.task.dest)) { return fs.promises.rm(this.task.dest, { force: true, recursive: true }) } } /** * Asynchronously write the given content to a given file path. * If the file exists the content is replaced. If the file does not exist, a new file will be created. * The file name is stored in the list of files written by this build plugin. * @param {string} dest - absolute or relative file path. Relative paths will be resolved to this task's destination path. * @param {any} data - If data is of type object the JSON-stringified version is written. */ write(data) { return { to: async (dest) => { if (this._hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM)) { if (!path.isAbsolute(dest)) { // relative to build task's destination path dest = path.resolve(this.task.dest, dest) } this.pushFile(dest) await fs.promises.mkdir(path.dirname(dest), { recursive: true }) await fs.promises.writeFile(dest, typeof data === "object" && !Buffer.isBuffer(data) ? JSON.stringify(data, null, 2) : data) } } } } /** * Asynchronously copies a single file or the entire directory structure from 'src' to 'dest', including subdirectories and files. * <p> * Note: The file names are stored in the list of files written by this build plugin. * </p> * @param {string} src The absolute or relative source path of the file or directory to copy. * Relative paths will be resolved to this task's source path. * @param {string} dest The absolute or relative target path. Relative paths will be resolved to this task's destination path. * */ copy(src) { return { to: async (dest) => { if (this._hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM)) { if (!path.isAbsolute(src)) { // relative to build task's source path src = path.resolve(this.task.src, src) } if (!path.isAbsolute(dest)) { // relative to build task's destination path dest = path.resolve(this.task.dest, dest) } // list all copied files recursively if (fs.lstatSync(src).isDirectory()) { await fs.promises.cp(src, dest, { recursive: true }) const files = getFiles(dest) files.forEach(file => this.pushFile(file)) } else { this.pushFile(dest) return fs.promises.cp(src, dest, { recursive: true }) } } } } } /** * Adds the given user message and severity to the list of messages issued by this build task. * <p> * User messages will be logged after CDS build has been finished based on the log-level that has been set. * By default messages with severity <em>warning</em> and <em>error</em> will be logged. * @param {string} message the message text * @param {string} severity the severity of the message */ pushMessage(message, severity) { this.messages.push(new BuildMessage(message, severity)) } /** * Returns a compiled CSN model according to the model paths defined by this build task. * The model includes model enhancements defined by feature toggles, if any. * @return {object} the compiled CSN model */ async model() { const files = cds.resolve(this.task.options.model || this.task.src) if (!files || files.length === 0) { console.log(`no CDS model found for [${this.task.for}] build task [${this.task.src}] - nothing to be done`) return null } DEBUG?.(`model: ${relativePaths(cds.root, files).join(", ")}`) // $location paths are relative to current working dir by default - make sure a given project root folder is taken const options = { ...this.options(), cwd: cds.root } const model = await cds.load(files, options) if (!model) { return null } return model } /** * Returns a compiled base model CSN according to the model paths defined by this build task. * The base model does not include any model enhancements defined by feature toggles. * @return {object} the compiled base model CSN */ async baseModel() { const model = await this.model() if (!model) { return model } const sources = filterFeaturePaths(model, this.task.options.model || this.task.src) return sources.features ? cds.load(sources.base, this.options()) : model } options() { return { messages: this._messages } } /** * Adds the given fully qualified file path to the list of files that are written by this build task. * @param {string} filePath */ pushFile(filePath) { this._files.add(filePath) } /** 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. */ _hasBuildOption(qualifiedName, value) { return hasOptionValue(this._getBuildOption(qualifiedName), value) } // Returns the value of the given build option defined for this build task. _getBuildOption(qualifiedName) { // build task options overwriting other settings let value = getProperty(this.context.options, qualifiedName) // command line if (value === undefined) { value = getProperty(this.task.options, qualifiedName) // task options } return value } } module.exports = Plugin