From 9b4ae00f62da1f5bb94aeb8a86711c2a2e98da20 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 16 Nov 2018 11:20:17 +0100 Subject: [PATCH] [FEATURE] Builder: Add handling for custom task configurations As per RFC0004: https://github.com/SAP/ui5-tooling/pull/54 --- index.js | 3 +- lib/builder/builder.js | 18 +-- lib/tasks/taskRepository.js | 41 ++++++ lib/types/AbstractBuilder.js | 142 +++++++++++++++++--- lib/types/application/ApplicationBuilder.js | 18 +-- lib/types/library/LibraryBuilder.js | 17 +-- lib/types/module/ModuleBuilder.js | 7 +- 7 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 lib/tasks/taskRepository.js diff --git a/index.js b/index.js index 8eedf56c0..c427024ce 100644 --- a/index.js +++ b/index.js @@ -23,7 +23,8 @@ const ui5Builder = { generateVersionInfo: require("./lib/tasks/generateVersionInfo"), replaceCopyright: require("./lib/tasks/replaceCopyright"), replaceVersion: require("./lib/tasks/replaceVersion"), - uglify: require("./lib/tasks/uglify") + uglify: require("./lib/tasks/uglify"), + taskRepository: require("./lib/tasks/taskRepository") }, types: { AbstractBuilder: require("./lib/types/AbstractBuilder"), diff --git a/lib/builder/builder.js b/lib/builder/builder.js index 50e3bb13d..a595989f0 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -1,23 +1,9 @@ const log = require("@ui5/logger").getGroupLogger("builder:builder"); const resourceFactory = require("@ui5/fs").resourceFactory; const typeRepository = require("../types/typeRepository"); +const taskRepository = require("../tasks/taskRepository"); -const definedTasks = { - replaceCopyright: require("../tasks/replaceCopyright"), - replaceVersion: require("../tasks/replaceVersion"), - createDebugFiles: require("../tasks/createDebugFiles"), - uglify: require("../tasks/uglify"), - buildThemes: require("../tasks/buildThemes"), - generateLibraryManifest: require("../tasks/generateLibraryManifest"), - generateVersionInfo: require("../tasks/generateVersionInfo"), - generateManifestBundle: require("../tasks/bundlers/generateManifestBundle"), - generateFlexChangesBundle: require("../tasks/bundlers/generateFlexChangesBundle"), - generateComponentPreload: require("../tasks/bundlers/generateComponentPreload"), - generateStandaloneAppBundle: require("../tasks/bundlers/generateStandaloneAppBundle"), - generateBundle: require("../tasks/bundlers/generateBundle"), - generateLibraryPreload: require("../tasks/bundlers/generateLibraryPreload") -}; - +const definedTasks = taskRepository.getAllTasks(); // Set of tasks for development const devTasks = [ diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js new file mode 100644 index 000000000..aa7e4dd76 --- /dev/null +++ b/lib/tasks/taskRepository.js @@ -0,0 +1,41 @@ +const tasks = { + replaceCopyright: require("./replaceCopyright"), + replaceVersion: require("./replaceVersion"), + createDebugFiles: require("./createDebugFiles"), + uglify: require("./uglify"), + buildThemes: require("./buildThemes"), + generateLibraryManifest: require("./generateLibraryManifest"), + generateVersionInfo: require("./generateVersionInfo"), + generateManifestBundle: require("./bundlers/generateManifestBundle"), + generateFlexChangesBundle: require("./bundlers/generateFlexChangesBundle"), + generateComponentPreload: require("./bundlers/generateComponentPreload"), + generateStandaloneAppBundle: require("./bundlers/generateStandaloneAppBundle"), + generateBundle: require("./bundlers/generateBundle"), + generateLibraryPreload: require("./bundlers/generateLibraryPreload") +}; + +function getTask(taskName) { + const task = tasks[taskName]; + + if (!task) { + throw new Error(`taskRepository: Unkown Task ${taskName}`); + } + return task; +} + +function addTask(name, task) { + if (tasks[name]) { + throw new Error(`taskRepository: Task ${name} already registered`); + } + tasks[name] = task; +} + +function getAllTasks() { + return tasks; +} + +module.exports = { + getTask: getTask, + addTask: addTask, + getAllTasks: getAllTasks +}; diff --git a/lib/types/AbstractBuilder.js b/lib/types/AbstractBuilder.js index c37cd1976..4c1939aa4 100644 --- a/lib/types/AbstractBuilder.js +++ b/lib/types/AbstractBuilder.js @@ -7,30 +7,140 @@ class AbstractBuilder { /** * Constructor * + * @param {object} resourceCollections Resource collections + * @param {DuplexCollection} resourceCollections.workspace Workspace Resource + * @param {ReaderCollection} resourceCollections.dependencies Workspace Resource * @param {object} project Project configuration * @param {GroupLogger} parentLogger Logger to use */ - constructor({project, parentLogger}) { - this.tasks = {}; + constructor({resourceCollections, project, parentLogger}) { + if (new.target === AbstractBuilder) { + throw new TypeError("Class 'AbstractBuilder' is abstract"); + } + + this.project = project; + this.log = parentLogger.createSubLogger(project.type + " " + project.metadata.name, 0.2); this.taskLog = this.log.createTaskLogger("🔨"); - this.availableTasks = []; + + this.tasks = {}; + this.taskExecutionOrder = []; + this.addStandardTasks({resourceCollections, project}); + this.addCustomTasks({resourceCollections, project}); + } + + /** + * Adds all standard tasks to execute + * + * @abstract + * @protected + * @param {object} resourceCollections Resource collections + * @param {DuplexCollection} resourceCollections.workspace Workspace Resource + * @param {ReaderCollection} resourceCollections.dependencies Workspace Resource + * @param {object} project Project configuration + */ + addStandardTasks() { + throw new Error("Function 'addStandardTasks' is not implemented"); + } + + /** + * Adds custom tasks to execute + * + * @private + * @param {object} resourceCollections Resource collections + * @param {DuplexCollection} resourceCollections.workspace Workspace Resource + * @param {ReaderCollection} resourceCollections.dependencies Workspace Resource + * @param {object} project Project configuration + */ + addCustomTasks({resourceCollections, project}) { + const projectCustomTasks = project.builder && project.builder.customTasks; + if (!projectCustomTasks || projectCustomTasks.length === 0) { + return; // No custom tasks defined + } + const taskRepository = require("../tasks/taskRepository"); + for (let i = 0; i < projectCustomTasks.length; i++) { + const taskDef = projectCustomTasks[i]; + if (!taskDef.name) { + throw new Error(`Missing name for custom task definition of project ${project.metadata.name} ` + + `at index ${i}`); + } + if (taskDef.beforeTask && taskDef.afterTask) { + throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + + `defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`); + } + if (!taskDef.beforeTask && !taskDef.afterTask) { + throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + + `defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`); + } + + let newTaskName = taskDef.name; + if (this.tasks[newTaskName]) { + // Task is already known + // => add a suffix to allow for multiple configurations of the same task + let suffixCounter = 0; + while (this.tasks[newTaskName]) { + suffixCounter++; // Start at 1 + newTaskName = `${newTaskName}--${suffixCounter}`; + } + } + // Create custom task if not already done (task might be referenced multiple times, first one wins) + const task = taskRepository.getTask(taskDef.name); + const execTask = function() { + /* Custom Task Interface + Parameters: + {Object} parameters Parameters + {DuplexCollection} parameters.workspace DuplexCollection to read and write files + {AbstractReader} parameters.dependencies Reader or Collection to read dependency files + {Object} parameters.options Options + {string} parameters.options.projectName Project name + {string} [parameters.options.configuration] Task configuration if given in ui5.yaml + Returns: + {Promise} Promise resolving with undefined once data has been written + */ + return task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name, + configuration: taskDef.configuration + } + }); + }; + + this.tasks[newTaskName] = execTask; + + const refTaskName = taskDef.beforeTask || taskDef.afterTask; + let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskName); + if (refTaskIdx === -1) { + throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` + + `to be scheduled for project ${project.metadata.name}`); + } + if (taskDef.afterTask) { + // Insert after index of referenced task + refTaskIdx++; + } + this.taskExecutionOrder.splice(refTaskIdx, 0, newTaskName); + } } /** * Adds a executable task to the builder * - * This does not ensure the correct build order. The order is maintained through the property - * [availableTasks]{@link AbstractBuilder#availableTasks} + * The order this function is being called defines the build order. FIFO. * * @param {string} taskName Name of the task which should be in the list availableTasks. * @param {function} taskFunction */ addTask(taskName, taskFunction) { - if (this.availableTasks.indexOf(taskName) === -1) { - throw new Error(`Task "${taskName}" does not exist.`); + if (this.tasks[taskName]) { + throw new Error(`Failed to add duplicative task ${taskName} for project ${this.project.metadata.name}`); + } + if (this.taskExecutionOrder.includes(taskName)) { + throw new Error(`Builder: Failed ot add task ${taskName} for project ${this.project.metadata.name}. ` + + `It has already been scheduled for execution.`); } this.tasks[taskName] = taskFunction; + this.taskExecutionOrder.push(taskName); } /** @@ -40,21 +150,17 @@ class AbstractBuilder { * @returns {Promise} Returns promise chain with tasks */ build(tasksToRun) { - const allTasksCount = tasksToRun.filter((value) => this.availableTasks.includes(value)).length; + const allTasksCount = tasksToRun.filter((value) => this.tasks.hasOwnProperty(value)).length; this.taskLog.addWork(allTasksCount); let taskChain = Promise.resolve(); - for (let i = 0; i < this.availableTasks.length; i++) { - const taskName = this.availableTasks[i]; - - if (!tasksToRun.includes(taskName)) { - continue; - } - - const taskFunction = this.tasks[taskName]; + for (const taskName in this.tasks) { + if (this.tasks.hasOwnProperty(taskName) && tasksToRun.includes(taskName)) { + const taskFunction = this.tasks[taskName]; - if (typeof taskFunction === "function") { - taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); + if (typeof taskFunction === "function") { + taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); + } } } return taskChain; diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index 949e44152..ab3249190 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -15,23 +15,7 @@ const tasks = { // can't require index.js due to circular dependency }; class ApplicationBuilder extends AbstractBuilder { - constructor({resourceCollections, project, parentLogger}) { - super({project, parentLogger}); - - // All available application tasks in execution order - this.availableTasks = [ - "replaceCopyright", - "replaceVersion", - "generateFlexChangesBundle", - "generateManifestBundle", - "generateComponentPreload", - "generateStandaloneAppBundle", - "generateBundle", - "createDebugFiles", - "uglify", - "generateVersionInfo" - ]; - + addStandardTasks({resourceCollections, project}) { this.addTask("replaceCopyright", () => { return tasks.replaceCopyright({ workspace: resourceCollections.workspace, diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index e4d564460..98fb709b4 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -16,22 +16,7 @@ const tasks = { // can't require index.js due to circular dependency }; class LibraryBuilder extends AbstractBuilder { - constructor({resourceCollections, project, parentLogger}) { - super({project, parentLogger}); - - // All available library tasks in execution order - this.availableTasks = [ - "replaceCopyright", - "replaceVersion", - "generateComponentPreload", - "generateBundle", - "generateLibraryManifest", - "generateLibraryPreload", - "buildThemes", - "createDebugFiles", - "uglify" - ]; - + addStandardTasks({resourceCollections, project, parentLogger}) { this.addTask("replaceCopyright", () => { const replaceCopyright = tasks.replaceCopyright; return replaceCopyright({ diff --git a/lib/types/module/ModuleBuilder.js b/lib/types/module/ModuleBuilder.js index 697d4a6ea..e6a2bdb83 100644 --- a/lib/types/module/ModuleBuilder.js +++ b/lib/types/module/ModuleBuilder.js @@ -2,11 +2,10 @@ const AbstractBuilder = require("../AbstractBuilder"); class ModuleBuilder extends AbstractBuilder { constructor({resourceCollections, project, parentLogger}) { - super({project, parentLogger}); - - // All available library tasks in execution order - this.availableTasks = []; + super({resourceCollections, project, parentLogger}); } + + addStandardTasks() {/* nothing to do*/} } module.exports = ModuleBuilder;