UNPKG

@sap/cds-dk

Version:

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

218 lines (196 loc) 7.61 kB
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