UNPKG

@sap/cds-dk

Version:

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

228 lines (208 loc) 9.12 kB
const fs = require('fs') const path = require('path') const cds = require('../cds') const BuildTaskProviderFactory = require('./buildTaskProviderFactory') const { hasJavaNature, getProperty, getDefaultModelOptions, hasOptionValue, BuildError } = require('./util') const { FILE_EXT_CDS, OPTION_WS, IGNORE_DEFAULT_MODELS, NODEJS_MODEL_EXCLUDE_LIST, BUILD_TASK_NODEJS, BUILD_TASK_NODE_CF } = require("./constants") const DEBUG = cds.debug('cli|build') class BuildTaskFactory { constructor(options) { this._providerFactory = new BuildTaskProviderFactory(options) } get providerFactory() { return this._providerFactory } get options() { return this.providerFactory.options } // the following order for determining build tasks is used // 1. create from command line input, e.g. cds build/all --for hana --src db --model srv --dest db // 2. read using cds.env.build.tasks // 3. create from cds.env.folders config data async getTasks() { const tasks = await this._createTasks() // always resolve tasks for input validation const resolvedTasks = this.providerFactory.resolveTasks(tasks) if (this.options.resolve) { return resolvedTasks } return tasks } createPlugin(task) { return this.providerFactory.createPlugin(task) } async _createTasks() { DEBUG?.(`determining build tasks for project [${cds.root}].`) let tasks = BuildTaskFactory._getExistingTasks() if (tasks.length === 0) { tasks = await this.providerFactory.lookupTasks() this._applyCliOptions(tasks) } else { this._applyCliOptions(tasks) // 1. apply default values including task.for and ensure that for all tasks a provider exists - throwing error otherwise await this.providerFactory.applyTaskDefaults(tasks) // ensure that dependencies get wired up before filtering await this.providerFactory.lookupTasks(tasks, true) } // 2. filters the list of build tasks // Note: A new task might get created, e.g. 'cds build --for hana' will enforce a hana build even if sqlite has been configured let existingTasks = tasks tasks = await this._filterTasksForCli(tasks) if (tasks.length === 0) { return tasks } // 3. add dependencies existingTasks = [...tasks] await this.providerFactory.lookupTasks(tasks, true) if (tasks.length > existingTasks.length) { const newTasks = tasks.filter(task => !existingTasks.includes(task)) // a dependant task was added this._applyCliOptions(newTasks) } // obligatory task defaults shared by all tasks await BuildTaskFactory._applyCommonTaskDefaults(tasks) BuildTaskFactory._setDefaultBuildTargetFolder(tasks) return tasks } static _getExistingTasks() { return Array.isArray(getProperty(cds.env, 'build.tasks')) ? JSON.parse(JSON.stringify(cds.env.build.tasks)) : [] } static async _applyCommonTaskDefaults(tasks) { // normalize model options tasks.forEach(task => { if (task.options?.model && !Array.isArray(task.options.model)) { task.options.model = [task.options.model] } }) const modelPaths = await getDefaultModelOptions(false) let wsModelPaths // calculate only once if (cds.cli.options?.[OPTION_WS] || tasks.some(task => hasOptionValue(task.options?.[OPTION_WS], true))) { wsModelPaths = await getDefaultModelOptions(true) } // set default model options tasks.forEach(task => { if (!task.src) { throw new BuildError(`Mandatory property 'src' not defined for build task '${task.for}'.`) } const optionWs = cds.cli.options?.[OPTION_WS] || hasOptionValue(task.options?.[OPTION_WS], true) // shallow copy model paths this._setTaskModelOptions(task, optionWs ? [...wsModelPaths] : [...modelPaths]) }) } static _setDefaultBuildTargetFolder() { // Java projects use "." as the default build target folder if (hasJavaNature() && this._adaptBuildTargetSettingForJava()) { DEBUG?.("using inplace build for java project instead of default staging build") } } /** * Use inplace build for java projects if build.target has not been configured. * @returns {boolean} true if changed, false otherwise */ static _adaptBuildTargetSettingForJava() { if (cds.env.build.target !== ".") { // filter user settings of cds.env const userEnv = cds.env.for("cds", cds.root, false) // use helper as env.build might be undefined if (!userEnv.build?.target) { cds.env.build.target = "." return true } } return false } async _filterTasksForCli(tasks) { const options = this.options // filter tasks using either option for, use, src let resultTasks = tasks.filter(task => { return (!options.for || options.for === task.for) && (!options.src || options.src === task.src) }) if (resultTasks.length === 0) { if (options.for) { const task = this.providerFactory.getTask({ for: options.for }) if (options.src) { task.src = options.src } resultTasks.push(task) this._applyCliOptions(resultTasks) await this.providerFactory.applyTaskDefaults(resultTasks) } } else if (resultTasks.length <= tasks.length) { // return the same array as long as it contains a subset of the given tasks tasks.length = 0 resultTasks.forEach(task => tasks.push(task)) resultTasks = tasks } return resultTasks } _applyCliOptions(tasks) { const options = this.options // apply remaining cli options to filtered tasks tasks.forEach(task => { if (options.dest) { task.dest = options.dest } if (options.taskOptions) { const taskOptions = JSON.parse(JSON.stringify(options.taskOptions)) task.options = task.options ? Object.assign(task.options, taskOptions) : taskOptions } }) } static _setTaskModelOptions(task, defaultModelPaths) { // bootstrap service model is only required for Nodejs build task - issues/12770#issuecomment-1805719 if (task.for !== BUILD_TASK_NODEJS && task.for !== BUILD_TASK_NODE_CF) { defaultModelPaths = defaultModelPaths.filter(p => !NODEJS_MODEL_EXCLUDE_LIST.includes(p)) } let taskModelPaths = task.options?.model if (taskModelPaths && !Array.isArray(taskModelPaths)) { taskModelPaths = [taskModelPaths] } const allowList = new RegExp(`^@sap/cds${cds.env.requires.toggles ? '|^' + cds.env.features.folders.replace('*', '') : ''}`) task.options = task.options || {} let modelPaths = [] if (taskModelPaths?.length) { if (!hasOptionValue(task.options?.[IGNORE_DEFAULT_MODELS], true)) { modelPaths = defaultModelPaths.filter(p => p.match(allowList)) } } else { modelPaths = [...defaultModelPaths] if (!modelPaths.includes(task.src)) { modelPaths.push(task.src) } if (hasOptionValue(task.options?.[IGNORE_DEFAULT_MODELS], true)) { // all default models except the built-in models modelPaths = modelPaths.filter(p => !p.match(allowList)) } } task.options.model = [...new Set(taskModelPaths?.length ? taskModelPaths.concat(modelPaths) : modelPaths)] } /** * Determines the module folder from the past list that may represent files or folders w or w/o .cds file extension. * @param {Array} filesOrFolders */ static _getModuleFolder(filesOrFolders) { const resources = [...filesOrFolders] filesOrFolders.forEach(fileOrFolder => { if (path.extname(fileOrFolder) !== FILE_EXT_CDS) { resources.push(fileOrFolder + FILE_EXT_CDS) } }) return resources.reduce((acc, resource) => { if (!acc) { let resourcePath = path.resolve(cds.root, resource) if (fs.existsSync(resourcePath)) { if (fs.lstatSync(resourcePath).isDirectory()) { acc = resource } else { // represents file acc = path.dirname(resource) } } } return acc }, null) } } module.exports = BuildTaskFactory