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