@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
261 lines (238 loc) • 9.76 kB
JavaScript
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