@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
218 lines (196 loc) • 7.61 kB
JavaScript
const fs = require('fs')
const path = require('path')
const cds = require('../cds')
const { OUTPUT_MODE_FILESYSTEM, DEFAULT_SRC_FOLDER } = require('./constants')
const InternalBuildTaskProvider = require('./provider/internalBuildTaskProvider')
const BuildTaskProvider = require('./buildTaskProvider')
const { BuildError, normalizePath } = require('./util')
const DEBUG = cds.debug('cli|build')
class BuildTaskProviderFactory {
constructor(options = {}) {
options.outputMode = options.outputMode || OUTPUT_MODE_FILESYSTEM
if (options.clean === undefined) {
options.clean = true
}
['for', 'src', 'dest'].forEach(key => {
if (options[key]) {
if (typeof options[key] !== 'string') {
throw new BuildError(`Invalid build options - property '${key}' must be a string`)
}
if (key === 'src' || key === 'dest') {
// normalize path separator and remove trailing slash
options[key] = normalizePath(options[key])
}
}
})
this._context = { options, tasks: [] } // resolved tasks
}
get options() {
return this._context.options
}
get context() {
return this._context
}
get providers() {
if (!this._providers) {
this._providers = this._loadProviders()
}
return this._providers
}
getTask(key) {
return this._getProvider(key).getTask(key)
}
async applyTaskDefaults(tasks) {
return Promise.all(tasks.map(async (task) => {
if (!task.for) {
throw new BuildError(`Mandatory property 'for' not defined for build task.`)
}
return this._applyTaskDefaults(this._getProvider(task), [task])
}))
}
async lookupTasks(tasks = [], dependencies) {
for (let i = 0; i < this.providers.length; i++) {
const provider = this.providers[i]
const existingTasks = [...tasks]
await this._lookupTasks(provider, tasks, dependencies)
if (existingTasks.length < tasks.length) {
// apply defaults
const newTasks = tasks.filter(task => !existingTasks.includes(task))
await this._applyTaskDefaults(provider, newTasks)
DEBUG?.(`Build task provider ${provider.constructor.name} returned build tasks ${JSON.stringify(newTasks)}`)
}
}
return tasks
}
/**
* Create a Plugin instance for the given build task.
* The implementation is loaded based on the build task's 'for' or 'use' option.
* @param {*} task
*/
createPlugin(task) {
const Plugin = this.getPlugin(task)
const resolvedTask = this._resolveTask(task)
DEBUG?.(`loaded build plugin [${resolvedTask.for}]`)
const plugin = new Plugin()
if (!(plugin instanceof Plugin)) {
throw new Error(`Invalid Build plugin type ${task.for}`)
}
plugin._task = resolvedTask
plugin._context = this.context
this.context.tasks.push(resolvedTask)
DEBUG?.(`created build plugin [${resolvedTask.for}]`)
return plugin
}
/**
* Loads the build plugin implementation for the given build task.
* 'for' defines an alias for built-in plugins like 'hana', 'java', 'node', 'fiori' or 'mtx'.
* 'use' defines the fully qualified module name of custom build plugins implementations.
* @param {object} task
*/
getPlugin(task) {
const provider = this._getProvider(task)
try {
return provider.getPlugin(task)
} catch (e) {
console.error(`Provider failed to load build plugin class ${task.for} - provider: ${provider.constructor.name}`)
throw e
}
}
resolveTasks(tasks) {
return tasks.map(task => this._resolveTask(task))
}
/**
* Resolves the given build task based on the project root folder.<br>
* The task is validated in order to ensure that 'src' refers to a valid folder and 'for' or 'use' reference can be required.
* @param {*} task
*/
_resolveTask(task) {
// second validate src path
const resolvedTask = JSON.parse(JSON.stringify(task))
// Do not store resolved symlinks as this is causing issues on Windows, e.g. if git projects are
// located under 'C:\SAPDevelop\git\...' using a sym-link from '%USERHOME%\git' to 'C:\SAPDevelop\git'.
// see cap/issues/#8694
resolvedTask.src = path.resolve(cds.root, task.src)
// if --ws is passed, the db/ folder is not required, as the task will gather the model from the workspaces
if (!this.options.ws) {
try {
//validate source path
fs.realpathSync(resolvedTask.src)
} catch (e) {
throw new BuildError(`The 'src' folder '${path.resolve(cds.root, task.src)}' for build task '${resolvedTask.for}' does not exist`)
}
}
resolvedTask.dest = path.resolve(cds.root, cds.env.build.target, task.dest || task.src)
resolvedTask.options = task.options || {}
return resolvedTask
}
_getProvider(key) {
const provider = this.providers.find(provider => {
try {
return provider.providesTask(key)
} catch (e) {
console.error(`Build task provider ${provider.constructor.name} returned an error`)
throw e
}
})
if (!provider) {
throw new BuildError(`No provider found for build task '${key.for}'. Ensure that all required dependencies have been added and 'npm install' has been executed.`)
}
return provider
}
async _lookupTasks(provider, tasks, dependencies) {
return provider.lookupTasks(tasks, dependencies)
}
async _applyTaskDefaults(provider, tasks) {
return Promise.all(tasks.map(task => provider.applyTaskDefaults(task)))
}
_loadProviders() {
return [
new InternalBuildTaskProvider(),
new PluginBuildTaskProvider()
]
}
}
/**
* Default provider implementation handling fully qualified custom build task declarations.
*/
class PluginBuildTaskProvider extends BuildTaskProvider {
constructor() {
super()
this._plugins = require('./plugins').plugins
}
get plugins() {
return this._plugins
}
providesTask(key) {
return this.plugins.has(key.for)
}
getPlugin(task) {
return this.plugins.get(task.for)
}
async lookupTasks(tasks, dependencies) {
if (!dependencies) {
for (const [id, plugin] of this.plugins) {
if (plugin.hasTask()) {
tasks.push(this.getTask({ for: id }))
}
}
}
}
async applyTaskDefaults(task) {
const defaultTask = this.getTask(task)
const names = Object.getOwnPropertyNames(defaultTask)
names.forEach(name => {
task[name] ??= defaultTask[name]
})
}
getTask(key) {
// plugin entry is already validated, a taskDefaults default member exists
const task = JSON.parse(JSON.stringify(this.plugins.get(key.for).taskDefaults ?? {}))
task.for = key.for
task.src ??= DEFAULT_SRC_FOLDER
task.src = task.src.replace(/\/$/, '')
return task
}
}
module.exports = BuildTaskProviderFactory