From a7074aeb8167330fd1b6d30bf5b387a212cd6b1b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 22 Jul 2020 18:22:19 +0200 Subject: [PATCH] [FEATURE] Implement TaskUtil class --- index.js | 6 +- lib/builder/ProjectBuildContext.js | 6 +- lib/builder/builder.js | 13 +- lib/tasks/TaskUtil.js | 133 ++++++++++++++++++++ lib/tasks/jsdoc/generateJsdoc.js | 4 +- lib/types/AbstractBuilder.js | 32 +++-- lib/types/application/ApplicationBuilder.js | 2 +- lib/types/application/applicationType.js | 4 +- lib/types/library/LibraryBuilder.js | 8 +- lib/types/library/libraryType.js | 4 +- lib/types/themeLibrary/themeLibraryType.js | 4 +- test/lib/builder/ProjectBuildContext.js | 6 +- test/lib/builder/builder.js | 4 +- test/lib/tasks/jsdoc/generateJsdoc.js | 8 +- 14 files changed, 199 insertions(+), 35 deletions(-) create mode 100644 lib/tasks/TaskUtil.js diff --git a/index.js b/index.js index 2c226c362..4ea2a17c2 100644 --- a/index.js +++ b/index.js @@ -151,7 +151,11 @@ module.exports = { /** * @type {import('./lib/tasks/taskRepository')} */ - taskRepository: "./lib/tasks/taskRepository" + taskRepository: "./lib/tasks/taskRepository", + /** + * @type {import('./lib/tasks/TaskUtil')} + */ + TaskUtil: "./lib/tasks/TaskUtil" }, /** * @private diff --git a/lib/builder/ProjectBuildContext.js b/lib/builder/ProjectBuildContext.js index f93ebb20f..270a13597 100644 --- a/lib/builder/ProjectBuildContext.js +++ b/lib/builder/ProjectBuildContext.js @@ -1,6 +1,6 @@ const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; -const TAGS = Object.freeze({ +const STANDARD_TAGS = Object.freeze({ HideFromBuildResult: "ui5:HideFromBuildResult" }); @@ -23,10 +23,10 @@ class ProjectBuildContext { cleanup: [] }; - this.TAGS = TAGS; + this.STANDARD_TAGS = STANDARD_TAGS; this._resourceTagCollection = new ResourceTagCollection({ - allowedTags: Object.values(this.TAGS) + allowedTags: Object.values(this.STANDARD_TAGS) }); } diff --git a/lib/builder/builder.js b/lib/builder/builder.js index 40129f859..0e0a6d0d6 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -332,6 +332,11 @@ module.exports = { } }); + const TaskUtil = require("../tasks/TaskUtil"); + const taskUtil = new TaskUtil({ + projectBuildContext: projectContext + }); + if (dev && devExcludeProject.indexOf(project.metadata.name) !== -1) { projectTasks = composeTaskList({dev: false, selfContained, includedTasks, excludedTasks}); } @@ -344,13 +349,19 @@ module.exports = { tasks: projectTasks, project, parentLogger: log, - buildContext: projectContext + taskUtil }).then(() => { log.verbose("Finished building project %s. Writing out files...", project.metadata.name); buildLogger.completeWork(1); return workspace.byGlob("/**/*.*").then((resources) => { + const tagCollection = projectContext.getResourceTagCollection(); return Promise.all(resources.map((resource) => { + if (tagCollection.getTag(resource, projectContext.STANDARD_TAGS.HideFromBuildResult)) { + log.verbose(`Skipping write of resource tagged as "HideFromBuildResult": ` + + resource.getPath()); + return; // Skip target write for this resource + } if (projectContext.isRootProject() && project.type === "application" && project.metadata.namespace) { // Root-application projects only: Remove namespace prefix if given resource.setPath(resource.getPath().replace( diff --git a/lib/tasks/TaskUtil.js b/lib/tasks/TaskUtil.js new file mode 100644 index 000000000..3e9261dad --- /dev/null +++ b/lib/tasks/TaskUtil.js @@ -0,0 +1,133 @@ +/** + * Convenience functions for UI5 Builder tasks. + * An instance of this class is passed to every standard UI5 Builder task. + * Custom tasks that define a specification version >= 2.2 will also receive an instance + * of this class when called. + * + * The set of functions that can be accessed by a custom tasks depends on the specification + * version defined for the extension. + * + * @public + * @memberof module:@ui5/builder.tasks + */ +class TaskUtil { + /** + * Standard Build Tags. See UI5 Tooling RFC 0008 for details. + * + * @public + * @typedef {object} StandardBuildTags + * @property {string} HideFromBuildResult + * Setting this tag to true for a resource will prevent it from being written to the build target + */ + + /** + * Constructor + * + * @param {object} parameters + * @param {module:@ui5/builder.builder.ProjectBuildContext} parameters.projectBuildContext ProjectBuildContext + * @public + * @hideconstructor + */ + constructor({projectBuildContext}) { + this._projectBuildContext = projectBuildContext; + + /** + * @member {StandardBuildTags} + * @public + */ + this.STANDARD_TAGS = this._projectBuildContext.STANDARD_TAGS; + } + + /** + * Stores a tag with value for a given resource's path. Note that the tag is independent of the supplied + * resource instance. For two resource instances with the same path, the same tag value is returned. + * If the path of a resource is changed, any tag information previously stored for that resource is lost. + * + * @param {module:@ui5/fs.Resource} resource Resource the tag should be stored for + * @param {string} tag Name of the tag. + * Currently only the [STANDARD_TAGS]{@link module:@ui5/builder.tasks.TaskUtil#STANDARD_TAGS} are allowed + * @param {string|boolean|integer} [value=true] Tag value. Must be primitive + * @public + */ + setTag(resource, tag, value) { + return this._projectBuildContext.getResourceTagCollection().setTag(resource, tag, value); + } + + /** + * Retrieves the value for a stored tag. If no value is stored, undefined is returned. + * + * @param {module:@ui5/fs.Resource} resource Resource the tag should be retrieved for + * @param {string} tag Name of the tag + * @returns {string|boolean|integer} Tag value for the given resource. + * undefined if no value is available + * @public + */ + getTag(resource, tag) { + return this._projectBuildContext.getResourceTagCollection().getTag(resource, tag); + } + + /** + * Clears the value of a tag stored for the given resource's path. + * It's like the tag was never set for that resource. + * + * @param {module:@ui5/fs.Resource} resource Resource the tag should be cleared for + * @param {string} tag Tag + * @public + */ + clearTag(resource, tag) { + return this._projectBuildContext.getResourceTagCollection().clearTag(resource, tag); + } + + /** + * Check whether the project currently being build is the root project. + * + * @returns {boolean} True if the currently built project is the root project + * @public + */ + isRootProject() { + return this._projectBuildContext.isRootProject(); + } + + /** + * Register a function that must be executed once the build is finished. This can be used to for example + * cleanup files temporarily created on the file system. If the callback returns a Promise, it will be waited for. + * + * @param {Function} callback Callback to register. If it returns a Promise, it will be waited for + * @public + */ + registerCleanupTask(callback) { + return this._projectBuildContext.registerCleanupTask(callback); + } + + /** + * Get an interface to an instance of this class that only provides those functions + * that are supported by the given custom middleware extension specification version. + * + * @param {string} specVersion Specification Version of custom middleware extension + * @returns {object} An object with bound instance methods supported by the given specification version + */ + getInterface(specVersion) { + const baseInterface = { + STANDARD_TAGS: this.STANDARD_TAGS, + setTag: this.setTag.bind(this), + clearTag: this.clearTag.bind(this), + getTag: this.getTag.bind(this), + isRootProject: this.isRootProject.bind(this), + registerCleanupTask: this.registerCleanupTask.bind(this) + }; + switch (specVersion) { + case "0.1": + case "1.0": + case "1.1": + case "2.0": + case "2.1": + return undefined; + case "2.2": + return baseInterface; + default: + throw new Error(`TaskUtil: Unknown or unsupported specification version ${specVersion}`); + } + } +} + +module.exports = TaskUtil; diff --git a/lib/tasks/jsdoc/generateJsdoc.js b/lib/tasks/jsdoc/generateJsdoc.js index c74369063..bc979dc22 100644 --- a/lib/tasks/jsdoc/generateJsdoc.js +++ b/lib/tasks/jsdoc/generateJsdoc.js @@ -32,7 +32,7 @@ const {resourceFactory} = require("@ui5/fs"); * @returns {Promise} Promise resolving with undefined once data has been written */ const generateJsdoc = async function({ - buildContext, + taskUtil, workspace, dependencies, options = {} @@ -46,7 +46,7 @@ const generateJsdoc = async function({ const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} = await generateJsdoc._createTmpDirs(projectName); - buildContext.registerCleanupTask(cleanup); + taskUtil.registerCleanupTask(cleanup); const [writtenResourcesCount] = await Promise.all([ generateJsdoc._writeResourcesToDir({ diff --git a/lib/types/AbstractBuilder.js b/lib/types/AbstractBuilder.js index 93281dd43..593df9ac8 100644 --- a/lib/types/AbstractBuilder.js +++ b/lib/types/AbstractBuilder.js @@ -23,7 +23,7 @@ class AbstractBuilder { * @param {GroupLogger} parameters.parentLogger Logger to use * @param {object} parameters.buildContext */ - constructor({resourceCollections, project, parentLogger, buildContext}) { + constructor({resourceCollections, project, parentLogger, taskUtil}) { if (new.target === AbstractBuilder) { throw new TypeError("Class 'AbstractBuilder' is abstract"); } @@ -35,8 +35,17 @@ class AbstractBuilder { this.tasks = {}; this.taskExecutionOrder = []; - this.addStandardTasks({resourceCollections, project, log: this.log, buildContext}); - this.addCustomTasks({resourceCollections, project, buildContext}); + this.addStandardTasks({ + resourceCollections, + project, + log: this.log, + taskUtil + }); + this.addCustomTasks({ + resourceCollections, + project, + taskUtil + }); } /** @@ -50,7 +59,7 @@ class AbstractBuilder { * @param {object} parameters.project Project configuration * @param {object} parameters.log @ui5/logger logger instance */ - addStandardTasks({resourceCollections, project, log, buildContext}) { + addStandardTasks({resourceCollections, project, log, taskUtil}) { throw new Error("Function 'addStandardTasks' is not implemented"); } @@ -63,7 +72,7 @@ class AbstractBuilder { * @param {object} parameters.buildContext * @param {object} parameters.project Project configuration */ - addCustomTasks({resourceCollections, project, buildContext}) { + addCustomTasks({resourceCollections, project, taskUtil}) { const projectCustomTasks = project.builder && project.builder.customTasks; if (!projectCustomTasks || projectCustomTasks.length === 0) { return; // No custom tasks defined @@ -96,13 +105,15 @@ class AbstractBuilder { } } // Create custom task if not already done (task might be referenced multiple times, first one wins) - const {task} = taskRepository.getTask(taskDef.name); + const {/* specVersion, */ task} = taskRepository.getTask(taskDef.name); const execTask = function() { /* Custom Task Interface Parameters: {Object} parameters Parameters {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files + {Object} taskUtil Specification Version dependent interface to a + [TaskUtil]{@link module:@ui5/builder.tasks.TaskUtil} instance {Object} parameters.options Options {string} parameters.options.projectName Project name {string} [parameters.options.projectNamespace] Project namespace if available @@ -110,7 +121,7 @@ class AbstractBuilder { Returns: {Promise} Promise resolving with undefined once data has been written */ - return task({ + const params = { workspace: resourceCollections.workspace, dependencies: resourceCollections.dependencies, options: { @@ -118,7 +129,12 @@ class AbstractBuilder { projectNamespace: project.metadata.namespace, configuration: taskDef.configuration } - }); + }; + // TODO: Decide whether custom tasks should already get access to TaskUtil + // if (specVersion === "2.2") { + // params.taskUtil = taskUtil.getInterface(specVersion); + // } + return task(params); }; this.tasks[newTaskName] = execTask; diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index 605087c46..4c86ca890 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -2,7 +2,7 @@ const AbstractBuilder = require("../AbstractBuilder"); const {getTask} = require("../../tasks/taskRepository"); class ApplicationBuilder extends AbstractBuilder { - addStandardTasks({resourceCollections, project, log}) { + addStandardTasks({resourceCollections, project, log, taskUtil}) { if (!project.metadata.namespace) { // TODO 3.0: Throw here log.info("Skipping some tasks due to missing application namespace information. If your project contains " + diff --git a/lib/types/application/applicationType.js b/lib/types/application/applicationType.js index 78bb461fb..8f27d5ca9 100644 --- a/lib/types/application/applicationType.js +++ b/lib/types/application/applicationType.js @@ -5,8 +5,8 @@ module.exports = { format: function(project) { return new ApplicationFormatter({project}).format(); }, - build: function({resourceCollections, tasks, project, parentLogger, buildContext}) { - return new ApplicationBuilder({resourceCollections, project, parentLogger, buildContext}).build(tasks); + build: function({resourceCollections, tasks, project, parentLogger, taskUtil}) { + return new ApplicationBuilder({resourceCollections, project, parentLogger, taskUtil}).build(tasks); }, // Export type classes for extensibility diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index f6797aa73..a5b4935a4 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -2,7 +2,7 @@ const AbstractBuilder = require("../AbstractBuilder"); const {getTask} = require("../../tasks/taskRepository"); class LibraryBuilder extends AbstractBuilder { - addStandardTasks({resourceCollections, project, log, buildContext}) { + addStandardTasks({resourceCollections, project, log, taskUtil}) { if (!project.metadata.namespace) { // TODO 3.0: Throw here log.info("Skipping some tasks due to missing library namespace information. Your project " + @@ -56,7 +56,7 @@ class LibraryBuilder extends AbstractBuilder { } return getTask("generateJsdoc").task({ - buildContext, + taskUtil, workspace: resourceCollections.workspace, dependencies: resourceCollections.dependencies, options: { @@ -153,8 +153,8 @@ class LibraryBuilder extends AbstractBuilder { dependencies: resourceCollections.dependencies, options: { projectName: project.metadata.name, - librariesPattern: !buildContext.isRootProject() ? "/resources/**/*.library" : undefined, - themesPattern: !buildContext.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/*.library" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, inputPattern: "/resources/**/themes/*/library.source.less" } }); diff --git a/lib/types/library/libraryType.js b/lib/types/library/libraryType.js index fa582b1ea..2a9576645 100644 --- a/lib/types/library/libraryType.js +++ b/lib/types/library/libraryType.js @@ -5,8 +5,8 @@ module.exports = { format: function(project) { return new LibraryFormatter({project}).format(); }, - build: function({resourceCollections, tasks, project, parentLogger, buildContext}) { - return new LibraryBuilder({resourceCollections, project, parentLogger, buildContext}).build(tasks); + build: function({resourceCollections, tasks, project, parentLogger, taskUtil}) { + return new LibraryBuilder({resourceCollections, project, parentLogger, taskUtil}).build(tasks); }, // Export type classes for extensibility diff --git a/lib/types/themeLibrary/themeLibraryType.js b/lib/types/themeLibrary/themeLibraryType.js index f1c28d903..5907e62d9 100644 --- a/lib/types/themeLibrary/themeLibraryType.js +++ b/lib/types/themeLibrary/themeLibraryType.js @@ -5,8 +5,8 @@ module.exports = { format: function(project) { return new ThemeLibraryFormatter({project}).format(); }, - build: function({resourceCollections, tasks, project, parentLogger, buildContext}) { - return new ThemeLibraryBuilder({resourceCollections, project, parentLogger, buildContext}).build(tasks); + build: function({resourceCollections, tasks, project, parentLogger, taskUtil}) { + return new ThemeLibraryBuilder({resourceCollections, project, parentLogger, taskUtil}).build(tasks); }, // Export type classes for extensibility diff --git a/test/lib/builder/ProjectBuildContext.js b/test/lib/builder/ProjectBuildContext.js index 30dd489d0..5bb4a2903 100644 --- a/test/lib/builder/ProjectBuildContext.js +++ b/test/lib/builder/ProjectBuildContext.js @@ -75,7 +75,7 @@ test("executeCleanupTasks", (t) => { t.is(task2.callCount, 1, "my task 2", "Cleanup task 2 got called"); }); -test("TAGS constant", (t) => { +test("STANDARD_TAGS constant", (t) => { const projectBuildContext = new ProjectBuildContext({ buildContext: { getRootProject: () => "root project" @@ -84,9 +84,9 @@ test("TAGS constant", (t) => { resources: "resources" }); - t.deepEqual(projectBuildContext.TAGS, { + t.deepEqual(projectBuildContext.STANDARD_TAGS, { HideFromBuildResult: "ui5:HideFromBuildResult" - }, "Exposes correct TAGS constant"); + }, "Exposes correct STANDARD_TAGS constant"); }); test.serial("getResourceTagCollection", (t) => { diff --git a/test/lib/builder/builder.js b/test/lib/builder/builder.js index 34315c47c..45405ec99 100644 --- a/test/lib/builder/builder.js +++ b/test/lib/builder/builder.js @@ -77,6 +77,7 @@ async function checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, des test.afterEach.always((t) => { sinon.restore(); + mock.stopAll(); }); test("Build application.a", (t) => { @@ -516,8 +517,7 @@ test("Build theme.j even without an library", (t) => { test.serial("Cleanup", async (t) => { const BuildContext = require("../../../lib/builder/BuildContext"); - const projectContext = {isRootProject: sinon.stub()}; - const createProjectContextStub = sinon.stub(BuildContext.prototype, "createProjectContext").returns(projectContext); + const createProjectContextStub = sinon.spy(BuildContext.prototype, "createProjectContext"); const executeCleanupTasksStub = sinon.stub(BuildContext.prototype, "executeCleanupTasks").resolves(); const applicationType = require("../../../lib/types/application/applicationType"); const appBuildStub = sinon.stub(applicationType, "build").resolves(); diff --git a/test/lib/tasks/jsdoc/generateJsdoc.js b/test/lib/tasks/jsdoc/generateJsdoc.js index e4c7d58a0..7035018f4 100644 --- a/test/lib/tasks/jsdoc/generateJsdoc.js +++ b/test/lib/tasks/jsdoc/generateJsdoc.js @@ -224,11 +224,11 @@ test.serial("generateJsdoc", async (t) => { }; const registerCleanupTaskStub = sinon.stub(); - const buildContext = { + const taskUtil = { registerCleanupTask: registerCleanupTaskStub }; await generateJsdoc({ - buildContext, + taskUtil, workspace, dependencies: "dependencies", options: { @@ -306,11 +306,11 @@ test.serial("generateJsdoc with missing resources", async (t) => { write: writeStub }; const registerCleanupTaskStub = sinon.stub(); - const buildContext = { + const taskUtil = { registerCleanupTask: registerCleanupTaskStub }; await generateJsdoc({ - buildContext, + taskUtil, workspace, dependencies: "dependencies", options: {