From 153944a3ddbddf134b5a2fc722e152677a7b076d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 4 Dec 2020 18:55:42 +0100 Subject: [PATCH 01/99] [INTERNAL] Initial refactoring for introducing a Project class --- lib/Module.js | 337 ++++++++++++++++++ lib/configurations/AbstractConfiguration.js | 29 ++ lib/configurations/ExtensionConfiguration.js | 10 + lib/configurations/ProjectConfiguration.js | 11 + lib/configurations/ShimCollection.js | 52 +++ lib/graph/ProjectGraph.js | 160 +++++++++ lib/graph/projectGraphFromTree.js | 90 +++++ lib/specifications/AbstractSpecification.js | 14 + lib/specifications/Extension.js | 82 +++++ lib/specifications/Project.js | 86 +++++ package.json | 1 + .../node_modules/application.cycle.b/ui5.yaml | 20 +- .../application.cycle.b/webapp/manifest.json | 13 + test/lib/Module.js | 39 ++ test/lib/graph/projectGraphFromTree.js | 270 ++++++++++++++ test/lib/projectPreprocessor.js | 197 ++++++++++ test/lib/specifications/Project.js | 43 +++ 17 files changed, 1453 insertions(+), 1 deletion(-) create mode 100644 lib/Module.js create mode 100644 lib/configurations/AbstractConfiguration.js create mode 100644 lib/configurations/ExtensionConfiguration.js create mode 100644 lib/configurations/ProjectConfiguration.js create mode 100644 lib/configurations/ShimCollection.js create mode 100644 lib/graph/ProjectGraph.js create mode 100644 lib/graph/projectGraphFromTree.js create mode 100644 lib/specifications/AbstractSpecification.js create mode 100644 lib/specifications/Extension.js create mode 100644 lib/specifications/Project.js create mode 100644 test/fixtures/cyclic-deps/node_modules/application.cycle.b/webapp/manifest.json create mode 100644 test/lib/Module.js create mode 100644 test/lib/graph/projectGraphFromTree.js create mode 100644 test/lib/specifications/Project.js diff --git a/lib/Module.js b/lib/Module.js new file mode 100644 index 000000000..d51f3603e --- /dev/null +++ b/lib/Module.js @@ -0,0 +1,337 @@ +const fs = require("graceful-fs"); +const path = require("path"); +const {promisify} = require("util"); +const readFile = promisify(fs.readFile); +const jsyaml = require("js-yaml"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const Project = require("./specifications/Project"); +const Extension = require("./specifications/Extension"); +const ProjectConfiguration = require("./configurations/ProjectConfiguration"); +const ExtensionConfiguration = require("./configurations/ExtensionConfiguration"); +const {validate} = require("./validation/validator"); + +const log = require("@ui5/logger").getLogger("Module"); + +const defaultConfigPath = "ui5.yaml"; + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +class Module { + /** + * @param {object} parameters Module parameters + * @param {string} parameters.id Unique ID for the project + * @param {string} parameters.version Version of the project + * @param {string} parameters.modulePath File System path to access the projects resources + * @param {string} [parameters.configPath=ui5.yaml] + * Either a path relative to `modulePath` which will be resolved by @ui5/fs (default), + * or an absolute File System path to the project configuration file. + * @param {object} [parameters.configuration] + * Configuration object to use. If supplied, no ui5.yaml will be read + * @param {@ui5/extension.extensions.ShimCollection} [parameters.shimCollection] + * Collection of shims that might be relevant for this module + */ + constructor({id, version, modulePath, configPath = defaultConfigPath, configuration, shimCollection}) { + if (!id || !version || !modulePath) { + throw new Error(`Could not create Module: One or more required parameters are missing`); + } + + this._id = id; + this._version = version; + this._modulePath = modulePath; + this._configPath = configPath; + this._dependencies = {}; + + if (shimCollection) { + // Retrieve and clone shims in constructor + // Shims added to the collection at a later point in time should not be applied in this module + const shims = shimCollection.getConfigurationShims(this.getId()); + if (shims && shims.length) { + this._configShims = clone(shims); + } + } + + if (configuration) { + if (!configuration.kind) { + throw new Error( + `Could not create module with supplied configuration object: Missing 'kind' property.`); + } + if (!["project", "extension"].includes(configuration.kind)) { + throw new Error( + `Could not create module with supplied configuration object: ` + + `Unknown kind '${configuration.kind}'. Expected 'project' or 'extension'`); + } + this._suppliedConfig = configuration; + } + } + + /** + * @private + */ + getId() { + return this._id; + } + + /** + * @private + */ + getVersion() { + return this._version; + } + + /** + * @private + */ + getPath() { + return this._modulePath; + } + + getDependencies() { + return []; + } + + async getProject() { + const configs = await this.getConfigurations(); + // getConfigurations promises us to return none or exactly one project configuration + const projectConfig = configs.find((config) => { + return (config instanceof ProjectConfiguration); + }); + + if (projectConfig) { + return new Project({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration: projectConfig + }); + } + } + + async getExtensions() { + const configs = await this.getConfigurations(); + const extensionConfigs = configs.filter((config) => { + return (config instanceof ExtensionConfiguration); + }); + return extensionConfigs.map((config) => { + return new Extension({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration: config + }); + }); + } + + /** + * Configuration + */ + async getConfigurations() { + if (this._pGetConfigurations) { + return this._pGetConfigurations; + } + + return this._pGetConfigurations = this._getConfigurations(); + } + + async _getConfigurations() { + let configurations; + if (this._suppliedConfig) { + configurations = [this._createConfigurationInstance(this._suppliedConfig)]; + } else { + configurations = await this._loadProjectConfiguration(); + } + return configurations; + } + + _createConfigurationInstance(config) { + if (config.kind === "project") { + this._applyShims(config); + return new ProjectConfiguration(config); + } else if (config.kind === "extension") { + return new ExtensionConfiguration(config); + } + } + + _createProjectConfigurationFromShim() { + const config = {}; + this._applyShims(config); + if (config) { + return new ProjectConfiguration(config); + } + } + + _applyShims(config) { + if (!this._configShims) { + return; + } + this._configShims.forEach(({name, shim}) => { + log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); + Object.assign(config, shim); + }); + return config; + } + + async _loadProjectConfiguration() { + const configs = await this._readConfigFile(); + + if (!configs || !configs.length) { + // No project configuration found + // => Try to create one from shims + const shimConfiguration = this._createProjectConfigurationFromShim(); + if (shimConfiguration) { + return [shimConfiguration]; + } + return []; + } + + for (let i = configs.length - 1; i >= 0; i--) { + this._normalizeConfig(configs[i]); + } + + const projectConfigs = configs.filter((config) => { + return config.kind === "project"; + }); + + const extensionConfigs = configs.filter((config) => { + return config.kind === "extension"; + }); + + // While a project can contain multiple configurations, + // from a dependency tree perspective it is always a single project + // This means it can represent one "project", plus multiple extensions or + // one extension, plus multiple extensions + + if (projectConfigs.length > 1) { + throw new Error( + `Found ${projectConfigs.length} configurations of kind 'project' for ` + + `project ${this.getId()}. There is only one project per configuration allowed.`); + } else if (projectConfigs.length === 0 && extensionConfigs.length === 0) { + throw new Error( + `Found ${configs.length} configurations for ` + + `project ${this.getId()}. However, none of them are of kind 'project' or 'extension'.`); + } + + const configurations = []; + if (projectConfigs.length) { + configurations.push(this._createConfigurationInstance(projectConfigs[0])); + } else { + // No project configuration found + // => Try to create one from shims + const shimConfiguration = this._createProjectConfigurationFromShim(); + if (shimConfiguration) { + configurations.push(shimConfiguration); + } + } + + extensionConfigs.forEach((config) => { + configurations.push(this._createConfigurationInstance(config)); + }); + + return configurations; + } + + async _readConfigFile() { + const configPath = this._configPath; + let configFile; + if (path.isAbsolute(configPath)) { + try { + configFile = await readFile(configPath, {encoding: "utf8"}); + } catch (err) { + // TODO: Caller might want to ignore this exception for ENOENT errors if non-root projects + // However, this decision should not be made here + throw new Error("Failed to read configuration for project " + + `${this.getId()} at '${configPath}'. Error: ${err.message}`); + } + } else { + const reader = await this.getReader(); + let configResource; + try { + configResource = await reader.byPath(path.posix.join("/", configPath)); + } catch (err) { + throw new Error("Failed to read configuration for module " + + `${this.getId()} at "${configPath}". Error: ${err.message}`); + } + if (!configResource) { + if (configPath !== defaultConfigPath) { + throw new Error("Failed to read configuration for module " + + `${this.getId()}: Could not find configuration file in module at path '${configPath}'`); + } + return null; + } + configFile = await configResource.getBuffer(); + } + + let configs; + + try { + // Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename". + // safeLoadAll doesn't handle its parameters properly. + // See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381 + configs = jsyaml.loadAll(configFile, undefined, { + filename: configPath, + schema: jsyaml.DEFAULT_SAFE_SCHEMA + }); + } catch (err) { + if (err.name === "YAMLException") { + throw new Error("Failed to parse configuration for project " + + `${this.getId()} at '${configPath}'\nError: ${err.message}`); + } else { + throw err; + } + } + + if (!configs || !configs.length) { + return configs; + } + + const validationResults = await Promise.all( + configs.map(async (config, documentIndex) => { + // Catch validation errors to ensure proper order of rejections within Promise.all + try { + await validate({ + config, + project: { + id: this.getId() + }, + yaml: { + path: configPath, + source: configFile, + documentIndex + } + }); + } catch (error) { + return error; + } + }) + ); + + const validationErrors = validationResults.filter(($) => $); + + if (validationErrors.length > 0) { + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + + return configs; + } + + _normalizeConfig(config) { + if (!config.kind) { + config.kind = "project"; // default + } + } + + /** + * Resource Access + */ + async getReader() { + return resourceFactory.createReader({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Reader for module ${this.getId()}` + }); + } +} + +module.exports = Module; diff --git a/lib/configurations/AbstractConfiguration.js b/lib/configurations/AbstractConfiguration.js new file mode 100644 index 000000000..290c169bd --- /dev/null +++ b/lib/configurations/AbstractConfiguration.js @@ -0,0 +1,29 @@ +class AbstractConfiguration { + /** + * @param {object} config Configuration object + */ + constructor(config) { + if (new.target === AbstractConfiguration) { + throw new TypeError("Class 'AbstractConfiguration' is abstract"); + } + this._config = config; + } + + getName() { + if (!this._config.metadata) { + console.log("No metadata:"); + console.log(this._config); + } + return this._config.metadata.name; + } + + getType() { + return this._config.type; + } + + _gimmeConfAndRenameMe() { + return this._config; + } +} + +module.exports = AbstractConfiguration; diff --git a/lib/configurations/ExtensionConfiguration.js b/lib/configurations/ExtensionConfiguration.js new file mode 100644 index 000000000..a8711114f --- /dev/null +++ b/lib/configurations/ExtensionConfiguration.js @@ -0,0 +1,10 @@ +const AbstractConfiguration = require("./AbstractConfiguration"); +const {validate} = require("../validation/validator"); + +class ExtensionConfiguration extends AbstractConfiguration { + async validate() { + + } +} + +module.exports = ExtensionConfiguration; diff --git a/lib/configurations/ProjectConfiguration.js b/lib/configurations/ProjectConfiguration.js new file mode 100644 index 000000000..a6c1e01dc --- /dev/null +++ b/lib/configurations/ProjectConfiguration.js @@ -0,0 +1,11 @@ +const AbstractConfiguration = require("./AbstractConfiguration"); +const {validate} = require("../validation/validator"); + +class ProjectConfiguration extends AbstractConfiguration { + async validate() { + + } +} + +module.exports = ProjectConfiguration; + diff --git a/lib/configurations/ShimCollection.js b/lib/configurations/ShimCollection.js new file mode 100644 index 000000000..ac303fcfd --- /dev/null +++ b/lib/configurations/ShimCollection.js @@ -0,0 +1,52 @@ +const log = require("@ui5/logger").getLogger("extensions:ShimCollection"); + +function addToMap(name, fromMap, toMap) { + for (const [moduleId, shim] of Object.entries(fromMap)) { + if (!toMap[moduleId]) { + toMap[moduleId] = []; + } + toMap[moduleId].push({ + name, + shim + }); + } +} + +class ShimCollection { + constructor() { + this._configShims = {}; + this._dependencyShims = {}; + this._collectionShims = {}; + } + + addShim(shimExtension) { + const name = shimExtension.getName(); + log.verbose(`Adding new shim ${name}...`); + // TODO: Move this into a dedicated ShimConfiguration class? + const config = shimExtension.getConfiguration()._gimmeConfAndRenameMe(); + const {configurations, dependencies, collections} = config.shims; + if (configurations) { + addToMap(name, configurations, this._configShims); + } + if (dependencies) { + addToMap(name, dependencies, this._dependencyShims); + } + if (collections) { + addToMap(name, collections, this._collectionShims); + } + } + + getConfigurationShims(moduleId) { + return this._configShims[moduleId]; + } + + getDependencyShims(moduleId) { + return this._dependencyShims[moduleId]; + } + + getCollectionShims(moduleId) { + return this._collectionShims[moduleId]; + } +} + +module.exports = ShimCollection; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js new file mode 100644 index 000000000..13cc234e2 --- /dev/null +++ b/lib/graph/ProjectGraph.js @@ -0,0 +1,160 @@ +const log = require("@ui5/logger").getLogger("graph:ProjectGraph"); +/** +* A rooted, directed graph representing a UI5 project that should be worked with and all its dependencies +*/ +class ProjectGraph { + /** + * @param {object} parameters Parameters + * @param {string} parameters.rootProjectName Root project name + * @param {Array.} parameters.extensions + * Final list of extensions to be used in this project tree + */ + constructor({rootProjectName, extensions = []}) { + if (!rootProjectName) { + throw new Error(`Could not create ProjectGraph: One or more required parameters are missing`); + } + this._rootProjectName = rootProjectName; + this._extensions = Object.freeze(extensions); + + this._projects = {}; // maps project name to instance + this._adjList = {}; // maps project name to edges/dependencies + } + + getRoot() { + const rootProject = this._projects[this._rootProjectName]; + if (!rootProject) { + throw new Error(`Unable to find root project with name ${this._rootProjectName} in graph`); + } + return rootProject; + } + + addProject(project, ignoreDuplicates) { + const projectName = project.getName(); + if (this._projects[projectName]) { + if (ignoreDuplicates) { + return; + } + throw new Error(`Could not add duplicate project '${projectName}' to project tree`); + } + this._projects[projectName] = project; + this._adjList[projectName] = []; + } + + getProject(projectName) { + return this._projects[projectName]; + } + + declareDependency(fromProjectName, toProjectName/* , optional*/) { + if (!this._projects[fromProjectName]) { + throw new Error( + `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + + `Unable to find project with name ${fromProjectName} in graph`); + } + if (!this._projects[toProjectName]) { + throw new Error( + `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + + `Unable to find project with name ${toProjectName} in graph`); + } + if (this._adjList[fromProjectName][toProjectName]) { + log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); + } else { + log.verbose(`Declaring new dependency: ${fromProjectName} depends on ${toProjectName}`); + this._adjList[fromProjectName][toProjectName] = {}; // maybe add optional information? + } + } + + /** + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @param {Function} callback + */ + async traverseBreadthFirst(callback) { + // TODO: Add parameter to define point of entry, defaulting to root + + const queue = [{ + projectNames: [this._rootProjectName], + predecessors: [] + }]; + + const visited = {}; + + while (queue.length) { + const {projectNames, predecessors} = queue.shift(); // Get and remove first entry from queue + + await Promise.all(projectNames.map(async (projectName) => { + if (predecessors.includes(projectName)) { + // We start to run in circles. That's neither expected nor something we can deal with + + // Mark first and last occurrence in chain with an asterisk + predecessors[predecessors.indexOf(projectName)] = `${projectName}*`; + throw new Error( + `Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`); + } + if (visited[projectName]) { + return visited[projectName]; + } + + return visited[projectName] = (async () => { + const newPredecessors = [...predecessors, projectName]; + const dependencies = Object.keys(this._adjList[projectName]); + + queue.push({ + projectNames: dependencies, + predecessors: newPredecessors + }); + + await callback({ + project: this.getProject(projectName), + getDependencies: () => { + return dependencies.map(($) => this.getProject($.projectName)); + } + }); + })(); + })); + } + } + + /** + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @param {Function} callback + */ + async traverseDepthFirst(callback) { + // TODO: Add parameter to define point of entry, defaulting to root + return this._traverseDepthFirst(this._rootProjectName, {}, [], callback); + } + + async _traverseDepthFirst(projectName, visited, predecessors, callback) { + if (predecessors.includes(projectName)) { + // We start to run in circles. That's neither expected nor something we can deal with + + // Mark first and last occurrence in chain with an asterisk + predecessors[predecessors.indexOf(projectName)] = `${projectName}*`; + throw new Error( + `Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`); + } + if (visited[projectName]) { + return visited[projectName]; + } + return visited[projectName] = (async () => { + const newPredecessors = [...predecessors, projectName]; + const dependencies = Object.keys(this._adjList[projectName]); + await Promise.all(dependencies.map((depName) => { + return this._traverseDepthFirst(depName, visited, newPredecessors, callback); + })); + + await callback({ + project: this.getProject(projectName), + getDependencies: () => { + return dependencies.map(($) => this.getProject($.projectName)); + } + }); + })(); + } +} + +module.exports = ProjectGraph; diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js new file mode 100644 index 000000000..fc073c6df --- /dev/null +++ b/lib/graph/projectGraphFromTree.js @@ -0,0 +1,90 @@ +const Module = require("../Module"); +const ProjectGraph = require("./ProjectGraph"); +const ShimCollection = require("../configurations/ShimCollection"); + +module.exports = async function(tree) { + const shimCollection = new ShimCollection(); + + function addShimsToCollection(ext) { + ext.forEach((e) => { + if (e.getType() === "project-shim") { + shimCollection.addShim(e); + } + }); + } + + const rootModule = new Module({ + id: tree.id, + version: tree.version, + modulePath: tree.path + }); + const rootProject = await rootModule.getProject(); + const rootProjectName = rootProject.getName(); + + const extensions = await rootModule.getExtensions(); + addShimsToCollection(extensions); + + const projectGraph = new ProjectGraph({ + rootProjectName: rootProjectName + }); + projectGraph.addProject(rootProject); + + const queue = [{ + nodes: tree.dependencies, + parentProjectName: rootProjectName + }]; + + // Breadth-first search + while (queue.length) { + const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue + const res = await Promise.all(nodes.map(async (node) => { + const ui5Module = new Module({ + id: node.id, + version: node.version, + modulePath: node.path, + shimCollection + }); + + const project = await ui5Module.getProject(); + const extensions = await ui5Module.getExtensions(); + + return { + node, + project, + extensions + }; + })); + + // Keep this out of the async map function to ensure + // all projects and extensions are applied a deterministic order + for (let i = 0; i < res.length; i++) { + const {node, project, extensions} = res[i]; + + if (extensions.length) { + addShimsToCollection(extensions); + extensions.push(extensions); + } + + if (!project) { + return; + } + + if (!node.deduped) { + projectGraph.addProject(project, true); + } + + const projectName = project.getName(); + if (parentProjectName) { + projectGraph.declareDependency(parentProjectName, projectName); + } + + queue.push({ + // copy array, so that the queue is stable while ignored project dependencies are removed + nodes: [...node.dependencies], + parentProjectName: projectName, + }); + } + } + + return projectGraph; +}; diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js new file mode 100644 index 000000000..417e31da8 --- /dev/null +++ b/lib/specifications/AbstractSpecification.js @@ -0,0 +1,14 @@ +class AbstractSpecification { + /** + * @param {object} config Configuration object + */ + constructor(config) { + if (new.target === AbstractSpecification) { + throw new TypeError("Class 'AbstractSpecification' is abstract"); + } + this._config = config; + } + +} + +module.exports = AbstractSpecification; diff --git a/lib/specifications/Extension.js b/lib/specifications/Extension.js new file mode 100644 index 000000000..221b091c7 --- /dev/null +++ b/lib/specifications/Extension.js @@ -0,0 +1,82 @@ +const path = require("path"); +const jsyaml = require("js-yaml"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const ExtensionConfiguration = require("../configurations/ExtensionConfiguration"); + +class Extension { + /** + * @param {object} parameters Extension parameters + * @param {string} parameters.id Unique ID for the extension + * @param {string} parameters.version Version of the extension + * @param {string} parameters.modulePath File System path to access the extensions resources + * @param {module:@ui5/project.configurations.ExtensionConfiguration} parameters.configuration + * Configuration instance for the extension + */ + constructor({id, version, modulePath, configuration}) { + if (!id || !version || !modulePath || !configuration) { + throw new Error(`Could not create Extension: One or more required parameters are missing`); + } + + if (!(configuration instanceof ExtensionConfiguration)) { + throw new Error(`Could not create extension: 'configuration' must be an instance of ` + + `@ui5/project.configurations.ExtensionConfiguration`); + } + + this._version = version; + this._modulePath = modulePath; + this._configuration = configuration; + + // Extensions should be identified by their configured name + // The ID property as supplied by the translators is only here for debugging and potential tracing purposes + this.__id = id; + } + + static getKind() { + return "extension"; + } + + /** + * Extension Metadata + */ + getName() { + const config = this.getConfiguration(); + return config.getName(); + } + + getNamespace() { + const config = this.getConfiguration(); + return config.getNamespace(); + } + + getSpecVersion() { + + } + + getType() { + const config = this.getConfiguration(); + return config.getType(); + } + + /** + * @private + */ + getVersion() { + return this._version; + } + + /** + * Configuration + */ + getConfiguration() { + return this._configuration; + } + + /** + * @private + */ + getPath() { + return this._modulePath; + } +} + +module.exports = Extension; diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js new file mode 100644 index 000000000..0fe8b12f8 --- /dev/null +++ b/lib/specifications/Project.js @@ -0,0 +1,86 @@ +const path = require("path"); +const jsyaml = require("js-yaml"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const ProjectConfiguration = require("../configurations/ProjectConfiguration"); + +class Project { + /** + * @param {object} parameters Project parameters + * @param {string} parameters.id Unique ID for the project + * @param {string} parameters.version Version of the project + * @param {string} parameters.modulePath File System path to access the projects resources + * @param {module:@ui5/project.configurations.ProjectConfiguration} parameters.configuration + * Configuration instance for the project + */ + constructor({id, version, modulePath, configuration}) { + if (!id || !version || !modulePath || !configuration) { + throw new Error(`Could not create Project: One or more required parameters are missing`); + } + + if (!(configuration instanceof ProjectConfiguration)) { + throw new Error(`Could not create project: 'configuration' must be an instance of ` + + `@ui5/project.configurations.ProjectConfiguration`); + } + + this._version = version; + this._modulePath = modulePath; + this._configuration = configuration; + + // Projects should be identified by their configured name + // The ID property as supplied by the translators is only here for debugging and potential tracing purposes + this.__id = id; + } + + static getKind() { + return "project"; + } + + /** + * @private + */ + getName() { + return this.getConfiguration().getName(); + } + + /** + * @private + */ + getVersion() { + return this._version; + } + + /** + * @private + */ + getPath() { + return this._modulePath; + } + + /** + * Configuration + */ + getConfiguration() { + return this._configuration; + } + + /** + * Resource Access + */ + + async getRootReader() { + return resourceFactory.createReader({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Root reader for project ${this.getName()}` + }); + } + + /** + * General functions + */ + async validate() { + + } +} + +module.exports = Project; diff --git a/package.json b/package.json index 5987c9644..40c15d31c 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ }, "dependencies": { "@ui5/builder": "^3.0.0-alpha.6", + "@ui5/fs": "3.0.0-alpha.2", "@ui5/logger": "^3.0.1-alpha.1", "@ui5/server": "^3.0.0-alpha.1", "ajv": "^6.12.6", diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.b/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.b/ui5.yaml index 557b0aa63..33e181aa9 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.b/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.b/ui5.yaml @@ -1,5 +1,23 @@ --- -specVersion: "0.1" +specVersion: "2.2" type: application metadata: name: application.cycle.b +--- +specVersion: "2.2" +kind: extension +type: project-shim +metadata: + name: application.cycle.b-shim +shims: + configurations: + module.d: + specVersion: "2.2" + type: module + metadata: + name: module.d + module.e: + specVersion: "2.2" + type: module + metadata: + name: module.e \ No newline at end of file diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.b/webapp/manifest.json b/test/fixtures/cyclic-deps/node_modules/application.cycle.b/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.b/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/lib/Module.js b/test/lib/Module.js new file mode 100644 index 000000000..148a15627 --- /dev/null +++ b/test/lib/Module.js @@ -0,0 +1,39 @@ +const test = require("ava"); +const sinon = require("sinon"); +const path = require("path"); +const Module = require("../../lib/Module"); + +const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); + +const basicModuleInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath +}; + +// test.beforeEach((t) => { +// }); + +test.afterEach.always(() => { + sinon.restore(); +}); + +test("Instantiate a basic module", async (t) => { + const ui5Module = new Module(basicModuleInput); + t.is(ui5Module.getId(), "application.a.id", "Returned correct ID"); + t.is(ui5Module.getVersion(), "1.0.0", "Returned correct version"); + t.is(ui5Module.getPath(), applicationAPath, "Returned correct module path"); +}); + +test("Access module root resources via reader", async (t) => { + const ui5Module = new Module(basicModuleInput); + const rootReader = await ui5Module.getReader(); + const packageJsonResource = await rootReader.byPath("/package.json"); + t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); +}); + +test("Get projects from module", async (t) => { + const ui5Module = new Module(basicModuleInput); + const project = await ui5Module.getProject(); + t.is(project.getName(), "application.a", "Returned correct project"); +}); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js new file mode 100644 index 000000000..d8ff22b58 --- /dev/null +++ b/test/lib/graph/projectGraphFromTree.js @@ -0,0 +1,270 @@ +const test = require("ava"); +const sinonGlobal = require("sinon"); +const path = require("path"); +const projectGraphFromTree = require("../../../lib/graph/projectGraphFromTree.js"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Application A", async (t) => { + const projectGraph = await projectGraphFromTree(applicationATree); + const rootProject = projectGraph.getRoot(); + t.is(rootProject.getName(), "application.a", "Returned correct root project"); +}); + +test("Application A: Traverse project graph breadth first", async (t) => { + const projectGraph = await projectGraphFromTree(applicationATree); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 5, "Five projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + // Since libraries a, b and c are processed in parallel, their callback order can vary + // Therefore we always sort the last three calls + const lastThreeCalls = callbackCalls.splice(2, 3).sort(); + callbackCalls.push(...lastThreeCalls); + t.deepEqual(callbackCalls, [ + "application.a", + "library.d", + "library.a", + "library.b", + "library.c" + ], "Traversed graph in correct order"); +}); + +test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => { + const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); + const callbackStub = t.context.sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub)); + + t.is(callbackStub.callCount, 4, "Four projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: application.cycle.a* -> component.cycle.a " + + "-> application.cycle.a*", + "Threw with expected error message"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.cycle.a", + "component.cycle.a", + "library.cycle.a", + "library.cycle.b", + ], "Traversed graph in correct order"); +}); + +test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => { + const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + // TODO: Confirm this behavior with FW. BFS works fine since all modules have already been visited + // before a cycle is entered. DFS fails because it dives into the cycle first. + + t.is(callbackStub.callCount, 3, "Four projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.cycle.b", + "module.d", + "module.e" + ], "Traversed graph in correct order"); +}); + +test("Application A: Traverse project graph depth first", async (t) => { + const projectGraph = await projectGraphFromTree(applicationATree); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 5, "Five projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + // Since libraries a, b and c are processed in parallel, their callback order can vary + // Therefore we always sort the first three calls + const firstThreeCalls = callbackCalls.splice(0, 3).sort(); + callbackCalls.unshift(...firstThreeCalls); + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + "library.c", + "library.d", + "application.a", + + ], "Traversed graph in correct order"); +}); + + +test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => { + const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); + const callbackStub = t.context.sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); + + t.is(callbackStub.callCount, 0, "Zero projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: application.cycle.a* -> component.cycle.a " + + "-> application.cycle.a*", + "Threw with expected error message"); +}); + +test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => { + const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); + const callbackStub = t.context.sinon.stub().resolves(); + const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); + + t.is(callbackStub.callCount, 0, "Zero projects have been visited"); + + t.is(error.message, + "Detected cyclic dependency chain: application.cycle.b -> module.d* " + + "-> module.e -> module.d*", + "Threw with expected error message"); +}); + + +/* ========================= */ +/* ======= Test data ======= */ + +const applicationATree = { + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [ + { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [] + }, + { + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, + { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + } + ] + } + ] +}; + + +const applicationCycleATreeIncDeduped = { + id: "application.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "application.cycle.a"), + dependencies: [ + { + id: "component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "component.cycle.a"), + dependencies: [ + { + id: "library.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "library.cycle.a"), + dependencies: [ + { + id: "component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "component.cycle.a"), + dependencies: [], + deduped: true + } + ] + }, + { + id: "library.cycle.b", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "library.cycle.b"), + dependencies: [ + { + id: "component.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "component.cycle.a"), + dependencies: [], + deduped: true + } + ] + }, + { + id: "application.cycle.a", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "application.cycle.a"), + dependencies: [], + deduped: true + } + ] + } + ] +}; + +const applicationCycleBTreeIncDeduped = { + id: "application.cycle.b", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "application.cycle.b"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [ + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [], + deduped: true + } + ] + } + ] + }, + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [ + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [], + deduped: true + } + ] + } + ] + } + ] +}; diff --git a/test/lib/projectPreprocessor.js b/test/lib/projectPreprocessor.js index 4b821ad2b..0843a5974 100644 --- a/test/lib/projectPreprocessor.js +++ b/test/lib/projectPreprocessor.js @@ -570,6 +570,13 @@ test("Project tree Cycle A with inline configs", (t) => { }); }); +test("Project tree Cycle B with inline configs", (t) => { + return projectPreprocessor.processTree(treeApplicationCycleB).then((parsedTree) => { + // console.log(JSON.stringify(parsedTree, null, 2)); + t.deepEqual(parsedTree, expectedTreeApplicationCycleB, "Parsed correctly"); + }); +}); + test("Project with nested invalid dependencies", (t) => { return projectPreprocessor.processTree(treeWithInvalidModules).then((parsedTree) => { t.deepEqual(parsedTree, expectedTreeWithInvalidModules); @@ -1568,6 +1575,196 @@ const expectedTreeApplicationCycleA = { } }; + +const treeApplicationCycleB = { + id: "application.cycle.b", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "application.cycle.b"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [ + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [], + deduped: true + } + ] + } + ] + }, + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [ + { + id: "module.e", + version: "1.0.0", + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [], + deduped: true + } + ] + } + ] + } + ] +}; + +const _treeApplicationCycleB = { + id: "application.cycle.b", + version: "1.0.0", + specVersion: "0.1", + type: "application", + metadata: { + name: "application.cycle.b", + }, + path: path.join(cycleDepsBasePath, "application.cycle.b"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + specVersion: "0.1", + type: "module", + metadata: { + name: "module.d", + }, + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [ + { + id: "module.e", + version: "1.0.0", + specVersion: "0.1", + type: "module", + metadata: { + name: "module.e", + }, + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [] + } + ] + }, + { + id: "module.e", + version: "1.0.0", + specVersion: "0.1", + type: "module", + metadata: { + name: "module.e", + }, + path: path.join(cycleDepsBasePath, "module.e"), + dependencies: [ + { + id: "module.d", + version: "1.0.0", + specVersion: "0.1", + type: "module", + metadata: { + name: "module.d", + }, + path: path.join(cycleDepsBasePath, "module.d"), + dependencies: [] + } + ] + } + ] +}; + +const expectedTreeModuleCycleD = { + id: "module.d", + version: "1.0.0", + specVersion: "2.2", + kind: "project", + type: "module", + metadata: { + name: "module.d", + }, + path: path.join(cycleDepsBasePath, "module.d"), + resources: { + configuration: { + paths: { + "/": "" + } + }, + pathMappings: { + "/": "" + } + }, + _level: 1, + dependencies: [] +}; + +const expectedTreeModuleCycleE = { + id: "module.e", + version: "1.0.0", + specVersion: "2.2", + kind: "project", + type: "module", + metadata: { + name: "module.e", + }, + path: path.join(cycleDepsBasePath, "module.e"), + resources: { + configuration: { + paths: { + "/": "" + } + }, + pathMappings: { + "/": "" + } + }, + _level: 1, + dependencies: [expectedTreeModuleCycleD] +}; + +expectedTreeModuleCycleD.dependencies.push(expectedTreeModuleCycleE); + +const expectedTreeApplicationCycleB = { + id: "application.cycle.b", + version: "1.0.0", + specVersion: "2.2", + path: path.join(cycleDepsBasePath, "application.cycle.b"), + type: "application", + metadata: { + name: "application.cycle.b", + namespace: "id1" + }, + dependencies: [ + expectedTreeModuleCycleD, + expectedTreeModuleCycleE + ], + _level: 0, + _isRoot: true, + kind: "project", + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + webapp: "webapp" + } + }, + pathMappings: { + "/": "webapp" + } + } +}; + /* ======= /Test data ======= */ /* ========================= */ diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js new file mode 100644 index 000000000..237283bf1 --- /dev/null +++ b/test/lib/specifications/Project.js @@ -0,0 +1,43 @@ +const test = require("ava"); +const sinon = require("sinon"); +const path = require("path"); +const Project = require("../../../lib/specifications/Project"); +const ProjectConfiguration = require("../../../lib/configurations/ProjectConfiguration"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); + +const emptyConfiguration = new ProjectConfiguration({ + metadata: {name: "application.a"} +}); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: emptyConfiguration +}; + +// test.beforeEach((t) => { +// }); + +test.afterEach.always(() => { + sinon.restore(); +}); + +test("Instantiate a basic project", async (t) => { + const project = new Project(basicProjectInput); + t.is(project.getName(), "application.a", "Returned correct name"); + t.is(project.getVersion(), "1.0.0", "Returned correct version"); + t.is(project.getPath(), applicationAPath, "Returned correct project path"); +}); + +test("getConfiguration", async (t) => { + const project = new Project(basicProjectInput); + t.is(await project.getConfiguration(), emptyConfiguration, "Returned correct configuration instance"); +}); + +test("Access project root resources via reader", async (t) => { + const project = new Project(basicProjectInput); + const rootReader = await project.getRootReader(); + const packageJsonResource = await rootReader.byPath("/package.json"); + t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); +}); From 74d0a7dc0b6eef520dce8b65c44cf010a1e4e255 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 13:37:40 +0100 Subject: [PATCH 02/99] Refactor Configuration into a single Class and create AbstractSpecification --- lib/Module.js | 14 ++- lib/configurations/AbstractConfiguration.js | 29 ------ lib/configurations/ExtensionConfiguration.js | 10 --- lib/configurations/ProjectConfiguration.js | 11 --- .../ShimCollection.js | 4 +- lib/graph/projectGraphFromTree.js | 4 +- lib/specifications/AbstractSpecification.js | 89 ++++++++++++++++++- lib/specifications/Configuration.js | 30 +++++++ lib/specifications/Extension.js | 77 +++------------- lib/specifications/Project.js | 81 +++-------------- test/lib/specifications/Project.js | 4 +- 11 files changed, 149 insertions(+), 204 deletions(-) delete mode 100644 lib/configurations/AbstractConfiguration.js delete mode 100644 lib/configurations/ExtensionConfiguration.js delete mode 100644 lib/configurations/ProjectConfiguration.js rename lib/{configurations => graph}/ShimCollection.js (87%) create mode 100644 lib/specifications/Configuration.js diff --git a/lib/Module.js b/lib/Module.js index d51f3603e..807f7daaf 100644 --- a/lib/Module.js +++ b/lib/Module.js @@ -6,8 +6,7 @@ const jsyaml = require("js-yaml"); const resourceFactory = require("@ui5/fs").resourceFactory; const Project = require("./specifications/Project"); const Extension = require("./specifications/Extension"); -const ProjectConfiguration = require("./configurations/ProjectConfiguration"); -const ExtensionConfiguration = require("./configurations/ExtensionConfiguration"); +const Configuration = require("./specifications/Configuration"); const {validate} = require("./validation/validator"); const log = require("@ui5/logger").getLogger("Module"); @@ -95,7 +94,7 @@ class Module { const configs = await this.getConfigurations(); // getConfigurations promises us to return none or exactly one project configuration const projectConfig = configs.find((config) => { - return (config instanceof ProjectConfiguration); + return config.getKind() === "project"; }); if (projectConfig) { @@ -111,7 +110,7 @@ class Module { async getExtensions() { const configs = await this.getConfigurations(); const extensionConfigs = configs.filter((config) => { - return (config instanceof ExtensionConfiguration); + return config.getKind() === "extension"; }); return extensionConfigs.map((config) => { return new Extension({ @@ -147,17 +146,15 @@ class Module { _createConfigurationInstance(config) { if (config.kind === "project") { this._applyShims(config); - return new ProjectConfiguration(config); - } else if (config.kind === "extension") { - return new ExtensionConfiguration(config); } + return new Configuration(config); } _createProjectConfigurationFromShim() { const config = {}; this._applyShims(config); if (config) { - return new ProjectConfiguration(config); + return new Configuration(config); } } @@ -169,6 +166,7 @@ class Module { log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); Object.assign(config, shim); }); + this._normalizeConfig(config); return config; } diff --git a/lib/configurations/AbstractConfiguration.js b/lib/configurations/AbstractConfiguration.js deleted file mode 100644 index 290c169bd..000000000 --- a/lib/configurations/AbstractConfiguration.js +++ /dev/null @@ -1,29 +0,0 @@ -class AbstractConfiguration { - /** - * @param {object} config Configuration object - */ - constructor(config) { - if (new.target === AbstractConfiguration) { - throw new TypeError("Class 'AbstractConfiguration' is abstract"); - } - this._config = config; - } - - getName() { - if (!this._config.metadata) { - console.log("No metadata:"); - console.log(this._config); - } - return this._config.metadata.name; - } - - getType() { - return this._config.type; - } - - _gimmeConfAndRenameMe() { - return this._config; - } -} - -module.exports = AbstractConfiguration; diff --git a/lib/configurations/ExtensionConfiguration.js b/lib/configurations/ExtensionConfiguration.js deleted file mode 100644 index a8711114f..000000000 --- a/lib/configurations/ExtensionConfiguration.js +++ /dev/null @@ -1,10 +0,0 @@ -const AbstractConfiguration = require("./AbstractConfiguration"); -const {validate} = require("../validation/validator"); - -class ExtensionConfiguration extends AbstractConfiguration { - async validate() { - - } -} - -module.exports = ExtensionConfiguration; diff --git a/lib/configurations/ProjectConfiguration.js b/lib/configurations/ProjectConfiguration.js deleted file mode 100644 index a6c1e01dc..000000000 --- a/lib/configurations/ProjectConfiguration.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractConfiguration = require("./AbstractConfiguration"); -const {validate} = require("../validation/validator"); - -class ProjectConfiguration extends AbstractConfiguration { - async validate() { - - } -} - -module.exports = ProjectConfiguration; - diff --git a/lib/configurations/ShimCollection.js b/lib/graph/ShimCollection.js similarity index 87% rename from lib/configurations/ShimCollection.js rename to lib/graph/ShimCollection.js index ac303fcfd..ea35b4c7c 100644 --- a/lib/configurations/ShimCollection.js +++ b/lib/graph/ShimCollection.js @@ -1,4 +1,4 @@ -const log = require("@ui5/logger").getLogger("extensions:ShimCollection"); +const log = require("@ui5/logger").getLogger("graph:ShimCollection"); function addToMap(name, fromMap, toMap) { for (const [moduleId, shim] of Object.entries(fromMap)) { @@ -23,7 +23,7 @@ class ShimCollection { const name = shimExtension.getName(); log.verbose(`Adding new shim ${name}...`); // TODO: Move this into a dedicated ShimConfiguration class? - const config = shimExtension.getConfiguration()._gimmeConfAndRenameMe(); + const config = shimExtension.getConfigurationObject(); const {configurations, dependencies, collections} = config.shims; if (configurations) { addToMap(name, configurations, this._configShims); diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index fc073c6df..3e8099a46 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -1,6 +1,6 @@ const Module = require("../Module"); const ProjectGraph = require("./ProjectGraph"); -const ShimCollection = require("../configurations/ShimCollection"); +const ShimCollection = require("./ShimCollection"); module.exports = async function(tree) { const shimCollection = new ShimCollection(); @@ -66,7 +66,7 @@ module.exports = async function(tree) { } if (!project) { - return; + continue; } if (!node.deduped) { diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js index 417e31da8..f7eeb3727 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/AbstractSpecification.js @@ -1,14 +1,97 @@ +const Configuration = require("./Configuration"); +const resourceFactory = require("@ui5/fs").resourceFactory; + class AbstractSpecification { /** - * @param {object} config Configuration object + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath File System path to access resources + * @param {module:@ui5/project.specifications.Configuration} parameters.configuration + * Configuration instance to use */ - constructor(config) { + constructor({id, version, modulePath, configuration}) { if (new.target === AbstractSpecification) { throw new TypeError("Class 'AbstractSpecification' is abstract"); } - this._config = config; + if (!id || !version || !modulePath || !configuration) { + throw new Error(`Could not create Project: One or more required parameters are missing`); + } + + if (!(configuration instanceof Configuration)) { + throw new Error(`Could not create project: 'configuration' must be an instance of ` + + `@ui5/project.specifications.Configuration`); + } + + this._version = version; + this._modulePath = modulePath; + this._configuration = configuration; + + // The configured name (metadata.name) should be the unique identifier + // The ID property as supplied by the translators is only here for debugging and potential tracing purposes + this.__id = id; + } + + /** + * @private + */ + getVersion() { + return this._version; + } + + /** + * @private + */ + getPath() { + return this._modulePath; } + /** + * Configuration + */ + getName() { + return this._getConfiguration().getName(); + } + + getKind() { + return this._getConfiguration().getKind(); + } + + getType() { + return this._getConfiguration().getType(); + } + + /** + * @private + */ + getConfigurationObject() { + return this._getConfiguration().getObject(); + } + + /** + * @private + */ + _getConfiguration() { + return this._configuration; + } + + /** + * Resource Access + */ + async getRootReader() { + return resourceFactory.createReader({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Root reader for ${this.getType()} ${this.getKind()} ${this.getName()}` + }); + } + + /** + * General functions + */ + async validate() { + + } } module.exports = AbstractSpecification; diff --git a/lib/specifications/Configuration.js b/lib/specifications/Configuration.js new file mode 100644 index 000000000..eb1cdcc15 --- /dev/null +++ b/lib/specifications/Configuration.js @@ -0,0 +1,30 @@ +/* +* Private configuration class for use in Module and specifications +*/ + +class Configuration { + /** + * @param {object} config Configuration object + */ + constructor(config) { + this._config = config; + } + + getName() { + return this._config.metadata.name; + } + + getKind() { + return this._config.kind; + } + + getType() { + return this._config.type; + } + + getObject() { + return this._config; + } +} + +module.exports = Configuration; diff --git a/lib/specifications/Extension.js b/lib/specifications/Extension.js index 221b091c7..4e87621b0 100644 --- a/lib/specifications/Extension.js +++ b/lib/specifications/Extension.js @@ -1,82 +1,25 @@ -const path = require("path"); -const jsyaml = require("js-yaml"); -const resourceFactory = require("@ui5/fs").resourceFactory; -const ExtensionConfiguration = require("../configurations/ExtensionConfiguration"); +const AbstractSpecification = require("./AbstractSpecification"); -class Extension { +class Extension extends AbstractSpecification { /** * @param {object} parameters Extension parameters * @param {string} parameters.id Unique ID for the extension * @param {string} parameters.version Version of the extension * @param {string} parameters.modulePath File System path to access the extensions resources - * @param {module:@ui5/project.configurations.ExtensionConfiguration} parameters.configuration + * @param {module:@ui5/project.specifications.Configuration} parameters.configuration * Configuration instance for the extension */ - constructor({id, version, modulePath, configuration}) { - if (!id || !version || !modulePath || !configuration) { - throw new Error(`Could not create Extension: One or more required parameters are missing`); + constructor(parameters) { + super(parameters); + if (this.getKind() !== "extension") { + throw new Error(`Could not create extension: Supplied configuration must be of kind extension but is ` + + this.getKind()); } - - if (!(configuration instanceof ExtensionConfiguration)) { - throw new Error(`Could not create extension: 'configuration' must be an instance of ` + - `@ui5/project.configurations.ExtensionConfiguration`); - } - - this._version = version; - this._modulePath = modulePath; - this._configuration = configuration; - - // Extensions should be identified by their configured name - // The ID property as supplied by the translators is only here for debugging and potential tracing purposes - this.__id = id; } - static getKind() { - return "extension"; - } - - /** - * Extension Metadata + /* + * TODO: Expose extension specific APIs */ - getName() { - const config = this.getConfiguration(); - return config.getName(); - } - - getNamespace() { - const config = this.getConfiguration(); - return config.getNamespace(); - } - - getSpecVersion() { - - } - - getType() { - const config = this.getConfiguration(); - return config.getType(); - } - - /** - * @private - */ - getVersion() { - return this._version; - } - - /** - * Configuration - */ - getConfiguration() { - return this._configuration; - } - - /** - * @private - */ - getPath() { - return this._modulePath; - } } module.exports = Extension; diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 0fe8b12f8..06c991e80 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -1,86 +1,25 @@ -const path = require("path"); -const jsyaml = require("js-yaml"); -const resourceFactory = require("@ui5/fs").resourceFactory; -const ProjectConfiguration = require("../configurations/ProjectConfiguration"); +const AbstractSpecification = require("./AbstractSpecification"); -class Project { +class Project extends AbstractSpecification { /** * @param {object} parameters Project parameters * @param {string} parameters.id Unique ID for the project * @param {string} parameters.version Version of the project * @param {string} parameters.modulePath File System path to access the projects resources - * @param {module:@ui5/project.configurations.ProjectConfiguration} parameters.configuration + * @param {module:@ui5/project.specifications.Configuration} parameters.configuration * Configuration instance for the project */ - constructor({id, version, modulePath, configuration}) { - if (!id || !version || !modulePath || !configuration) { - throw new Error(`Could not create Project: One or more required parameters are missing`); + constructor(parameters) { + super(parameters); + if (this.getKind() !== "project") { + throw new Error(`Could not create project: Supplied configuration must be of kind project but is ` + + this.getKind()); } - - if (!(configuration instanceof ProjectConfiguration)) { - throw new Error(`Could not create project: 'configuration' must be an instance of ` + - `@ui5/project.configurations.ProjectConfiguration`); - } - - this._version = version; - this._modulePath = modulePath; - this._configuration = configuration; - - // Projects should be identified by their configured name - // The ID property as supplied by the translators is only here for debugging and potential tracing purposes - this.__id = id; - } - - static getKind() { - return "project"; - } - - /** - * @private - */ - getName() { - return this.getConfiguration().getName(); - } - - /** - * @private - */ - getVersion() { - return this._version; - } - - /** - * @private - */ - getPath() { - return this._modulePath; - } - - /** - * Configuration - */ - getConfiguration() { - return this._configuration; } - /** - * Resource Access + /* + * TODO: Expose project specific APIs */ - - async getRootReader() { - return resourceFactory.createReader({ - fsBasePath: this.getPath(), - virBasePath: "/", - name: `Root reader for project ${this.getName()}` - }); - } - - /** - * General functions - */ - async validate() { - - } } module.exports = Project; diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 237283bf1..23721e927 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -2,11 +2,13 @@ const test = require("ava"); const sinon = require("sinon"); const path = require("path"); const Project = require("../../../lib/specifications/Project"); -const ProjectConfiguration = require("../../../lib/configurations/ProjectConfiguration"); +const ProjectConfiguration = require("../../../lib/specifications/Configuration"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const emptyConfiguration = new ProjectConfiguration({ + specVersion: "2.3", + kind: "project", metadata: {name: "application.a"} }); const basicProjectInput = { From 0a697dc03a0b81aeb69a1274799ada16bda45e17 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 13:41:03 +0100 Subject: [PATCH 03/99] Minor cleanup and fix tests --- test/lib/projectPreprocessor.js | 60 ------------------------------ test/lib/specifications/Project.js | 8 ++-- 2 files changed, 4 insertions(+), 64 deletions(-) diff --git a/test/lib/projectPreprocessor.js b/test/lib/projectPreprocessor.js index 0843a5974..5cea8c789 100644 --- a/test/lib/projectPreprocessor.js +++ b/test/lib/projectPreprocessor.js @@ -572,7 +572,6 @@ test("Project tree Cycle A with inline configs", (t) => { test("Project tree Cycle B with inline configs", (t) => { return projectPreprocessor.processTree(treeApplicationCycleB).then((parsedTree) => { - // console.log(JSON.stringify(parsedTree, null, 2)); t.deepEqual(parsedTree, expectedTreeApplicationCycleB, "Parsed correctly"); }); }); @@ -1626,65 +1625,6 @@ const treeApplicationCycleB = { ] }; -const _treeApplicationCycleB = { - id: "application.cycle.b", - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "application.cycle.b", - }, - path: path.join(cycleDepsBasePath, "application.cycle.b"), - dependencies: [ - { - id: "module.d", - version: "1.0.0", - specVersion: "0.1", - type: "module", - metadata: { - name: "module.d", - }, - path: path.join(cycleDepsBasePath, "module.d"), - dependencies: [ - { - id: "module.e", - version: "1.0.0", - specVersion: "0.1", - type: "module", - metadata: { - name: "module.e", - }, - path: path.join(cycleDepsBasePath, "module.e"), - dependencies: [] - } - ] - }, - { - id: "module.e", - version: "1.0.0", - specVersion: "0.1", - type: "module", - metadata: { - name: "module.e", - }, - path: path.join(cycleDepsBasePath, "module.e"), - dependencies: [ - { - id: "module.d", - version: "1.0.0", - specVersion: "0.1", - type: "module", - metadata: { - name: "module.d", - }, - path: path.join(cycleDepsBasePath, "module.d"), - dependencies: [] - } - ] - } - ] -}; - const expectedTreeModuleCycleD = { id: "module.d", version: "1.0.0", diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 23721e927..8f46812e9 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -2,11 +2,11 @@ const test = require("ava"); const sinon = require("sinon"); const path = require("path"); const Project = require("../../../lib/specifications/Project"); -const ProjectConfiguration = require("../../../lib/specifications/Configuration"); +const Configuration = require("../../../lib/specifications/Configuration"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const emptyConfiguration = new ProjectConfiguration({ +const emptyConfiguration = new Configuration({ specVersion: "2.3", kind: "project", metadata: {name: "application.a"} @@ -32,9 +32,9 @@ test("Instantiate a basic project", async (t) => { t.is(project.getPath(), applicationAPath, "Returned correct project path"); }); -test("getConfiguration", async (t) => { +test("_getConfiguration", async (t) => { const project = new Project(basicProjectInput); - t.is(await project.getConfiguration(), emptyConfiguration, "Returned correct configuration instance"); + t.is(await project._getConfiguration(), emptyConfiguration, "Returned correct configuration instance"); }); test("Access project root resources via reader", async (t) => { From d35e4982d5f97446fe4a015a0bfb21b0ad6e69df Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 14:13:47 +0100 Subject: [PATCH 04/99] projectGraphFromTree: Allow for node-level configuration parameters --- lib/Module.js | 6 ++-- lib/graph/ProjectGraph.js | 6 ++++ lib/graph/projectGraphFromTree.js | 22 ++++++++++++- test/lib/graph/projectGraphFromTree.js | 43 +++++++++++++++++++++++--- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/lib/Module.js b/lib/Module.js index 807f7daaf..7415c8bd9 100644 --- a/lib/Module.js +++ b/lib/Module.js @@ -52,10 +52,7 @@ class Module { } if (configuration) { - if (!configuration.kind) { - throw new Error( - `Could not create module with supplied configuration object: Missing 'kind' property.`); - } + this._normalizeConfig(configuration); if (!["project", "extension"].includes(configuration.kind)) { throw new Error( `Could not create module with supplied configuration object: ` + @@ -318,6 +315,7 @@ class Module { if (!config.kind) { config.kind = "project"; // default } + return config; } /** diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 13cc234e2..164444ed8 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -44,6 +44,12 @@ class ProjectGraph { return this._projects[projectName]; } + /** + * Declare a dependency from one project in the graph to another + * + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ declareDependency(fromProjectName, toProjectName/* , optional*/) { if (!this._projects[fromProjectName]) { throw new Error( diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 3e8099a46..5a16b1ea4 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -2,6 +2,22 @@ const Module = require("../Module"); const ProjectGraph = require("./ProjectGraph"); const ShimCollection = require("./ShimCollection"); +/** + * Tree node + * + * @public + * @typedef {object} TreeNode + * @param {string} node.id Unique ID for the project + * @param {string} node.version Version of the project + * @param {string} node.path File System path to access the projects resources + * @param {string} [node.configuration] Configuration object to use instead of reading from a configuration file + * @param {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {TreeNode[]} dependencies + */ + +/** + * @param {TreeNode} tree Dependency tree as returned by a translator + */ module.exports = async function(tree) { const shimCollection = new ShimCollection(); @@ -16,7 +32,9 @@ module.exports = async function(tree) { const rootModule = new Module({ id: tree.id, version: tree.version, - modulePath: tree.path + modulePath: tree.path, + configPath: tree.configPath, + configuration: tree.configuration }); const rootProject = await rootModule.getProject(); const rootProjectName = rootProject.getName(); @@ -42,6 +60,8 @@ module.exports = async function(tree) { id: node.id, version: node.version, modulePath: node.path, + configPath: node.configPath, + configuration: node.configuration, shimCollection }); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index d8ff22b58..63cc4f695 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -31,8 +31,8 @@ test("Application A: Traverse project graph breadth first", async (t) => { // Since libraries a, b and c are processed in parallel, their callback order can vary // Therefore we always sort the last three calls - const lastThreeCalls = callbackCalls.splice(2, 3).sort(); - callbackCalls.push(...lastThreeCalls); + // const lastThreeCalls = callbackCalls.splice(2, 3).sort(); + // callbackCalls.push(...lastThreeCalls); t.deepEqual(callbackCalls, [ "application.a", "library.d", @@ -92,8 +92,8 @@ test("Application A: Traverse project graph depth first", async (t) => { // Since libraries a, b and c are processed in parallel, their callback order can vary // Therefore we always sort the first three calls - const firstThreeCalls = callbackCalls.splice(0, 3).sort(); - callbackCalls.unshift(...firstThreeCalls); + // const firstThreeCalls = callbackCalls.splice(0, 3).sort(); + // callbackCalls.unshift(...firstThreeCalls); t.deepEqual(callbackCalls, [ "library.a", "library.b", @@ -131,6 +131,41 @@ test("Application Cycle B: Traverse project graph depth first with cycles", asyn "Threw with expected error message"); }); +async function testBasicGraphCreation(t, tree, expectedOrder, bfs) { + const projectGraph = await projectGraphFromTree(tree); + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await projectGraph.traverseBreathFirst(callbackStub); + } else { + await projectGraph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); +} + +test("Project with inline configuration", async (t) => { + const tree = { + id: "application.a", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: { + specVersion: "1.0", + type: "application", + metadata: { + name: "xy" + } + } + }; + + await testBasicGraphCreation(t, tree, [ + "xy" + ]); +}); /* ========================= */ /* ======= Test data ======= */ From f8d75b456b5ace8d5a925ce98993cc0b457a843a Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 15:03:27 +0100 Subject: [PATCH 05/99] Upgrade fixtures to specVersion 2.3 --- .../collection/library.a/ui5.yaml | 2 +- .../collection/library.b/ui5.yaml | 2 +- .../collection/library.c/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- test/fixtures/application.a/ui5.yaml | 2 +- test/fixtures/application.b/ui5.yaml | 2 +- test/fixtures/application.c/ui5.yaml | 2 +- test/fixtures/application.c2/ui5.yaml | 2 +- test/fixtures/application.c3/ui5.yaml | 2 +- test/fixtures/application.d/ui5.yaml | 2 +- test/fixtures/application.e/ui5.yaml | 2 +- test/fixtures/application.f/ui5.yaml | 2 +- test/fixtures/application.g/ui5.yaml | 2 +- test/fixtures/collection/library.a/ui5.yaml | 2 +- test/fixtures/collection/library.b/ui5.yaml | 2 +- test/fixtures/collection/library.c/ui5.yaml | 2 +- .../node_modules/application.cycle.a/ui5.yaml | 2 +- .../node_modules/application.cycle.c/ui5.yaml | 2 +- .../node_modules/application.cycle.d/ui5.yaml | 2 +- .../node_modules/application.cycle.e/ui5.yaml | 2 +- .../node_modules/application.cycle.f/ui5.yaml | 2 +- .../node_modules/component.cycle.a/ui5.yaml | 2 +- .../node_modules/library.cycle.a/ui5.yaml | 2 +- .../node_modules/library.cycle.b/ui5.yaml | 2 +- .../node_modules/library.cycle.c/ui5.yaml | 2 +- .../node_modules/library.cycle.d/ui5.yaml | 2 +- .../node_modules/library.cycle.e/ui5.yaml | 2 +- test/fixtures/glob/application.a/ui5.yaml | 2 +- test/fixtures/glob/application.b/ui5.yaml | 2 +- test/fixtures/library.d-depender/ui5.yaml | 2 +- test/fixtures/library.d/ui5.yaml | 2 +- test/fixtures/library.e/ui5.yaml | 2 +- test/fixtures/library.f/ui5.yaml | 2 +- test/fixtures/library.g/ui5.yaml | 2 +- test/lib/projectPreprocessor.js | 209 +++++++++--------- 35 files changed, 140 insertions(+), 137 deletions(-) diff --git a/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml b/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml index 676f166c3..8d4784313 100644 --- a/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml +++ b/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.a diff --git a/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml b/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml index 3275ac753..b2fe5be59 100644 --- a/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml +++ b/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.b diff --git a/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml b/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml index 159b14118..7c5e38a7f 100644 --- a/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml +++ b/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.c diff --git a/test/fixtures/application.a/node_modules/library.d/ui5.yaml b/test/fixtures/application.a/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.a/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.a/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.a/ui5.yaml b/test/fixtures/application.a/ui5.yaml index 898f4e816..b9dde7b16 100644 --- a/test/fixtures/application.a/ui5.yaml +++ b/test/fixtures/application.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "1.0" +specVersion: "2.3" type: application metadata: name: application.a diff --git a/test/fixtures/application.b/ui5.yaml b/test/fixtures/application.b/ui5.yaml index 25326df45..7b5e5dd23 100644 --- a/test/fixtures/application.b/ui5.yaml +++ b/test/fixtures/application.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.b diff --git a/test/fixtures/application.c/ui5.yaml b/test/fixtures/application.c/ui5.yaml index 3c1565db5..fd28471ba 100644 --- a/test/fixtures/application.c/ui5.yaml +++ b/test/fixtures/application.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.c diff --git a/test/fixtures/application.c2/ui5.yaml b/test/fixtures/application.c2/ui5.yaml index 132223e42..c982fa9dc 100644 --- a/test/fixtures/application.c2/ui5.yaml +++ b/test/fixtures/application.c2/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.c2 diff --git a/test/fixtures/application.c3/ui5.yaml b/test/fixtures/application.c3/ui5.yaml index b0fbde29c..3cecacec1 100644 --- a/test/fixtures/application.c3/ui5.yaml +++ b/test/fixtures/application.c3/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.c3 diff --git a/test/fixtures/application.d/ui5.yaml b/test/fixtures/application.d/ui5.yaml index 60bbbc1c3..1b43352b1 100644 --- a/test/fixtures/application.d/ui5.yaml +++ b/test/fixtures/application.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.d diff --git a/test/fixtures/application.e/ui5.yaml b/test/fixtures/application.e/ui5.yaml index 9828f57ae..97537e5ca 100644 --- a/test/fixtures/application.e/ui5.yaml +++ b/test/fixtures/application.e/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.e diff --git a/test/fixtures/application.f/ui5.yaml b/test/fixtures/application.f/ui5.yaml index a6eda2444..3df51b3a9 100644 --- a/test/fixtures/application.f/ui5.yaml +++ b/test/fixtures/application.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.f diff --git a/test/fixtures/application.g/ui5.yaml b/test/fixtures/application.g/ui5.yaml index 7d2ca7964..d4e5b20f9 100644 --- a/test/fixtures/application.g/ui5.yaml +++ b/test/fixtures/application.g/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.g diff --git a/test/fixtures/collection/library.a/ui5.yaml b/test/fixtures/collection/library.a/ui5.yaml index 676f166c3..8d4784313 100644 --- a/test/fixtures/collection/library.a/ui5.yaml +++ b/test/fixtures/collection/library.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.a diff --git a/test/fixtures/collection/library.b/ui5.yaml b/test/fixtures/collection/library.b/ui5.yaml index 3275ac753..b2fe5be59 100644 --- a/test/fixtures/collection/library.b/ui5.yaml +++ b/test/fixtures/collection/library.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.b diff --git a/test/fixtures/collection/library.c/ui5.yaml b/test/fixtures/collection/library.c/ui5.yaml index 159b14118..7c5e38a7f 100644 --- a/test/fixtures/collection/library.c/ui5.yaml +++ b/test/fixtures/collection/library.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.c diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.a/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.a/ui5.yaml index 16ac128fb..750138723 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.a/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.cycle.a diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.c/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.c/ui5.yaml index 44420029d..18e669785 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.c/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.cycle.c diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.d/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.d/ui5.yaml index 13f735d20..72d79ae25 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.d/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.cycle.d diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.e/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.e/ui5.yaml index 199e29c6d..c3d85496b 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.e/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.e/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.cycle.e diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.f/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/application.cycle.f/ui5.yaml index 55d3a6d8d..6c39c5b3f 100644 --- a/test/fixtures/cyclic-deps/node_modules/application.cycle.f/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.cycle.f diff --git a/test/fixtures/cyclic-deps/node_modules/component.cycle.a/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/component.cycle.a/ui5.yaml index 87e92d6d1..f63b38ea3 100644 --- a/test/fixtures/cyclic-deps/node_modules/component.cycle.a/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/component.cycle.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: component.cycle.a diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.a/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.a/ui5.yaml index 124aef34f..5c42ee2e1 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.a/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.a diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.b/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.b/ui5.yaml index 13ef9e228..fb6a25de0 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.b/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.b diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.c/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.c/ui5.yaml index 5ebeb043f..f6b4f2ac1 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.c/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.c diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.d/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.d/ui5.yaml index 3fb70271d..52bb969a8 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.d/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.d diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.e/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.e/ui5.yaml index df84c329f..d20d4477a 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.e/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.e/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.e diff --git a/test/fixtures/glob/application.a/ui5.yaml b/test/fixtures/glob/application.a/ui5.yaml index 7e28c5d7c..b9dde7b16 100644 --- a/test/fixtures/glob/application.a/ui5.yaml +++ b/test/fixtures/glob/application.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.a diff --git a/test/fixtures/glob/application.b/ui5.yaml b/test/fixtures/glob/application.b/ui5.yaml index 25326df45..7b5e5dd23 100644 --- a/test/fixtures/glob/application.b/ui5.yaml +++ b/test/fixtures/glob/application.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: application metadata: name: application.b diff --git a/test/fixtures/library.d-depender/ui5.yaml b/test/fixtures/library.d-depender/ui5.yaml index d3a67f6da..517442188 100644 --- a/test/fixtures/library.d-depender/ui5.yaml +++ b/test/fixtures/library.d-depender/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d-depender diff --git a/test/fixtures/library.d/ui5.yaml b/test/fixtures/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/library.d/ui5.yaml +++ b/test/fixtures/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/library.e/ui5.yaml b/test/fixtures/library.e/ui5.yaml index 5dee17f60..88ba07e82 100644 --- a/test/fixtures/library.e/ui5.yaml +++ b/test/fixtures/library.e/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e diff --git a/test/fixtures/library.f/ui5.yaml b/test/fixtures/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/library.f/ui5.yaml +++ b/test/fixtures/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/fixtures/library.g/ui5.yaml b/test/fixtures/library.g/ui5.yaml index 9c5281718..a20d2d499 100644 --- a/test/fixtures/library.g/ui5.yaml +++ b/test/fixtures/library.g/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.g diff --git a/test/lib/projectPreprocessor.js b/test/lib/projectPreprocessor.js index 5cea8c789..93b9c40ae 100644 --- a/test/lib/projectPreprocessor.js +++ b/test/lib/projectPreprocessor.js @@ -27,7 +27,7 @@ test("Project with inline configuration", (t) => { path: applicationAPath, dependencies: [], version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", type: "application", metadata: { name: "xy" @@ -44,7 +44,7 @@ test("Project with inline configuration", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "webapp" } @@ -57,7 +57,7 @@ test("Project with inline configuration", (t) => { id: "application.a", kind: "project", version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", path: applicationAPath }, "Parsed correctly"); }); @@ -82,7 +82,7 @@ test("Project with configPath", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "webapp" } @@ -95,7 +95,7 @@ test("Project with configPath", (t) => { id: "application.a", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: applicationAPath, configPath: path.join(applicationBPath, "ui5.yaml") }, "Parsed correctly"); @@ -120,7 +120,7 @@ test("Project with ui5.yaml at default location", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "webapp" } @@ -133,7 +133,7 @@ test("Project with ui5.yaml at default location", (t) => { id: "application.a", kind: "project", version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", path: applicationAPath }, "Parsed correctly"); }); @@ -157,7 +157,7 @@ test("Project with ui5.yaml at default location and some configuration", (t) => }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "src" } @@ -170,7 +170,7 @@ test("Project with ui5.yaml at default location and some configuration", (t) => id: "application.c", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: applicationCPath }, "Parsed correctly"); }); @@ -180,6 +180,7 @@ test("Missing configuration for root project", async (t) => { const tree = { id: "application.a", path: "non-existent", + version: "1.0.0", dependencies: [] }; const exception = await t.throwsAsync(projectPreprocessor.processTree(tree)); @@ -197,11 +198,11 @@ test("Missing id for root project", (t) => { {message: "Encountered project with missing id (root project)"}, "Rejected with error"); }); -test("No type configured for root project", (t) => { +test("No type configured for root project", async (t) => { const tree = { id: "application.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(__dirname, "../fixtures/application.a"), dependencies: [], metadata: { @@ -209,17 +210,19 @@ test("No type configured for root project", (t) => { namespace: "id1" } }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - {message: "No type configured for root project application.a"}, - "Rejected with error"); + const error = await t.throwsAsync(projectPreprocessor.processTree(tree)); + + t.is(error.message, `Invalid ui5.yaml configuration for project application.a + +Configuration must have required property 'type'`, + "Rejected with expected error"); }); test("Missing dependencies", (t) => { const tree = ({ id: "application.a", version: "1.0.0", - path: applicationAPath, - dependencies: [] + path: applicationAPath }); return t.notThrowsAsync(projectPreprocessor.processTree(tree), "Gracefully accepted project with no dependency attribute"); @@ -346,7 +349,7 @@ test("Ignores additional application-projects", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "webapp" } @@ -359,7 +362,7 @@ test("Ignores additional application-projects", (t) => { id: "application.a", kind: "project", version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", path: applicationAPath }, "Parsed correctly"); }); @@ -370,7 +373,7 @@ test("Inconsistent dependencies with same ID", (t) => { const tree = { id: "application.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: applicationAPath, type: "application", metadata: { @@ -380,7 +383,7 @@ test("Inconsistent dependencies with same ID", (t) => { { id: "library.d", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryDPath, type: "library", metadata: { @@ -388,7 +391,7 @@ test("Inconsistent dependencies with same ID", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "main/src", test: "main/test" @@ -399,7 +402,7 @@ test("Inconsistent dependencies with same ID", (t) => { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryBPath, // B, not A - inconsistency! type: "library", metadata: { @@ -412,7 +415,7 @@ test("Inconsistent dependencies with same ID", (t) => { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, type: "library", metadata: { @@ -427,7 +430,7 @@ test("Inconsistent dependencies with same ID", (t) => { id: "application.a", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: applicationAPath, _level: 0, _isRoot: true, @@ -438,7 +441,7 @@ test("Inconsistent dependencies with same ID", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { webapp: "webapp" } @@ -452,7 +455,7 @@ test("Inconsistent dependencies with same ID", (t) => { id: "library.d", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryDPath, _level: 1, type: "library", @@ -463,7 +466,7 @@ test("Inconsistent dependencies with same ID", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "main/src", test: "main/test" @@ -479,7 +482,7 @@ test("Inconsistent dependencies with same ID", (t) => { id: "library.a", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, _level: 1, type: "library", @@ -490,7 +493,7 @@ test("Inconsistent dependencies with same ID", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "src", test: "test" @@ -509,7 +512,7 @@ test("Inconsistent dependencies with same ID", (t) => { id: "library.a", kind: "project", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, _level: 1, type: "library", @@ -520,7 +523,7 @@ test("Inconsistent dependencies with same ID", (t) => { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "src", test: "test" @@ -611,7 +614,7 @@ const treeWithInvalidModules = { } ], version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", type: "library", metadata: {name: "library.a"} }, @@ -636,13 +639,13 @@ const treeWithInvalidModules = { } ], version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", type: "library", metadata: {name: "library.b"} } ], version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", type: "application", metadata: { name: "application.a" @@ -657,7 +660,7 @@ const expectedTreeWithInvalidModules = { "path": libraryAPath, "dependencies": [], "version": "1.0.0", - "specVersion": "1.0", + "specVersion": "2.3", "type": "library", "metadata": { "name": "library.a", @@ -668,7 +671,7 @@ const expectedTreeWithInvalidModules = { "_level": 1, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -684,7 +687,7 @@ const expectedTreeWithInvalidModules = { "path": libraryBPath, "dependencies": [], "version": "1.0.0", - "specVersion": "1.0", + "specVersion": "2.3", "type": "library", "metadata": { "name": "library.b", @@ -695,7 +698,7 @@ const expectedTreeWithInvalidModules = { "_level": 1, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -708,7 +711,7 @@ const expectedTreeWithInvalidModules = { } }], "version": "1.0.0", - "specVersion": "1.0", + "specVersion": "2.3", "type": "application", "metadata": { "name": "application.a", @@ -719,7 +722,7 @@ const expectedTreeWithInvalidModules = { "kind": "project", "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "webapp": "webapp" } @@ -734,7 +737,7 @@ const expectedTreeWithInvalidModules = { const treeAWithInlineConfigs = { id: "application.a", version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", path: applicationAPath, type: "application", metadata: { @@ -744,7 +747,7 @@ const treeAWithInlineConfigs = { { id: "library.d", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryDPath, type: "library", metadata: { @@ -752,7 +755,7 @@ const treeAWithInlineConfigs = { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "main/src", test: "main/test" @@ -763,7 +766,7 @@ const treeAWithInlineConfigs = { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, type: "library", metadata: { @@ -776,7 +779,7 @@ const treeAWithInlineConfigs = { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, type: "library", metadata: { @@ -849,7 +852,7 @@ const expectedTreeAWithInlineConfigs = { "id": "application.a", "kind": "project", "version": "1.0.0", - "specVersion": "1.0", + "specVersion": "2.3", "path": applicationAPath, "_level": 0, "_isRoot": true, @@ -860,7 +863,7 @@ const expectedTreeAWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "webapp": "webapp" } @@ -874,7 +877,7 @@ const expectedTreeAWithInlineConfigs = { "id": "library.d", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryDPath, "_level": 1, "type": "library", @@ -885,7 +888,7 @@ const expectedTreeAWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "main/src", "test": "main/test" @@ -901,7 +904,7 @@ const expectedTreeAWithInlineConfigs = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "_level": 1, "type": "library", @@ -912,7 +915,7 @@ const expectedTreeAWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -931,7 +934,7 @@ const expectedTreeAWithInlineConfigs = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "_level": 1, "type": "library", @@ -942,7 +945,7 @@ const expectedTreeAWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -964,7 +967,7 @@ const expectedTreeAWithConfigPaths = { "id": "application.a", "kind": "project", "version": "1.0.0", - "specVersion": "1.0", + "specVersion": "2.3", "path": applicationAPath, "configPath": path.join(applicationAPath, "ui5.yaml"), "_level": 0, @@ -976,7 +979,7 @@ const expectedTreeAWithConfigPaths = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "webapp": "webapp" } @@ -990,7 +993,7 @@ const expectedTreeAWithConfigPaths = { "id": "library.d", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryDPath, "configPath": path.join(libraryDPath, "ui5.yaml"), "_level": 1, @@ -1002,7 +1005,7 @@ const expectedTreeAWithConfigPaths = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "main/src", "test": "main/test" @@ -1018,7 +1021,7 @@ const expectedTreeAWithConfigPaths = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "configPath": path.join(libraryAPath, "ui5.yaml"), "_level": 1, @@ -1030,7 +1033,7 @@ const expectedTreeAWithConfigPaths = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1049,7 +1052,7 @@ const expectedTreeAWithConfigPaths = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "configPath": path.join(libraryAPath, "ui5.yaml"), "_level": 1, @@ -1061,7 +1064,7 @@ const expectedTreeAWithConfigPaths = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1081,7 +1084,7 @@ const expectedTreeAWithConfigPaths = { const treeBWithInlineConfigs = { id: "application.b", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: applicationBPath, type: "application", metadata: { @@ -1091,7 +1094,7 @@ const treeBWithInlineConfigs = { { id: "library.b", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryBPath, type: "library", metadata: { @@ -1101,7 +1104,7 @@ const treeBWithInlineConfigs = { { id: "library.d", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryDPath, type: "library", metadata: { @@ -1109,7 +1112,7 @@ const treeBWithInlineConfigs = { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "main/src", test: "main/test" @@ -1120,7 +1123,7 @@ const treeBWithInlineConfigs = { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, type: "library", metadata: { @@ -1135,7 +1138,7 @@ const treeBWithInlineConfigs = { { id: "library.d", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryDPath, type: "library", metadata: { @@ -1143,7 +1146,7 @@ const treeBWithInlineConfigs = { }, resources: { configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", + propertiesFileSourceEncoding: "UTF-8", paths: { src: "main/src", test: "main/test" @@ -1154,7 +1157,7 @@ const treeBWithInlineConfigs = { { id: "library.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: libraryAPath, type: "library", metadata: { @@ -1171,7 +1174,7 @@ const expectedTreeBWithInlineConfigs = { "id": "application.b", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": applicationBPath, "_level": 0, "_isRoot": true, @@ -1182,7 +1185,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "webapp": "webapp" } @@ -1196,7 +1199,7 @@ const expectedTreeBWithInlineConfigs = { "id": "library.b", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryBPath, "_level": 1, "type": "library", @@ -1207,7 +1210,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1223,7 +1226,7 @@ const expectedTreeBWithInlineConfigs = { "id": "library.d", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryDPath, "_level": 1, "type": "library", @@ -1234,7 +1237,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "main/src", "test": "main/test" @@ -1250,7 +1253,7 @@ const expectedTreeBWithInlineConfigs = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "_level": 2, "type": "library", @@ -1261,7 +1264,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1282,7 +1285,7 @@ const expectedTreeBWithInlineConfigs = { "id": "library.d", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryDPath, "_level": 1, "type": "library", @@ -1293,7 +1296,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "main/src", "test": "main/test" @@ -1309,7 +1312,7 @@ const expectedTreeBWithInlineConfigs = { "id": "library.a", "kind": "project", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": libraryAPath, "_level": 2, "type": "library", @@ -1320,7 +1323,7 @@ const expectedTreeBWithInlineConfigs = { }, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1341,7 +1344,7 @@ const expectedTreeBWithInlineConfigs = { const treeApplicationCycleA = { id: "application.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "application.cycle.a"), type: "application", metadata: { @@ -1351,7 +1354,7 @@ const treeApplicationCycleA = { { id: "component.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "component.cycle.a"), type: "library", metadata: { @@ -1361,7 +1364,7 @@ const treeApplicationCycleA = { { id: "library.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "library.cycle.a"), type: "library", metadata: { @@ -1371,7 +1374,7 @@ const treeApplicationCycleA = { { id: "component.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "component.cycle.a"), type: "library", metadata: { @@ -1385,7 +1388,7 @@ const treeApplicationCycleA = { { id: "library.cycle.b", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "library.cycle.b"), type: "library", metadata: { @@ -1395,7 +1398,7 @@ const treeApplicationCycleA = { { id: "component.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "component.cycle.a"), type: "library", metadata: { @@ -1409,7 +1412,7 @@ const treeApplicationCycleA = { { id: "application.cycle.a", version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", path: path.join(cycleDepsBasePath, "application.cycle.a"), type: "application", metadata: { @@ -1426,7 +1429,7 @@ const treeApplicationCycleA = { const expectedTreeApplicationCycleA = { "id": "application.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "application.cycle.a"), "type": "application", "metadata": { @@ -1437,7 +1440,7 @@ const expectedTreeApplicationCycleA = { { "id": "component.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "component.cycle.a"), "type": "library", "metadata": { @@ -1449,7 +1452,7 @@ const expectedTreeApplicationCycleA = { { "id": "library.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "library.cycle.a"), "type": "library", "metadata": { @@ -1461,7 +1464,7 @@ const expectedTreeApplicationCycleA = { { "id": "component.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "component.cycle.a"), "type": "library", "metadata": { @@ -1475,7 +1478,7 @@ const expectedTreeApplicationCycleA = { "_level": 2, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1490,7 +1493,7 @@ const expectedTreeApplicationCycleA = { { "id": "library.cycle.b", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "library.cycle.b"), "type": "library", "metadata": { @@ -1502,7 +1505,7 @@ const expectedTreeApplicationCycleA = { { "id": "component.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "component.cycle.a"), "type": "library", "metadata": { @@ -1516,7 +1519,7 @@ const expectedTreeApplicationCycleA = { "_level": 2, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1531,7 +1534,7 @@ const expectedTreeApplicationCycleA = { { "id": "application.cycle.a", "version": "1.0.0", - "specVersion": "0.1", + "specVersion": "2.3", "path": path.join(cycleDepsBasePath, "application.cycle.a"), "type": "application", "metadata": { @@ -1545,7 +1548,7 @@ const expectedTreeApplicationCycleA = { "_level": 1, "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "src": "src", "test": "test" @@ -1563,7 +1566,7 @@ const expectedTreeApplicationCycleA = { "kind": "project", "resources": { "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", + "propertiesFileSourceEncoding": "UTF-8", "paths": { "webapp": "webapp" } @@ -1781,14 +1784,14 @@ test("specVersion: Project with valid version 0.1", async (t) => { path: applicationAPath, dependencies: [], version: "1.0.0", - specVersion: "0.1", + specVersion: "2.3", type: "application", metadata: { name: "xy" } }; const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "0.1", "Correct spec version"); + t.deepEqual(res.specVersion, "2.3", "Correct spec version"); }); test("specVersion: Project with valid version 1.0", async (t) => { @@ -1797,14 +1800,14 @@ test("specVersion: Project with valid version 1.0", async (t) => { path: applicationAPath, dependencies: [], version: "1.0.0", - specVersion: "1.0", + specVersion: "2.3", type: "application", metadata: { name: "xy" } }; const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "1.0", "Correct spec version"); + t.deepEqual(res.specVersion, "2.3", "Correct spec version"); }); test("specVersion: Project with valid version 1.1", async (t) => { From db80a023f89346eb08712e8fcaea2a7ee1c81263 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 15:03:59 +0100 Subject: [PATCH 06/99] Limit Module API to specVersions >= 2.0 Add validation --- lib/Module.js | 108 +++++++++++++++++++------ lib/graph/projectGraphFromTree.js | 3 + lib/projectPreprocessor.js | 3 +- test/lib/graph/projectGraphFromTree.js | 90 ++++++++++++++++++++- 4 files changed, 177 insertions(+), 27 deletions(-) diff --git a/lib/Module.js b/lib/Module.js index 7415c8bd9..17d4b9692 100644 --- a/lib/Module.js +++ b/lib/Module.js @@ -32,8 +32,14 @@ class Module { * Collection of shims that might be relevant for this module */ constructor({id, version, modulePath, configPath = defaultConfigPath, configuration, shimCollection}) { - if (!id || !version || !modulePath) { - throw new Error(`Could not create Module: One or more required parameters are missing`); + if (!id) { + throw new Error(`Could not create Module: Missing or empty id parameter`); + } + if (!version) { + throw new Error(`Could not create Module: Missing or empty version parameter`); + } + if (!modulePath) { + throw new Error(`Could not create Module: Missing or empty modulePath parameter`); } this._id = id; @@ -41,6 +47,7 @@ class Module { this._modulePath = modulePath; this._configPath = configPath; this._dependencies = {}; + this._suppliedConfig = configuration; if (shimCollection) { // Retrieve and clone shims in constructor @@ -50,16 +57,6 @@ class Module { this._configShims = clone(shims); } } - - if (configuration) { - this._normalizeConfig(configuration); - if (!["project", "extension"].includes(configuration.kind)) { - throw new Error( - `Could not create module with supplied configuration object: ` + - `Unknown kind '${configuration.kind}'. Expected 'project' or 'extension'`); - } - this._suppliedConfig = configuration; - } } /** @@ -133,29 +130,32 @@ class Module { async _getConfigurations() { let configurations; if (this._suppliedConfig) { - configurations = [this._createConfigurationInstance(this._suppliedConfig)]; + configurations = [await this._createConfigurationInstance(this._suppliedConfig)]; } else { configurations = await this._loadProjectConfiguration(); } return configurations; } - _createConfigurationInstance(config) { + async _createConfigurationInstance(config) { + this._normalizeConfig(config); if (config.kind === "project") { this._applyShims(config); } + await this._validateConfig(config); return new Configuration(config); } - _createProjectConfigurationFromShim() { - const config = {}; - this._applyShims(config); + async _createProjectConfigurationFromShim() { + const config = this._applyShims(); if (config) { + this._normalizeConfig(config); + await this._validateConfig(config); return new Configuration(config); } } - _applyShims(config) { + _applyShims(config = {}) { if (!this._configShims) { return; } @@ -163,7 +163,6 @@ class Module { log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); Object.assign(config, shim); }); - this._normalizeConfig(config); return config; } @@ -173,7 +172,7 @@ class Module { if (!configs || !configs.length) { // No project configuration found // => Try to create one from shims - const shimConfiguration = this._createProjectConfigurationFromShim(); + const shimConfiguration = await this._createProjectConfigurationFromShim(); if (shimConfiguration) { return [shimConfiguration]; } @@ -209,19 +208,19 @@ class Module { const configurations = []; if (projectConfigs.length) { - configurations.push(this._createConfigurationInstance(projectConfigs[0])); + configurations.push(await this._createConfigurationInstance(projectConfigs[0])); } else { // No project configuration found // => Try to create one from shims - const shimConfiguration = this._createProjectConfigurationFromShim(); + const shimConfiguration = await this._createProjectConfigurationFromShim(); if (shimConfiguration) { configurations.push(shimConfiguration); } } - extensionConfigs.forEach((config) => { - configurations.push(this._createConfigurationInstance(config)); - }); + await Promise.all(extensionConfigs.map(async (config) => { + configurations.push(await this._createConfigurationInstance(config)); + })); return configurations; } @@ -318,6 +317,65 @@ class Module { return config; } + async _validateConfig(config) { + if (config.specVersion === "0.1" || config.specVersion === "1.0" || + config.specVersion === "1.1") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in module ` + + `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + if (config.specVersion !== "2.0" && + config.specVersion !== "2.1" && config.specVersion !== "2.2" && + config.specVersion !== "2.3") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in module ` + + `${this.getId()}. Your UI5 CLI installation might be outdated. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + + await validate({ + config, + project: { + id: this.getId() + } + }); + } + + _isConfigValid(project) { + if (!project.type) { + if (project._isRoot) { + throw new Error(`No type configured for root project ${project.id}`); + } + log.verbose(`No type configured for project ${project.id}`); + return false; // ignore this project + } + + if (project.kind !== "project" && project._isRoot) { + // This is arguable. It is not the concern of ui5-project to define the entry point of a project tree + // On the other hand, there is no known use case for anything else right now and failing early here + // makes sense in that regard + throw new Error(`Root project needs to be of kind "project". ${project.id} is of kind ${project.kind}`); + } + + if (project.kind === "project" && project.type === "application") { + // There must be exactly one application project per dependency tree + // If multiple are found, all but the one closest to the root are rejected (ignored) + // If there are two projects equally close to the root, an error is being thrown + if (!this.qualifiedApplicationProject) { + this.qualifiedApplicationProject = project; + } else if (this.qualifiedApplicationProject._level === project._level) { + throw new Error(`Found at least two projects ${this.qualifiedApplicationProject.id} and ` + + `${project.id} of type application with the same distance to the root project. ` + + "Only one project of type application can be used. Failed to decide which one to ignore."); + } else { + return false; // ignore this project + } + } + + return true; + } + /** * Resource Access */ diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 5a16b1ea4..f1d5c65d4 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -37,6 +37,9 @@ module.exports = async function(tree) { configuration: tree.configuration }); const rootProject = await rootModule.getProject(); + if (!rootProject) { + throw new Error(`Failed to crate a project from root module ${tree.id} (${tree.path})`); + } const rootProjectName = rootProject.getName(); const extensions = await rootModule.getExtensions(); diff --git a/lib/projectPreprocessor.js b/lib/projectPreprocessor.js index c4f080f61..f98022703 100644 --- a/lib/projectPreprocessor.js +++ b/lib/projectPreprocessor.js @@ -621,7 +621,8 @@ class ProjectPreprocessor { "path", "dependencies", "_level", - "_isRoot" + "_isRoot", + "configPath" ]; const config = {}; for (const key of Object.keys(project)) { diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 63cc4f695..28ca7d3e0 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -4,6 +4,8 @@ const path = require("path"); const projectGraphFromTree = require("../../../lib/graph/projectGraphFromTree.js"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b"); +const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); test.beforeEach((t) => { @@ -131,6 +133,10 @@ test("Application Cycle B: Traverse project graph depth first with cycles", asyn "Threw with expected error message"); }); + +/* ================================================================================================= */ +/* ======= The following tests have been derived from the existing projectPreprocessor tests ======= */ + async function testBasicGraphCreation(t, tree, expectedOrder, bfs) { const projectGraph = await projectGraphFromTree(tree); const callbackStub = t.context.sinon.stub().resolves(); @@ -154,7 +160,7 @@ test("Project with inline configuration", async (t) => { dependencies: [], version: "1.0.0", configuration: { - specVersion: "1.0", + specVersion: "2.3", type: "application", metadata: { name: "xy" @@ -167,6 +173,88 @@ test("Project with inline configuration", async (t) => { ]); }); +test("Project with configPath", async (t) => { + const tree = { + id: "application.a", + path: applicationAPath, + configPath: path.join(applicationBPath, "ui5.yaml"), // B, not A - just to have something different + dependencies: [], + version: "1.0.0" + }; + + await testBasicGraphCreation(t, tree, [ + "application.b" + ]); +}); + +test("Project with ui5.yaml at default location", async (t) => { + const tree = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }; + + await testBasicGraphCreation(t, tree, [ + "application.a" + ]); +}); + +test("Project with ui5.yaml at default location and some configuration", async (t) => { + const tree = { + id: "application.c", + version: "1.0.0", + path: applicationCPath, + dependencies: [] + }; + + await testBasicGraphCreation(t, tree, [ + "application.c" + ]); +}); + +test("Missing configuration file for root project", async (t) => { + const tree = { + id: "application.a", + version: "1.0.0", + path: "non-existent", + dependencies: [] + }; + await t.throwsAsync(projectGraphFromTree(tree), + {message: "Failed to crate a project from root module application.a (non-existent)"}, "Rejected with error"); +}); + +test("Missing id for root project", (t) => { + const tree = { + path: path.join(__dirname, "../fixtures/application.a"), + dependencies: [] + }; + return t.throwsAsync(projectGraphFromTree(tree), + {message: "Could not create Module: Missing or empty id parameter"}, "Rejected with error"); +}); + +test("No type configured for root project", async (t) => { + const tree = { + id: "application.a", + version: "1.0.0", + path: path.join(__dirname, "../fixtures/application.a"), + dependencies: [], + configuration: { + specVersion: "2.1", + metadata: { + name: "application.a", + namespace: "id1" + } + } + }; + const error = await t.throwsAsync(projectGraphFromTree(tree)); + + t.is(error.message, `Invalid ui5.yaml configuration for project application.a + +Configuration must have required property 'type'`, + "Rejected with expected error"); +}); + /* ========================= */ /* ======= Test data ======= */ From 98854ec9949e4d3bc234f56d8eeb800410229d3f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 15:26:07 +0100 Subject: [PATCH 07/99] Process modules once per ID Finish transfer of projectPreprocessor tests to projectGraphFromTree tests --- lib/graph/projectGraphFromTree.js | 94 +++- test/lib/graph/projectGraphFromTree.js | 693 ++++++++++++++++++++++++- 2 files changed, 740 insertions(+), 47 deletions(-) diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index f1d5c65d4..8d87564f7 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -1,6 +1,7 @@ const Module = require("../Module"); const ProjectGraph = require("./ProjectGraph"); const ShimCollection = require("./ShimCollection"); +const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); /** * Tree node @@ -29,6 +30,8 @@ module.exports = async function(tree) { }); } + const moduleCollection = {}; + const rootModule = new Module({ id: tree.id, version: tree.version, @@ -38,10 +41,21 @@ module.exports = async function(tree) { }); const rootProject = await rootModule.getProject(); if (!rootProject) { - throw new Error(`Failed to crate a project from root module ${tree.id} (${tree.path})`); + throw new Error( + `Failed to crate a UI5 project from module ${tree.id} at ${tree.path}. ` + + `Make sure the path is correct and a project configuration is present or supplied.`); } + + moduleCollection[tree.id] = rootModule; + const rootProjectName = rootProject.getName(); + let qualifiedApplicationProject = null; + if (rootProject.getType() === "application") { + log.verbose(`Root project ${rootProjectName} qualified as application project for project graph`); + qualifiedApplicationProject = rootProject; + } + const extensions = await rootModule.getExtensions(); addShimsToCollection(extensions); @@ -50,23 +64,35 @@ module.exports = async function(tree) { }); projectGraph.addProject(rootProject); - const queue = [{ - nodes: tree.dependencies, - parentProjectName: rootProjectName - }]; + const queue = []; + + if (tree.dependencies) { + queue.push({ + nodes: tree.dependencies, + parentProjectName: rootProjectName + }); + } // Breadth-first search while (queue.length) { const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue const res = await Promise.all(nodes.map(async (node) => { - const ui5Module = new Module({ - id: node.id, - version: node.version, - modulePath: node.path, - configPath: node.configPath, - configuration: node.configuration, - shimCollection - }); + let ui5Module = moduleCollection[node.id]; + if (!ui5Module) { + ui5Module = moduleCollection[node.id] = new Module({ + id: node.id, + version: node.version, + modulePath: node.path, + configPath: node.configPath, + configuration: node.configuration, + shimCollection + }); + } else if (ui5Module.getPath() !== node.path) { + log.verbose( + `Inconsistency detected: Tree contains multiple nodes with ID ${node.id} and different paths:` + + `\nPath of already added node (this one will be used): ${ui5Module.getPath()}` + + `\nPath of additional node (this one will be ignored in favor of the other): ${node.path}`); + } const project = await ui5Module.getProject(); const extensions = await ui5Module.getExtensions(); @@ -92,20 +118,46 @@ module.exports = async function(tree) { continue; } - if (!node.deduped) { - projectGraph.addProject(project, true); - } const projectName = project.getName(); + if (project.getType() === "application") { + // Special handling of application projects of which there must be exactly *one* + // in the graph. Others shall be ignored. + if (!qualifiedApplicationProject) { + log.verbose(`Project ${projectName} qualified as application project for project graph`); + qualifiedApplicationProject = project; + } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { + // Project is not a duplicate of an already qualified project (which should + // still be processed below), but a unique, additional application project + + // TODO: Should this rather be a verbose logging? + // projectPreprocessor handled this like any project that got ignored for some reason and did a + // (in this case misleading) general verbose logging "Ignoring project with missing configuration" + log.info( + `Excluding additional application project ${projectName} from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); + continue; + } + } + + // if (!node.deduped) { + // Even if not deduped, the node might occur multiple times in the tree (on separate branches). + // Therefore still supplying the ignore duplicates parameter here (true) + projectGraph.addProject(project, true); + // } + if (parentProjectName) { projectGraph.declareDependency(parentProjectName, projectName); } - queue.push({ - // copy array, so that the queue is stable while ignored project dependencies are removed - nodes: [...node.dependencies], - parentProjectName: projectName, - }); + if (node.dependencies && !node.deduped) { + queue.push({ + // copy array, so that the queue is stable while ignored project dependencies are removed + nodes: [...node.dependencies], + parentProjectName: projectName, + }); + } } } diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 28ca7d3e0..a860834d5 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -1,15 +1,33 @@ const test = require("ava"); -const sinonGlobal = require("sinon"); const path = require("path"); -const projectGraphFromTree = require("../../../lib/graph/projectGraphFromTree.js"); +const sinonGlobal = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b"); const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); +const libraryAPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a"); +const libraryBPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b"); +// const libraryCPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.c"); +const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); +const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); test.beforeEach((t) => { - t.context.sinon = sinonGlobal.createSandbox(); + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + warn: sinon.stub(), + verbose: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + isLevelEnabled: () => true + }; + sinon.stub(logger, "getLogger").callThrough() + .withArgs("graph:projectGraphFromTree").returns(t.context.log); + t.context.projectGraphFromTree = mock.reRequire("../../../lib/graph/projectGraphFromTree.js"); + logger.getLogger.restore(); // Immediately restore global stub for following tests }); test.afterEach.always((t) => { @@ -17,12 +35,14 @@ test.afterEach.always((t) => { }); test("Application A", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationATree); const rootProject = projectGraph.getRoot(); t.is(rootProject.getName(), "application.a", "Returned correct root project"); }); test("Application A: Traverse project graph breadth first", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationATree); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); @@ -31,10 +51,6 @@ test("Application A: Traverse project graph breadth first", async (t) => { const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); - // Since libraries a, b and c are processed in parallel, their callback order can vary - // Therefore we always sort the last three calls - // const lastThreeCalls = callbackCalls.splice(2, 3).sort(); - // callbackCalls.push(...lastThreeCalls); t.deepEqual(callbackCalls, [ "application.a", "library.d", @@ -45,6 +61,7 @@ test("Application A: Traverse project graph breadth first", async (t) => { }); test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub)); @@ -66,6 +83,7 @@ test("Application Cycle A: Traverse project graph breadth first with cycles", as }); test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); @@ -84,6 +102,7 @@ test("Application Cycle B: Traverse project graph breadth first with cycles", as }); test("Application A: Traverse project graph depth first", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationATree); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseDepthFirst(callbackStub); @@ -92,10 +111,6 @@ test("Application A: Traverse project graph depth first", async (t) => { const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); - // Since libraries a, b and c are processed in parallel, their callback order can vary - // Therefore we always sort the first three calls - // const firstThreeCalls = callbackCalls.splice(0, 3).sort(); - // callbackCalls.unshift(...firstThreeCalls); t.deepEqual(callbackCalls, [ "library.a", "library.b", @@ -108,6 +123,7 @@ test("Application A: Traverse project graph depth first", async (t) => { test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); @@ -121,6 +137,7 @@ test("Application Cycle A: Traverse project graph depth first with cycles", asyn }); test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => { + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); @@ -137,11 +154,23 @@ test("Application Cycle B: Traverse project graph depth first with cycles", asyn /* ================================================================================================= */ /* ======= The following tests have been derived from the existing projectPreprocessor tests ======= */ -async function testBasicGraphCreation(t, tree, expectedOrder, bfs) { +function testBasicGraphCreationBfs(...args) { + return _testBasicGraphCreation(...args, true); +} + +function testBasicGraphCreationDfs(...args) { + return _testBasicGraphCreation(...args, false); +} + +async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(tree); const callbackStub = t.context.sinon.stub().resolves(); if (bfs) { - await projectGraph.traverseBreathFirst(callbackStub); + await projectGraph.traverseBreadthFirst(callbackStub); } else { await projectGraph.traverseDepthFirst(callbackStub); } @@ -155,7 +184,7 @@ async function testBasicGraphCreation(t, tree, expectedOrder, bfs) { test("Project with inline configuration", async (t) => { const tree = { - id: "application.a", + id: "application.a.id", path: applicationAPath, dependencies: [], version: "1.0.0", @@ -168,34 +197,34 @@ test("Project with inline configuration", async (t) => { } }; - await testBasicGraphCreation(t, tree, [ + await testBasicGraphCreationDfs(t, tree, [ "xy" ]); }); test("Project with configPath", async (t) => { const tree = { - id: "application.a", + id: "application.a.id", path: applicationAPath, configPath: path.join(applicationBPath, "ui5.yaml"), // B, not A - just to have something different dependencies: [], version: "1.0.0" }; - await testBasicGraphCreation(t, tree, [ + await testBasicGraphCreationDfs(t, tree, [ "application.b" ]); }); test("Project with ui5.yaml at default location", async (t) => { const tree = { - id: "application.a", + id: "application.a.id", version: "1.0.0", path: applicationAPath, dependencies: [] }; - await testBasicGraphCreation(t, tree, [ + await testBasicGraphCreationDfs(t, tree, [ "application.a" ]); }); @@ -208,34 +237,42 @@ test("Project with ui5.yaml at default location and some configuration", async ( dependencies: [] }; - await testBasicGraphCreation(t, tree, [ + await testBasicGraphCreationDfs(t, tree, [ "application.c" ]); }); test("Missing configuration file for root project", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { - id: "application.a", + id: "application.a.id", version: "1.0.0", path: "non-existent", dependencies: [] }; await t.throwsAsync(projectGraphFromTree(tree), - {message: "Failed to crate a project from root module application.a (non-existent)"}, "Rejected with error"); + { + message: + "Failed to crate a UI5 project from module application.a.id at non-existent. " + + "Make sure the path is correct and a project configuration is present or supplied." + }, + "Rejected with error"); }); -test("Missing id for root project", (t) => { +test("Missing id for root project", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { path: path.join(__dirname, "../fixtures/application.a"), dependencies: [] }; - return t.throwsAsync(projectGraphFromTree(tree), + await t.throwsAsync(projectGraphFromTree(tree), {message: "Could not create Module: Missing or empty id parameter"}, "Rejected with error"); }); test("No type configured for root project", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { - id: "application.a", + id: "application.a.id", version: "1.0.0", path: path.join(__dirname, "../fixtures/application.a"), dependencies: [], @@ -249,12 +286,320 @@ test("No type configured for root project", async (t) => { }; const error = await t.throwsAsync(projectGraphFromTree(tree)); - t.is(error.message, `Invalid ui5.yaml configuration for project application.a + t.is(error.message, `Invalid ui5.yaml configuration for project application.a.id Configuration must have required property 'type'`, "Rejected with expected error"); }); +test("Missing dependencies", async (t) => { + const {projectGraphFromTree} = t.context; + const tree = ({ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath + }); + await t.notThrowsAsync(projectGraphFromTree(tree), + "Gracefully accepted project with no dependencies attribute"); +}); + +test("Missing second-level dependencies", async (t) => { + const {projectGraphFromTree} = t.context; + const tree = ({ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d") + }] + }); + return t.notThrowsAsync(projectGraphFromTree(tree), + "Gracefully accepted project with no dependencies attribute"); +}); + +test("Single non-root application-project", async (t) => { + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.a" + ]); +}); + +test("Multiple non-root application-projects on same level", async (t) => { + const {log} = t.context; + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }, { + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.a" + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Multiple non-root application-projects on different levels", async (t) => { + const {log} = t.context; + const tree = ({ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }, { + id: "library.b", + version: "1.0.0", + path: libraryBPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }] + }); + + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + "library.b", + "library.a" + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Root- and non-root application-projects", async (t) => { + const {log} = t.context; + const tree = ({ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }] + }); + await testBasicGraphCreationDfs(t, tree, [ + "library.a", + "application.a", + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Ignores additional application-projects", async (t) => { + const {log} = t.context; + const tree = ({ + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "application.b", + version: "1.0.0", + path: applicationBPath, + dependencies: [] + }] + }); + await testBasicGraphCreationDfs(t, tree, [ + "application.a", + ]); + + t.is(log.info.callCount, 1, "log.info should be called once"); + t.is(log.info.getCall(0).args[0], + `Excluding additional application project application.b from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project application.a has already qualified for that role.`, + "log.info should be called once with the expected argument"); +}); + +test("Inconsistent dependencies with same ID", async (t) => { + // The one closer to the root should win + const tree = { + id: "application.a", + version: "1.0.0", + specVersion: "2.3", + path: applicationAPath, + type: "application", + metadata: { + name: "application.a" + }, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + specVersion: "2.3", + path: libraryDPath, + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + specVersion: "2.3", + path: libraryBPath, // B, not A - inconsistency! + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.XY", + } + }, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + specVersion: "2.3", + path: libraryAPath, + type: "library", + metadata: { + name: "library.a", + }, + dependencies: [] + } + ] + }; + await testBasicGraphCreationDfs(t, tree, [ + // "library.XY" is ignored since the ID has already been processed and resolved to library A + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with inline configs depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithInlineConfigs, [ + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with configPaths depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithConfigPaths, [ + "library.a", + "library.d", + "application.a" + + ]); +}); + +test("Project tree A with default YAMLs depth first", async (t) => { + await testBasicGraphCreationDfs(t, applicationATreeWithDefaultYamls, [ + "library.a", + "library.d", + "application.a" + ]); +}); + +test("Project tree A with inline configs breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithInlineConfigs, [ + "application.a", + "library.d", + "library.a", + ]); +}); + +test("Project tree A with configPaths breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithConfigPaths, [ + "application.a", + "library.d", + "library.a" + + ]); +}); + +test("Project tree A with default YAMLs breadth first", async (t) => { + await testBasicGraphCreationBfs(t, applicationATreeWithDefaultYamls, [ + "application.a", + "library.d", + "library.a" + ]); +}); + +test("Project tree B with inline configs", async (t) => { + // Tree B depends on Library B which has a dependency to Library D + await testBasicGraphCreationDfs(t, applicationBTreeWithInlineConfigs, [ + "library.a", + "library.d", + "library.b", + "application.b" + ]); +}); + +test("Project with nested invalid dependencies", async (t) => { + await testBasicGraphCreationDfs(t, treeWithInvalidModules, [ + "library.a", + "library.b", + "application.a" + ]); +}); + /* ========================= */ /* ======= Test data ======= */ @@ -391,3 +736,299 @@ const applicationCycleBTreeIncDeduped = { } ] }; + + +/* === Tree A === */ +const applicationATreeWithInlineConfigs = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + }, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a", + }, + }, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + }, + }, + dependencies: [] + } + ] +}; + +const applicationATreeWithConfigPaths = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + configPath: path.join(applicationAPath, "ui5.yaml"), + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configPath: path.join(libraryDPath, "ui5.yaml"), + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configPath: path.join(libraryAPath, "ui5.yaml"), + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configPath: path.join(libraryAPath, "ui5.yaml"), + dependencies: [] + } + ] +}; + +const applicationATreeWithDefaultYamls = { + id: "application.a", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + dependencies: [] + } + ] +}; + +/* === Tree B === */ +const applicationBTreeWithInlineConfigs = { + id: "application.b", + version: "1.0.0", + path: applicationBPath, + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.b" + } + }, + dependencies: [ + { + id: "library.b", + version: "1.0.0", + path: libraryBPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.b", + } + }, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + } + }, + dependencies: [] + } + ] + } + ] + }, + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.a" + } + }, + dependencies: [] + } + ] + } + ] +}; + +/* === Invalid Modules */ +const treeWithInvalidModules = { + id: "application.a", + path: applicationAPath, + dependencies: [ + // A + { + id: "library.a", + path: libraryAPath, + dependencies: [ + { + // C - invalid - should be missing in preprocessed tree + id: "module.c", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + }, + { + // D - invalid - should be missing in preprocessed tree + id: "module.d", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "library", + metadata: {name: "library.a"} + } + }, + // B + { + id: "library.b", + path: libraryBPath, + dependencies: [ + { + // C - invalid - should be missing in preprocessed tree + id: "module.c", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + }, + { + // D - invalid - should be missing in preprocessed tree + id: "module.d", + dependencies: [], + path: pathToInvalidModule, + version: "1.0.0" + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "library", + metadata: {name: "library.b"} + } + } + ], + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + } +}; From 3b59a1b575e6cfaeba570f7ef6acd26b63ffbf50 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 13 Feb 2021 19:44:27 +0100 Subject: [PATCH 08/99] Add ProjectGraph tests --- lib/Module.js | 8 +- lib/graph/ProjectGraph.js | 108 +++- lib/specifications/AbstractSpecification.js | 16 +- test/lib/graph/ProjectGraph.js | 645 ++++++++++++++++++++ test/lib/graph/projectGraphFromTree.js | 4 +- test/lib/specifications/Project.js | 14 +- 6 files changed, 743 insertions(+), 52 deletions(-) create mode 100644 test/lib/graph/ProjectGraph.js diff --git a/lib/Module.js b/lib/Module.js index 17d4b9692..2308b6ed2 100644 --- a/lib/Module.js +++ b/lib/Module.js @@ -28,18 +28,18 @@ class Module { * or an absolute File System path to the project configuration file. * @param {object} [parameters.configuration] * Configuration object to use. If supplied, no ui5.yaml will be read - * @param {@ui5/extension.extensions.ShimCollection} [parameters.shimCollection] + * @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection] * Collection of shims that might be relevant for this module */ constructor({id, version, modulePath, configPath = defaultConfigPath, configuration, shimCollection}) { if (!id) { - throw new Error(`Could not create Module: Missing or empty id parameter`); + throw new Error(`Could not create Module: Missing or empty parameter 'id'`); } if (!version) { - throw new Error(`Could not create Module: Missing or empty version parameter`); + throw new Error(`Could not create Module: Missing or empty parameter 'version'`); } if (!modulePath) { - throw new Error(`Could not create Module: Missing or empty modulePath parameter`); + throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`); } this._id = id; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 164444ed8..82db23ac7 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -1,17 +1,21 @@ const log = require("@ui5/logger").getLogger("graph:ProjectGraph"); /** -* A rooted, directed graph representing a UI5 project that should be worked with and all its dependencies -*/ + * A rooted, directed graph representing a UI5 project and all its dependencies + * + * @public + * @memberof module:@ui5/project.graph + */ class ProjectGraph { /** + * @public * @param {object} parameters Parameters * @param {string} parameters.rootProjectName Root project name - * @param {Array.} parameters.extensions + * @param {Array.} parameters.extensions * Final list of extensions to be used in this project tree */ constructor({rootProjectName, extensions = []}) { if (!rootProjectName) { - throw new Error(`Could not create ProjectGraph: One or more required parameters are missing`); + throw new Error(`Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'`); } this._rootProjectName = rootProjectName; this._extensions = Object.freeze(extensions); @@ -34,10 +38,17 @@ class ProjectGraph { if (ignoreDuplicates) { return; } - throw new Error(`Could not add duplicate project '${projectName}' to project tree`); + throw new Error( + `Failed to add project ${projectName} to the graph: A project with that name has already been added`); + } + if (!isNaN(projectName)) { + // Reject integer-like project names. They would take precedence when traversing object keys which + // could lead to unexpected behavior. We don't really expect anyone to use such names anyways + throw new Error( + `Failed to add project ${projectName} to graph: Project name must not be integer-like`); } this._projects[projectName] = project; - this._adjList[projectName] = []; + this._adjList[projectName] = {}; } getProject(projectName) { @@ -45,11 +56,12 @@ class ProjectGraph { } /** - * Declare a dependency from one project in the graph to another - * - * @param {string} fromProjectName Name of the depending project - * @param {string} toProjectName Name of project on which the other depends - */ + * Declare a dependency from one project in the graph to another + * + * @public + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ declareDependency(fromProjectName, toProjectName/* , optional*/) { if (!this._projects[fromProjectName]) { throw new Error( @@ -69,18 +81,49 @@ class ProjectGraph { } } + /** - * Visit every project in the graph that can be reached by the given entry project exactly once. - * The entry project defaults to the root project. - * In case a cycle is detected, an error is thrown - * - * @param {Function} callback - */ - async traverseBreadthFirst(callback) { - // TODO: Add parameter to define point of entry, defaulting to root + * Callback for graph traversal operations + * + * @public + * @async + * @callback module:@ui5/project.graph.ProjectGraph~traversalCallback + * @param {object} parameters Parameters passed to the callback + * @param {module:@ui5/project.specifications.Project} parameters.project The project that is currently visited + * @param {module:@ui5/project.graph.ProjectGraph~getDependencies} parameters.getDependencies + * Function to access the dependencies of the project that is currently visited. + * @returns {Promise} Must return a promise on which the graph traversal will wait + */ + + /** + * Helper function available in the + * traversalCallback]{@link module:@ui5/project.graph.ProjectGraph~traversalCallback} to access the + * dependencies of the corresponding project in the current graph. + *

+ * Note that transitive dependencies can't be accessed this way. Projects should rather add a direct + * dependency to projects they need access to. + * + * @public + * @function module:@ui5/project.graph.ProjectGraph~getDependencies + * @returns {Array.} Direct dependencies of the visited project + */ + + /** + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @public + * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called + * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + */ + async traverseBreadthFirst(callback, startName = this._rootProjectName) { + if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`); + } const queue = [{ - projectNames: [this._rootProjectName], + projectNames: [startName], predecessors: [] }]; @@ -114,7 +157,7 @@ class ProjectGraph { await callback({ project: this.getProject(projectName), getDependencies: () => { - return dependencies.map(($) => this.getProject($.projectName)); + return dependencies.map(($) => this.getProject($)); } }); })(); @@ -123,15 +166,18 @@ class ProjectGraph { } /** - * Visit every project in the graph that can be reached by the given entry project exactly once. - * The entry project defaults to the root project. - * In case a cycle is detected, an error is thrown - * - * @param {Function} callback - */ - async traverseDepthFirst(callback) { - // TODO: Add parameter to define point of entry, defaulting to root - return this._traverseDepthFirst(this._rootProjectName, {}, [], callback); + * Visit every project in the graph that can be reached by the given entry project exactly once. + * The entry project defaults to the root project. + * In case a cycle is detected, an error is thrown + * + * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called + * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + */ + async traverseDepthFirst(callback, startName = this._rootProjectName) { + if (!this.getProject(startName)) { + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`); + } + return this._traverseDepthFirst(startName, {}, [], callback); } async _traverseDepthFirst(projectName, visited, predecessors, callback) { @@ -156,7 +202,7 @@ class ProjectGraph { await callback({ project: this.getProject(projectName), getDependencies: () => { - return dependencies.map(($) => this.getProject($.projectName)); + return dependencies.map(($) => this.getProject($)); } }); })(); diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js index f7eeb3727..291cc0b80 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/AbstractSpecification.js @@ -14,12 +14,20 @@ class AbstractSpecification { if (new.target === AbstractSpecification) { throw new TypeError("Class 'AbstractSpecification' is abstract"); } - if (!id || !version || !modulePath || !configuration) { - throw new Error(`Could not create Project: One or more required parameters are missing`); + if (!id) { + throw new Error(`Could not create specification: Missing or empty parameter 'id'`); + } + if (!version) { + throw new Error(`Could not create specification: Missing or empty parameter 'version'`); + } + if (!modulePath) { + throw new Error(`Could not create specification: Missing or empty parameter 'modulePath'`); + } + if (!configuration) { + throw new Error(`Could not create specification: Missing or empty parameter 'configuration'`); } - if (!(configuration instanceof Configuration)) { - throw new Error(`Could not create project: 'configuration' must be an instance of ` + + throw new Error(`Could not create specification: 'configuration' must be an instance of ` + `@ui5/project.specifications.Configuration`); } diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js new file mode 100644 index 000000000..fd064a3b1 --- /dev/null +++ b/test/lib/graph/ProjectGraph.js @@ -0,0 +1,645 @@ +const test = require("ava"); +const sinonGlobal = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); +const Configuration = require("../../../lib/specifications/Configuration"); +const Project = require("../../../lib/specifications/Project"); +// const Extension = require("../../../lib/specifications/Extension"); + +function createProject(name) { + const basicConfiguration = new Configuration({ + specVersion: "2.3", + kind: "project", + metadata: {name} + }); + + return new Project({ + id: "application.a.id", + version: "1.0.0", + modulePath: "some path", + configuration: basicConfiguration + }); +} + +function traverseBreadthFirst(...args) { + return _traverse(...args, true); +} + +function traverseDepthFirst(...args) { + return _traverse(...args, false); +} + +async function _traverse(t, graph, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await graph.traverseBreadthFirst(callbackStub); + } else { + await graph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); +} + +test.beforeEach((t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.log = { + warn: sinon.stub(), + verbose: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + isLevelEnabled: () => true + }; + sinon.stub(logger, "getLogger").callThrough() + .withArgs("graph:ProjectGraph").returns(t.context.log); + t.context.ProjectGraph = mock.reRequire("../../../lib/graph/ProjectGraph"); + logger.getLogger.restore(); // Immediately restore global stub for following tests +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Instantiate a basic project graph", async (t) => { + const {ProjectGraph} = t.context; + t.notThrows(() => { + new ProjectGraph({ + rootProjectName: "my root project", + extensions: [ + "some extension" + ] + }); + }, "Should not throw"); +}); + +test("Instantiate a basic project with missing parameter rootProjectName", async (t) => { + const {ProjectGraph} = t.context; + const error = t.throws(() => { + new ProjectGraph({ + extensions: [ + "some extension" + ] + }); + }); + t.is(error.message, "Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'", + "Should throw with expected error message"); +}); + +test("getRoot", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "application.a" + }); + const project = createProject("application.a"); + graph.addProject(project); + const res = graph.getRoot(); + t.is(res, project, "Should return correct root project"); +}); + +test("getRoot: Root not added to graph", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "application.a" + }); + + const error = t.throws(() => { + graph.getRoot(); + }); + t.is(error.message, + "Unable to find root project with name application.a in graph", + "Should throw with expected error message"); +}); + +test("add-/getProject", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project = createProject("application.a"); + graph.addProject(project); + const res = graph.getProject("application.a"); + t.is(res, project, "Should return correct project"); +}); + +test("addProject: Add duplicate", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = createProject("application.a"); + graph.addProject(project1); + + const project2 = createProject("application.a"); + const error = t.throws(() => { + graph.addProject(project2); + }); + t.is(error.message, + "Failed to add project application.a to the graph: A project with that name has already been added", + "Should throw with expected error message"); + + const res = graph.getProject("application.a"); + t.is(res, project1, "Should return correct project"); +}); + +test("addProject: Add duplicate with ignoreDuplicates", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = createProject("application.a"); + graph.addProject(project1); + + const project2 = createProject("application.a"); + t.notThrows(() => { + graph.addProject(project2, true); + }, "Should not throw when adding duplicates"); + + const res = graph.getProject("application.a"); + t.is(res, project1, "Should return correct project"); +}); + +test("addProject: Add project with integer-like name", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project = createProject("1337"); + + const error = t.throws(() => { + graph.addProject(project); + }); + t.is(error.message, + "Failed to add project 1337 to graph: Project name must not be integer-like", + "Should throw with expected error message"); +}); + +test("getProject: Project is not in graph", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const res = graph.getProject("application.a"); + t.is(res, undefined, "Should return undefined"); +}); + +test("declareDependency", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + t.deepEqual(graph._adjList, { + "library.a": { + "library.b": {} + }, + "library.b": {} + }, "Should store dependency correctly in internal structure"); + + graph.declareDependency("library.b", "library.a"); + t.deepEqual(graph._adjList, { + "library.a": { + "library.b": {} + }, + "library.b": { + "library.a": {} + } + }, "Should store additional dependency correctly in internal structure"); +}); + +test("declareDependency: Unknown source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.b")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.b"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.b: Unable " + + "to find project with name library.a in graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Unknown target", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.b"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.b: Unable " + + "to find project with name library.b in graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Already declared", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 1, "log.warn should be called once"); + t.is(log.warn.getCall(0).args[0], + `Dependency has already been declared: library.a depends on library.b`, + "log.warn should be called once with the expected argument"); +}); + +test("traverseBreadthFirst", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b" + ]); +}); + +test("traverseBreadthFirst: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); +}); + +test("traverseBreadthFirst: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: library.a* -> library.b -> library.a*", + "Should throw with expected error message"); +}); + +test("traverseBreadthFirst: No cycle when visited breadth first", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.b"); + + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); +}); + +test("traverseBreadthFirst: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + + const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {})); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.a in graph", + "Should throw with expected error message"); +}); + +test("traverseBreadthFirst: Custom start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseBreadthFirst(callbackStub, "library.b"); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.b", + "library.c" + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseBreadthFirst: getDependencies callback", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 3, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + const dependencies = callbackStub.getCalls().map((call) => { + return call.args[0].getDependencies().map((dep) => { + return dep.getName(); + }); + }); + + t.deepEqual(callbackCalls, [ + "library.a", + "library.b", + "library.c" + ], "Traversed graph in correct order"); + + t.deepEqual(dependencies, [ + ["library.b", "library.c"], + ["library.c"], + [] + ], "Provided correct dependencies for each visited project"); +}); + +test("traverseBreadthFirst: Dependency declaration order is followed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph1.addProject(createProject("library.a")); + graph1.addProject(createProject("library.b")); + graph1.addProject(createProject("library.c")); + graph1.addProject(createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + await traverseBreadthFirst(t, graph1, [ + "library.a", + "library.b", + "library.c", + "library.d" + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph2.addProject(createProject("library.a")); + graph2.addProject(createProject("library.b")); + graph2.addProject(createProject("library.c")); + graph2.addProject(createProject("library.d")); + + graph2.declareDependency("library.a", "library.d"); + graph2.declareDependency("library.a", "library.c"); + graph2.declareDependency("library.a", "library.b"); + + await traverseBreadthFirst(t, graph2, [ + "library.a", + "library.d", + "library.c", + "library.b" + ]); +}); + +test("traverseDepthFirst", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + await traverseDepthFirst(t, graph, [ + "library.b", + "library.a" + ]); +}); + +test("traverseDepthFirst: No project visited twice", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await traverseDepthFirst(t, graph, [ + "library.c", + "library.b", + "library.a" + ]); +}); + +test("traverseDepthFirst: Detect cycle", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.a"); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: library.a* -> library.b -> library.a*", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Cycle which does not occur in BFS", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.b"); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Detected cyclic dependency chain: library.a -> library.b* -> library.c -> library.b*", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Can't find start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + + const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); + t.is(error.message, + "Failed to start graph traversal: Could not find project library.a in graph", + "Should throw with expected error message"); +}); + +test("traverseDepthFirst: Custom start node", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseDepthFirst(callbackStub, "library.b"); + + t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "library.c", + "library.b" + ], "Traversed graph in correct order, starting with library.b"); +}); + +test("traverseDepthFirst: getDependencies callback", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 3, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + const dependencies = callbackStub.getCalls().map((call) => { + return call.args[0].getDependencies().map((dep) => { + return dep.getName(); + }); + }); + + t.deepEqual(callbackCalls, [ + "library.c", + "library.b", + "library.a", + ], "Traversed graph in correct order"); + + t.deepEqual(dependencies, [ + [], + ["library.c"], + ["library.b", "library.c"], + ], "Provided correct dependencies for each visited project"); +}); + +test("traverseDepthFirst: Dependency declaration order is followed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph1.addProject(createProject("library.a")); + graph1.addProject(createProject("library.b")); + graph1.addProject(createProject("library.c")); + graph1.addProject(createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + await traverseDepthFirst(t, graph1, [ + "library.b", + "library.c", + "library.d", + "library.a", + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph2.addProject(createProject("library.a")); + graph2.addProject(createProject("library.b")); + graph2.addProject(createProject("library.c")); + graph2.addProject(createProject("library.d")); + + graph2.declareDependency("library.a", "library.d"); + graph2.declareDependency("library.a", "library.c"); + graph2.declareDependency("library.a", "library.b"); + + await traverseDepthFirst(t, graph2, [ + "library.d", + "library.c", + "library.b", + "library.a", + ]); +}); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index a860834d5..b74d5a617 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -26,7 +26,7 @@ test.beforeEach((t) => { }; sinon.stub(logger, "getLogger").callThrough() .withArgs("graph:projectGraphFromTree").returns(t.context.log); - t.context.projectGraphFromTree = mock.reRequire("../../../lib/graph/projectGraphFromTree.js"); + t.context.projectGraphFromTree = mock.reRequire("../../../lib/graph/projectGraphFromTree"); logger.getLogger.restore(); // Immediately restore global stub for following tests }); @@ -266,7 +266,7 @@ test("Missing id for root project", async (t) => { dependencies: [] }; await t.throwsAsync(projectGraphFromTree(tree), - {message: "Could not create Module: Missing or empty id parameter"}, "Rejected with error"); + {message: "Could not create Module: Missing or empty parameter 'id'"}, "Rejected with error"); }); test("No type configured for root project", async (t) => { diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 8f46812e9..a4683addb 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -1,12 +1,11 @@ const test = require("ava"); -const sinon = require("sinon"); const path = require("path"); const Project = require("../../../lib/specifications/Project"); const Configuration = require("../../../lib/specifications/Configuration"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const emptyConfiguration = new Configuration({ +const basicConfiguration = new Configuration({ specVersion: "2.3", kind: "project", metadata: {name: "application.a"} @@ -15,16 +14,9 @@ const basicProjectInput = { id: "application.a.id", version: "1.0.0", modulePath: applicationAPath, - configuration: emptyConfiguration + configuration: basicConfiguration }; -// test.beforeEach((t) => { -// }); - -test.afterEach.always(() => { - sinon.restore(); -}); - test("Instantiate a basic project", async (t) => { const project = new Project(basicProjectInput); t.is(project.getName(), "application.a", "Returned correct name"); @@ -34,7 +26,7 @@ test("Instantiate a basic project", async (t) => { test("_getConfiguration", async (t) => { const project = new Project(basicProjectInput); - t.is(await project._getConfiguration(), emptyConfiguration, "Returned correct configuration instance"); + t.is(await project._getConfiguration(), basicConfiguration, "Returned correct configuration instance"); }); test("Access project root resources via reader", async (t) => { From 6bed2f0f57b93c8bbb8314efa7e1b62b2b42c2e0 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 15:50:00 +0100 Subject: [PATCH 09/99] Refactor Module. Add support for dependency and collection shims --- lib/{ => graph}/Module.js | 156 ++++++---- lib/graph/ProjectGraph.js | 7 +- lib/graph/ShimCollection.js | 4 +- lib/graph/projectGraphFromTree.js | 147 ++++++--- test/lib/{ => graph}/Module.js | 17 +- test/lib/graph/ProjectGraph.js | 28 +- test/lib/graph/projectGraphFromTree.js | 415 +++++++++++++++++++++++++ 7 files changed, 641 insertions(+), 133 deletions(-) rename lib/{ => graph}/Module.js (75%) rename test/lib/{ => graph}/Module.js (55%) diff --git a/lib/Module.js b/lib/graph/Module.js similarity index 75% rename from lib/Module.js rename to lib/graph/Module.js index 2308b6ed2..c00615180 100644 --- a/lib/Module.js +++ b/lib/graph/Module.js @@ -4,12 +4,12 @@ const {promisify} = require("util"); const readFile = promisify(fs.readFile); const jsyaml = require("js-yaml"); const resourceFactory = require("@ui5/fs").resourceFactory; -const Project = require("./specifications/Project"); -const Extension = require("./specifications/Extension"); -const Configuration = require("./specifications/Configuration"); -const {validate} = require("./validation/validator"); +const Project = require("../specifications/Project"); +const Extension = require("../specifications/Extension"); +const Configuration = require("../specifications/Configuration"); +const {validate} = require("../validation/validator"); -const log = require("@ui5/logger").getLogger("Module"); +const log = require("@ui5/logger").getLogger("graph:Module"); const defaultConfigPath = "ui5.yaml"; @@ -26,12 +26,12 @@ class Module { * @param {string} [parameters.configPath=ui5.yaml] * Either a path relative to `modulePath` which will be resolved by @ui5/fs (default), * or an absolute File System path to the project configuration file. - * @param {object} [parameters.configuration] - * Configuration object to use. If supplied, no ui5.yaml will be read + * @param {object|object[]} [parameters.configuration] + * Configuration object or array of objects to use. If supplied, no ui5.yaml will be read * @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection] * Collection of shims that might be relevant for this module */ - constructor({id, version, modulePath, configPath = defaultConfigPath, configuration, shimCollection}) { + constructor({id, version, modulePath, configPath = defaultConfigPath, configuration = [], shimCollection}) { if (!id) { throw new Error(`Could not create Module: Missing or empty parameter 'id'`); } @@ -47,7 +47,11 @@ class Module { this._modulePath = modulePath; this._configPath = configPath; this._dependencies = {}; - this._suppliedConfig = configuration; + + if (!Array.isArray(configuration)) { + configuration = [configuration]; + } + this._suppliedConfigs = configuration; if (shimCollection) { // Retrieve and clone shims in constructor @@ -80,61 +84,74 @@ class Module { return this._modulePath; } - getDependencies() { - return []; + async getSpecifications() { + if (this._pGetSpecifications) { + return this._pGetSpecifications; + } + + return this._pGetSpecifications = this._getSpecifications(); } - async getProject() { - const configs = await this.getConfigurations(); - // getConfigurations promises us to return none or exactly one project configuration - const projectConfig = configs.find((config) => { - return config.getKind() === "project"; - }); + async _getSpecifications() { + const configs = await this._getConfigurations(); - if (projectConfig) { - return new Project({ - id: this.getId(), - version: this.getVersion(), - modulePath: this.getPath(), - configuration: projectConfig - }); - } - } + let project; + const extensions = []; + configs.forEach((configuration) => { + const kind = configuration.getKind(); - async getExtensions() { - const configs = await this.getConfigurations(); - const extensionConfigs = configs.filter((config) => { - return config.getKind() === "extension"; - }); - return extensionConfigs.map((config) => { - return new Extension({ - id: this.getId(), - version: this.getVersion(), - modulePath: this.getPath(), - configuration: config - }); + switch (kind) { + case "project": + if (project) { + throw new Error( + `Invalid configuration for module ${this.getId()}: Per module there ` + + `must be no more than one configuration of kind 'project'`); + } + log.verbose(`Module ${this.getId()} contains project ${configuration.getName()}`); + project = new Project({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration + }); + break; + case "extension": + log.verbose(`Module ${this.getId()} contains extension ${configuration.getName()}`); + extensions.push(new Extension({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration + })); + break; + default: + throw new Error( + `Encountered unexpected specification configuration of kind ${kind} ` + + `Supported kinds are 'project' and 'extension'`); + } }); + + return { + project, + extensions + }; } /** * Configuration */ - async getConfigurations() { - if (this._pGetConfigurations) { - return this._pGetConfigurations; - } - - return this._pGetConfigurations = this._getConfigurations(); - } - async _getConfigurations() { let configurations; - if (this._suppliedConfig) { - configurations = [await this._createConfigurationInstance(this._suppliedConfig)]; - } else { - configurations = await this._loadProjectConfiguration(); + + configurations = await this._getSuppliedConfigurations(); + + if (!configurations || !configurations.length) { + configurations = await this._getYamlConfigurations(); } - return configurations; + if (!configurations || !configurations.length) { + configurations = await this._getShimConfigurations(); + } + return configurations || []; } async _createConfigurationInstance(config) { @@ -146,7 +163,7 @@ class Module { return new Configuration(config); } - async _createProjectConfigurationFromShim() { + async _createConfigurationFromShim() { const config = this._applyShims(); if (config) { this._normalizeConfig(config); @@ -166,16 +183,30 @@ class Module { return config; } - async _loadProjectConfiguration() { + async _getSuppliedConfigurations() { + if (this._suppliedConfigs.length) { + log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); + return await Promise.all(this._suppliedConfigs.map(async (config) => { + return this._createConfigurationInstance(config); + })); + } + } + + async _getShimConfigurations() { + // No project configuration found + // => Try to create one from shims + const shimConfiguration = await this._createConfigurationFromShim(); + if (shimConfiguration) { + log.verbose(`Created configuration from shim extensions for module ${this.getId()}`); + return [shimConfiguration]; + } + } + + async _getYamlConfigurations() { const configs = await this._readConfigFile(); if (!configs || !configs.length) { - // No project configuration found - // => Try to create one from shims - const shimConfiguration = await this._createProjectConfigurationFromShim(); - if (shimConfiguration) { - return [shimConfiguration]; - } + log.verbose(`Could not find a configuration file for module ${this.getId()}`); return []; } @@ -209,13 +240,6 @@ class Module { const configurations = []; if (projectConfigs.length) { configurations.push(await this._createConfigurationInstance(projectConfigs[0])); - } else { - // No project configuration found - // => Try to create one from shims - const shimConfiguration = await this._createProjectConfigurationFromShim(); - if (shimConfiguration) { - configurations.push(shimConfiguration); - } } await Promise.all(extensionConfigs.map(async (config) => { diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 82db23ac7..6f4d64734 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -81,6 +81,9 @@ class ProjectGraph { } } + getDependencies(projectName) { + return Object.keys(this._adjList[projectName]); + } /** * Callback for graph traversal operations @@ -147,7 +150,7 @@ class ProjectGraph { return visited[projectName] = (async () => { const newPredecessors = [...predecessors, projectName]; - const dependencies = Object.keys(this._adjList[projectName]); + const dependencies = this.getDependencies(projectName); queue.push({ projectNames: dependencies, @@ -194,7 +197,7 @@ class ProjectGraph { } return visited[projectName] = (async () => { const newPredecessors = [...predecessors, projectName]; - const dependencies = Object.keys(this._adjList[projectName]); + const dependencies = this.getDependencies(projectName); await Promise.all(dependencies.map((depName) => { return this._traverseDepthFirst(depName, visited, newPredecessors, callback); })); diff --git a/lib/graph/ShimCollection.js b/lib/graph/ShimCollection.js index ea35b4c7c..5fb0e87f0 100644 --- a/lib/graph/ShimCollection.js +++ b/lib/graph/ShimCollection.js @@ -40,8 +40,8 @@ class ShimCollection { return this._configShims[moduleId]; } - getDependencyShims(moduleId) { - return this._dependencyShims[moduleId]; + getAllDependencyShims() { + return this._dependencyShims; } getCollectionShims(moduleId) { diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 8d87564f7..0d005c259 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -1,4 +1,5 @@ -const Module = require("../Module"); +const path = require("path"); +const Module = require("./Module"); const ProjectGraph = require("./ProjectGraph"); const ShimCollection = require("./ShimCollection"); const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); @@ -39,7 +40,7 @@ module.exports = async function(tree) { configPath: tree.configPath, configuration: tree.configuration }); - const rootProject = await rootModule.getProject(); + const {project: rootProject, extensions: rootExtensions} = await rootModule.getSpecifications(); if (!rootProject) { throw new Error( `Failed to crate a UI5 project from module ${tree.id} at ${tree.path}. ` + @@ -56,8 +57,7 @@ module.exports = async function(tree) { qualifiedApplicationProject = rootProject; } - const extensions = await rootModule.getExtensions(); - addShimsToCollection(extensions); + addShimsToCollection(rootExtensions); const projectGraph = new ProjectGraph({ rootProjectName: rootProjectName @@ -77,8 +77,38 @@ module.exports = async function(tree) { while (queue.length) { const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue const res = await Promise.all(nodes.map(async (node) => { + // First check for collection shims + const collectionShims = shimCollection.getCollectionShims(node.id); + if (collectionShims && collectionShims.length) { + log.verbose( + `One or more module collection shims have been defined for module ${node.id}. ` + + `Therefore the module itself will not be resolved.`); + + const shimmedNodes = collectionShims.map(({name, shim}) => { + log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); + return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { + const shimModulePath = path.join(node.path, shimModuleRelPath); + log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); + return { + id: shimModuleId, + version: node.version, + path: shimModulePath + }; + }); + }); + + queue.push({ + nodes: Array.prototype.concat.apply([], shimmedNodes), + parentProjectName, + }); + return { + skip: true + }; + } + let ui5Module = moduleCollection[node.id]; if (!ui5Module) { + log.verbose(`Creating module ${node.id}...`); ui5Module = moduleCollection[node.id] = new Module({ id: node.id, version: node.version, @@ -94,8 +124,7 @@ module.exports = async function(tree) { `\nPath of additional node (this one will be ignored in favor of the other): ${node.path}`); } - const project = await ui5Module.getProject(); - const extensions = await ui5Module.getExtensions(); + const {project, extensions} = await ui5Module.getSpecifications(); return { node, @@ -107,59 +136,97 @@ module.exports = async function(tree) { // Keep this out of the async map function to ensure // all projects and extensions are applied a deterministic order for (let i = 0; i < res.length; i++) { - const {node, project, extensions} = res[i]; + const {node, project, extensions, skip} = res[i]; + if (skip) { + // Skip this node + continue; + } if (extensions.length) { addShimsToCollection(extensions); extensions.push(extensions); } - if (!project) { - continue; - } - - - const projectName = project.getName(); - if (project.getType() === "application") { - // Special handling of application projects of which there must be exactly *one* - // in the graph. Others shall be ignored. - if (!qualifiedApplicationProject) { - log.verbose(`Project ${projectName} qualified as application project for project graph`); - qualifiedApplicationProject = project; - } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { - // Project is not a duplicate of an already qualified project (which should - // still be processed below), but a unique, additional application project - - // TODO: Should this rather be a verbose logging? - // projectPreprocessor handled this like any project that got ignored for some reason and did a - // (in this case misleading) general verbose logging "Ignoring project with missing configuration" - log.info( - `Excluding additional application project ${projectName} from graph. `+ - `The project graph can only feature a single project of type application. ` + - `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); - continue; + if (project) { + const projectName = project.getName(); + if (project.getType() === "application") { + // Special handling of application projects of which there must be exactly *one* + // in the graph. Others shall be ignored. + if (!qualifiedApplicationProject) { + log.verbose(`Project ${projectName} qualified as application project for project graph`); + qualifiedApplicationProject = project; + } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { + // Project is not a duplicate of an already qualified project (which should + // still be processed below), but a unique, additional application project + + // TODO: Should this rather be a verbose logging? + // projectPreprocessor handled this like any project that got ignored for some reason and did a + // (in this case misleading) general verbose logging: + // "Ignoring project with missing configuration" + log.info( + `Excluding additional application project ${projectName} from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); + continue; + } } - } - // if (!node.deduped) { - // Even if not deduped, the node might occur multiple times in the tree (on separate branches). - // Therefore still supplying the ignore duplicates parameter here (true) - projectGraph.addProject(project, true); - // } + // if (!node.deduped) { + // Even if not deduped, the node might occur multiple times in the tree (on separate branches). + // Therefore still supplying the ignore duplicates parameter here (true) + projectGraph.addProject(project, true); + // } - if (parentProjectName) { - projectGraph.declareDependency(parentProjectName, projectName); + if (parentProjectName) { + projectGraph.declareDependency(parentProjectName, projectName); + } } if (node.dependencies && !node.deduped) { queue.push({ // copy array, so that the queue is stable while ignored project dependencies are removed nodes: [...node.dependencies], - parentProjectName: projectName, + parentProjectName: project ? project.getName() : parentProjectName, }); } } } + // Appply dependency shims + for (const [shimmedModuleId, moduleDepShims] of Object.entries(shimCollection.getAllDependencyShims())) { + const sourceModule = moduleCollection[shimmedModuleId]; + + for (let j = 0; j < moduleDepShims.length; j++) { + const depShim = moduleDepShims[j]; + if (!sourceModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Module ${shimmedModuleId} is unknown`); + continue; + } + const {project: sourceProject} = await sourceModule.getSpecifications(); + if (!sourceProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Source module ${shimmedModuleId} does not contain a project`); + continue; + } + for (let k = 0; k < depShim.shim.length; k++) { + const targetModuleId = depShim.shim[k]; + const targetModule = moduleCollection[targetModuleId]; + if (!targetModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module $${depShim} is unknown`); + continue; + } + const {project: targetProject} = await targetModule.getSpecifications(); + if (!targetProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module ${targetModuleId} does not contain a project`); + continue; + } + projectGraph.declareDependency(sourceProject.getName(), targetProject.getName()); + } + } + } + return projectGraph; }; diff --git a/test/lib/Module.js b/test/lib/graph/Module.js similarity index 55% rename from test/lib/Module.js rename to test/lib/graph/Module.js index 148a15627..02d1803eb 100644 --- a/test/lib/Module.js +++ b/test/lib/graph/Module.js @@ -1,9 +1,9 @@ const test = require("ava"); const sinon = require("sinon"); const path = require("path"); -const Module = require("../../lib/Module"); +const Module = require("../../../lib/graph/Module"); -const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const basicModuleInput = { id: "application.a.id", @@ -20,9 +20,9 @@ test.afterEach.always(() => { test("Instantiate a basic module", async (t) => { const ui5Module = new Module(basicModuleInput); - t.is(ui5Module.getId(), "application.a.id", "Returned correct ID"); - t.is(ui5Module.getVersion(), "1.0.0", "Returned correct version"); - t.is(ui5Module.getPath(), applicationAPath, "Returned correct module path"); + t.is(ui5Module.getId(), "application.a.id", "Should return correct ID"); + t.is(ui5Module.getVersion(), "1.0.0", "Should return correct version"); + t.is(ui5Module.getPath(), applicationAPath, "Should return correct module path"); }); test("Access module root resources via reader", async (t) => { @@ -32,8 +32,9 @@ test("Access module root resources via reader", async (t) => { t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); }); -test("Get projects from module", async (t) => { +test("Get specifications from module", async (t) => { const ui5Module = new Module(basicModuleInput); - const project = await ui5Module.getProject(); - t.is(project.getName(), "application.a", "Returned correct project"); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "application.a", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); }); diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index fd064a3b1..07d456c4e 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -189,7 +189,7 @@ test("getProject: Project is not in graph", async (t) => { t.is(res, undefined, "Should return undefined"); }); -test("declareDependency", async (t) => { +test("declareDependency / getDependencies", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ rootProjectName: "my root project" @@ -198,22 +198,20 @@ test("declareDependency", async (t) => { graph.addProject(createProject("library.b")); graph.declareDependency("library.a", "library.b"); - t.deepEqual(graph._adjList, { - "library.a": { - "library.b": {} - }, - "library.b": {} - }, "Should store dependency correctly in internal structure"); + t.deepEqual(graph.getDependencies("library.a"), [ + "library.b" + ], "Should store and return correct dependencies for library.a"); + t.deepEqual(graph.getDependencies("library.b"), [], + "Should store and return correct dependencies for library.b"); graph.declareDependency("library.b", "library.a"); - t.deepEqual(graph._adjList, { - "library.a": { - "library.b": {} - }, - "library.b": { - "library.a": {} - } - }, "Should store additional dependency correctly in internal structure"); + + t.deepEqual(graph.getDependencies("library.a"), [ + "library.b" + ], "Should store and return correct dependencies for library.a"); + t.deepEqual(graph.getDependencies("library.b"), [ + "library.a" + ], "Should store and return correct dependencies for library.b"); }); test("declareDependency: Unknown source", async (t) => { diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index b74d5a617..b3dc52077 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -3,6 +3,7 @@ const path = require("path"); const sinonGlobal = require("sinon"); const mock = require("mock-require"); const logger = require("@ui5/logger"); +const ValidationError = require("../../../lib/validation/ValidationError"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b"); @@ -14,6 +15,14 @@ const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); +const legacyLibraryAPath = path.join(__dirname, "..", "fixtures", "legacy.library.a"); +const legacyLibraryBPath = path.join(__dirname, "..", "fixtures", "legacy.library.b"); +const legacyCollectionAPath = path.join(__dirname, "..", "fixtures", "legacy.collection.a"); +const legacyCollectionLibraryX = path.join(__dirname, "..", "fixtures", "legacy.collection.a", + "src", "legacy.library.x"); +const legacyCollectionLibraryY = path.join(__dirname, "..", "fixtures", "legacy.collection.a", + "src", "legacy.library.y"); + test.beforeEach((t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); @@ -180,6 +189,7 @@ async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) { const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); + return projectGraph; } test("Project with inline configuration", async (t) => { @@ -202,6 +212,58 @@ test("Project with inline configuration", async (t) => { ]); }); + +test("Project with inline configuration as array", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }] + }; + + await testBasicGraphCreationDfs(t, tree, [ + "xy" + ]); +}); + +test("Project with inline configuration for two projects", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, { + specVersion: "2.3", + type: "library", + metadata: { + name: "yz" + } + }] + }; + + const {projectGraphFromTree} = t.context; + await t.throwsAsync(projectGraphFromTree(tree), + { + message: + "Invalid configuration for module application.a.id: Per module there " + + "must be no more than one configuration of kind 'project'" + }, + "Rejected with error"); +}); + test("Project with configPath", async (t) => { const tree = { id: "application.a.id", @@ -1032,3 +1094,356 @@ const treeWithInvalidModules = { } } }; + +/* ======================================================================================= */ +/* ======= The following tests have been derived from the existing extension tests ======= */ + +/* The following scenario is supported by the projectPreprocessor but not by projectGraphFromTree + * A shim extension located in a project's dependencies can't influence other dependencies of that project anymore + * TODO: Check whether the above is fine for us + +test.only("Legacy: Project with project-shim extension with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + dependencies: [], + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + } + }, { + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); +});*/ + +test("Project with project-shim extension with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + }], + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); +}); + +test("Project with project-shim extension dependency with dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shim.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + } + } + } + }, + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }], + }] + }; + await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.a", + "application.a", + ]); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); + +test("Project with project-shim extension with invalid dependency configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library" + } + } + } + }], + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }] + }; + const {projectGraphFromTree} = t.context; + const validationError = await t.throwsAsync(projectGraphFromTree(tree), { + instanceOf: ValidationError + }); + t.true(validationError.message.includes("Configuration must have required property 'metadata'"), + "ValidationError should contain error about missing metadata configuration"); +}); + +test("Project with project-shim extension with dependency declaration and configuration", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.a.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.a", + } + }, + "legacy.library.b.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.b", + } + } + }, + dependencies: { + "legacy.library.a.id": [ + "legacy.library.b.id" + ] + } + } + }, + dependencies: [{ + id: "legacy.library.a.id", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }, { + id: "legacy.library.b.id", + version: "1.0.0", + path: legacyLibraryBPath, + dependencies: [] + }], + }] + }; + // application.a and legacy.library.a will both have a dependency to legacy.library.b + // (one because it's the actual dependency and one because it's a shimmed dependency) + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.b", + "legacy.library.a", + "application.a", + ]); + t.deepEqual(graph.getDependencies("legacy.library.a"), [ + "legacy.library.b" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); + +test("Project with project-shim extension with collection", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "extension.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.x.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.x", + } + }, + "legacy.library.y.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.y", + } + } + }, + dependencies: { + "application.a.id": [ + "legacy.library.x.id", + "legacy.library.y.id" + ], + "legacy.library.x.id": [ + "legacy.library.y.id" + ] + }, + collections: { + "legacy.collection.a": { + modules: { + "legacy.library.x.id": "src/legacy.library.x", + "legacy.library.y.id": "src/legacy.library.y" + } + } + } + } + }, + dependencies: [{ + id: "legacy.collection.a", + version: "1.0.0", + path: legacyCollectionAPath, + dependencies: [] + }] + }] + }; + + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.y", + "legacy.library.x", + "application.a", + ]); + t.deepEqual(graph.getDependencies("application.a"), [ + "legacy.library.x", + "legacy.library.y" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); +}); From 4f0829892ecdfe661d21ee92e53b7a5c6538cfad Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 16:20:21 +0100 Subject: [PATCH 10/99] Add extension support to ProjectGraph --- lib/graph/ProjectGraph.js | 50 ++++++++++-- lib/graph/projectGraphFromTree.js | 42 ++++++---- test/lib/extensions.js | 4 - test/lib/graph/ProjectGraph.js | 86 +++++++++++++++++--- test/lib/graph/projectGraphFromTree.js | 107 +++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 34 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 6f4d64734..68c573bcc 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -10,18 +10,17 @@ class ProjectGraph { * @public * @param {object} parameters Parameters * @param {string} parameters.rootProjectName Root project name - * @param {Array.} parameters.extensions - * Final list of extensions to be used in this project tree */ - constructor({rootProjectName, extensions = []}) { + constructor({rootProjectName}) { if (!rootProjectName) { throw new Error(`Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'`); } this._rootProjectName = rootProjectName; - this._extensions = Object.freeze(extensions); this._projects = {}; // maps project name to instance this._adjList = {}; // maps project name to edges/dependencies + + this._extensions = {}; // maps extension name to instance } getRoot() { @@ -32,6 +31,11 @@ class ProjectGraph { return rootProject; } + /** + * @public + * @param {module:@ui5/project.specification.Project} project Project which should be added to the graph + * @param {boolean} [ignoreDuplicates=false] Whether an error should be thrown when a duplicate project is added + */ addProject(project, ignoreDuplicates) { const projectName = project.getName(); if (this._projects[projectName]) { @@ -39,7 +43,7 @@ class ProjectGraph { return; } throw new Error( - `Failed to add project ${projectName} to the graph: A project with that name has already been added`); + `Failed to add project ${projectName} to graph: A project with that name has already been added`); } if (!isNaN(projectName)) { // Reject integer-like project names. They would take precedence when traversing object keys which @@ -51,10 +55,46 @@ class ProjectGraph { this._adjList[projectName] = {}; } + /** + * @public + * @param {string} projectName Name of the project to retrieve + * @returns {module:@ui5/project.specification.project|undefined} + * project instance or undefined if the project is unknown to the graph + */ getProject(projectName) { return this._projects[projectName]; } + /** + * @public + * @param {module:@ui5/project.specification.Extension} extension Extension which should be available in the graph + */ + addExtension(extension) { + const extensionName = extension.getName(); + if (this._extensions[extensionName]) { + throw new Error( + `Failed to add extension ${extensionName} to graph: ` + + `An extension with that name has already been added`); + } + if (!isNaN(extensionName)) { + // Reject integer-like extension names. They would take precedence when traversing object keys which + // might lead to unexpected behavior in the future. We don't really expect anyone to use such names anyways + throw new Error( + `Failed to add extension ${extensionName} to graph: Extension name must not be integer-like`); + } + this._extensions[extensionName] = extension; + } + + /** + * @public + * @param {string} extensionName Name of the extension to retrieve + * @returns {module:@ui5/project.specification.Extension|undefined} + * Extension instance or undefined if the extension is unknown to the graph + */ + getExtension(extensionName) { + return this._extensions[extensionName]; + } + /** * Declare a dependency from one project in the graph to another * diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 0d005c259..332232897 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -4,6 +4,26 @@ const ProjectGraph = require("./ProjectGraph"); const ShimCollection = require("./ShimCollection"); const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); + +function _handleExtensions(graph, shimCollection, extensions) { + extensions.forEach((extension) => { + const type = extension.getType(); + switch (type) { + case "project-shim": + shimCollection.addShim(extension); + break; + case "task": + case "server-middleware": + graph.addExtension(extension); + break; + default: + throw new Error( + `Encountered unexpected extension of type ${type} ` + + `Supported types are 'project-shim', 'task' and 'middleware'`); + } + }); +} + /** * Tree node * @@ -22,15 +42,6 @@ const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); */ module.exports = async function(tree) { const shimCollection = new ShimCollection(); - - function addShimsToCollection(ext) { - ext.forEach((e) => { - if (e.getType() === "project-shim") { - shimCollection.addShim(e); - } - }); - } - const moduleCollection = {}; const rootModule = new Module({ @@ -57,13 +68,18 @@ module.exports = async function(tree) { qualifiedApplicationProject = rootProject; } - addShimsToCollection(rootExtensions); const projectGraph = new ProjectGraph({ rootProjectName: rootProjectName }); projectGraph.addProject(rootProject); + function handleExtensions(extensions) { + return _handleExtensions(projectGraph, shimCollection, extensions); + } + + handleExtensions(rootExtensions); + const queue = []; if (tree.dependencies) { @@ -142,10 +158,8 @@ module.exports = async function(tree) { // Skip this node continue; } - if (extensions.length) { - addShimsToCollection(extensions); - extensions.push(extensions); - } + + handleExtensions(extensions); if (project) { const projectName = project.getName(); diff --git a/test/lib/extensions.js b/test/lib/extensions.js index 9b309d06d..70527f992 100644 --- a/test/lib/extensions.js +++ b/test/lib/extensions.js @@ -583,7 +583,6 @@ test("Project with unknown extension dependency inline configuration", (t) => { }); test("Project with task extension dependency", (t) => { - // "project-type" extension handling not yet implemented => test currently checks for error const tree = { id: "application.a", path: applicationAPath, @@ -617,7 +616,6 @@ test("Project with task extension dependency", (t) => { }); test("Project with task extension dependency - does not throw for invalid task path", async (t) => { - // "project-type" extension handling not yet implemented => test currently checks for error const tree = { id: "application.a", path: applicationAPath, @@ -648,7 +646,6 @@ test("Project with task extension dependency - does not throw for invalid task p test("Project with middleware extension dependency", (t) => { - // "project-type" extension handling not yet implemented => test currently checks for error const tree = { id: "application.a", path: applicationAPath, @@ -683,7 +680,6 @@ test("Project with middleware extension dependency", (t) => { }); test("Project with middleware extension dependency - middleware is missing configuration", async (t) => { - // "project-type" extension handling not yet implemented => test currently checks for error const tree = { id: "application.a", path: applicationAPath, diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 07d456c4e..1682c3053 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -4,12 +4,13 @@ const mock = require("mock-require"); const logger = require("@ui5/logger"); const Configuration = require("../../../lib/specifications/Configuration"); const Project = require("../../../lib/specifications/Project"); -// const Extension = require("../../../lib/specifications/Extension"); +const Extension = require("../../../lib/specifications/Extension"); function createProject(name) { const basicConfiguration = new Configuration({ specVersion: "2.3", kind: "project", + type: "application", metadata: {name} }); @@ -20,6 +21,21 @@ function createProject(name) { configuration: basicConfiguration }); } +function createExtension(name) { + const basicConfiguration = new Configuration({ + specVersion: "2.3", + kind: "extension", + type: "task", + metadata: {name} + }); + + return new Extension({ + id: "application.a.id", + version: "1.0.0", + modulePath: "some path", + configuration: basicConfiguration + }); +} function traverseBreadthFirst(...args) { return _traverse(...args, true); @@ -71,10 +87,7 @@ test("Instantiate a basic project graph", async (t) => { const {ProjectGraph} = t.context; t.notThrows(() => { new ProjectGraph({ - rootProjectName: "my root project", - extensions: [ - "some extension" - ] + rootProjectName: "my root project" }); }, "Should not throw"); }); @@ -82,11 +95,7 @@ test("Instantiate a basic project graph", async (t) => { test("Instantiate a basic project with missing parameter rootProjectName", async (t) => { const {ProjectGraph} = t.context; const error = t.throws(() => { - new ProjectGraph({ - extensions: [ - "some extension" - ] - }); + new ProjectGraph({}); }); t.is(error.message, "Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'", "Should throw with expected error message"); @@ -141,7 +150,7 @@ test("addProject: Add duplicate", async (t) => { graph.addProject(project2); }); t.is(error.message, - "Failed to add project application.a to the graph: A project with that name has already been added", + "Failed to add project application.a to graph: A project with that name has already been added", "Should throw with expected error message"); const res = graph.getProject("application.a"); @@ -189,6 +198,61 @@ test("getProject: Project is not in graph", async (t) => { t.is(res, undefined, "Should return undefined"); }); +test("add-/getExtension", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension = createExtension("extension.a"); + graph.addExtension(extension); + const res = graph.getExtension("extension.a"); + t.is(res, extension, "Should return correct extension"); +}); + +test("addExtension: Add duplicate", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension1 = createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = createExtension("extension.a"); + const error = t.throws(() => { + graph.addExtension(extension2); + }); + t.is(error.message, + "Failed to add extension extension.a to graph: An extension with that name has already been added", + "Should throw with expected error message"); + + const res = graph.getExtension("extension.a"); + t.is(res, extension1, "Should return correct extension"); +}); + +test("addExtension: Add extension with integer-like name", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension = createExtension("1337"); + + const error = t.throws(() => { + graph.addExtension(extension); + }); + t.is(error.message, + "Failed to add extension 1337 to graph: Extension name must not be integer-like", + "Should throw with expected error message"); +}); + +test("getExtension: Project is not in graph", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const res = graph.getExtension("extension.a"); + t.is(res, undefined, "Should return undefined"); +}); + test("declareDependency / getDependencies", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index b3dc52077..1478c94a3 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -1447,3 +1447,110 @@ test("Project with project-shim extension with collection", async (t) => { t.is(log.warn.callCount, 0, "log.warn should not have been called"); t.is(log.info.callCount, 0, "log.info should not have been called"); }); + +test("Project with unknown extension dependency inline configuration", async (t) => { + const tree = { + id: "application.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "xy" + } + }, + dependencies: [{ + id: "extension.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "phony-pony", + metadata: { + name: "pinky.pie" + } + }, + dependencies: [], + }], + }; + const {projectGraphFromTree} = t.context; + const validationError = await t.throwsAsync(projectGraphFromTree(tree), { + instanceOf: ValidationError + }); + t.true(validationError.message.includes("Configuration type must be equal to one of the allowed value"), + "ValidationError should contain error about missing metadata configuration"); +}); + +test("Project with task extension dependency", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "ext.task.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "task", + metadata: { + name: "task.a" + }, + task: { + path: "task.a.js" + } + }, + dependencies: [], + }] + }; + const graph = await testBasicGraphCreationDfs(t, tree, [ + "application.a" + ]); + t.truthy(graph.getExtension("task.a"), "Extension should be added to the graph"); +}); + +test("Project with middleware extension dependency", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "ext.middleware.a", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + kind: "extension", + type: "server-middleware", + metadata: { + name: "middleware.a" + }, + middleware: { + path: "middleware.a.js" + } + }, + dependencies: [], + }], + }; + const graph = await testBasicGraphCreationDfs(t, tree, [ + "application.a" + ]); + t.truthy(graph.getExtension("middleware.a"), "Extension should be added to the graph"); +}); From 035688fd2c242df7f8d70b883adcae7b906518e7 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 16:33:35 +0100 Subject: [PATCH 11/99] JSdoc and test cleanup --- index.js | 17 ++++++++++++++++- lib/graph/Module.js | 24 +++++++++++------------- lib/graph/ProjectGraph.js | 2 +- lib/graph/projectGraphFromTree.js | 17 +++++++++++------ test/lib/graph/projectGraphFromTree.js | 5 ----- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index e21fbfee6..33d786896 100644 --- a/index.js +++ b/index.js @@ -55,7 +55,22 @@ module.exports = { * @type {import('./lib/translators/static')} */ static: "./lib/translators/static" - } + }, + /** + * @public + * @alias module:@ui5/project.graph + * @namespace + */ + graph: { + /** + * @type {typeof import('./lib/graph/ProjectGraph')} + */ + ProjectGraph: "./lib/graph/ProjectGraph", + /** + * @type {typeof import('./lib/graph/projectGraphFromTree')} + */ + projectGraphFromTree: "./lib/graph/projectGraphFromTree" + }, }; function exportModules(exportRoot, modulePaths) { diff --git a/lib/graph/Module.js b/lib/graph/Module.js index c00615180..abe04ad21 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -16,16 +16,23 @@ const defaultConfigPath = "ui5.yaml"; function clone(obj) { return JSON.parse(JSON.stringify(obj)); } - +/** + * Raw representation of a UI5 Project. A module can contain zero to one projects and n extensions. + * This class is intended for private use by the + * [@ui5/project.graph.projectGraphFromTree]{@link module:@ui5/project.graph.projectGraphFromTree} module + * + * @private + * @memberof module:@ui5/project.graph + */ class Module { /** * @param {object} parameters Module parameters - * @param {string} parameters.id Unique ID for the project - * @param {string} parameters.version Version of the project + * @param {string} parameters.id Unique ID for the module + * @param {string} parameters.version Version of the module * @param {string} parameters.modulePath File System path to access the projects resources * @param {string} [parameters.configPath=ui5.yaml] * Either a path relative to `modulePath` which will be resolved by @ui5/fs (default), - * or an absolute File System path to the project configuration file. + * or an absolute File System path to the configuration file. * @param {object|object[]} [parameters.configuration] * Configuration object or array of objects to use. If supplied, no ui5.yaml will be read * @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection] @@ -63,23 +70,14 @@ class Module { } } - /** - * @private - */ getId() { return this._id; } - /** - * @private - */ getVersion() { return this._version; } - /** - * @private - */ getPath() { return this._modulePath; } diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 68c573bcc..c5f4a0a7a 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -1,6 +1,6 @@ const log = require("@ui5/logger").getLogger("graph:ProjectGraph"); /** - * A rooted, directed graph representing a UI5 project and all its dependencies + * A rooted, directed graph representing a UI5 project, its dependencies and available extensions * * @public * @memberof module:@ui5/project.graph diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 332232897..9e7ebc1e1 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -4,7 +4,6 @@ const ProjectGraph = require("./ProjectGraph"); const ShimCollection = require("./ShimCollection"); const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); - function _handleExtensions(graph, shimCollection, extensions) { extensions.forEach((extension) => { const type = extension.getType(); @@ -29,16 +28,22 @@ function _handleExtensions(graph, shimCollection, extensions) { * * @public * @typedef {object} TreeNode - * @param {string} node.id Unique ID for the project - * @param {string} node.version Version of the project - * @param {string} node.path File System path to access the projects resources - * @param {string} [node.configuration] Configuration object to use instead of reading from a configuration file - * @param {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {string} node.id Unique ID for the project + * @property {string} node.version Version of the project + * @property {string} node.path File System path to access the projects resources + * @property {string} [node.configuration] Configuration object to use instead of reading from a configuration file + * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml * @property {TreeNode[]} dependencies */ /** + * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} + * from a dependency tree as returned by translators. + * + * @public + * @alias module:@ui5/project.graph.projectGraphFromTree * @param {TreeNode} tree Dependency tree as returned by a translator + * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance */ module.exports = async function(tree) { const shimCollection = new ShimCollection(); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 1478c94a3..5ab45cbf1 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -10,7 +10,6 @@ const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "applicati const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); const libraryAPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a"); const libraryBPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b"); -// const libraryCPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.c"); const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); @@ -18,10 +17,6 @@ const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invali const legacyLibraryAPath = path.join(__dirname, "..", "fixtures", "legacy.library.a"); const legacyLibraryBPath = path.join(__dirname, "..", "fixtures", "legacy.library.b"); const legacyCollectionAPath = path.join(__dirname, "..", "fixtures", "legacy.collection.a"); -const legacyCollectionLibraryX = path.join(__dirname, "..", "fixtures", "legacy.collection.a", - "src", "legacy.library.x"); -const legacyCollectionLibraryY = path.join(__dirname, "..", "fixtures", "legacy.collection.a", - "src", "legacy.library.y"); test.beforeEach((t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); From 35dd0d749fa64603d2e33a1d4d4d63744a0ee02b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 16:48:23 +0100 Subject: [PATCH 12/99] projectGraphFromTree: Add parameter to use an existing graph Not sure whether this is actually useful in the long run --- lib/graph/projectGraphFromTree.js | 14 ++++++++------ test/lib/graph/projectGraphFromTree.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 9e7ebc1e1..d3559da55 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -43,9 +43,10 @@ function _handleExtensions(graph, shimCollection, extensions) { * @public * @alias module:@ui5/project.graph.projectGraphFromTree * @param {TreeNode} tree Dependency tree as returned by a translator - * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance + * @param {module:@ui5/project.graph.ProjectGraph} [projectGraph] Project graph to use instead of creating a new one + * @returns {Promise} A project graph instance */ -module.exports = async function(tree) { +module.exports = async function(tree, projectGraph) { const shimCollection = new ShimCollection(); const moduleCollection = {}; @@ -73,10 +74,11 @@ module.exports = async function(tree) { qualifiedApplicationProject = rootProject; } - - const projectGraph = new ProjectGraph({ - rootProjectName: rootProjectName - }); + if (!projectGraph) { + projectGraph = new ProjectGraph({ + rootProjectName: rootProjectName + }); + } projectGraph.addProject(rootProject); function handleExtensions(extensions) { diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 5ab45cbf1..a3ae45af5 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -3,6 +3,7 @@ const path = require("path"); const sinonGlobal = require("sinon"); const mock = require("mock-require"); const logger = require("@ui5/logger"); +const ProjectGraph = require("../../../lib/graph/ProjectGraph"); const ValidationError = require("../../../lib/validation/ValidationError"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); @@ -42,6 +43,17 @@ test("Application A", async (t) => { const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationATree); const rootProject = projectGraph.getRoot(); + t.is(rootProject.getName(), "application.a", "Should return correct root project"); +}); + +test("Application A with supplied graph", async (t) => { + const {projectGraphFromTree} = t.context; + const projectGraph = new ProjectGraph({ + rootProjectName: "application.a" + }); + const res = await projectGraphFromTree(applicationATree, projectGraph); + t.is(res, projectGraph, "Should return correct project graph"); + const rootProject = projectGraph.getRoot(); t.is(rootProject.getName(), "application.a", "Returned correct root project"); }); From a7a3f5590a7d7dc443cb56d3edea516740d36e84 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 18:48:34 +0100 Subject: [PATCH 13/99] Enhance normalizer and ui5Framework translator for project graph --- lib/graph/Module.js | 33 +- lib/graph/ProjectGraph.js | 25 +- lib/normalizer.js | 31 +- lib/specifications/AbstractSpecification.js | 9 + lib/specifications/Project.js | 15 +- lib/translators/ui5Framework.js | 197 ++++++++++++ test/lib/graph/ProjectGraph.js | 5 +- .../translators/ui5Framework.integration.js | 299 ++++++++++++++++++ 8 files changed, 592 insertions(+), 22 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index abe04ad21..2e1a2c021 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -340,26 +340,29 @@ class Module { } async _validateConfig(config) { - if (config.specVersion === "0.1" || config.specVersion === "1.0" || - config.specVersion === "1.1") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in module ` + - `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - if (config.specVersion !== "2.0" && - config.specVersion !== "2.1" && config.specVersion !== "2.2" && - config.specVersion !== "2.3") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in module ` + - `${this.getId()}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + const moduleId = this.getId(); + if (!moduleId.startsWith("@openui5/") && !moduleId.startsWith("@sapui5/")) { + if (config.specVersion === "0.1" || config.specVersion === "1.0" || + config.specVersion === "1.1") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in module ` + + `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + if (config.specVersion !== "2.0" && + config.specVersion !== "2.1" && config.specVersion !== "2.2" && + config.specVersion !== "2.3") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in module ` + + `${this.getId()}. Your UI5 CLI installation might be outdated. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } } await validate({ config, project: { - id: this.getId() + id: moduleId } }); } diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index c5f4a0a7a..dff0643b1 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -106,12 +106,12 @@ class ProjectGraph { if (!this._projects[fromProjectName]) { throw new Error( `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + - `Unable to find project with name ${fromProjectName} in graph`); + `Unable to find depending project with name ${fromProjectName} in graph`); } if (!this._projects[toProjectName]) { throw new Error( `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + - `Unable to find project with name ${toProjectName} in graph`); + `Unable to find dependency project with name ${toProjectName} in graph`); } if (this._adjList[fromProjectName][toProjectName]) { log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); @@ -250,6 +250,27 @@ class ProjectGraph { }); })(); } + + /** + * Join another project graph into this one. + * Projects and extensions which already exist in this graph will cause an error to be thrown + * + * @param {module:@ui5/project.graph.ProjectGraph} projectGraph Project Graph to merge into this one + */ + join(projectGraph) { + mergeMap(this._projects, projectGraph._projects); + mergeMap(this._adjList, projectGraph._adjList); + mergeMap(this._extensions, projectGraph._extensions); + } +} + +function mergeMap(target, source) { + for (const [key, value] of Object.entries(source)) { + if (target[key]) { + throw new Error(`Failed to merge map: Key ${key} already present in target set`); + } + target[key] = value; + } } module.exports = ProjectGraph; diff --git a/lib/normalizer.js b/lib/normalizer.js index 00b38a2fa..3001cd7b4 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -1,5 +1,4 @@ const log = require("@ui5/logger").getLogger("normalizer:normalizer"); -const projectPreprocessor = require("./projectPreprocessor"); /** @@ -26,6 +25,7 @@ const Normalizer = { * @returns {Promise} Promise resolving to tree object */ generateProjectTree: async function(options = {}) { + const projectPreprocessor = require("./projectPreprocessor"); let tree = await Normalizer.generateDependencyTree(options); if (options.configPath) { @@ -46,6 +46,35 @@ const Normalizer = { return tree; }, + /** + * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} + * + * @public + * @param {object} [options] + * @param {string} [options.cwd] Current working directory + * @param {string} [options.configPath] Path to configuration file + * @param {string} [options.translatorName] Translator to use + * @param {object} [options.translatorOptions] Options to pass to translator + * @param {object} [options.frameworkOptions] Options to pass to the framework installer + * @param {string} [options.frameworkOptions.versionOverride] Framework version to use instead of the root projects + * framework + * @returns {Promise} Promise resolving to a Project Graph instance + */ + generateProjectGraph: async function(options = {}) { + const projectGraphFromTree = require("./graph/projectGraphFromTree"); + const tree = await Normalizer.generateDependencyTree(options); + + if (options.configPath) { + tree.configPath = options.configPath; + } + const projectGraph = await projectGraphFromTree(tree); + + const ui5Framework = require("./translators/ui5Framework"); + await ui5Framework.enrichProjectGraph(projectGraph, options.frameworkOptions); + + return projectGraph; + }, + /** * Generates a project and dependency tree via translators * diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js index 291cc0b80..3020c1e9c 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/AbstractSpecification.js @@ -57,14 +57,23 @@ class AbstractSpecification { /** * Configuration */ + /** + * @public + */ getName() { return this._getConfiguration().getName(); } + /** + * @public + */ getKind() { return this._getConfiguration().getKind(); } + /** + * @public + */ getType() { return this._getConfiguration().getType(); } diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 06c991e80..84a029796 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -17,9 +17,20 @@ class Project extends AbstractSpecification { } } - /* - * TODO: Expose project specific APIs + /** + * Configuration + */ + /** + * @public */ + getFrameworkConfiguration() { + // TODO: Clone or freeze object before exposing? + return this._getConfiguration().getObject().framework; + } + + isFrameworkProject() { + return this.__id.startsWith("@openui5/") || this.__id.startsWith("@sapui5/"); + } } module.exports = Project; diff --git a/lib/translators/ui5Framework.js b/lib/translators/ui5Framework.js index de086bb10..107d95f38 100644 --- a/lib/translators/ui5Framework.js +++ b/lib/translators/ui5Framework.js @@ -1,9 +1,12 @@ +const Module = require("../graph/Module"); +const ProjectGraph = require("../graph/ProjectGraph"); const log = require("@ui5/logger").getLogger("normalizer:translators:ui5Framework"); class ProjectProcessor { constructor({libraryMetadata}) { this._libraryMetadata = libraryMetadata; this._projectCache = {}; + this._projectGraphPromises = {}; } getProject(libName) { log.verbose(`Creating project for library ${libName}...`); @@ -43,6 +46,55 @@ class ProjectProcessor { }; return this._projectCache[libName]; } + async addProjectToGraph(libName, projectGraph) { + if (this._projectGraphPromises[libName]) { + return this._projectGraphPromises[libName]; + } + return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, projectGraph); + } + async _addProjectToGraph(libName, projectGraph) { + log.verbose(`Creating project for library ${libName}...`); + + + if (!this._libraryMetadata[libName]) { + throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); + } + + const depMetadata = this._libraryMetadata[libName]; + + if (projectGraph.getProject(depMetadata.id)) { + // Already added + return; + } + + const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { + await this.addProjectToGraph(depName, projectGraph); + return depName; + })); + + if (depMetadata.optionalDependencies) { + const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { + if (this._libraryMetadata[depName]) { + log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); + await this.addProjectToGraph(depName, projectGraph); + return depName; + } + })); + + dependencies.push(...resolvedOptionals.filter(($)=>$)); + } + + const ui5Module = new Module({ + id: depMetadata.id, + version: depMetadata.version, + modulePath: depMetadata.path + }); + const {project} = await ui5Module.getSpecifications(); + projectGraph.addProject(project); + dependencies.forEach((dependency) => { + projectGraph.declareDependency(libName, dependency); + }); + } } const utils = { @@ -107,6 +159,59 @@ const utils = { } }); }, + async getFrameworkLibrariesFromGraph(projectGraph) { + const ui5Dependencies = []; + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Module API is >= 2.0 anyways + const frameworkConfig = project.getFrameworkConfiguration(); + + if (!frameworkConfig) { + return; + } + + if (!frameworkConfig.libraries || !frameworkConfig.libraries.length) { + log.verbose(`Project ${project.getName()} defines no framework.libraries configuration`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + frameworkConfig.libraries.forEach((dependency) => { + if (!ui5Dependencies.includes(dependency.name) && + utils.shouldIncludeDependency(dependency, project === rootProject)) { + ui5Dependencies.push(dependency.name); + } + }); + }); + return ui5Dependencies; + }, + async declareFrameworkDependenciesInGraph(projectGraph) { + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Module API is >= 2.0 anyways + const frameworkConfig = project.getFrameworkConfiguration(); + + if (!frameworkConfig || !frameworkConfig.libraries || !frameworkConfig.libraries.length) { + log.verbose(`Project ${project.getName()} has no framework configuration defined`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + frameworkConfig.libraries.forEach((dependency) => { + if (utils.shouldIncludeDependency(dependency, project === rootProject)) { + projectGraph.declareDependency(project.getName(), dependency.name); + } + }); + }); + }, ProjectProcessor }; @@ -289,6 +394,98 @@ module.exports = { return projectTree; }, + /** + * + * + * @public + * @param {module:@ui5/project.graph.ProjectGraph} projectGraph + * @param {object} [options] + * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework + * version from the provided tree + * @returns {Promise} Promise + */ + enrichProjectGraph: async function(projectGraph, options = {}) { + const rootProject = projectGraph.getRoot(); + const rootFrameworkConfig = rootProject.getFrameworkConfiguration(); + if (!rootFrameworkConfig) { + log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); + return projectGraph; + } + + const frameworkName = rootFrameworkConfig.name; + if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { + throw new Error( + `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + + `Must be "OpenUI5" or "SAPUI5"` + ); + } + + let Resolver; + if (frameworkName === "OpenUI5") { + Resolver = require("../ui5Framework/Openui5Resolver"); + } else if (frameworkName === "SAPUI5") { + Resolver = require("../ui5Framework/Sapui5Resolver"); + } + + let version; + if (!rootFrameworkConfig.version) { + throw new Error( + `No framework version defined for root project ${rootProject.getName()}` + ); + } else if (options.versionOverride) { + version = await Resolver.resolveVersion(options.versionOverride, {cwd: rootProject.getPath()}); + log.info( + `Overriding configured ${frameworkName} version ` + + `${rootFrameworkConfig.version} with version ${version}` + ); + } else { + version = rootFrameworkConfig.version; + } + + const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); + if (!referencedLibraries.length) { + log.verbose( + `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + + `or in any of its dependencies`); + return null; + } + + log.info(`Using ${frameworkName} version: ${version}`); + + const resolver = new Resolver({cwd: rootProject.getPath(), version}); + + let startTime; + if (log.isLevelEnabled("verbose")) { + startTime = process.hrtime(); + } + + const {libraryMetadata} = await resolver.install(referencedLibraries); + + if (log.isLevelEnabled("verbose")) { + const timeDiff = process.hrtime(startTime); + const prettyHrtime = require("pretty-hrtime"); + log.verbose( + `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + + `resolved in ${prettyHrtime(timeDiff)}`); + } + + const projectProcessor = new utils.ProjectProcessor({ + libraryMetadata + }); + + const frameworkGraph = new ProjectGraph({ + rootProjectName: "sonic-rainboom" + }); + await Promise.all(referencedLibraries.map(async (libName) => { + await projectProcessor.addProjectToGraph(libName, frameworkGraph); + })); + + log.verbose("Joining framework graph into project graph..."); + projectGraph.join(frameworkGraph); + await utils.declareFrameworkDependenciesInGraph(projectGraph); + return projectGraph; + }, + // Export for testing only _utils: process.env.NODE_ENV === "test" ? utils : undefined }; diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 1682c3053..1ad4e16e7 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -21,6 +21,7 @@ function createProject(name) { configuration: basicConfiguration }); } + function createExtension(name) { const basicConfiguration = new Configuration({ specVersion: "2.3", @@ -290,7 +291,7 @@ test("declareDependency: Unknown source", async (t) => { }); t.is(error.message, "Failed to declare dependency from project library.a to library.b: Unable " + - "to find project with name library.a in graph", + "to find depending project with name library.a in graph", "Should throw with expected error message"); }); @@ -306,7 +307,7 @@ test("declareDependency: Unknown target", async (t) => { }); t.is(error.message, "Failed to declare dependency from project library.a to library.b: Unable " + - "to find project with name library.b in graph", + "to find dependency project with name library.b in graph", "Should throw with expected error message"); }); diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/translators/ui5Framework.integration.js index ad6842195..9380afc3b 100644 --- a/test/lib/translators/ui5Framework.integration.js +++ b/test/lib/translators/ui5Framework.integration.js @@ -11,6 +11,7 @@ const lockfile = require("lockfile"); const logger = require("@ui5/logger"); const normalizer = require("../../../lib/normalizer"); const projectPreprocessor = require("../../../lib/projectPreprocessor"); +const Module = require("../../../lib/graph/Module"); let ui5Framework; let Installer; @@ -943,6 +944,304 @@ test.serial( Failed to resolve library does.not.exist: Could not find library "does.not.exist"`}); }); +function defineGraphTest(testName, { + frameworkName, + verbose = false +}) { + const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; + + const distributionMetadata = { + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + }, + "sap.ui.lib8": { + npmPackageName: "@sapui5/sap.ui.lib8", + version: "1.75.8", + dependencies: [], + optionalDependencies: [] + } + } + }; + + test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + // Enable verbose logging + if (verbose) { + logger.setLevel("verbose"); + } + + const testDependency = { + id: "test-dependency-id", + version: "4.5.6", + path: path.join(fakeBaseDir, "project-test-dependency"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency" + }, + framework: { + version: "1.99.0", + name: frameworkName, + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + }, + { + name: "sap.ui.lib5", + optional: true + }, + { + name: "sap.ui.lib6", + development: true + }, + { + name: "sap.ui.lib8", + // optional dependency gets resolved by dev-dependency of root project + optional: true + } + ] + } + } + }; + const translatorTree = { + id: "test-application-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "project-test-application"), + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-application" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + }, + { + name: "sap.ui.lib8", + development: true + } + ] + } + }, + dependencies: [ + testDependency, + { + id: "test-dependency-no-framework-id", + version: "7.8.9", + path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency-no-framework" + } + }, + dependencies: [ + testDependency + ] + } + ] + }; + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + + sinon.stub(Module.prototype, "_readConfigFile") + .callsFake(async function() { + switch (this.getPath()) { + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", + frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib1" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", + frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib2" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", + frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib3" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", + frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib4" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", + frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib8" + }, + framework: {libraries: []} + }]; + default: + throw new Error( + "Module#_readConfigFile stub called with unknown project: " + + (this.getId()) + ); + } + }); + + sinon.stub(pacote, "extract").resolves(); + + if (frameworkName === "OpenUI5") { + sinon.stub(pacote, "manifest") + .callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }) + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib2@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib3": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib3@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib3", + version: "1.75.0", + devDependencies: { + "@openui5/sap.ui.lib4": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib1": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib8@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib8", + version: "1.75.0", + dependencies: {} + }); + } else if (frameworkName === "SAPUI5") { + sinon.stub(Installer.prototype, "readJson") + .callsFake(async (path) => { + throw new Error("Installer#readJson stub called with unknown path: " + path); + }) + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves(distributionMetadata); + } + + const projectGraph = await normalizer.generateProjectGraph(); + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 8, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "sap.ui.lib1", + "sap.ui.lib8", + "sap.ui.lib4", + "sap.ui.lib3", + "sap.ui.lib2", + "test-dependency", + "test-dependency-no-framework", + "test-application", + ], "Traversed graph in correct order"); + + const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() + .map(($) => $.firstArg) + .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); + t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); + }); +} + +defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5" +}); +defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5", + verbose: true +}); +defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5" +}); +defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5", + verbose: true +}); + // TODO test: Should not download packages again in case they are already installed // TODO test: Should ignore framework libraries in dependencies From 4342fc776af925cd8cfd239a2c4c2205d68deb3d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 18:50:52 +0100 Subject: [PATCH 14/99] Use object-spread operator instead of Object.assign For no particular reason --- lib/graph/Module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 2e1a2c021..039630083 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -176,7 +176,7 @@ class Module { } this._configShims.forEach(({name, shim}) => { log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); - Object.assign(config, shim); + config = {...config, ...shim}; }); return config; } From 42958883a566ae898dd26e8ff01ed7b35688e32f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 18:50:59 +0100 Subject: [PATCH 15/99] Revert "projectGraphFromTree: Add parameter to use an existing graph" No use case yet This reverts commit 933242476617c843648df76e74c14335aaa0f77e. --- lib/graph/projectGraphFromTree.js | 14 ++++++-------- test/lib/graph/projectGraphFromTree.js | 12 ------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index d3559da55..9e7ebc1e1 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -43,10 +43,9 @@ function _handleExtensions(graph, shimCollection, extensions) { * @public * @alias module:@ui5/project.graph.projectGraphFromTree * @param {TreeNode} tree Dependency tree as returned by a translator - * @param {module:@ui5/project.graph.ProjectGraph} [projectGraph] Project graph to use instead of creating a new one - * @returns {Promise} A project graph instance + * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance */ -module.exports = async function(tree, projectGraph) { +module.exports = async function(tree) { const shimCollection = new ShimCollection(); const moduleCollection = {}; @@ -74,11 +73,10 @@ module.exports = async function(tree, projectGraph) { qualifiedApplicationProject = rootProject; } - if (!projectGraph) { - projectGraph = new ProjectGraph({ - rootProjectName: rootProjectName - }); - } + + const projectGraph = new ProjectGraph({ + rootProjectName: rootProjectName + }); projectGraph.addProject(rootProject); function handleExtensions(extensions) { diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index a3ae45af5..5ab45cbf1 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -3,7 +3,6 @@ const path = require("path"); const sinonGlobal = require("sinon"); const mock = require("mock-require"); const logger = require("@ui5/logger"); -const ProjectGraph = require("../../../lib/graph/ProjectGraph"); const ValidationError = require("../../../lib/validation/ValidationError"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); @@ -43,17 +42,6 @@ test("Application A", async (t) => { const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree(applicationATree); const rootProject = projectGraph.getRoot(); - t.is(rootProject.getName(), "application.a", "Should return correct root project"); -}); - -test("Application A with supplied graph", async (t) => { - const {projectGraphFromTree} = t.context; - const projectGraph = new ProjectGraph({ - rootProjectName: "application.a" - }); - const res = await projectGraphFromTree(applicationATree, projectGraph); - t.is(res, projectGraph, "Should return correct project graph"); - const rootProject = projectGraph.getRoot(); t.is(rootProject.getName(), "application.a", "Returned correct root project"); }); From 7a7ad3182db385088f2b96f2e145fec612be7dc1 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 20:16:14 +0100 Subject: [PATCH 16/99] Add ProjectGraph#getAllExtensions plus tests --- lib/graph/ProjectGraph.js | 18 ++++-- test/lib/graph/ProjectGraph.js | 108 +++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index dff0643b1..7032b3229 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -95,6 +95,10 @@ class ProjectGraph { return this._extensions[extensionName]; } + getAllExtensions() { + return this._extensions; + } + /** * Declare a dependency from one project in the graph to another * @@ -258,16 +262,22 @@ class ProjectGraph { * @param {module:@ui5/project.graph.ProjectGraph} projectGraph Project Graph to merge into this one */ join(projectGraph) { - mergeMap(this._projects, projectGraph._projects); - mergeMap(this._adjList, projectGraph._adjList); - mergeMap(this._extensions, projectGraph._extensions); + try { + mergeMap(this._projects, projectGraph._projects); + mergeMap(this._adjList, projectGraph._adjList); + mergeMap(this._extensions, projectGraph._extensions); + } catch (err) { + throw new Error( + `Failed to join graph with root project ${projectGraph._rootProjectName} into ` + + `${this._rootProjectName}: ${err.message}`); + } } } function mergeMap(target, source) { for (const [key, value] of Object.entries(source)) { if (target[key]) { - throw new Error(`Failed to merge map: Key ${key} already present in target set`); + throw new Error(`Failed to merge map: Key '${key}' already present in target set`); } target[key] = value; } diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 1ad4e16e7..4aa962bc7 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -254,6 +254,23 @@ test("getExtension: Project is not in graph", async (t) => { t.is(res, undefined, "Should return undefined"); }); +test("getAllExtensions", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension1 = createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = createExtension("extension.b"); + graph.addExtension(extension2); + const res = graph.getAllExtensions(); + t.deepEqual(res, { + "extension.a": extension1, + "extension.b": extension2 + }, "Should return all extensions"); +}); + test("declareDependency / getDependencies", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ @@ -706,3 +723,94 @@ test("traverseDepthFirst: Dependency declaration order is followed", async (t) = "library.a", ]); }); + +test("join", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + graph1.addProject(createProject("library.a")); + graph1.addProject(createProject("library.b")); + graph1.addProject(createProject("library.c")); + graph1.addProject(createProject("library.d")); + + graph1.declareDependency("library.a", "library.b"); + graph1.declareDependency("library.a", "library.c"); + graph1.declareDependency("library.a", "library.d"); + + const extensionA = createExtension("extension.a"); + graph1.addExtension(extensionA); + + graph2.addProject(createProject("theme.a")); + graph2.addProject(createProject("theme.b")); + graph2.addProject(createProject("theme.c")); + graph2.addProject(createProject("theme.d")); + + graph2.declareDependency("theme.a", "theme.d"); + graph2.declareDependency("theme.a", "theme.c"); + graph2.declareDependency("theme.b", "theme.a"); // This causes theme.b to not appear + + const extensionB = createExtension("extension.b"); + graph2.addExtension(extensionB); + + graph1.join(graph2); + graph1.declareDependency("library.d", "theme.a"); + + await traverseDepthFirst(t, graph1, [ + "library.b", + "library.c", + "theme.d", + "theme.c", + "theme.a", + "library.d", + "library.a", + ]); + + t.is(graph1.getExtension("extension.a"), extensionA, "Should return correct extension"); + t.is(graph1.getExtension("extension.b"), extensionB, "Should return correct joined extension"); +}); + +test("join: Unexpected project intersection", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addProject(createProject("library.a")); + graph2.addProject(createProject("library.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + "Failed to join graph with root project 😼 into 😹: Failed to merge map: " + + "Key 'library.a' already present in target set", + "Should throw with expected error message"); +}); + +test("join: Unexpected extension intersection", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addExtension(createExtension("extension.a")); + graph2.addExtension(createExtension("extension.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + "Failed to join graph with root project 😼 into 😹: Failed to merge map: " + + "Key 'extension.a' already present in target set", + "Should throw with expected error message"); +}); From 668ae9efa94fc05091e27ff5a4b001d14bd565c3 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 22:48:08 +0100 Subject: [PATCH 17/99] Split graph-agnostic ui5Framework implementation into separate file --- lib/graph/providers/ui5Framework.js | 228 +++++++++++ lib/normalizer.js | 2 +- lib/translators/ui5Framework.js | 197 ---------- .../providers/ui5Framework.integration.js | 361 ++++++++++++++++++ .../translators/ui5Framework.integration.js | 299 --------------- 5 files changed, 590 insertions(+), 497 deletions(-) create mode 100644 lib/graph/providers/ui5Framework.js create mode 100644 test/lib/graph/providers/ui5Framework.integration.js diff --git a/lib/graph/providers/ui5Framework.js b/lib/graph/providers/ui5Framework.js new file mode 100644 index 000000000..aa81f14bb --- /dev/null +++ b/lib/graph/providers/ui5Framework.js @@ -0,0 +1,228 @@ +const Module = require("../Module"); +const ProjectGraph = require("../ProjectGraph"); +const log = require("@ui5/logger").getLogger("graph:providers:ui5Framework"); + +class ProjectProcessor { + constructor({libraryMetadata}) { + this._libraryMetadata = libraryMetadata; + this._projectGraphPromises = {}; + } + async addProjectToGraph(libName, projectGraph) { + if (this._projectGraphPromises[libName]) { + return this._projectGraphPromises[libName]; + } + return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, projectGraph); + } + async _addProjectToGraph(libName, projectGraph) { + log.verbose(`Creating project for library ${libName}...`); + + + if (!this._libraryMetadata[libName]) { + throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); + } + + const depMetadata = this._libraryMetadata[libName]; + + if (projectGraph.getProject(depMetadata.id)) { + // Already added + return; + } + + const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { + await this.addProjectToGraph(depName, projectGraph); + return depName; + })); + + if (depMetadata.optionalDependencies) { + const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { + if (this._libraryMetadata[depName]) { + log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); + await this.addProjectToGraph(depName, projectGraph); + return depName; + } + })); + + dependencies.push(...resolvedOptionals.filter(($)=>$)); + } + + const ui5Module = new Module({ + id: depMetadata.id, + version: depMetadata.version, + modulePath: depMetadata.path + }); + const {project} = await ui5Module.getSpecifications(); + projectGraph.addProject(project); + dependencies.forEach((dependency) => { + projectGraph.declareDependency(libName, dependency); + }); + } +} + +const utils = { + isFrameworkProject(project) { + return project.id.startsWith("@openui5/") || project.id.startsWith("@sapui5/"); + }, + shouldIncludeDependency({optional, development}, root) { + // Root project should include all dependencies + // Otherwise only non-optional and non-development dependencies should be included + return root || (optional !== true && development !== true); + }, + async getFrameworkLibrariesFromGraph(projectGraph) { + const ui5Dependencies = []; + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Module API is >= 2.0 anyways + const frameworkConfig = project.getFrameworkConfiguration(); + + if (!frameworkConfig) { + return; + } + + if (!frameworkConfig.libraries || !frameworkConfig.libraries.length) { + log.verbose(`Project ${project.getName()} defines no framework.libraries configuration`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + frameworkConfig.libraries.forEach((dependency) => { + if (!ui5Dependencies.includes(dependency.name) && + utils.shouldIncludeDependency(dependency, project === rootProject)) { + ui5Dependencies.push(dependency.name); + } + }); + }); + return ui5Dependencies; + }, + async declareFrameworkDependenciesInGraph(projectGraph) { + const rootProject = projectGraph.getRoot(); + await projectGraph.traverseBreadthFirst(async ({project}) => { + if (project.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + // No need to check for specVersion since Module API is >= 2.0 anyways + const frameworkConfig = project.getFrameworkConfiguration(); + + if (!frameworkConfig || !frameworkConfig.libraries || !frameworkConfig.libraries.length) { + log.verbose(`Project ${project.getName()} has no framework configuration defined`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + return; + } + + frameworkConfig.libraries.forEach((dependency) => { + if (utils.shouldIncludeDependency(dependency, project === rootProject)) { + projectGraph.declareDependency(project.getName(), dependency.name); + } + }); + }); + }, + ProjectProcessor +}; + +/** + * + * + * @private + * @namespace + * @alias module:@ui5/project.translators.ui5Framework + */ +module.exports = { + /** + * + * + * @public + * @param {module:@ui5/project.graph.ProjectGraph} projectGraph + * @param {object} [options] + * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework + * version from the provided tree + * @returns {Promise} Promise + */ + enrichProjectGraph: async function(projectGraph, options = {}) { + const rootProject = projectGraph.getRoot(); + const rootFrameworkConfig = rootProject.getFrameworkConfiguration(); + if (!rootFrameworkConfig) { + log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); + return projectGraph; + } + + const frameworkName = rootFrameworkConfig.name; + if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { + throw new Error( + `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + + `Must be "OpenUI5" or "SAPUI5"` + ); + } + + let Resolver; + if (frameworkName === "OpenUI5") { + Resolver = require("../../ui5Framework/Openui5Resolver"); + } else if (frameworkName === "SAPUI5") { + Resolver = require("../../ui5Framework/Sapui5Resolver"); + } + + let version; + if (!rootFrameworkConfig.version) { + throw new Error( + `No framework version defined for root project ${rootProject.getName()}` + ); + } else if (options.versionOverride) { + version = await Resolver.resolveVersion(options.versionOverride, {cwd: rootProject.getPath()}); + log.info( + `Overriding configured ${frameworkName} version ` + + `${rootFrameworkConfig.version} with version ${version}` + ); + } else { + version = rootFrameworkConfig.version; + } + + const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); + if (!referencedLibraries.length) { + log.verbose( + `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + + `or in any of its dependencies`); + return null; + } + + log.info(`Using ${frameworkName} version: ${version}`); + + const resolver = new Resolver({cwd: rootProject.getPath(), version}); + + let startTime; + if (log.isLevelEnabled("verbose")) { + startTime = process.hrtime(); + } + + const {libraryMetadata} = await resolver.install(referencedLibraries); + + if (log.isLevelEnabled("verbose")) { + const timeDiff = process.hrtime(startTime); + const prettyHrtime = require("pretty-hrtime"); + log.verbose( + `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + + `resolved in ${prettyHrtime(timeDiff)}`); + } + + const projectProcessor = new utils.ProjectProcessor({ + libraryMetadata + }); + + const frameworkGraph = new ProjectGraph({ + rootProjectName: "sonic-rainboom" + }); + await Promise.all(referencedLibraries.map(async (libName) => { + await projectProcessor.addProjectToGraph(libName, frameworkGraph); + })); + + log.verbose("Joining framework graph into project graph..."); + projectGraph.join(frameworkGraph); + await utils.declareFrameworkDependenciesInGraph(projectGraph); + return projectGraph; + }, + + // Export for testing only + _utils: process.env.NODE_ENV === "test" ? utils : undefined +}; diff --git a/lib/normalizer.js b/lib/normalizer.js index 3001cd7b4..544bb97e4 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -69,7 +69,7 @@ const Normalizer = { } const projectGraph = await projectGraphFromTree(tree); - const ui5Framework = require("./translators/ui5Framework"); + const ui5Framework = require("./graph/providers/ui5Framework"); await ui5Framework.enrichProjectGraph(projectGraph, options.frameworkOptions); return projectGraph; diff --git a/lib/translators/ui5Framework.js b/lib/translators/ui5Framework.js index 107d95f38..de086bb10 100644 --- a/lib/translators/ui5Framework.js +++ b/lib/translators/ui5Framework.js @@ -1,12 +1,9 @@ -const Module = require("../graph/Module"); -const ProjectGraph = require("../graph/ProjectGraph"); const log = require("@ui5/logger").getLogger("normalizer:translators:ui5Framework"); class ProjectProcessor { constructor({libraryMetadata}) { this._libraryMetadata = libraryMetadata; this._projectCache = {}; - this._projectGraphPromises = {}; } getProject(libName) { log.verbose(`Creating project for library ${libName}...`); @@ -46,55 +43,6 @@ class ProjectProcessor { }; return this._projectCache[libName]; } - async addProjectToGraph(libName, projectGraph) { - if (this._projectGraphPromises[libName]) { - return this._projectGraphPromises[libName]; - } - return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, projectGraph); - } - async _addProjectToGraph(libName, projectGraph) { - log.verbose(`Creating project for library ${libName}...`); - - - if (!this._libraryMetadata[libName]) { - throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); - } - - const depMetadata = this._libraryMetadata[libName]; - - if (projectGraph.getProject(depMetadata.id)) { - // Already added - return; - } - - const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { - await this.addProjectToGraph(depName, projectGraph); - return depName; - })); - - if (depMetadata.optionalDependencies) { - const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { - if (this._libraryMetadata[depName]) { - log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); - await this.addProjectToGraph(depName, projectGraph); - return depName; - } - })); - - dependencies.push(...resolvedOptionals.filter(($)=>$)); - } - - const ui5Module = new Module({ - id: depMetadata.id, - version: depMetadata.version, - modulePath: depMetadata.path - }); - const {project} = await ui5Module.getSpecifications(); - projectGraph.addProject(project); - dependencies.forEach((dependency) => { - projectGraph.declareDependency(libName, dependency); - }); - } } const utils = { @@ -159,59 +107,6 @@ const utils = { } }); }, - async getFrameworkLibrariesFromGraph(projectGraph) { - const ui5Dependencies = []; - const rootProject = projectGraph.getRoot(); - await projectGraph.traverseBreadthFirst(async ({project}) => { - if (project.isFrameworkProject()) { - // Ignoring UI5 Framework libraries in dependencies - return; - } - // No need to check for specVersion since Module API is >= 2.0 anyways - const frameworkConfig = project.getFrameworkConfiguration(); - - if (!frameworkConfig) { - return; - } - - if (!frameworkConfig.libraries || !frameworkConfig.libraries.length) { - log.verbose(`Project ${project.getName()} defines no framework.libraries configuration`); - // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json - return; - } - - frameworkConfig.libraries.forEach((dependency) => { - if (!ui5Dependencies.includes(dependency.name) && - utils.shouldIncludeDependency(dependency, project === rootProject)) { - ui5Dependencies.push(dependency.name); - } - }); - }); - return ui5Dependencies; - }, - async declareFrameworkDependenciesInGraph(projectGraph) { - const rootProject = projectGraph.getRoot(); - await projectGraph.traverseBreadthFirst(async ({project}) => { - if (project.isFrameworkProject()) { - // Ignoring UI5 Framework libraries in dependencies - return; - } - // No need to check for specVersion since Module API is >= 2.0 anyways - const frameworkConfig = project.getFrameworkConfiguration(); - - if (!frameworkConfig || !frameworkConfig.libraries || !frameworkConfig.libraries.length) { - log.verbose(`Project ${project.getName()} has no framework configuration defined`); - // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json - return; - } - - frameworkConfig.libraries.forEach((dependency) => { - if (utils.shouldIncludeDependency(dependency, project === rootProject)) { - projectGraph.declareDependency(project.getName(), dependency.name); - } - }); - }); - }, ProjectProcessor }; @@ -394,98 +289,6 @@ module.exports = { return projectTree; }, - /** - * - * - * @public - * @param {module:@ui5/project.graph.ProjectGraph} projectGraph - * @param {object} [options] - * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework - * version from the provided tree - * @returns {Promise} Promise - */ - enrichProjectGraph: async function(projectGraph, options = {}) { - const rootProject = projectGraph.getRoot(); - const rootFrameworkConfig = rootProject.getFrameworkConfiguration(); - if (!rootFrameworkConfig) { - log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); - return projectGraph; - } - - const frameworkName = rootFrameworkConfig.name; - if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { - throw new Error( - `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + - `Must be "OpenUI5" or "SAPUI5"` - ); - } - - let Resolver; - if (frameworkName === "OpenUI5") { - Resolver = require("../ui5Framework/Openui5Resolver"); - } else if (frameworkName === "SAPUI5") { - Resolver = require("../ui5Framework/Sapui5Resolver"); - } - - let version; - if (!rootFrameworkConfig.version) { - throw new Error( - `No framework version defined for root project ${rootProject.getName()}` - ); - } else if (options.versionOverride) { - version = await Resolver.resolveVersion(options.versionOverride, {cwd: rootProject.getPath()}); - log.info( - `Overriding configured ${frameworkName} version ` + - `${rootFrameworkConfig.version} with version ${version}` - ); - } else { - version = rootFrameworkConfig.version; - } - - const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); - if (!referencedLibraries.length) { - log.verbose( - `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + - `or in any of its dependencies`); - return null; - } - - log.info(`Using ${frameworkName} version: ${version}`); - - const resolver = new Resolver({cwd: rootProject.getPath(), version}); - - let startTime; - if (log.isLevelEnabled("verbose")) { - startTime = process.hrtime(); - } - - const {libraryMetadata} = await resolver.install(referencedLibraries); - - if (log.isLevelEnabled("verbose")) { - const timeDiff = process.hrtime(startTime); - const prettyHrtime = require("pretty-hrtime"); - log.verbose( - `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + - `resolved in ${prettyHrtime(timeDiff)}`); - } - - const projectProcessor = new utils.ProjectProcessor({ - libraryMetadata - }); - - const frameworkGraph = new ProjectGraph({ - rootProjectName: "sonic-rainboom" - }); - await Promise.all(referencedLibraries.map(async (libName) => { - await projectProcessor.addProjectToGraph(libName, frameworkGraph); - })); - - log.verbose("Joining framework graph into project graph..."); - projectGraph.join(frameworkGraph); - await utils.declareFrameworkDependenciesInGraph(projectGraph); - return projectGraph; - }, - // Export for testing only _utils: process.env.NODE_ENV === "test" ? utils : undefined }; diff --git a/test/lib/graph/providers/ui5Framework.integration.js b/test/lib/graph/providers/ui5Framework.integration.js new file mode 100644 index 000000000..0c5540158 --- /dev/null +++ b/test/lib/graph/providers/ui5Framework.integration.js @@ -0,0 +1,361 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const path = require("path"); +const os = require("os"); +const fs = require("graceful-fs"); + +const pacote = require("pacote"); +const libnpmconfig = require("libnpmconfig"); +const lockfile = require("lockfile"); +const logger = require("@ui5/logger"); +const normalizer = require("../../../../lib/normalizer"); +const Module = require("../../../../lib/graph/Module"); +// let ui5Framework; +let Installer; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); +const ui5FrameworkBaseDir = path.join(fakeBaseDir, "homedir", ".ui5", "framework"); +const ui5PackagesBaseDir = path.join(ui5FrameworkBaseDir, "packages"); + +test.before((t) => { + sinon.stub(fs, "rename").yieldsAsync(); +}); + +test.beforeEach((t) => { + sinon.stub(libnpmconfig, "read").returns({ + toJSON: () => { + return { + registry: "https://registry.fake", + cache: path.join(ui5FrameworkBaseDir, "cacache"), + proxy: "" + }; + } + }); + sinon.stub(os, "homedir").returns(path.join(fakeBaseDir, "homedir")); + + sinon.stub(lockfile, "lock").yieldsAsync(); + sinon.stub(lockfile, "unlock").yieldsAsync(); + + const testLogger = logger.getLogger(); + sinon.stub(logger, "getLogger").returns(testLogger); + t.context.logInfoSpy = sinon.spy(testLogger, "info"); + + mock("mkdirp", sinon.stub().resolves()); + + // Re-require to ensure that mocked modules are used + // ui5Framework = mock.reRequire("../../../../lib/graph/providers/ui5Framework"); + Installer = require("../../../../lib/ui5Framework/npm/Installer"); +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); + logger.setLevel("info"); // default log level +}); + +function defineTest(testName, { + frameworkName, + verbose = false +}) { + const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; + + const distributionMetadata = { + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + }, + "sap.ui.lib8": { + npmPackageName: "@sapui5/sap.ui.lib8", + version: "1.75.8", + dependencies: [], + optionalDependencies: [] + } + } + }; + + test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + // Enable verbose logging + if (verbose) { + logger.setLevel("verbose"); + } + + const testDependency = { + id: "test-dependency-id", + version: "4.5.6", + path: path.join(fakeBaseDir, "project-test-dependency"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency" + }, + framework: { + version: "1.99.0", + name: frameworkName, + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + }, + { + name: "sap.ui.lib5", + optional: true + }, + { + name: "sap.ui.lib6", + development: true + }, + { + name: "sap.ui.lib8", + // optional dependency gets resolved by dev-dependency of root project + optional: true + } + ] + } + } + }; + const translatorTree = { + id: "test-application-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "project-test-application"), + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-application" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + }, + { + name: "sap.ui.lib8", + development: true + } + ] + } + }, + dependencies: [ + testDependency, + { + id: "test-dependency-no-framework-id", + version: "7.8.9", + path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency-no-framework" + } + }, + dependencies: [ + testDependency + ] + } + ] + }; + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + + sinon.stub(Module.prototype, "_readConfigFile") + .callsFake(async function() { + switch (this.getPath()) { + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", + frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib1" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", + frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib2" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", + frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib3" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", + frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib4" + }, + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", + frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib8" + }, + framework: {libraries: []} + }]; + default: + throw new Error( + "Module#_readConfigFile stub called with unknown project: " + + (this.getId()) + ); + } + }); + + sinon.stub(pacote, "extract").resolves(); + + if (frameworkName === "OpenUI5") { + sinon.stub(pacote, "manifest") + .callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }) + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib2@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib3": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib3@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib3", + version: "1.75.0", + devDependencies: { + "@openui5/sap.ui.lib4": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib1": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib8@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib8", + version: "1.75.0", + dependencies: {} + }); + } else if (frameworkName === "SAPUI5") { + sinon.stub(Installer.prototype, "readJson") + .callsFake(async (path) => { + throw new Error("Installer#readJson stub called with unknown path: " + path); + }) + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves(distributionMetadata); + } + + const projectGraph = await normalizer.generateProjectGraph(); + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 8, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "sap.ui.lib1", + "sap.ui.lib8", + "sap.ui.lib4", + "sap.ui.lib3", + "sap.ui.lib2", + "test-dependency", + "test-dependency-no-framework", + "test-application", + ], "Traversed graph in correct order"); + + const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() + .map(($) => $.firstArg) + .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); + t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); + }); +} + +defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5" +}); +defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "SAPUI5", + verbose: true +}); +defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5" +}); +defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { + frameworkName: "OpenUI5", + verbose: true +}); + +// TODO missing tests from non-graph ui5Framework.integration.js + +// TODO test: Should not download packages again in case they are already installed + +// TODO test: Should ignore framework libraries in dependencies diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/translators/ui5Framework.integration.js index 9380afc3b..ad6842195 100644 --- a/test/lib/translators/ui5Framework.integration.js +++ b/test/lib/translators/ui5Framework.integration.js @@ -11,7 +11,6 @@ const lockfile = require("lockfile"); const logger = require("@ui5/logger"); const normalizer = require("../../../lib/normalizer"); const projectPreprocessor = require("../../../lib/projectPreprocessor"); -const Module = require("../../../lib/graph/Module"); let ui5Framework; let Installer; @@ -944,304 +943,6 @@ test.serial( Failed to resolve library does.not.exist: Could not find library "does.not.exist"`}); }); -function defineGraphTest(testName, { - frameworkName, - verbose = false -}) { - const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; - - const distributionMetadata = { - libraries: { - "sap.ui.lib1": { - npmPackageName: "@sapui5/sap.ui.lib1", - version: "1.75.1", - dependencies: [], - optionalDependencies: [] - }, - "sap.ui.lib2": { - npmPackageName: "@sapui5/sap.ui.lib2", - version: "1.75.2", - dependencies: [ - "sap.ui.lib3" - ], - optionalDependencies: [] - }, - "sap.ui.lib3": { - npmPackageName: "@sapui5/sap.ui.lib3", - version: "1.75.3", - dependencies: [], - optionalDependencies: [ - "sap.ui.lib4" - ] - }, - "sap.ui.lib4": { - npmPackageName: "@openui5/sap.ui.lib4", - version: "1.75.4", - dependencies: [ - "sap.ui.lib1" - ], - optionalDependencies: [] - }, - "sap.ui.lib8": { - npmPackageName: "@sapui5/sap.ui.lib8", - version: "1.75.8", - dependencies: [], - optionalDependencies: [] - } - } - }; - - test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { - // Enable verbose logging - if (verbose) { - logger.setLevel("verbose"); - } - - const testDependency = { - id: "test-dependency-id", - version: "4.5.6", - path: path.join(fakeBaseDir, "project-test-dependency"), - dependencies: [], - configuration: { - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency" - }, - framework: { - version: "1.99.0", - name: frameworkName, - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib2" - }, - { - name: "sap.ui.lib5", - optional: true - }, - { - name: "sap.ui.lib6", - development: true - }, - { - name: "sap.ui.lib8", - // optional dependency gets resolved by dev-dependency of root project - optional: true - } - ] - } - } - }; - const translatorTree = { - id: "test-application-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "project-test-application"), - configuration: { - specVersion: "2.0", - type: "application", - metadata: { - name: "test-application" - }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib4", - optional: true - }, - { - name: "sap.ui.lib8", - development: true - } - ] - } - }, - dependencies: [ - testDependency, - { - id: "test-dependency-no-framework-id", - version: "7.8.9", - path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), - configuration: { - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency-no-framework" - } - }, - dependencies: [ - testDependency - ] - } - ] - }; - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - - sinon.stub(Module.prototype, "_readConfigFile") - .callsFake(async function() { - switch (this.getPath()) { - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", - frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib1" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", - frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib2" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", - frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib3" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", - frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib4" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", - frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib8" - }, - framework: {libraries: []} - }]; - default: - throw new Error( - "Module#_readConfigFile stub called with unknown project: " + - (this.getId()) - ); - } - }); - - sinon.stub(pacote, "extract").resolves(); - - if (frameworkName === "OpenUI5") { - sinon.stub(pacote, "manifest") - .callsFake(async (spec) => { - throw new Error("pacote.manifest stub called with unknown spec: " + spec); - }) - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib1", - version: "1.75.0", - dependencies: {} - }) - .withArgs("@openui5/sap.ui.lib2@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib2", - version: "1.75.0", - dependencies: { - "@openui5/sap.ui.lib3": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib3@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib3", - version: "1.75.0", - devDependencies: { - "@openui5/sap.ui.lib4": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib4", - version: "1.75.0", - dependencies: { - "@openui5/sap.ui.lib1": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib8@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib8", - version: "1.75.0", - dependencies: {} - }); - } else if (frameworkName === "SAPUI5") { - sinon.stub(Installer.prototype, "readJson") - .callsFake(async (path) => { - throw new Error("Installer#readJson stub called with unknown path: " + path); - }) - .withArgs(path.join(fakeBaseDir, - "homedir", ".ui5", "framework", "packages", - "@sapui5", "distribution-metadata", "1.75.0", - "metadata.json")) - .resolves(distributionMetadata); - } - - const projectGraph = await normalizer.generateProjectGraph(); - - const callbackStub = sinon.stub().resolves(); - await projectGraph.traverseDepthFirst(callbackStub); - - t.is(callbackStub.callCount, 8, "Correct number of projects have been visited"); - - const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); - t.deepEqual(callbackCalls, [ - "sap.ui.lib1", - "sap.ui.lib8", - "sap.ui.lib4", - "sap.ui.lib3", - "sap.ui.lib2", - "test-dependency", - "test-dependency-no-framework", - "test-application", - ], "Traversed graph in correct order"); - - const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() - .map(($) => $.firstArg) - .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); - t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); - }); -} - -defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { - frameworkName: "SAPUI5" -}); -defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { - frameworkName: "SAPUI5", - verbose: true -}); -defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { - frameworkName: "OpenUI5" -}); -defineGraphTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { - frameworkName: "OpenUI5", - verbose: true -}); - // TODO test: Should not download packages again in case they are already installed // TODO test: Should ignore framework libraries in dependencies From acd32fd761abcf1d731558ea26d0971404c84734 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sun, 14 Feb 2021 23:30:24 +0100 Subject: [PATCH 18/99] Introduce projectGraphFromDirectory as normalizer successor --- lib/graph/projectGraphFromDirectory.js | 67 ++++++++++++++++++++++++++ lib/graph/projectGraphFromTree.js | 3 +- lib/graph/providers/npm.js | 23 +++++++++ lib/normalizer.js | 4 +- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 lib/graph/projectGraphFromDirectory.js create mode 100644 lib/graph/providers/npm.js diff --git a/lib/graph/projectGraphFromDirectory.js b/lib/graph/projectGraphFromDirectory.js new file mode 100644 index 000000000..33f74a2ac --- /dev/null +++ b/lib/graph/projectGraphFromDirectory.js @@ -0,0 +1,67 @@ +const projectGraphFromTree = require("./projectGraphFromTree"); +const ui5Framework = require("./providers/ui5Framework"); +const log = require("@ui5/logger").getLogger("graph:projectGraphFromDirectory"); + +/** + * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} + * from a directory + * + * @public + * @alias module:@ui5/project.graph.projectGraphFromTree + * @param {TreeNode} tree Dependency tree as returned by a translator + * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance + */ +const projectGraphFromDirectory = { + /** + * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} by resolving + * dependencies from package.json files and configuring projects from ui5.yaml files + * + * @public + * @param {object} [options] + * @param {string} [options.cwd=.] Directory of the root module + * @param {object} [options.configuration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.configPath] Configuration file to use for the root module instead the default ui5.yaml + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @returns {Promise} Promise resolving to a Project Graph instance + */ + usingNpm: async function({cwd = ".", configuration, configPath, versionOverride}) { + log.verbose(`Creating project graph using npm provider...`); + const npmProvider = require("./providers/npm"); + + const projectGraph = await npmProvider.createProjectGraph(cwd); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + + return projectGraph; + }, + + /** + * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} from on a + * YAML file following the structure of the + * [@ui5/project.graph.projectGraphFromTree]{@link module:@ui5/project.graph.projectGraphFromTree} API + * + * @public + * @param {object} options + * @param {object} options.filePath Path to the file dependency configuration file + * @param {string} [options.cwd=.] Directory of the root module + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @returns {Promise} Promise resolving to a Project Graph instance + */ + usingStaticFile: async function({cwd = ".", filePath, versionOverride}) { + log.verbose(`Creating project graph using static file...`); + const staticTranslator = require("../translators/static"); + + const tree = await staticTranslator.generateDependencyTree(null, { + parameters: [filePath] // *sigh* + }); + + const projectGraph = await projectGraphFromTree(tree); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + + return projectGraph; + } +}; + +module.exports = projectGraphFromDirectory; diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 9e7ebc1e1..58d69153b 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -31,7 +31,8 @@ function _handleExtensions(graph, shimCollection, extensions) { * @property {string} node.id Unique ID for the project * @property {string} node.version Version of the project * @property {string} node.path File System path to access the projects resources - * @property {string} [node.configuration] Configuration object to use instead of reading from a configuration file + * @property {object|object[]} [node.configuration] + * Configuration object or array of objects to use instead of reading from a configuration file * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml * @property {TreeNode[]} dependencies */ diff --git a/lib/graph/providers/npm.js b/lib/graph/providers/npm.js new file mode 100644 index 000000000..fa2135350 --- /dev/null +++ b/lib/graph/providers/npm.js @@ -0,0 +1,23 @@ +// const ProjectGraph = require("../ProjectGraph"); +// const log = require("@ui5/logger").getLogger("graph:providers:npm"); + +/** + * Graph provider for npm based projects + * + * @public + * @namespace + * @alias module:@ui5/project.graph.providers.npm + */ +module.exports = { + /** + * Generates a project graph from npm modules + * + * @public + * @param {string} dirPath Project path + * @returns {Promise} Promise resolving with a project graph + */ + createProjectGraph(dirPath) { + // TODO: Make arborist happen + // https://github.com/npm/arborist + } +}; diff --git a/lib/normalizer.js b/lib/normalizer.js index 544bb97e4..7aa6cbdb5 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -1,6 +1,5 @@ const log = require("@ui5/logger").getLogger("normalizer:normalizer"); - /** * Generate project and dependency trees via translators. * Optionally configure all projects with the projectPreprocessor. @@ -56,7 +55,8 @@ const Normalizer = { * @param {string} [options.translatorName] Translator to use * @param {object} [options.translatorOptions] Options to pass to translator * @param {object} [options.frameworkOptions] Options to pass to the framework installer - * @param {string} [options.frameworkOptions.versionOverride] Framework version to use instead of the root projects + * @param {string} [options.frameworkOptions.versionOverride] + * Framework version to use instead of the one defined in the root project * framework * @returns {Promise} Promise resolving to a Project Graph instance */ From 7eb4ce9600e8d4f3e51c01b7eb07fe11f06d96c9 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 16 Feb 2021 18:11:58 +0100 Subject: [PATCH 19/99] Fix collection shim handling --- lib/graph/Module.js | 2 +- lib/graph/projectGraphFromTree.js | 63 +++++++-------- lib/specifications/AbstractSpecification.js | 2 +- test/lib/graph/projectGraphFromTree.js | 88 +++++++++++++++++++++ 4 files changed, 119 insertions(+), 36 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 039630083..2e1a2c021 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -176,7 +176,7 @@ class Module { } this._configShims.forEach(({name, shim}) => { log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); - config = {...config, ...shim}; + Object.assign(config, shim); }); return config; } diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 58d69153b..1e84ab8be 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -99,35 +99,6 @@ module.exports = async function(tree) { while (queue.length) { const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue const res = await Promise.all(nodes.map(async (node) => { - // First check for collection shims - const collectionShims = shimCollection.getCollectionShims(node.id); - if (collectionShims && collectionShims.length) { - log.verbose( - `One or more module collection shims have been defined for module ${node.id}. ` + - `Therefore the module itself will not be resolved.`); - - const shimmedNodes = collectionShims.map(({name, shim}) => { - log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); - return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { - const shimModulePath = path.join(node.path, shimModuleRelPath); - log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); - return { - id: shimModuleId, - version: node.version, - path: shimModulePath - }; - }); - }); - - queue.push({ - nodes: Array.prototype.concat.apply([], shimmedNodes), - parentProjectName, - }); - return { - skip: true - }; - } - let ui5Module = moduleCollection[node.id]; if (!ui5Module) { log.verbose(`Creating module ${node.id}...`); @@ -158,15 +129,39 @@ module.exports = async function(tree) { // Keep this out of the async map function to ensure // all projects and extensions are applied a deterministic order for (let i = 0; i < res.length; i++) { - const {node, project, extensions, skip} = res[i]; + const {node, project, extensions} = res[i]; + + handleExtensions(extensions); + + // Check for collection shims + const collectionShims = shimCollection.getCollectionShims(node.id); + if (collectionShims && collectionShims.length) { + log.verbose( + `One or more module collection shims have been defined for module ${node.id}. ` + + `Therefore the module itself will not be resolved.`); - if (skip) { - // Skip this node + const shimmedNodes = collectionShims.map(({name, shim}) => { + log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); + return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { + const shimModulePath = path.join(node.path, shimModuleRelPath); + log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); + return { + id: shimModuleId, + version: node.version, + path: shimModulePath, + configuration: project && project.getConfigurationObject() + }; + }); + }); + + queue.push({ + nodes: Array.prototype.concat.apply([], shimmedNodes), + parentProjectName, + }); + // Skip collection node continue; } - handleExtensions(extensions); - if (project) { const projectName = project.getName(); if (project.getType() === "application") { diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js index 3020c1e9c..926a3436f 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/AbstractSpecification.js @@ -82,7 +82,7 @@ class AbstractSpecification { * @private */ getConfigurationObject() { - return this._getConfiguration().getObject(); + return JSON.parse(JSON.stringify(this._getConfiguration().getObject())); } /** diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 5ab45cbf1..7b1b7f748 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -1443,6 +1443,94 @@ test("Project with project-shim extension with collection", async (t) => { t.is(log.info.callCount, 0, "log.info should not have been called"); }); +test("Project with project-shim extension with self-containing collection shim", async (t) => { + const tree = { + id: "application.a.id", + path: applicationAPath, + version: "1.0.0", + configuration: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a" + } + }, + dependencies: [{ + id: "legacy.collection.a", + path: legacyCollectionAPath, + version: "1.0.0", + configuration: [{ + specVersion: "2.3", + type: "library", + metadata: { + name: "my.fe" + }, + framework: { + name: "OpenUI5" + } + }, { + specVersion: "2.3", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.x.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.x", + } + }, + "legacy.library.y.id": { + specVersion: "2.3", + type: "library", + metadata: { + name: "legacy.library.y", + } + } + }, + dependencies: { + "legacy.library.x.id": [ + "legacy.library.y.id" + ] + }, + collections: { + "legacy.collection.a": { + modules: { + "legacy.library.x.id": "src/legacy.library.x", + "legacy.library.y.id": "src/legacy.library.y" + } + } + } + } + }], + dependencies: [] + }] + }; + + const graph = await testBasicGraphCreationDfs(t, tree, [ + "legacy.library.y", + "legacy.library.x", + "application.a", + ]); + t.deepEqual(graph.getDependencies("application.a"), [ + "legacy.library.x", + "legacy.library.y" + ], "Shimmed dependencies should be applied"); + + const {log} = t.context; + t.is(log.warn.callCount, 0, "log.warn should not have been called"); + t.is(log.info.callCount, 0, "log.info should not have been called"); + + const libraryY = graph.getProject("legacy.library.y"); + t.deepEqual(libraryY.getConfigurationObject().framework, { + name: "OpenUI5" + }, "Configuration from collection project should be taken over into shimmed project"); +}); + test("Project with unknown extension dependency inline configuration", async (t) => { const tree = { id: "application.a", From c4926c0ee10098f66ca9feade2b069596303413b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 17 Feb 2021 13:25:30 +0100 Subject: [PATCH 20/99] ProjectGraph: Add seal function --- lib/graph/ProjectGraph.js | 33 ++++++++++++++- test/lib/graph/ProjectGraph.js | 75 ++++++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 7032b3229..d26939616 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -21,6 +21,8 @@ class ProjectGraph { this._adjList = {}; // maps project name to edges/dependencies this._extensions = {}; // maps extension name to instance + + this._sealed = false; } getRoot() { @@ -37,6 +39,7 @@ class ProjectGraph { * @param {boolean} [ignoreDuplicates=false] Whether an error should be thrown when a duplicate project is added */ addProject(project, ignoreDuplicates) { + this._checkSealed(); const projectName = project.getName(); if (this._projects[projectName]) { if (ignoreDuplicates) { @@ -70,6 +73,7 @@ class ProjectGraph { * @param {module:@ui5/project.specification.Extension} extension Extension which should be available in the graph */ addExtension(extension) { + this._checkSealed(); const extensionName = extension.getName(); if (this._extensions[extensionName]) { throw new Error( @@ -107,6 +111,7 @@ class ProjectGraph { * @param {string} toProjectName Name of project on which the other depends */ declareDependency(fromProjectName, toProjectName/* , optional*/) { + this._checkSealed(); if (!this._projects[fromProjectName]) { throw new Error( `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + @@ -217,6 +222,7 @@ class ProjectGraph { * The entry project defaults to the root project. * In case a cycle is detected, an error is thrown * + * @public * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project */ @@ -259,17 +265,40 @@ class ProjectGraph { * Join another project graph into this one. * Projects and extensions which already exist in this graph will cause an error to be thrown * + * @public * @param {module:@ui5/project.graph.ProjectGraph} projectGraph Project Graph to merge into this one */ join(projectGraph) { try { + this._checkSealed(); mergeMap(this._projects, projectGraph._projects); mergeMap(this._adjList, projectGraph._adjList); mergeMap(this._extensions, projectGraph._extensions); } catch (err) { throw new Error( - `Failed to join graph with root project ${projectGraph._rootProjectName} into ` + - `${this._rootProjectName}: ${err.message}`); + `Failed to join project graph with root project ${projectGraph._rootProjectName} into ` + + `project graph with root project ${this._rootProjectName}: ${err.message}`); + } + } + + /** + * Seal the project graph so that no further changes can be made to it + * + * @public + */ + seal() { + this._sealed = true; + } + + /** + * Helper function to check and throw in case the project graph has been sealed. + * Intended for use in any function that attempts to make changes to the graph. + * + * @throws Throws in case the project graph has been sealed + */ + _checkSealed() { + if (this._sealed) { + throw new Error(`Project graph with root node ${this._rootProjectName} has been sealed`); } } } diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 4aa962bc7..97b7f09ed 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -789,8 +789,8 @@ test("join: Unexpected project intersection", async (t) => { graph1.join(graph2); }); t.is(error.message, - "Failed to join graph with root project 😼 into 😹: Failed to merge map: " + - "Key 'library.a' already present in target set", + `Failed to join project graph with root project 😼 into project graph with root ` + + `project 😹: Failed to merge map: Key 'library.a' already present in target set`, "Should throw with expected error message"); }); @@ -810,7 +810,74 @@ test("join: Unexpected extension intersection", async (t) => { graph1.join(graph2); }); t.is(error.message, - "Failed to join graph with root project 😼 into 😹: Failed to merge map: " + - "Key 'extension.a' already present in target set", + `Failed to join project graph with root project 😼 into project graph with root ` + + `project 😹: Failed to merge map: Key 'extension.a' already present in target set`, "Should throw with expected error message"); }); + + +test("Seal", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + graph.addExtension(createExtension("extension.a")); + + // Seal it + graph.seal(); + + const expectedSealMsg = "Project graph with root node library.a has been sealed"; + + t.throws(() => { + graph.addProject(createProject("library.x")); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.declareDependency("library.c", "library.b"); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.addExtension(createExtension("extension.b")); + }, { + message: expectedSealMsg + }); + + + const graph2 = new ProjectGraph({ + rootProjectName: "library.x" + }); + t.throws(() => { + graph.join(graph2); + }, { + message: + `Failed to join project graph with root project library.x into project graph ` + + `with root project library.a: ${expectedSealMsg}` + }); + await traverseBreadthFirst(t, graph, [ + "library.a", + "library.b", + "library.c" + ]); + + await traverseDepthFirst(t, graph, [ + "library.c", + "library.b", + "library.a", + ]); + + const project = graph.getProject("library.x"); + t.is(project, undefined, "library.x has not been added"); + + const extension = graph.getExtension("extension.b"); + t.is(extension, undefined, "extension.b has not been added"); +}); From e8114f278db6f9fc74e0b6de61e9d3506de3386f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 17 Feb 2021 13:43:03 +0100 Subject: [PATCH 21/99] ProjectGraph: Projects must not depend on themselves --- lib/graph/ProjectGraph.js | 5 +++++ test/lib/graph/ProjectGraph.js | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index d26939616..1a6353d30 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -122,6 +122,11 @@ class ProjectGraph { `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + `Unable to find dependency project with name ${toProjectName} in graph`); } + if (fromProjectName === toProjectName) { + throw new Error( + `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + + `A project can't depend on itself`); + } if (this._adjList[fromProjectName][toProjectName]) { log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); } else { diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 97b7f09ed..e0819d4f0 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -345,6 +345,23 @@ test("declareDependency: Already declared", async (t) => { "log.warn should be called once with the expected argument"); }); +test("declareDependency: Same target as source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.a"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.a: " + + "A project can't depend on itself", + "Should throw with expected error message"); +}); + test("traverseBreadthFirst", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ From f84156273050af8e1379301da92c98abd892c1c4 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 17 Feb 2021 19:57:44 +0100 Subject: [PATCH 22/99] Implement optional dependencies, npm provider --- lib/graph/Module.js | 2 +- lib/graph/ProjectGraph.js | 153 +++++++++- lib/graph/projectGraphBuilder.js | 275 ++++++++++++++++++ lib/graph/projectGraphFromDirectory.js | 30 +- lib/graph/projectGraphFromTree.js | 248 +--------------- lib/graph/providers/DependencyTree.js | 42 +++ lib/graph/providers/npm.js | 154 ++++++++-- lib/normalizer.js | 16 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/collection/ui5.yaml | 12 + .../collection/library.a/ui5.yaml | 2 +- .../collection/library.b/ui5.yaml | 2 +- .../collection/library.c/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.e/ui5.yaml | 12 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.d-depender/ui5.yaml | 2 +- .../library.e/node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.e/ui5.yaml | 12 +- .../node_modules/library.d-depender/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.e/ui5.yaml | 12 +- .../library.e/node_modules/library.d/ui5.yaml | 9 +- .../node_modules/library.e/ui5.yaml | 12 +- .../node_modules/library.e/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.e/ui5.yaml | 12 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../node_modules/library.cycle.d/ui5.yaml | 2 +- .../node_modules/module.c/ui5.yaml | 5 + .../node_modules/module.d/ui5.yaml | 5 + .../node_modules/module.e/ui5.yaml | 5 + .../node_modules/module.f/ui5.yaml | 5 + .../node_modules/module.g/ui5.yaml | 5 + .../node_modules/library.f/ui5.yaml | 2 +- .../node_modules/library.f/ui5.yaml | 2 +- .../node_modules/library.f/ui5.yaml | 2 +- .../node_modules/library.f/ui5.yaml | 2 +- .../node_modules/library.d/ui5.yaml | 2 +- .../library.f/node_modules/library.g/ui5.yaml | 2 +- .../library.g/node_modules/library.f/ui5.yaml | 2 +- test/lib/graph/ProjectGraph.js | 215 +++++++++++++- test/lib/graph/projectGraphFromTree.js | 3 +- test/lib/graph/providers/npm.integration.js | 265 +++++++++++++++++ .../providers/ui5Framework.integration.js | 15 +- 48 files changed, 1198 insertions(+), 374 deletions(-) create mode 100644 lib/graph/projectGraphBuilder.js create mode 100644 lib/graph/providers/DependencyTree.js create mode 100644 test/fixtures/application.a/node_modules/collection/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.c/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.d/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.e/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.f/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.g/ui5.yaml create mode 100644 test/lib/graph/providers/npm.integration.js diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 2e1a2c021..c4658892f 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -275,7 +275,7 @@ class Module { } return null; } - configFile = await configResource.getBuffer(); + configFile = await configResource.getString(); } let configs; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 1a6353d30..542fd8ae9 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -19,16 +19,18 @@ class ProjectGraph { this._projects = {}; // maps project name to instance this._adjList = {}; // maps project name to edges/dependencies + this._optAdjList = {}; // maps project name to optional dependencies this._extensions = {}; // maps extension name to instance this._sealed = false; + this._shouldResolveOptionalDependencies = false; // Performance optimization flag } getRoot() { const rootProject = this._projects[this._rootProjectName]; if (!rootProject) { - throw new Error(`Unable to find root project with name ${this._rootProjectName} in graph`); + throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`); } return rootProject; } @@ -55,7 +57,8 @@ class ProjectGraph { `Failed to add project ${projectName} to graph: Project name must not be integer-like`); } this._projects[projectName] = project; - this._adjList[projectName] = {}; + this._adjList[projectName] = []; + this._optAdjList[projectName] = []; } /** @@ -110,33 +113,139 @@ class ProjectGraph { * @param {string} fromProjectName Name of the depending project * @param {string} toProjectName Name of project on which the other depends */ - declareDependency(fromProjectName, toProjectName/* , optional*/) { + declareDependency(fromProjectName, toProjectName) { this._checkSealed(); - if (!this._projects[fromProjectName]) { + try { + // if (this._optAdjList[fromProjectName] && this._optAdjList[fromProjectName][toProjectName]) { + // // TODO: Do we even care? + // throw new Error(`Dependency has already been declared as optional`); + // } + log.verbose(`Declaring dependency: ${fromProjectName} depends on ${toProjectName}`); + this._declareDependency(this._adjList, fromProjectName, toProjectName); + } catch (err) { throw new Error( `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + - `Unable to find depending project with name ${fromProjectName} in graph`); + err.message); + } + } + + + /** + * Declare a dependency from one project in the graph to another + * + * @public + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ + declareOptionalDependency(fromProjectName, toProjectName) { + this._checkSealed(); + try { + // if (this._adjList[fromProjectName] && this._adjList[fromProjectName][toProjectName]) { + // // TODO: Do we even care? + // throw new Error(`Dependency has already been declared as non-optional`); + // } + log.verbose(`Declaring optional dependency: ${fromProjectName} depends on ${toProjectName}`); + this._declareDependency(this._optAdjList, fromProjectName, toProjectName); + this._shouldResolveOptionalDependencies = true; + } catch (err) { + throw new Error( + `Failed to declare optional dependency from project ${fromProjectName} to ${toProjectName}: ` + + err.message); + } + } + + /** + * Declare a dependency from one project in the graph to another + * + * @param {object} map Adjacency map to use + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + */ + _declareDependency(map, fromProjectName, toProjectName) { + if (!this._projects[fromProjectName]) { + throw new Error( + `Unable to find depending project with name ${fromProjectName} in project graph`); } if (!this._projects[toProjectName]) { throw new Error( - `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + - `Unable to find dependency project with name ${toProjectName} in graph`); + `Unable to find dependency project with name ${toProjectName} in project graph`); } if (fromProjectName === toProjectName) { throw new Error( - `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + `A project can't depend on itself`); } - if (this._adjList[fromProjectName][toProjectName]) { + if (map[fromProjectName].includes(toProjectName)) { log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); } else { - log.verbose(`Declaring new dependency: ${fromProjectName} depends on ${toProjectName}`); - this._adjList[fromProjectName][toProjectName] = {}; // maybe add optional information? + map[fromProjectName].push(toProjectName); } } + /** + * @public + * @param {string} projectName Name of the project to retrieve the dependencies of + * @returns {string[]} Project names of the given project's dependencies + */ getDependencies(projectName) { - return Object.keys(this._adjList[projectName]); + const adjacencies = this._adjList[projectName]; + if (!adjacencies) { + throw new Error( + `Failed to get dependencies for project ${projectName}: ` + + `Unable to find project in project graph`); + } + return adjacencies; + } + + /** + * Checks whether a dependency is declared as optional or not. + * Currently only used in tests. + * + * @private + * @param {string} fromProjectName Name of the depending project + * @param {string} toProjectName Name of project on which the other depends + * @returns {boolean} True if the dependency is currently declared as optional + * @throws Throws in case the no dependency (optional or not) has been declared + */ + hasOptionalDependency(fromProjectName, toProjectName) { + const adjacencies = this._adjList[fromProjectName]; + if (!adjacencies) { + throw new Error( + `Failed to determine whether dependency from ${fromProjectName} to ${toProjectName} ` + + `is optional: ` + + `Unable to find project with name ${fromProjectName} in project graph`); + } + if (adjacencies.includes(toProjectName)) { + return false; + } + const optAdjacencies = this._optAdjList[fromProjectName]; + if (optAdjacencies.includes(toProjectName)) { + return true; + } + return false; + } + + resolveOptionalDependencies() { + if (!this._shouldResolveOptionalDependencies) { + log.verbose(`Skipping resolution of optional dependencies since none have been declared`); + return; + } + log.verbose(`Resolving optional dependencies...`); + const resolvedProjects = new Set; + for (const [, dependencies] of Object.entries(this._adjList)) { + for (let i = dependencies.length - 1; i >= 0; i--) { + resolvedProjects.add(dependencies[i]); + } + } + for (const [projectName, dependencies] of Object.entries(this._optAdjList)) { + for (let i = dependencies.length - 1; i >= 0; i--) { + if (resolvedProjects.has(dependencies[i])) { + // Resolve optional dependency + log.verbose(`Resolved optional dependency from ${projectName} to ${dependencies[i]}`); + this.declareDependency(projectName, dependencies[i]); + dependencies.splice(i, 1); + } + } + } } /** @@ -176,7 +285,7 @@ class ProjectGraph { */ async traverseBreadthFirst(callback, startName = this._rootProjectName) { if (!this.getProject(startName)) { - throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`); + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } const queue = [{ @@ -233,7 +342,7 @@ class ProjectGraph { */ async traverseDepthFirst(callback, startName = this._rootProjectName) { if (!this.getProject(startName)) { - throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`); + throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } return this._traverseDepthFirst(startName, {}, [], callback); } @@ -276,6 +385,12 @@ class ProjectGraph { join(projectGraph) { try { this._checkSealed(); + if (!projectGraph.isSealed()) { + log.verbose( + `Sealing project graph with root project ${projectGraph._rootProjectName} ` + + `before joining it into project graph with root project ${this._rootProjectName}...`); + projectGraph.seal(); + } mergeMap(this._projects, projectGraph._projects); mergeMap(this._adjList, projectGraph._adjList); mergeMap(this._extensions, projectGraph._extensions); @@ -295,6 +410,16 @@ class ProjectGraph { this._sealed = true; } + /** + * Check whether the project graph has been sealed + * + * @public + * @returns {boolean} True if the project graph has been sealed + */ + isSealed() { + return this._sealed; + } + /** * Helper function to check and throw in case the project graph has been sealed. * Intended for use in any function that attempts to make changes to the graph. diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js new file mode 100644 index 000000000..0495b546b --- /dev/null +++ b/lib/graph/projectGraphBuilder.js @@ -0,0 +1,275 @@ +const path = require("path"); +const Module = require("./Module"); +const ProjectGraph = require("./ProjectGraph"); +const ShimCollection = require("./ShimCollection"); +const log = require("@ui5/logger").getLogger("graph:projectGraphBuilder"); + +function _handleExtensions(graph, shimCollection, extensions) { + extensions.forEach((extension) => { + const type = extension.getType(); + switch (type) { + case "project-shim": + shimCollection.addShim(extension); + break; + case "task": + case "server-middleware": + graph.addExtension(extension); + break; + default: + throw new Error( + `Encountered unexpected extension of type ${type} ` + + `Supported types are 'project-shim', 'task' and 'middleware'`); + } + }); +} + +/** + * Tree node + * + * @public + * @typedef {object} TreeNode + * @property {string} node.id Unique ID for the project + * @property {string} node.version Version of the project + * @property {string} node.path File System path to access the projects resources + * @property {object|object[]} [node.configuration] + * Configuration object or array of objects to use instead of reading from a configuration file + * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {TreeNode[]} dependencies + */ + +/** + * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} + * from a dependency tree as returned by translators. + * + * @public + * @alias module:@ui5/project.graph.projectGraphBuilder + * @param {ModuleProvider} nodeProvider Dependency tree as returned by a translator + * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance + */ +module.exports = async function(nodeProvider) { + const shimCollection = new ShimCollection(); + const moduleCollection = {}; + + const rootNode = await nodeProvider.getRootNode(); + const rootModule = new Module({ + id: rootNode.id, + version: rootNode.version, + modulePath: rootNode.path, + configPath: rootNode.configPath, + configuration: rootNode.configuration + }); + const {project: rootProject, extensions: rootExtensions} = await rootModule.getSpecifications(); + if (!rootProject) { + throw new Error( + `Failed to crate a UI5 project from module ${rootNode.id} at ${rootNode.path}. ` + + `Make sure the path is correct and a project configuration is present or supplied.`); + } + + moduleCollection[rootNode.id] = rootModule; + + const rootProjectName = rootProject.getName(); + + let qualifiedApplicationProject = null; + if (rootProject.getType() === "application") { + log.verbose(`Root project ${rootProjectName} qualified as application project for project graph`); + qualifiedApplicationProject = rootProject; + } + + + const projectGraph = new ProjectGraph({ + rootProjectName: rootProjectName + }); + projectGraph.addProject(rootProject); + + function handleExtensions(extensions) { + return _handleExtensions(projectGraph, shimCollection, extensions); + } + + handleExtensions(rootExtensions); + + const queue = []; + + const rootDependencies = await nodeProvider.getDependencies(rootNode); + + if (rootDependencies.length) { + queue.push({ + nodes: rootDependencies, + parentProjectName: rootProjectName + }); + } + + // Breadth-first search + while (queue.length) { + const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue + const res = await Promise.all(nodes.map(async (node) => { + let ui5Module = moduleCollection[node.id]; + if (!ui5Module) { + log.verbose(`Creating module ${node.id}...`); + ui5Module = moduleCollection[node.id] = new Module({ + id: node.id, + version: node.version, + modulePath: node.path, + configPath: node.configPath, + configuration: node.configuration, + shimCollection + }); + } else if (ui5Module.getPath() !== node.path) { + log.verbose( + `Inconsistency detected: Tree contains multiple nodes with ID ${node.id} and different paths:` + + `\nPath of already added node (this one will be used): ${ui5Module.getPath()}` + + `\nPath of additional node (this one will be ignored in favor of the other): ${node.path}`); + } + + const {project, extensions} = await ui5Module.getSpecifications(); + + return { + node, + project, + extensions + }; + })); + + // Keep this out of the async map function to ensure + // all projects and extensions are applied a deterministic order + for (let i = 0; i < res.length; i++) { + const {node, project, extensions} = res[i]; + + handleExtensions(extensions); + + // Check for collection shims + const collectionShims = shimCollection.getCollectionShims(node.id); + if (collectionShims && collectionShims.length) { + log.verbose( + `One or more module collection shims have been defined for module ${node.id}. ` + + `Therefore the module itself will not be resolved.`); + + const shimmedNodes = collectionShims.map(({name, shim}) => { + log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); + return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { + const shimModulePath = path.join(node.path, shimModuleRelPath); + log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); + return { + id: shimModuleId, + version: node.version, + path: shimModulePath, + configuration: project && project.getConfigurationObject() + }; + }); + }); + + queue.push({ + nodes: Array.prototype.concat.apply([], shimmedNodes), + parentProjectName, + }); + // Skip collection node + continue; + } + let skipDependencies = false; + if (project) { + const projectName = project.getName(); + if (project.getType() === "application") { + // Special handling of application projects of which there must be exactly *one* + // in the graph. Others shall be ignored. + if (!qualifiedApplicationProject) { + log.verbose(`Project ${projectName} qualified as application project for project graph`); + qualifiedApplicationProject = project; + } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { + // Project is not a duplicate of an already qualified project (which should + // still be processed below), but a unique, additional application project + + // TODO: Should this rather be a verbose logging? + // projectPreprocessor handled this like any project that got ignored for some reason and did a + // (in this case misleading) general verbose logging: + // "Ignoring project with missing configuration" + log.info( + `Excluding additional application project ${projectName} from graph. `+ + `The project graph can only feature a single project of type application. ` + + `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); + continue; + } + } + if (projectGraph.getProject(projectName)) { + log.verbose( + `Project ${projectName} has already been added to the graph. ` + + `Skipping repeated dependency resolution..`); + skipDependencies = true; + } else { + projectGraph.addProject(project); + } + + // if (!node.deduped) { + // Even if not deduped, the node might occur multiple times in the tree (on separate branches). + // Therefore still supplying the ignore duplicates parameter here (true) + // } + + if (parentProjectName) { + if (node.optional) { + projectGraph.declareOptionalDependency(parentProjectName, projectName); + } else { + projectGraph.declareDependency(parentProjectName, projectName); + } + } + } + + if (!project && !extensions.length) { + // Module provided neither a project nor an extension + // => Do not follow its dependencies + log.verbose( + `Module ${node.id} neither provided a project nor an extension. Skipping dependency resolution.`); + skipDependencies = true; + } + + if (skipDependencies) { + continue; + } + + const nodeDependencies = await nodeProvider.getDependencies(node); + if (nodeDependencies) { + queue.push({ + // copy array, so that the queue is stable while ignored project dependencies are removed + nodes: [...nodeDependencies], + parentProjectName: project ? project.getName() : parentProjectName, + }); + } + } + } + + // Appply dependency shims + for (const [shimmedModuleId, moduleDepShims] of Object.entries(shimCollection.getAllDependencyShims())) { + const sourceModule = moduleCollection[shimmedModuleId]; + + for (let j = 0; j < moduleDepShims.length; j++) { + const depShim = moduleDepShims[j]; + if (!sourceModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Module ${shimmedModuleId} is unknown`); + continue; + } + const {project: sourceProject} = await sourceModule.getSpecifications(); + if (!sourceProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Source module ${shimmedModuleId} does not contain a project`); + continue; + } + for (let k = 0; k < depShim.shim.length; k++) { + const targetModuleId = depShim.shim[k]; + const targetModule = moduleCollection[targetModuleId]; + if (!targetModule) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module $${depShim} is unknown`); + continue; + } + const {project: targetProject} = await targetModule.getSpecifications(); + if (!targetProject) { + log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + + `Target module ${targetModuleId} does not contain a project`); + continue; + } + projectGraph.declareDependency(sourceProject.getName(), targetProject.getName()); + } + } + } + projectGraph.resolveOptionalDependencies(); + + return projectGraph; +}; diff --git a/lib/graph/projectGraphFromDirectory.js b/lib/graph/projectGraphFromDirectory.js index 33f74a2ac..112c1d84d 100644 --- a/lib/graph/projectGraphFromDirectory.js +++ b/lib/graph/projectGraphFromDirectory.js @@ -1,4 +1,4 @@ -const projectGraphFromTree = require("./projectGraphFromTree"); +const projectGraphBuilder = require("./projectGraphBuilder"); const ui5Framework = require("./providers/ui5Framework"); const log = require("@ui5/logger").getLogger("graph:projectGraphFromDirectory"); @@ -18,18 +18,25 @@ const projectGraphFromDirectory = { * * @public * @param {object} [options] - * @param {string} [options.cwd=.] Directory of the root module - * @param {object} [options.configuration] + * @param {string} [options.cwd=.] Directory to start searching for the root module + * @param {object} [options.rootConfiguration] * Configuration object to use for the root module instead of reading from a configuration file - * @param {string} [options.configPath] Configuration file to use for the root module instead the default ui5.yaml + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @returns {Promise} Promise resolving to a Project Graph instance */ - usingNpm: async function({cwd = ".", configuration, configPath, versionOverride}) { + usingNpm: async function({cwd = ".", rootConfiguration, rootConfigPath, versionOverride}) { log.verbose(`Creating project graph using npm provider...`); - const npmProvider = require("./providers/npm"); + const NpmProvider = require("./providers/npm"); - const projectGraph = await npmProvider.createProjectGraph(cwd); + const provider = new NpmProvider({ + cwd: cwd, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); @@ -44,19 +51,24 @@ const projectGraphFromDirectory = { * @public * @param {object} options * @param {object} options.filePath Path to the file dependency configuration file - * @param {string} [options.cwd=.] Directory of the root module + * @param {string} [options.cwd=.] Directory to start searching for the root module * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @returns {Promise} Promise resolving to a Project Graph instance */ usingStaticFile: async function({cwd = ".", filePath, versionOverride}) { log.verbose(`Creating project graph using static file...`); const staticTranslator = require("../translators/static"); + const DependencyTreeProvider = require("./providers/DependencyTree"); const tree = await staticTranslator.generateDependencyTree(null, { parameters: [filePath] // *sigh* }); - const projectGraph = await projectGraphFromTree(tree); + const provider = new DependencyTreeProvider({ + tree + }); + + const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js index 1e84ab8be..08490c209 100644 --- a/lib/graph/projectGraphFromTree.js +++ b/lib/graph/projectGraphFromTree.js @@ -1,247 +1,7 @@ -const path = require("path"); -const Module = require("./Module"); -const ProjectGraph = require("./ProjectGraph"); -const ShimCollection = require("./ShimCollection"); -const log = require("@ui5/logger").getLogger("graph:projectGraphFromTree"); +const projectGraphBuilder = require("./projectGraphBuilder"); +const DependencyTreeProvider = require("./providers/DependencyTree"); -function _handleExtensions(graph, shimCollection, extensions) { - extensions.forEach((extension) => { - const type = extension.getType(); - switch (type) { - case "project-shim": - shimCollection.addShim(extension); - break; - case "task": - case "server-middleware": - graph.addExtension(extension); - break; - default: - throw new Error( - `Encountered unexpected extension of type ${type} ` + - `Supported types are 'project-shim', 'task' and 'middleware'`); - } - }); -} - -/** - * Tree node - * - * @public - * @typedef {object} TreeNode - * @property {string} node.id Unique ID for the project - * @property {string} node.version Version of the project - * @property {string} node.path File System path to access the projects resources - * @property {object|object[]} [node.configuration] - * Configuration object or array of objects to use instead of reading from a configuration file - * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml - * @property {TreeNode[]} dependencies - */ - -/** - * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} - * from a dependency tree as returned by translators. - * - * @public - * @alias module:@ui5/project.graph.projectGraphFromTree - * @param {TreeNode} tree Dependency tree as returned by a translator - * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance - */ module.exports = async function(tree) { - const shimCollection = new ShimCollection(); - const moduleCollection = {}; - - const rootModule = new Module({ - id: tree.id, - version: tree.version, - modulePath: tree.path, - configPath: tree.configPath, - configuration: tree.configuration - }); - const {project: rootProject, extensions: rootExtensions} = await rootModule.getSpecifications(); - if (!rootProject) { - throw new Error( - `Failed to crate a UI5 project from module ${tree.id} at ${tree.path}. ` + - `Make sure the path is correct and a project configuration is present or supplied.`); - } - - moduleCollection[tree.id] = rootModule; - - const rootProjectName = rootProject.getName(); - - let qualifiedApplicationProject = null; - if (rootProject.getType() === "application") { - log.verbose(`Root project ${rootProjectName} qualified as application project for project graph`); - qualifiedApplicationProject = rootProject; - } - - - const projectGraph = new ProjectGraph({ - rootProjectName: rootProjectName - }); - projectGraph.addProject(rootProject); - - function handleExtensions(extensions) { - return _handleExtensions(projectGraph, shimCollection, extensions); - } - - handleExtensions(rootExtensions); - - const queue = []; - - if (tree.dependencies) { - queue.push({ - nodes: tree.dependencies, - parentProjectName: rootProjectName - }); - } - - // Breadth-first search - while (queue.length) { - const {nodes, parentProjectName} = queue.shift(); // Get and remove first entry from queue - const res = await Promise.all(nodes.map(async (node) => { - let ui5Module = moduleCollection[node.id]; - if (!ui5Module) { - log.verbose(`Creating module ${node.id}...`); - ui5Module = moduleCollection[node.id] = new Module({ - id: node.id, - version: node.version, - modulePath: node.path, - configPath: node.configPath, - configuration: node.configuration, - shimCollection - }); - } else if (ui5Module.getPath() !== node.path) { - log.verbose( - `Inconsistency detected: Tree contains multiple nodes with ID ${node.id} and different paths:` + - `\nPath of already added node (this one will be used): ${ui5Module.getPath()}` + - `\nPath of additional node (this one will be ignored in favor of the other): ${node.path}`); - } - - const {project, extensions} = await ui5Module.getSpecifications(); - - return { - node, - project, - extensions - }; - })); - - // Keep this out of the async map function to ensure - // all projects and extensions are applied a deterministic order - for (let i = 0; i < res.length; i++) { - const {node, project, extensions} = res[i]; - - handleExtensions(extensions); - - // Check for collection shims - const collectionShims = shimCollection.getCollectionShims(node.id); - if (collectionShims && collectionShims.length) { - log.verbose( - `One or more module collection shims have been defined for module ${node.id}. ` + - `Therefore the module itself will not be resolved.`); - - const shimmedNodes = collectionShims.map(({name, shim}) => { - log.verbose(`Applying module collection shim ${name} for module ${node.id}:`); - return Object.entries(shim.modules).map(([shimModuleId, shimModuleRelPath]) => { - const shimModulePath = path.join(node.path, shimModuleRelPath); - log.verbose(` Injecting module ${shimModuleId} with path ${shimModulePath}`); - return { - id: shimModuleId, - version: node.version, - path: shimModulePath, - configuration: project && project.getConfigurationObject() - }; - }); - }); - - queue.push({ - nodes: Array.prototype.concat.apply([], shimmedNodes), - parentProjectName, - }); - // Skip collection node - continue; - } - - if (project) { - const projectName = project.getName(); - if (project.getType() === "application") { - // Special handling of application projects of which there must be exactly *one* - // in the graph. Others shall be ignored. - if (!qualifiedApplicationProject) { - log.verbose(`Project ${projectName} qualified as application project for project graph`); - qualifiedApplicationProject = project; - } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { - // Project is not a duplicate of an already qualified project (which should - // still be processed below), but a unique, additional application project - - // TODO: Should this rather be a verbose logging? - // projectPreprocessor handled this like any project that got ignored for some reason and did a - // (in this case misleading) general verbose logging: - // "Ignoring project with missing configuration" - log.info( - `Excluding additional application project ${projectName} from graph. `+ - `The project graph can only feature a single project of type application. ` + - `Project ${qualifiedApplicationProject.getName()} has already qualified for that role.`); - continue; - } - } - - // if (!node.deduped) { - // Even if not deduped, the node might occur multiple times in the tree (on separate branches). - // Therefore still supplying the ignore duplicates parameter here (true) - projectGraph.addProject(project, true); - // } - - if (parentProjectName) { - projectGraph.declareDependency(parentProjectName, projectName); - } - } - - if (node.dependencies && !node.deduped) { - queue.push({ - // copy array, so that the queue is stable while ignored project dependencies are removed - nodes: [...node.dependencies], - parentProjectName: project ? project.getName() : parentProjectName, - }); - } - } - } - - // Appply dependency shims - for (const [shimmedModuleId, moduleDepShims] of Object.entries(shimCollection.getAllDependencyShims())) { - const sourceModule = moduleCollection[shimmedModuleId]; - - for (let j = 0; j < moduleDepShims.length; j++) { - const depShim = moduleDepShims[j]; - if (!sourceModule) { - log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + - `Module ${shimmedModuleId} is unknown`); - continue; - } - const {project: sourceProject} = await sourceModule.getSpecifications(); - if (!sourceProject) { - log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + - `Source module ${shimmedModuleId} does not contain a project`); - continue; - } - for (let k = 0; k < depShim.shim.length; k++) { - const targetModuleId = depShim.shim[k]; - const targetModule = moduleCollection[targetModuleId]; - if (!targetModule) { - log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + - `Target module $${depShim} is unknown`); - continue; - } - const {project: targetProject} = await targetModule.getSpecifications(); - if (!targetProject) { - log.warn(`Could not apply dependency shim ${depShim.name} for ${shimmedModuleId}: ` + - `Target module ${targetModuleId} does not contain a project`); - continue; - } - projectGraph.declareDependency(sourceProject.getName(), targetProject.getName()); - } - } - } - - return projectGraph; + const dependencyTreeProvider = new DependencyTreeProvider(tree); + return projectGraphBuilder(dependencyTreeProvider); }; diff --git a/lib/graph/providers/DependencyTree.js b/lib/graph/providers/DependencyTree.js new file mode 100644 index 000000000..633c34016 --- /dev/null +++ b/lib/graph/providers/DependencyTree.js @@ -0,0 +1,42 @@ +/** + * Tree node + * + * @public + * @typedef {object} TreeNode + * @property {string} node.id Unique ID for the project + * @property {string} node.version Version of the project + * @property {string} node.path File System path to access the projects resources + * @property {object|object[]} [node.configuration] + * Configuration object or array of objects to use instead of reading from a configuration file + * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml + * @property {TreeNode[]} dependencies + */ +class DependencyTree { + /** + * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} + * from a dependency tree as returned by translators. + * + * @public + * @alias module:@ui5/project.graph.projectGraphFromTree + * @param {TreeNode} tree Dependency tree as returned by a translator + */ + constructor(tree) { + if (!tree) { + throw new Error(`Failed to instantiate DependencyTree provider: Missing parameter 'tree'`); + } + this._tree= tree; + } + + async getRootNode() { + return this._tree; + } + + async getDependencies(node) { + if (node.deduped || !node.dependencies) { + return []; + } + return node.dependencies; + } +} + +module.exports = DependencyTree; diff --git a/lib/graph/providers/npm.js b/lib/graph/providers/npm.js index fa2135350..d9bfb18db 100644 --- a/lib/graph/providers/npm.js +++ b/lib/graph/providers/npm.js @@ -1,23 +1,143 @@ -// const ProjectGraph = require("../ProjectGraph"); -// const log = require("@ui5/logger").getLogger("graph:providers:npm"); - -/** - * Graph provider for npm based projects - * - * @public - * @namespace - * @alias module:@ui5/project.graph.providers.npm - */ -module.exports = { +const path = require("path"); +const readPkgUp = require("read-pkg-up"); +const readPkg = require("read-pkg"); +const {promisify} = require("util"); +const fs = require("graceful-fs"); +const realpath = promisify(fs.realpath); +const resolveModulePath = promisify(require("resolve")); +const log = require("@ui5/logger").getLogger("graph:providers:npm"); + +// Packages to consider: +// * https://github.com/npm/read-package-json-fast +// * https://github.com/npm/name-from-folder ? + + +class Npm { /** * Generates a project graph from npm modules * * @public - * @param {string} dirPath Project path - * @returns {Promise} Promise resolving with a project graph + * @param {object} options + * @param {string} options.cwd Directory to start searching for the root module + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml */ - createProjectGraph(dirPath) { - // TODO: Make arborist happen - // https://github.com/npm/arborist + constructor({cwd, rootConfiguration, rootConfigPath}) { + this._cwd = cwd; + this._rootConfiguration = rootConfiguration; + this._rootConfigPath = rootConfigPath; + + // this._nodes = {}; + } + + async getRootNode() { + const rootPkg = await readPkgUp({ + cwd: this._cwd, + normalize: false + }); + + if (!rootPkg || !rootPkg.packageJson) { + throw new Error( + `Failed to locate package.json for directory ${path.resolve(this._cwd)}`); + } + const modulePath = path.dirname(rootPkg.path); + // this._nodes[rootPkg.packageJson.name] = { + // dependencies: Object.keys(rootPkg.packageJson.dependencies) + // }; + return { + id: rootPkg.packageJson.name, + version: rootPkg.packageJson.version, + path: modulePath, + configuration: this._rootConfiguration, + configPath: this._rootConfigPath, + _dependencies: await this._getDependencies(modulePath, rootPkg.packageJson, true) + }; + } + + async getDependencies(node) { + log.verbose(`Resolving dependencies of ${node.id}...`); + if (!node._dependencies) { + return []; + } + return Promise.all(node._dependencies.map(async ({name, optional}) => { + const modulePath = await this._resolveModulePath(node.path, name); + return this._getNode(modulePath, optional); + })); } -}; + + async _resolveModulePath(baseDir, moduleName) { + log.verbose(`Resolving module path for '${moduleName}'...`); + let packageJsonPath = await resolveModulePath(moduleName + "/package.json", { + basedir: baseDir, + preserveSymlinks: false + }); + packageJsonPath = await realpath(packageJsonPath); + + const modulePath = path.dirname(packageJsonPath); + log.verbose(`Resolved module ${moduleName} to path ${modulePath}`); + return modulePath; + } + + async _getNode(modulePath, optional) { + log.verbose(`Reading package.json in directory ${modulePath}...`); + const packageJson = await readPkg({ + cwd: modulePath, + normalize: false + }); + + return { + id: packageJson.name, + version: packageJson.version, + path: modulePath, + optional, + _dependencies: await this._getDependencies(modulePath, packageJson) + }; + } + + async _getDependencies(modulePath, packageJson, rootModule = false) { + const dependencies = []; + if (packageJson.dependencies) { + const packageJsonDependencies = Object.keys(packageJson.dependencies); + if (rootModule && packageJson.devDependencies) { + packageJsonDependencies.push(...Object.keys(packageJson.devDependencies)); + } + packageJsonDependencies.forEach((depName) => { + dependencies.push({ + name: depName, + optional: false + }); + }); + } + if (!rootModule && packageJson.devDependencies) { + await Promise.all(Object.keys(packageJson.devDependencies).map(async (depName) => { + try { + await this._resolveModulePath(modulePath, depName); + dependencies.push({ + name: depName, + optional: true + }); + } catch (err) { + // Ignore error since it's a development dependency of a non-root module + } + })); + } + if (packageJson.optionalDependencies) { + await Promise.all(Object.keys(packageJson.optionalDependencies).map(async (depName) => { + try { + await this._resolveModulePath(modulePath, depName); + dependencies.push({ + name: depName, + optional: false + }); + } catch (err) { + // Ignore error since it's an optional dependency + } + })); + } + return dependencies; + } +} + +module.exports = Npm; diff --git a/lib/normalizer.js b/lib/normalizer.js index 7aa6cbdb5..cd2e8cf4e 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -61,16 +61,14 @@ const Normalizer = { * @returns {Promise} Promise resolving to a Project Graph instance */ generateProjectGraph: async function(options = {}) { - const projectGraphFromTree = require("./graph/projectGraphFromTree"); - const tree = await Normalizer.generateDependencyTree(options); + // const projectGraphFromTree = require("./graph/projectGraphFromTree"); + // const tree = await Normalizer.generateDependencyTree(options); + const projectGraphFromDirectory = require("./graph/projectGraphFromDirectory"); - if (options.configPath) { - tree.configPath = options.configPath; - } - const projectGraph = await projectGraphFromTree(tree); - - const ui5Framework = require("./graph/providers/ui5Framework"); - await ui5Framework.enrichProjectGraph(projectGraph, options.frameworkOptions); + const projectGraph = await projectGraphFromDirectory.usingNpm({ + rootConfigPath: options.configPath, + versionOverride: options.versionOverride + }); return projectGraph; }, diff --git a/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml b/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.a/node_modules/collection/ui5.yaml b/test/fixtures/application.a/node_modules/collection/ui5.yaml new file mode 100644 index 000000000..e47048de6 --- /dev/null +++ b/test/fixtures/application.a/node_modules/collection/ui5.yaml @@ -0,0 +1,12 @@ +specVersion: "2.1" +metadata: + name: application.a.collection.dependency.shim +kind: extension +type: project-shim +shims: + collections: + collection: + modules: + "library.a": "./library.a" + "library.b": "./library.b" + "library.c": "./library.c" \ No newline at end of file diff --git a/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml b/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml index 676f166c3..8d4784313 100644 --- a/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml +++ b/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.a diff --git a/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml b/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml index 3275ac753..b2fe5be59 100644 --- a/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml +++ b/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.b diff --git a/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml b/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml index 159b14118..7c5e38a7f 100644 --- a/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml +++ b/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.c diff --git a/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml b/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.b/node_modules/library.d/ui5.yaml b/test/fixtures/application.b/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.b/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.b/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.c/node_modules/library.d/ui5.yaml b/test/fixtures/application.c/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.c/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.c/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.c/node_modules/library.e/ui5.yaml b/test/fixtures/application.c/node_modules/library.e/ui5.yaml index a1e7f1214..88ba07e82 100644 --- a/test/fixtures/application.c/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.c/node_modules/library.e/ui5.yaml @@ -1,11 +1,9 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e -builder: - configuration: - copyright: |- - UI development toolkit for HTML5 (OpenUI5) - * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. - * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml b/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml b/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml index d3a67f6da..517442188 100644 --- a/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml +++ b/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d-depender diff --git a/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml b/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.c2/node_modules/library.e/ui5.yaml b/test/fixtures/application.c2/node_modules/library.e/ui5.yaml index a1e7f1214..99dbbf3bc 100644 --- a/test/fixtures/application.c2/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.c2/node_modules/library.e/ui5.yaml @@ -1,11 +1,9 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e -builder: - configuration: - copyright: |- - UI development toolkit for HTML5 (OpenUI5) - * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. - * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml b/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml index d3a67f6da..517442188 100644 --- a/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml +++ b/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d-depender diff --git a/test/fixtures/application.c3/node_modules/library.d/ui5.yaml b/test/fixtures/application.c3/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.c3/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.c3/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.c3/node_modules/library.e/ui5.yaml b/test/fixtures/application.c3/node_modules/library.e/ui5.yaml index a1e7f1214..99dbbf3bc 100644 --- a/test/fixtures/application.c3/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.c3/node_modules/library.e/ui5.yaml @@ -1,11 +1,9 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e -builder: - configuration: - copyright: |- - UI development toolkit for HTML5 (OpenUI5) - * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. - * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml b/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml index 7b731df83..a47c1f64c 100644 --- a/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -1,3 +1,10 @@ --- -name: library.d +specVersion: "2.3" type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/test/fixtures/application.d/node_modules/library.e/ui5.yaml b/test/fixtures/application.d/node_modules/library.e/ui5.yaml index a1e7f1214..99dbbf3bc 100644 --- a/test/fixtures/application.d/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.d/node_modules/library.e/ui5.yaml @@ -1,11 +1,9 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e -builder: - configuration: - copyright: |- - UI development toolkit for HTML5 (OpenUI5) - * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. - * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/fixtures/application.e/node_modules/library.e/ui5.yaml b/test/fixtures/application.e/node_modules/library.e/ui5.yaml index a1e7f1214..3852a732d 100644 --- a/test/fixtures/application.e/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.e/node_modules/library.e/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e diff --git a/test/fixtures/application.f/node_modules/library.d/ui5.yaml b/test/fixtures/application.f/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.f/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.f/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/application.f/node_modules/library.e/ui5.yaml b/test/fixtures/application.f/node_modules/library.e/ui5.yaml index a1e7f1214..99dbbf3bc 100644 --- a/test/fixtures/application.f/node_modules/library.e/ui5.yaml +++ b/test/fixtures/application.f/node_modules/library.e/ui5.yaml @@ -1,11 +1,9 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.e -builder: - configuration: - copyright: |- - UI development toolkit for HTML5 (OpenUI5) - * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. - * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/fixtures/application.g/node_modules/library.d/ui5.yaml b/test/fixtures/application.g/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/application.g/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.g/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/collection/node_modules/library.d/ui5.yaml b/test/fixtures/collection/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/collection/node_modules/library.d/ui5.yaml +++ b/test/fixtures/collection/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/cyclic-deps/node_modules/library.cycle.c/node_modules/library.cycle.d/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/library.cycle.c/node_modules/library.cycle.d/ui5.yaml index 3fb70271d..52bb969a8 100644 --- a/test/fixtures/cyclic-deps/node_modules/library.cycle.c/node_modules/library.cycle.d/ui5.yaml +++ b/test/fixtures/cyclic-deps/node_modules/library.cycle.c/node_modules/library.cycle.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.cycle.d diff --git a/test/fixtures/cyclic-deps/node_modules/module.c/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.c/ui5.yaml new file mode 100644 index 000000000..f79d97826 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.c diff --git a/test/fixtures/cyclic-deps/node_modules/module.d/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.d/ui5.yaml new file mode 100644 index 000000000..c65780ec5 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.d/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.d diff --git a/test/fixtures/cyclic-deps/node_modules/module.e/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.e/ui5.yaml new file mode 100644 index 000000000..e6487041a --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.e/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.e diff --git a/test/fixtures/cyclic-deps/node_modules/module.f/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.f/ui5.yaml new file mode 100644 index 000000000..5834bf479 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.f/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.f diff --git a/test/fixtures/cyclic-deps/node_modules/module.g/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.g/ui5.yaml new file mode 100644 index 000000000..dfadd749b --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.g/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.g diff --git a/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml b/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml +++ b/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml b/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml +++ b/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml b/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml +++ b/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml b/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml +++ b/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml b/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml index c39a8461d..a47c1f64c 100644 --- a/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml +++ b/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.d diff --git a/test/fixtures/library.f/node_modules/library.g/ui5.yaml b/test/fixtures/library.f/node_modules/library.g/ui5.yaml index 9c5281718..a20d2d499 100644 --- a/test/fixtures/library.f/node_modules/library.g/ui5.yaml +++ b/test/fixtures/library.f/node_modules/library.g/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.g diff --git a/test/fixtures/library.g/node_modules/library.f/ui5.yaml b/test/fixtures/library.g/node_modules/library.f/ui5.yaml index 38440bacb..52c17922b 100644 --- a/test/fixtures/library.g/node_modules/library.f/ui5.yaml +++ b/test/fixtures/library.g/node_modules/library.f/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "0.1" +specVersion: "2.3" type: library metadata: name: library.f diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index e0819d4f0..34b1006fc 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -123,7 +123,7 @@ test("getRoot: Root not added to graph", async (t) => { graph.getRoot(); }); t.is(error.message, - "Unable to find root project with name application.a in graph", + "Unable to find root project with name application.a in project graph", "Should throw with expected error message"); }); @@ -294,6 +294,12 @@ test("declareDependency / getDependencies", async (t) => { t.deepEqual(graph.getDependencies("library.b"), [ "library.a" ], "Should store and return correct dependencies for library.b"); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); + + t.is(graph.hasOptionalDependency("library.b", "library.a"), false, + "Should declare dependency as non-optional"); }); test("declareDependency: Unknown source", async (t) => { @@ -308,7 +314,7 @@ test("declareDependency: Unknown source", async (t) => { }); t.is(error.message, "Failed to declare dependency from project library.a to library.b: Unable " + - "to find depending project with name library.a in graph", + "to find depending project with name library.a in project graph", "Should throw with expected error message"); }); @@ -324,7 +330,24 @@ test("declareDependency: Unknown target", async (t) => { }); t.is(error.message, "Failed to declare dependency from project library.a to library.b: Unable " + - "to find dependency project with name library.b in graph", + "to find dependency project with name library.b in project graph", + "Should throw with expected error message"); +}); + +test("declareDependency: Same target as source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + const error = t.throws(() => { + graph.declareDependency("library.a", "library.a"); + }); + t.is(error.message, + "Failed to declare dependency from project library.a to library.a: " + + "A project can't depend on itself", "Should throw with expected error message"); }); @@ -345,23 +368,149 @@ test("declareDependency: Already declared", async (t) => { "log.warn should be called once with the expected argument"); }); -test("declareDependency: Same target as source", async (t) => { - const {ProjectGraph} = t.context; +test("declareDependency: Already declared as optional", async (t) => { + const {ProjectGraph, log} = t.context; const graph = new ProjectGraph({ rootProjectName: "my root project" }); graph.addProject(createProject("library.a")); graph.addProject(createProject("library.b")); + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 1, "log.warn should be called once"); + t.is(log.warn.getCall(0).args[0], + `Dependency has already been declared: library.a depends on library.b`, + "log.warn should be called once with the expected argument"); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), true, + "Should declare dependency as optional"); +}); + +test("declareDependency: Already declared as non-optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + + graph.declareOptionalDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 0, "log.warn should not be called"); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); +}); + +test("declareDependency: Already declared as optional, now non-optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.b"); + + t.is(log.warn.callCount, 0, "log.warn should not be called"); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); +}); + + +test("getDependencies: Project without dependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + + graph.addProject(createProject("library.a")); + + t.deepEqual(graph.getDependencies("library.a"), [], + "Should return an empty array for project without dependencies"); +}); + +test("getDependencies: Unknown project", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const error = t.throws(() => { - graph.declareDependency("library.a", "library.a"); + graph.getDependencies("library.x"); }); t.is(error.message, - "Failed to declare dependency from project library.a to library.a: " + - "A project can't depend on itself", + "Failed to get dependencies for project library.x: Unable to find project in project graph", "Should throw with expected error message"); }); +test("resolveOptionalDependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + graph.addProject(createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.a", "library.d"); + graph.declareDependency("library.d", "library.b"); + graph.declareDependency("library.d", "library.c"); + + graph.resolveOptionalDependencies(); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + "library.a should have no optional dependency to library.b anymore"); + t.is(graph.hasOptionalDependency("library.a", "library.c"), false, + "library.a should have no optional dependency to library.c anymore"); + + await traverseDepthFirst(t, graph, [ + "library.b", + "library.c", + "library.d", + "library.a" + ]); +}); + + +test("resolveOptionalDependencies: Optional dependency has not been resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(createProject("library.a")); + graph.addProject(createProject("library.b")); + graph.addProject(createProject("library.c")); + graph.addProject(createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.a", "library.d"); + + graph.resolveOptionalDependencies(); + + t.is(graph.hasOptionalDependency("library.a", "library.b"), true, + "Dependency from library.a to library.b should still be optional"); + + t.is(graph.hasOptionalDependency("library.a", "library.c"), true, + "Dependency from library.a to library.c should still be optional"); + + await traverseDepthFirst(t, graph, [ + "library.d", + "library.a" + ]); +}); + + test("traverseBreadthFirst", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ @@ -444,7 +593,7 @@ test("traverseBreadthFirst: Can't find start node", async (t) => { const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {})); t.is(error.message, - "Failed to start graph traversal: Could not find project library.a in graph", + "Failed to start graph traversal: Could not find project library.a in project graph", "Should throw with expected error message"); }); @@ -633,7 +782,7 @@ test("traverseDepthFirst: Can't find start node", async (t) => { const error = await t.throwsAsync(graph.traverseDepthFirst(() => {})); t.is(error.message, - "Failed to start graph traversal: Could not find project library.a in graph", + "Failed to start graph traversal: Could not find project library.a in project graph", "Should throw with expected error message"); }); @@ -790,6 +939,38 @@ test("join", async (t) => { t.is(graph1.getExtension("extension.b"), extensionB, "Should return correct joined extension"); }); +test("join: Seals incoming graph", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + + + const sealSpy = t.context.sinon.spy(graph2, "seal"); + graph1.join(graph2); + + t.is(sealSpy.callCount, 1, "Should call seal() on incoming graph once"); +}); + +test("join: Incoming graph already sealed", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "library.a" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "theme.a" + }); + + graph2.seal(); + const sealSpy = t.context.sinon.spy(graph2, "seal"); + graph1.join(graph2); + + t.is(sealSpy.callCount, 0, "Should not call seal() on incoming graph"); +}); + test("join: Unexpected project intersection", async (t) => { const {ProjectGraph} = t.context; const graph1 = new ProjectGraph({ @@ -833,7 +1014,7 @@ test("join: Unexpected extension intersection", async (t) => { }); -test("Seal", async (t) => { +test("Seal/isSealed", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ rootProjectName: "library.a" @@ -845,11 +1026,14 @@ test("Seal", async (t) => { graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); graph.declareDependency("library.b", "library.c"); + graph.declareOptionalDependency("library.c", "library.a"); graph.addExtension(createExtension("extension.a")); + t.is(graph.isSealed(), false, "Graph should not be sealed"); // Seal it graph.seal(); + t.is(graph.isSealed(), true, "Graph should be sealed"); const expectedSealMsg = "Project graph with root node library.a has been sealed"; @@ -863,6 +1047,11 @@ test("Seal", async (t) => { }, { message: expectedSealMsg }); + t.throws(() => { + graph.declareOptionalDependency("library.b", "library.a"); + }, { + message: expectedSealMsg + }); t.throws(() => { graph.addExtension(createExtension("extension.b")); }, { @@ -893,8 +1082,8 @@ test("Seal", async (t) => { ]); const project = graph.getProject("library.x"); - t.is(project, undefined, "library.x has not been added"); + t.is(project, undefined, "library.x should not be added"); const extension = graph.getExtension("extension.b"); - t.is(extension, undefined, "extension.b has not been added"); + t.is(extension, undefined, "extension.b should not be added"); }); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 7b1b7f748..07d19fc60 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -29,7 +29,8 @@ test.beforeEach((t) => { isLevelEnabled: () => true }; sinon.stub(logger, "getLogger").callThrough() - .withArgs("graph:projectGraphFromTree").returns(t.context.log); + .withArgs("graph:projectGraphBuilder").returns(t.context.log); + mock.reRequire("../../../lib/graph/projectGraphBuilder"); t.context.projectGraphFromTree = mock.reRequire("../../../lib/graph/projectGraphFromTree"); logger.getLogger.restore(); // Immediately restore global stub for following tests }); diff --git a/test/lib/graph/providers/npm.integration.js b/test/lib/graph/providers/npm.integration.js new file mode 100644 index 000000000..d46e1158a --- /dev/null +++ b/test/lib/graph/providers/npm.integration.js @@ -0,0 +1,265 @@ +const test = require("ava"); +const path = require("path"); +const sinonGlobal = require("sinon"); +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +// const applicationBPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.b"); +const applicationCPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.c"); +const applicationC2Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c2"); +const applicationC3Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c3"); +const applicationDPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.d"); +const applicationFPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.f"); +const applicationGPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.g"); +const errApplicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "err.application.a"); +const cycleDepsBasePath = path.join(__dirname, "..", "..", "..", "fixtures", "cyclic-deps", "node_modules"); + +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); +const NpmProvider = require("../../../../lib/graph/providers/npm"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +// function testGraphCreationBfs(...args) { +// return _testGraphCreation(...args, true); +// } + +function testGraphCreationDfs(...args) { + return _testGraphCreation(...args, false); +} + +async function _testGraphCreation(t, npmProvider, expectedOrder, bfs) { + if (bfs === undefined) { + throw new Error("Test error: Parameter 'bfs' must be specified"); + } + const projectGraph = await projectGraphBuilder(npmProvider); + const callbackStub = t.context.sinon.stub().resolves(); + if (bfs) { + await projectGraph.traverseBreadthFirst(callbackStub); + } else { + await projectGraph.traverseDepthFirst(callbackStub); + } + + t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order"); + return projectGraph; +} + +test("AppA: project with collection dependency", async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.a", + "library.b", + "library.c", + "application.a", + ]); +}); + +test("AppC: project with dependency with optional dependency resolved through root project", async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "application.c", + ]); +}); + +test("AppC2: project with dependency with optional dependency resolved through other project", async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationC2Path + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "library.d-depender", + "application.c2" + ]); +}); + +test("AppC3: project with dependency with optional dependency resolved " + + "through other project (but got hoisted)", async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationC3Path + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "library.d-depender", + "application.c3" + ]); +}); + +test("AppD: project with dependency with unresolved optional dependency", async (t) => { + // application.d`s dependency "library.e" has an optional dependency to "library.d" + // which is already present in the node_modules directory of library.e + const npmProvider = new NpmProvider({ + cwd: applicationDPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.e", + "application.d" + ]); +}); + +test("AppF: UI5-dependencies in package.json are ignored", async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationFPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "library.e", + "application.f" + ]); +}); + +test("AppG: project with npm 'optionalDependencies' should not fail if optional dependency cannot be resolved", + async (t) => { + const npmProvider = new NpmProvider({ + cwd: applicationGPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "application.g" + ]); + }); + +test.skip("AppCycleA: cyclic dev deps", async (t) => { + const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); + + const npmProvider = new NpmProvider({ + cwd: applicationCycleAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // => applicationCycleATree +}); + +test.skip("AppCycleA: cyclic dev deps - include deduped", async (t) => { + const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); + + const npmProvider = new NpmProvider({ + cwd: applicationCycleAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: true}) + // => applicationCycleATreeIncDeduped +}); + +test.skip("AppCycleB: cyclic npm deps - Cycle via devDependency on second level - include deduped", async (t) => { + const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleBPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: true}) + // => applicationCycleBTreeIncDeduped +}); + +test.skip("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { + const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleBPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: false}) + // => pplicationCycleBTree +}); + +test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => { + const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.f", + "module.g", + "application.cycle.c" + ]); + // {includeDeduped: false}) + // => applicationCycleCTree +}); + +test.skip("AppCycleC: cyclic npm deps - Cycle on third level (one indirection) - include deduped", async (t) => { + const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: true}) + // => applicationCycleCTreeIncDeduped +}); + +test.skip("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { + const applicationCycleDPath = path.join(cycleDepsBasePath, "application.cycle.d"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleDPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: true}) + // => applicationCycleDTree +}); + +test.skip("AppCycleE: cyclic npm deps - Cycle via devDependency - include deduped", async (t) => { + const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleEPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: true}) + // => applicationCycleETreeIncDeduped +}); + +test.skip("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { + const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); + const npmProvider = new NpmProvider({ + cwd: applicationCycleEPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "", "", "" + ]); + // {includeDeduped: false}) + // => pplicationCycleETree +}); + +test("Error: missing package.json", async (t) => { + const dir = path.parse(__dirname).root; + const npmProvider = new NpmProvider({ + cwd: dir + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, `Failed to locate package.json for directory ${dir}`); +}); + +test.skip("Error: missing dependency", async (t) => { + const npmProvider = new NpmProvider({ + cwd: errApplicationAPath + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, "[npm translator] Could not locate " + + "module library.xx via resolve logic (error: Cannot find module 'library.xx/package.json' from '" + + errApplicationAPath + "') or in a collection"); +}); diff --git a/test/lib/graph/providers/ui5Framework.integration.js b/test/lib/graph/providers/ui5Framework.integration.js index 0c5540158..453d4fef9 100644 --- a/test/lib/graph/providers/ui5Framework.integration.js +++ b/test/lib/graph/providers/ui5Framework.integration.js @@ -9,9 +9,10 @@ const pacote = require("pacote"); const libnpmconfig = require("libnpmconfig"); const lockfile = require("lockfile"); const logger = require("@ui5/logger"); -const normalizer = require("../../../../lib/normalizer"); const Module = require("../../../../lib/graph/Module"); -// let ui5Framework; +const DependencyTreeProvider = require("../../../../lib/graph/providers/DependencyTree"); +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); +let ui5Framework; let Installer; // Use path within project as mocking base directory to reduce chance of side effects @@ -46,7 +47,7 @@ test.beforeEach((t) => { mock("mkdirp", sinon.stub().resolves()); // Re-require to ensure that mocked modules are used - // ui5Framework = mock.reRequire("../../../../lib/graph/providers/ui5Framework"); + ui5Framework = mock.reRequire("../../../../lib/graph/providers/ui5Framework"); Installer = require("../../../../lib/ui5Framework/npm/Installer"); }); @@ -195,8 +196,6 @@ function defineTest(testName, { ] }; - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(Module.prototype, "_readConfigFile") .callsFake(async function() { switch (this.getPath()) { @@ -313,7 +312,11 @@ function defineTest(testName, { .resolves(distributionMetadata); } - const projectGraph = await normalizer.generateProjectGraph(); + const provider = new DependencyTreeProvider(translatorTree); + + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); const callbackStub = sinon.stub().resolves(); await projectGraph.traverseDepthFirst(callbackStub); From 518ee89a49466d72364909ed289a3a9040791040 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 17 Feb 2021 20:11:43 +0100 Subject: [PATCH 23/99] ProjectGraph: No need to remove resolved optional dependencies from adjacency list --- lib/graph/ProjectGraph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 542fd8ae9..5566d9390 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -242,7 +242,7 @@ class ProjectGraph { // Resolve optional dependency log.verbose(`Resolved optional dependency from ${projectName} to ${dependencies[i]}`); this.declareDependency(projectName, dependencies[i]); - dependencies.splice(i, 1); + // Optional dependency is not removed as non-optional should overwrite anyways } } } From 00430ba4d75bb59d2b278e978d4f531ef6920384 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 18 Feb 2021 11:30:51 +0100 Subject: [PATCH 24/99] Resolve a bunch of npm tests --- lib/graph/ProjectGraph.js | 54 ++++++----- .../node_modules/module.h/ui5.yaml | 5 + .../node_modules/module.i/ui5.yaml | 5 + .../node_modules/module.j/ui5.yaml | 5 + .../node_modules/module.k/ui5.yaml | 5 + .../node_modules/module.l/ui5.yaml | 5 + .../node_modules/module.m/ui5.yaml | 5 + test/lib/graph/ProjectGraph.js | 18 ++-- test/lib/graph/providers/npm.integration.js | 93 +++++-------------- 9 files changed, 95 insertions(+), 100 deletions(-) create mode 100644 test/fixtures/cyclic-deps/node_modules/module.h/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.i/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.j/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.k/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.l/ui5.yaml create mode 100644 test/fixtures/cyclic-deps/node_modules/module.m/ui5.yaml diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 5566d9390..46ac45776 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -197,16 +197,15 @@ class ProjectGraph { } /** - * Checks whether a dependency is declared as optional or not. + * Checks whether a dependency is optional or not. * Currently only used in tests. * * @private * @param {string} fromProjectName Name of the depending project * @param {string} toProjectName Name of project on which the other depends - * @returns {boolean} True if the dependency is currently declared as optional - * @throws Throws in case the no dependency (optional or not) has been declared + * @returns {boolean} True if the dependency is currently optional */ - hasOptionalDependency(fromProjectName, toProjectName) { + isOptionalDependency(fromProjectName, toProjectName) { const adjacencies = this._adjList[fromProjectName]; if (!adjacencies) { throw new Error( @@ -224,6 +223,9 @@ class ProjectGraph { return false; } + /** + * @public + */ resolveOptionalDependencies() { if (!this._shouldResolveOptionalDependencies) { log.verbose(`Skipping resolution of optional dependencies since none have been declared`); @@ -238,11 +240,21 @@ class ProjectGraph { } for (const [projectName, dependencies] of Object.entries(this._optAdjList)) { for (let i = dependencies.length - 1; i >= 0; i--) { - if (resolvedProjects.has(dependencies[i])) { + const targetProjectName = dependencies[i]; + if (resolvedProjects.has(targetProjectName)) { // Resolve optional dependency - log.verbose(`Resolved optional dependency from ${projectName} to ${dependencies[i]}`); - this.declareDependency(projectName, dependencies[i]); - // Optional dependency is not removed as non-optional should overwrite anyways + log.verbose(`Resolving optional dependency from ${projectName} to ${targetProjectName}...`); + + if (this._adjList[targetProjectName].includes(projectName)) { + log.verbose( + ` Cyclic optional dependency detected: ${targetProjectName} already has a non-optional ` + + `dependency to ${projectName}`); + log.verbose( + ` Optional dependency from ${projectName} to ${targetProjectName} ` + + `will not be declared as it would introduce a cycle`); + } else { + this.declareDependency(projectName, targetProjectName); + } } } } @@ -299,14 +311,8 @@ class ProjectGraph { const {projectNames, predecessors} = queue.shift(); // Get and remove first entry from queue await Promise.all(projectNames.map(async (projectName) => { - if (predecessors.includes(projectName)) { - // We start to run in circles. That's neither expected nor something we can deal with + this._checkCycle(predecessors, projectName); - // Mark first and last occurrence in chain with an asterisk - predecessors[predecessors.indexOf(projectName)] = `${projectName}*`; - throw new Error( - `Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`); - } if (visited[projectName]) { return visited[projectName]; } @@ -348,14 +354,8 @@ class ProjectGraph { } async _traverseDepthFirst(projectName, visited, predecessors, callback) { - if (predecessors.includes(projectName)) { - // We start to run in circles. That's neither expected nor something we can deal with + this._checkCycle(predecessors, projectName); - // Mark first and last occurrence in chain with an asterisk - predecessors[predecessors.indexOf(projectName)] = `${projectName}*`; - throw new Error( - `Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`); - } if (visited[projectName]) { return visited[projectName]; } @@ -375,6 +375,16 @@ class ProjectGraph { })(); } + _checkCycle(predecessors, projectName) { + if (predecessors.includes(projectName)) { + // We start to run in circles. That's neither expected nor something we can deal with + + // Mark first and last occurrence in chain with an asterisk + predecessors[predecessors.indexOf(projectName)] = `${projectName}*`; + throw new Error(`Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`); + } + } + /** * Join another project graph into this one. * Projects and extensions which already exist in this graph will cause an error to be thrown diff --git a/test/fixtures/cyclic-deps/node_modules/module.h/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.h/ui5.yaml new file mode 100644 index 000000000..d80eec70e --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.h/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.h diff --git a/test/fixtures/cyclic-deps/node_modules/module.i/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.i/ui5.yaml new file mode 100644 index 000000000..d2872d254 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.i/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.i diff --git a/test/fixtures/cyclic-deps/node_modules/module.j/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.j/ui5.yaml new file mode 100644 index 000000000..e9cb9133d --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.j/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.j diff --git a/test/fixtures/cyclic-deps/node_modules/module.k/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.k/ui5.yaml new file mode 100644 index 000000000..6c7638847 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.k/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.k diff --git a/test/fixtures/cyclic-deps/node_modules/module.l/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.l/ui5.yaml new file mode 100644 index 000000000..9c50648e6 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.l/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.l diff --git a/test/fixtures/cyclic-deps/node_modules/module.m/ui5.yaml b/test/fixtures/cyclic-deps/node_modules/module.m/ui5.yaml new file mode 100644 index 000000000..5f02be618 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/module.m/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: module +metadata: + name: module.m diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 34b1006fc..2db9aff8b 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -295,10 +295,10 @@ test("declareDependency / getDependencies", async (t) => { "library.a" ], "Should store and return correct dependencies for library.b"); - t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + t.is(graph.isOptionalDependency("library.a", "library.b"), false, "Should declare dependency as non-optional"); - t.is(graph.hasOptionalDependency("library.b", "library.a"), false, + t.is(graph.isOptionalDependency("library.b", "library.a"), false, "Should declare dependency as non-optional"); }); @@ -384,7 +384,7 @@ test("declareDependency: Already declared as optional", async (t) => { `Dependency has already been declared: library.a depends on library.b`, "log.warn should be called once with the expected argument"); - t.is(graph.hasOptionalDependency("library.a", "library.b"), true, + t.is(graph.isOptionalDependency("library.a", "library.b"), true, "Should declare dependency as optional"); }); @@ -402,7 +402,7 @@ test("declareDependency: Already declared as non-optional", async (t) => { t.is(log.warn.callCount, 0, "log.warn should not be called"); - t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + t.is(graph.isOptionalDependency("library.a", "library.b"), false, "Should declare dependency as non-optional"); }); @@ -419,7 +419,7 @@ test("declareDependency: Already declared as optional, now non-optional", async t.is(log.warn.callCount, 0, "log.warn should not be called"); - t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + t.is(graph.isOptionalDependency("library.a", "library.b"), false, "Should declare dependency as non-optional"); }); @@ -468,9 +468,9 @@ test("resolveOptionalDependencies", async (t) => { graph.resolveOptionalDependencies(); - t.is(graph.hasOptionalDependency("library.a", "library.b"), false, + t.is(graph.isOptionalDependency("library.a", "library.b"), false, "library.a should have no optional dependency to library.b anymore"); - t.is(graph.hasOptionalDependency("library.a", "library.c"), false, + t.is(graph.isOptionalDependency("library.a", "library.c"), false, "library.a should have no optional dependency to library.c anymore"); await traverseDepthFirst(t, graph, [ @@ -498,10 +498,10 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a graph.resolveOptionalDependencies(); - t.is(graph.hasOptionalDependency("library.a", "library.b"), true, + t.is(graph.isOptionalDependency("library.a", "library.b"), true, "Dependency from library.a to library.b should still be optional"); - t.is(graph.hasOptionalDependency("library.a", "library.c"), true, + t.is(graph.isOptionalDependency("library.a", "library.c"), true, "Dependency from library.a to library.c should still be optional"); await traverseDepthFirst(t, graph, [ diff --git a/test/lib/graph/providers/npm.integration.js b/test/lib/graph/providers/npm.integration.js index d46e1158a..4490f0833 100644 --- a/test/lib/graph/providers/npm.integration.js +++ b/test/lib/graph/providers/npm.integration.js @@ -23,9 +23,9 @@ test.afterEach.always((t) => { t.context.sinon.restore(); }); -// function testGraphCreationBfs(...args) { -// return _testGraphCreation(...args, true); -// } +function testGraphCreationBfs(...args) { + return _testGraphCreation(...args, true); +} function testGraphCreationDfs(...args) { return _testGraphCreation(...args, false); @@ -134,53 +134,30 @@ test("AppG: project with npm 'optionalDependencies' should not fail if optional ]); }); -test.skip("AppCycleA: cyclic dev deps", async (t) => { - const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); - - const npmProvider = new NpmProvider({ - cwd: applicationCycleAPath - }); - await testGraphCreationDfs(t, npmProvider, [ - "", "", "" - ]); - // => applicationCycleATree -}); - -test.skip("AppCycleA: cyclic dev deps - include deduped", async (t) => { +test("AppCycleA: cyclic dev deps", async (t) => { const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); const npmProvider = new NpmProvider({ cwd: applicationCycleAPath }); await testGraphCreationDfs(t, npmProvider, [ - "", "", "" + "library.cycle.a", + "library.cycle.b", + "component.cycle.a", + "application.cycle.a" ]); - // {includeDeduped: true}) - // => applicationCycleATreeIncDeduped }); -test.skip("AppCycleB: cyclic npm deps - Cycle via devDependency on second level - include deduped", async (t) => { +test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); const npmProvider = new NpmProvider({ cwd: applicationCycleBPath }); await testGraphCreationDfs(t, npmProvider, [ - "", "", "" + "module.e", + "module.d", + "application.cycle.b" ]); - // {includeDeduped: true}) - // => applicationCycleBTreeIncDeduped -}); - -test.skip("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { - const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); - const npmProvider = new NpmProvider({ - cwd: applicationCycleBPath - }); - await testGraphCreationDfs(t, npmProvider, [ - "", "", "" - ]); - // {includeDeduped: false}) - // => pplicationCycleBTree }); test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => { @@ -193,56 +170,34 @@ test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", asyn "module.g", "application.cycle.c" ]); - // {includeDeduped: false}) - // => applicationCycleCTree -}); - -test.skip("AppCycleC: cyclic npm deps - Cycle on third level (one indirection) - include deduped", async (t) => { - const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); - const npmProvider = new NpmProvider({ - cwd: applicationCycleCPath - }); - await testGraphCreationDfs(t, npmProvider, [ - "", "", "" + await testGraphCreationBfs(t, npmProvider, [ + "application.cycle.c", + "module.f", + "module.g", ]); - // {includeDeduped: true}) - // => applicationCycleCTreeIncDeduped }); -test.skip("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { +test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { const applicationCycleDPath = path.join(cycleDepsBasePath, "application.cycle.d"); const npmProvider = new NpmProvider({ cwd: applicationCycleDPath }); - await testGraphCreationDfs(t, npmProvider, [ - "", "", "" - ]); - // {includeDeduped: true}) - // => applicationCycleDTree -}); -test.skip("AppCycleE: cyclic npm deps - Cycle via devDependency - include deduped", async (t) => { - const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); - const npmProvider = new NpmProvider({ - cwd: applicationCycleEPath - }); - await testGraphCreationDfs(t, npmProvider, [ - "", "", "" - ]); - // {includeDeduped: true}) - // => applicationCycleETreeIncDeduped + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, + `Detected cyclic dependency chain: application.cycle.d -> module.h* -> module.i -> module.k -> module.h*`); }); -test.skip("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { +test("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); const npmProvider = new NpmProvider({ cwd: applicationCycleEPath }); await testGraphCreationDfs(t, npmProvider, [ - "", "", "" + "module.l", + "module.m", + "application.cycle.e" ]); - // {includeDeduped: false}) - // => pplicationCycleETree }); test("Error: missing package.json", async (t) => { From 707004d5a8ba4b3d223b6edde6385afc02fcca1d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 18 Feb 2021 11:51:37 +0100 Subject: [PATCH 25/99] npm provider: Improve error message for unresolved modules --- lib/graph/providers/npm.js | 21 ++++++++++++------- test/fixtures/err.application.a/ui5.yaml | 5 +++++ .../err.application.a/webapp/index.html | 9 ++++++++ .../err.application.a/webapp/manifest.json | 13 ++++++++++++ .../fixtures/err.application.a/webapp/test.js | 5 +++++ test/lib/graph/providers/npm.integration.js | 8 +++---- 6 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/err.application.a/ui5.yaml create mode 100644 test/fixtures/err.application.a/webapp/index.html create mode 100644 test/fixtures/err.application.a/webapp/manifest.json create mode 100644 test/fixtures/err.application.a/webapp/test.js diff --git a/lib/graph/providers/npm.js b/lib/graph/providers/npm.js index d9bfb18db..0559de69b 100644 --- a/lib/graph/providers/npm.js +++ b/lib/graph/providers/npm.js @@ -69,15 +69,20 @@ class Npm { async _resolveModulePath(baseDir, moduleName) { log.verbose(`Resolving module path for '${moduleName}'...`); - let packageJsonPath = await resolveModulePath(moduleName + "/package.json", { - basedir: baseDir, - preserveSymlinks: false - }); - packageJsonPath = await realpath(packageJsonPath); + try { + let packageJsonPath = await resolveModulePath(moduleName + "/package.json", { + basedir: baseDir, + preserveSymlinks: false + }); + packageJsonPath = await realpath(packageJsonPath); - const modulePath = path.dirname(packageJsonPath); - log.verbose(`Resolved module ${moduleName} to path ${modulePath}`); - return modulePath; + const modulePath = path.dirname(packageJsonPath); + log.verbose(`Resolved module ${moduleName} to path ${modulePath}`); + return modulePath; + } catch (err) { + throw new Error( + `Unable to locate module ${moduleName} via resolve logic: ${err.message}`); + } } async _getNode(modulePath, optional) { diff --git a/test/fixtures/err.application.a/ui5.yaml b/test/fixtures/err.application.a/ui5.yaml new file mode 100644 index 000000000..00090cd96 --- /dev/null +++ b/test/fixtures/err.application.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: err.app.a diff --git a/test/fixtures/err.application.a/webapp/index.html b/test/fixtures/err.application.a/webapp/index.html new file mode 100644 index 000000000..d86c19d3d --- /dev/null +++ b/test/fixtures/err.application.a/webapp/index.html @@ -0,0 +1,9 @@ + + + + Error Application A + + + + + diff --git a/test/fixtures/err.application.a/webapp/manifest.json b/test/fixtures/err.application.a/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/err.application.a/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/fixtures/err.application.a/webapp/test.js b/test/fixtures/err.application.a/webapp/test.js new file mode 100644 index 000000000..a3df410c3 --- /dev/null +++ b/test/fixtures/err.application.a/webapp/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/test/lib/graph/providers/npm.integration.js b/test/lib/graph/providers/npm.integration.js index 4490f0833..91a98cb22 100644 --- a/test/lib/graph/providers/npm.integration.js +++ b/test/lib/graph/providers/npm.integration.js @@ -209,12 +209,12 @@ test("Error: missing package.json", async (t) => { t.is(error.message, `Failed to locate package.json for directory ${dir}`); }); -test.skip("Error: missing dependency", async (t) => { +test("Error: missing dependency", async (t) => { const npmProvider = new NpmProvider({ cwd: errApplicationAPath }); const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); - t.is(error.message, "[npm translator] Could not locate " + - "module library.xx via resolve logic (error: Cannot find module 'library.xx/package.json' from '" + - errApplicationAPath + "') or in a collection"); + t.is(error.message, + `Unable to locate module library.xx via resolve logic: Cannot find module 'library.xx/package.json' from ` + + `'${errApplicationAPath}'`); }); From db2d45af62b6edbaeef4c882e665a9ba6ac03eb1 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 18 Feb 2021 17:37:10 +0100 Subject: [PATCH 26/99] Merge Configuration and Specification into one entity Implement full Application spec --- lib/Specification.js | 63 +++++ lib/graph/Module.js | 236 ++++++++++-------- lib/graph/ProjectGraph.js | 4 +- lib/graph/providers/ui5Framework.js | 37 ++- lib/specifications/AbstractSpecification.js | 116 ++++++--- lib/specifications/Configuration.js | 62 +++-- lib/specifications/Extension.js | 15 +- lib/specifications/Project.js | 130 ++++++++-- .../configurations/AbstractConfiguration.js | 38 +++ .../configurations/Application.js | 13 + lib/specifications/types/Application.js | 204 +++++++++++++++ test/lib/specifications/Project.js | 32 +-- 12 files changed, 723 insertions(+), 227 deletions(-) create mode 100644 lib/Specification.js create mode 100644 lib/specifications/configurations/AbstractConfiguration.js create mode 100644 lib/specifications/configurations/Application.js create mode 100644 lib/specifications/types/Application.js diff --git a/lib/Specification.js b/lib/Specification.js new file mode 100644 index 000000000..4dd3820f6 --- /dev/null +++ b/lib/Specification.js @@ -0,0 +1,63 @@ +/** + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath File System path to access resources + * @param {object} parameters.configuration Configuration object to use + */ +module.exports = { + // async create(specParams) { + // if (!specParams.configuration) { + // throw new Error(`Unable to create Specification: No configuration provided`); + // } + // switch (specParams.configuration.kind) { + // case "project": + // return Project.create(specParams); + // case "extension": + // return Extension.create(specParams); + // default: + // throw new Error( + // `Encountered unexpected specification configuration of kind ${specParams.configuration.kind} ` + + // `Supported kinds are 'project' and 'extension'`); + // } + // } + + async create(params) { + if (!["project", "extension"].includes(params.configuration.kind)) { + throw new Error(`Unable to create Specification instance: Unknown kind '${params.configuration.kind}'`); + } + + switch (params.configuration.type) { + case "application": { + return createAndInitializeSpec("Application", params); + } + case "library": { + return createAndInitializeSpec("Library", params); + } + case "theme-library": { + return createAndInitializeSpec("ThemeLibrary", params); + } + case "module": { + return createAndInitializeSpec("Module", params); + } + case "task": { + return createAndInitializeSpec("Task", params); + } + case "middleware": { + return createAndInitializeSpec("Middleware", params); + } + case "project-shim": { + return createAndInitializeSpec("ProjectShim", params); + } + default: + throw new Error( + `Unable to create Specification instance: Unknown specification type '${params.configuration.type}'`); + } + } +}; + +function createAndInitializeSpec(moduleName, params) { + const Spec = require(`./specifications/types/${moduleName}`); + const bla = new Spec().init(params); + return bla; +} diff --git a/lib/graph/Module.js b/lib/graph/Module.js index c4658892f..e70adafa5 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -4,9 +4,7 @@ const {promisify} = require("util"); const readFile = promisify(fs.readFile); const jsyaml = require("js-yaml"); const resourceFactory = require("@ui5/fs").resourceFactory; -const Project = require("../specifications/Project"); -const Extension = require("../specifications/Extension"); -const Configuration = require("../specifications/Configuration"); +const Specification = require("../Specification"); const {validate} = require("../validation/validator"); const log = require("@ui5/logger").getLogger("graph:Module"); @@ -93,44 +91,66 @@ class Module { async _getSpecifications() { const configs = await this._getConfigurations(); - let project; - const extensions = []; - configs.forEach((configuration) => { - const kind = configuration.getKind(); - - switch (kind) { - case "project": - if (project) { - throw new Error( - `Invalid configuration for module ${this.getId()}: Per module there ` + - `must be no more than one configuration of kind 'project'`); - } - log.verbose(`Module ${this.getId()} contains project ${configuration.getName()}`); - project = new Project({ - id: this.getId(), - version: this.getVersion(), - modulePath: this.getPath(), - configuration - }); - break; - case "extension": - log.verbose(`Module ${this.getId()} contains extension ${configuration.getName()}`); - extensions.push(new Extension({ - id: this.getId(), - version: this.getVersion(), - modulePath: this.getPath(), - configuration - })); - break; - default: - throw new Error( - `Encountered unexpected specification configuration of kind ${kind} ` + - `Supported kinds are 'project' and 'extension'`); - } + // let project; + // const extensions = []; + const specs = await Promise.all(configs.map(async (configuration) => { + const spec = await Specification.create({ + id: this.getId(), + version: this.getVersion(), + modulePath: this.getPath(), + configuration + }); + + log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); + return spec; + + // switch (configuration.kind) { + // case "project": + // if (project) { + // throw new Error( + // `Invalid configuration for module ${this.getId()}: Per module there ` + + // `must be no more than one configuration of kind 'project'`); + // } + // log.verbose(`Module ${this.getId()} contains project ${configuration.getName()}`); + // project = await Project.create({ + // id: this.getId(), + // version: this.getVersion(), + // modulePath: this.getPath(), + // configuration + // }); + // break; + // case "extension": + // log.verbose(`Module ${this.getId()} contains extension ${configuration.getName()}`); + // extensions.push(new Extension({ + // id: this.getId(), + // version: this.getVersion(), + // modulePath: this.getPath(), + // configuration + // })); + // break; + // default: + // throw new Error( + // `Encountered unexpected specification configuration of kind ${configuration.kind} ` + + // `Supported kinds are 'project' and 'extension'`); + // } + })); + + const projects = specs.filter((spec) => { + return spec.getKind() === "project"; + }); + + const extensions = specs.filter((spec) => { + return spec.getKind() === "extension"; }); + if (projects.length > 1) { + throw new Error( + `Found ${projects.length} configurations of kind 'project' for ` + + `module ${this.getId()}. There must be only one project per module.`); + } + return { - project, + project: projects[0], extensions }; } @@ -152,21 +172,21 @@ class Module { return configurations || []; } - async _createConfigurationInstance(config) { + async _createConfigurationObject(config) { this._normalizeConfig(config); if (config.kind === "project") { this._applyShims(config); } - await this._validateConfig(config); - return new Configuration(config); + // await this._validateConfig(config); + return config; } async _createConfigurationFromShim() { const config = this._applyShims(); if (config) { this._normalizeConfig(config); - await this._validateConfig(config); - return new Configuration(config); + // await this._validateConfig(config); + return config; } } @@ -185,7 +205,7 @@ class Module { if (this._suppliedConfigs.length) { log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); return await Promise.all(this._suppliedConfigs.map(async (config) => { - return this._createConfigurationInstance(config); + return this._createConfigurationObject(config); })); } } @@ -208,43 +228,45 @@ class Module { return []; } - for (let i = configs.length - 1; i >= 0; i--) { - this._normalizeConfig(configs[i]); - } - - const projectConfigs = configs.filter((config) => { - return config.kind === "project"; - }); - - const extensionConfigs = configs.filter((config) => { - return config.kind === "extension"; - }); - - // While a project can contain multiple configurations, - // from a dependency tree perspective it is always a single project - // This means it can represent one "project", plus multiple extensions or - // one extension, plus multiple extensions - - if (projectConfigs.length > 1) { - throw new Error( - `Found ${projectConfigs.length} configurations of kind 'project' for ` + - `project ${this.getId()}. There is only one project per configuration allowed.`); - } else if (projectConfigs.length === 0 && extensionConfigs.length === 0) { - throw new Error( - `Found ${configs.length} configurations for ` + - `project ${this.getId()}. However, none of them are of kind 'project' or 'extension'.`); - } - - const configurations = []; - if (projectConfigs.length) { - configurations.push(await this._createConfigurationInstance(projectConfigs[0])); - } - - await Promise.all(extensionConfigs.map(async (config) => { - configurations.push(await this._createConfigurationInstance(config)); + // for (let i = configs.length - 1; i >= 0; i--) { + // this._normalizeConfig(configs[i]); + // } + + // const projectConfigs = configs.filter((config) => { + // return config.kind === "project"; + // }); + + // const extensionConfigs = configs.filter((config) => { + // return config.kind === "extension"; + // }); + + // // While a project can contain multiple configurations, + // // from a dependency tree perspective it is always a single project + // // This means it can represent one "project", plus multiple extensions or + // // one extension, plus multiple extensions + + // if (projectConfigs.length > 1) { + // throw new Error( + // `Found ${projectConfigs.length} configurations of kind 'project' for ` + + // `project ${this.getId()}. There is only one project per configuration allowed.`); + // } else if (projectConfigs.length === 0 && extensionConfigs.length === 0) { + // throw new Error( + // `Found ${configs.length} configurations for ` + + // `project ${this.getId()}. However, none of them are of kind 'project' or 'extension'.`); + // } + + // const configurations = []; + // if (projectConfigs.length) { + // configurations.push(await this._createConfigurationObject(projectConfigs[0])); + // } + + // await Promise.all(extensionConfigs.map(async (config) => { + // configurations.push(await this._createConfigurationObject(config)); + // })); + + return await Promise.all(configs.map((config) => { + return this._createConfigurationObject(config); })); - - return configurations; } async _readConfigFile() { @@ -339,33 +361,33 @@ class Module { return config; } - async _validateConfig(config) { - const moduleId = this.getId(); - if (!moduleId.startsWith("@openui5/") && !moduleId.startsWith("@sapui5/")) { - if (config.specVersion === "0.1" || config.specVersion === "1.0" || - config.specVersion === "1.1") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in module ` + - `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - if (config.specVersion !== "2.0" && - config.specVersion !== "2.1" && config.specVersion !== "2.2" && - config.specVersion !== "2.3") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in module ` + - `${this.getId()}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - } - - await validate({ - config, - project: { - id: moduleId - } - }); - } + // async _validateConfig(config) { + // const moduleId = this.getId(); + // if (!moduleId.startsWith("@openui5/") && !moduleId.startsWith("@sapui5/")) { + // if (config.specVersion === "0.1" || config.specVersion === "1.0" || + // config.specVersion === "1.1") { + // throw new Error( + // `Unsupported specification version ${config.specVersion} defined in module ` + + // `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + + // `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + // } + // if (config.specVersion !== "2.0" && + // config.specVersion !== "2.1" && config.specVersion !== "2.2" && + // config.specVersion !== "2.3") { + // throw new Error( + // `Unsupported specification version ${config.specVersion} defined in module ` + + // `${this.getId()}. Your UI5 CLI installation might be outdated. ` + + // `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + // } + // } + + // await validate({ + // config, + // project: { + // id: moduleId + // } + // }); + // } _isConfigValid(project) { if (!project.type) { diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 46ac45776..f389b0b0d 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -275,7 +275,7 @@ class ProjectGraph { /** * Helper function available in the - * traversalCallback]{@link module:@ui5/project.graph.ProjectGraph~traversalCallback} to access the + * [traversalCallback]{@link module:@ui5/project.graph.ProjectGraph~traversalCallback} to access the * dependencies of the corresponding project in the current graph. *

* Note that transitive dependencies can't be accessed this way. Projects should rather add a direct @@ -286,6 +286,8 @@ class ProjectGraph { * @returns {Array.} Direct dependencies of the visited project */ + + // TODO: Use generator functions instead? /** * Visit every project in the graph that can be reached by the given entry project exactly once. * The entry project defaults to the root project. diff --git a/lib/graph/providers/ui5Framework.js b/lib/graph/providers/ui5Framework.js index aa81f14bb..a71cdd00e 100644 --- a/lib/graph/providers/ui5Framework.js +++ b/lib/graph/providers/ui5Framework.js @@ -75,20 +75,15 @@ const utils = { // Ignoring UI5 Framework libraries in dependencies return; } - // No need to check for specVersion since Module API is >= 2.0 anyways - const frameworkConfig = project.getFrameworkConfiguration(); - - if (!frameworkConfig) { - return; - } - - if (!frameworkConfig.libraries || !frameworkConfig.libraries.length) { - log.verbose(`Project ${project.getName()} defines no framework.libraries configuration`); + // No need to check for specVersion since Specification API is >= 2.0 anyways + const frameworkDependencies = rootProject.getFrameworkDependencies(); + if (!frameworkDependencies.length) { + log.verbose(`Project ${project.getName()} has no framework dependencies`); // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json return; } - frameworkConfig.libraries.forEach((dependency) => { + frameworkDependencies.forEach((dependency) => { if (!ui5Dependencies.includes(dependency.name) && utils.shouldIncludeDependency(dependency, project === rootProject)) { ui5Dependencies.push(dependency.name); @@ -104,16 +99,16 @@ const utils = { // Ignoring UI5 Framework libraries in dependencies return; } - // No need to check for specVersion since Module API is >= 2.0 anyways - const frameworkConfig = project.getFrameworkConfiguration(); + // No need to check for specVersion since Specification API is >= 2.0 anyways + const frameworkDependencies = project.getFrameworkDependencies(); - if (!frameworkConfig || !frameworkConfig.libraries || !frameworkConfig.libraries.length) { - log.verbose(`Project ${project.getName()} has no framework configuration defined`); + if (!frameworkDependencies.length) { + log.verbose(`Project ${project.getName()} has no framework dependencies`); // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json return; } - frameworkConfig.libraries.forEach((dependency) => { + frameworkDependencies.forEach((dependency) => { if (utils.shouldIncludeDependency(dependency, project === rootProject)) { projectGraph.declareDependency(project.getName(), dependency.name); } @@ -143,13 +138,13 @@ module.exports = { */ enrichProjectGraph: async function(projectGraph, options = {}) { const rootProject = projectGraph.getRoot(); - const rootFrameworkConfig = rootProject.getFrameworkConfiguration(); - if (!rootFrameworkConfig) { + const frameworkName = rootProject.getFrameworkName(); + const frameworkVersion = rootProject.getFrameworkVersion(); + if (!frameworkName && !frameworkVersion) { log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); return projectGraph; } - const frameworkName = rootFrameworkConfig.name; if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { throw new Error( `Unknown framework.name "${frameworkName}" for project ${rootProject.getName()}. ` + @@ -165,7 +160,7 @@ module.exports = { } let version; - if (!rootFrameworkConfig.version) { + if (!frameworkVersion) { throw new Error( `No framework version defined for root project ${rootProject.getName()}` ); @@ -173,10 +168,10 @@ module.exports = { version = await Resolver.resolveVersion(options.versionOverride, {cwd: rootProject.getPath()}); log.info( `Overriding configured ${frameworkName} version ` + - `${rootFrameworkConfig.version} with version ${version}` + `${frameworkVersion} with version ${version}` ); } else { - version = rootFrameworkConfig.version; + version = frameworkVersion; } const referencedLibraries = await utils.getFrameworkLibrariesFromGraph(projectGraph); diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/AbstractSpecification.js index 926a3436f..8fcd0fb09 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/AbstractSpecification.js @@ -1,19 +1,21 @@ -const Configuration = require("./Configuration"); const resourceFactory = require("@ui5/fs").resourceFactory; class AbstractSpecification { + constructor() { + if (new.target === AbstractSpecification) { + throw new TypeError("Class 'AbstractSpecification' is abstract"); + } + this._log = require("@ui5/logger").getLogger(`specifications:types:${this.constructor.name}`); + } + /** * @param {object} parameters Specification parameters * @param {string} parameters.id Unique ID * @param {string} parameters.version Version * @param {string} parameters.modulePath File System path to access resources - * @param {module:@ui5/project.specifications.Configuration} parameters.configuration - * Configuration instance to use + * @param {object} parameters.configuration Configuration object */ - constructor({id, version, modulePath, configuration}) { - if (new.target === AbstractSpecification) { - throw new TypeError("Class 'AbstractSpecification' is abstract"); - } + async init({id, version, modulePath, configuration}) { if (!id) { throw new Error(`Could not create specification: Missing or empty parameter 'id'`); } @@ -26,76 +28,93 @@ class AbstractSpecification { if (!configuration) { throw new Error(`Could not create specification: Missing or empty parameter 'configuration'`); } - if (!(configuration instanceof Configuration)) { - throw new Error(`Could not create specification: 'configuration' must be an instance of ` + - `@ui5/project.specifications.Configuration`); - } this._version = version; this._modulePath = modulePath; - this._configuration = configuration; // The configured name (metadata.name) should be the unique identifier // The ID property as supplied by the translators is only here for debugging and potential tracing purposes this.__id = id; - } - /** - * @private - */ - getVersion() { - return this._version; - } + const {validate} = require("../validation/validator"); + await validate({ + config: configuration, + project: { + id + } + }); - /** - * @private - */ - getPath() { - return this._modulePath; + if (!id.startsWith("@openui5/") && !id.startsWith("@sapui5/")) { + if (configuration.specVersion === "0.1" || configuration.specVersion === "1.0" || + configuration.specVersion === "1.1") { + throw new Error( + `Unsupported specification version ${configuration.specVersion} defined for ` + + `${configuration.kind} ${configuration.metadata.name}. The new Specification API can only be ` + + `used with specification versions >= 2.0. For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + if (configuration.specVersion !== "2.0" && + configuration.specVersion !== "2.1" && configuration.specVersion !== "2.2" && + configuration.specVersion !== "2.3") { + throw new Error( + `Unsupported specification version ${configuration.specVersion} defined in ${configuration.kind} ` + + `${configuration.metadata.name}. Your UI5 CLI installation might be outdated. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + } + + await this._parseConfiguration(configuration); + await this._validate(); + return this; } - /** - * Configuration - */ + /* === Attributes === */ /** * @public */ getName() { - return this._getConfiguration().getName(); + return this._name; } /** * @public */ getKind() { - return this._getConfiguration().getKind(); + return this._kind; } /** * @public */ getType() { - return this._getConfiguration().getType(); + return this._type; } /** - * @private + * @public */ - getConfigurationObject() { - return JSON.parse(JSON.stringify(this._getConfiguration().getObject())); + getSpecVersion() { + return this._specVersion; + } + + /** + * @public + */ + getVersion() { + return this._version; } /** * @private */ - _getConfiguration() { - return this._configuration; + getPath() { + return this._modulePath; } + /* === Resource Access === */ /** - * Resource Access + * @public */ - async getRootReader() { + getRootReader() { return resourceFactory.createReader({ fsBasePath: this.getPath(), virBasePath: "/", @@ -103,11 +122,30 @@ class AbstractSpecification { }); } + /* === Internals === */ /** - * General functions + * @private + * @param {object} config Configuration object */ - async validate() { + async _parseConfiguration(config) { + this._name = config.metadata.name; + this._kind = config.kind; + this._type = config.type; + this._specVersion = config.specVersion; + } + + async _validate() {} + /* === Helper === */ + /** + * @private + * @param {string} dirPath Path of directory, relative to the project root + */ + async _dirExists(dirPath) { + if (await this.getRootReader().byPath(dirPath, {nodir: false})) { + return true; + } + return false; } } diff --git a/lib/specifications/Configuration.js b/lib/specifications/Configuration.js index eb1cdcc15..c5f80f19e 100644 --- a/lib/specifications/Configuration.js +++ b/lib/specifications/Configuration.js @@ -2,29 +2,53 @@ * Private configuration class for use in Module and specifications */ -class Configuration { +module.exports = { /** - * @param {object} config Configuration object + * @param {object} parameters + * @param {object} parameters.specification Specification instance + * @param {object} parameters.configObject Configuration object */ - constructor(config) { - this._config = config; - } - - getName() { - return this._config.metadata.name; - } + async create({specification, configObject}) { + if (specification) { + throw new Error(`Unable to create Configuration: No specification provided`); + } - getKind() { - return this._config.kind; - } + if (!configObject) { + throw new Error(`Unable to create Configuration: No configuration provided`); + } + if (!configObject.kind.includes(["project", "extension"])) { + throw new Error(`Unable to create Configuration: Unknown kind '${configObject.kind}'`); + } - getType() { - return this._config.type; + switch (configObject.type) { + case "application": { + return createConfig("Application", {configObject, specification}); + } + case "library": { + return createConfig("Library", {configObject, specification}); + } + case "theme-library": { + return createConfig("ThemeLibrary", {configObject, specification}); + } + case "module": { + return createConfig("Module", {configObject, specification}); + } + case "task": { + return createConfig("Task", {configObject, specification}); + } + case "middleware": { + return createConfig("Middleware", {configObject, specification}); + } + case "project-shim": { + return createConfig("ProjectShim", {configObject, specification}); + } + default: + throw new Error(`Unable to create Configuration: Unknown specification type '${configObject.type}'`); + } } +}; - getObject() { - return this._config; - } +function createConfig(moduleName, params) { + const Configuration = require(`./configurations/${moduleName}`); + return new Configuration(params).init(); } - -module.exports = Configuration; diff --git a/lib/specifications/Extension.js b/lib/specifications/Extension.js index 4e87621b0..f09e1f672 100644 --- a/lib/specifications/Extension.js +++ b/lib/specifications/Extension.js @@ -1,24 +1,15 @@ const AbstractSpecification = require("./AbstractSpecification"); class Extension extends AbstractSpecification { - /** - * @param {object} parameters Extension parameters - * @param {string} parameters.id Unique ID for the extension - * @param {string} parameters.version Version of the extension - * @param {string} parameters.modulePath File System path to access the extensions resources - * @param {module:@ui5/project.specifications.Configuration} parameters.configuration - * Configuration instance for the extension - */ constructor(parameters) { super(parameters); - if (this.getKind() !== "extension") { - throw new Error(`Could not create extension: Supplied configuration must be of kind extension but is ` + - this.getKind()); + if (new.target === Extension) { + throw new TypeError("Class 'Project' is abstract"); } } /* - * TODO: Expose extension specific APIs + * TODO */ } diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 84a029796..c594aaaa9 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -1,36 +1,138 @@ const AbstractSpecification = require("./AbstractSpecification"); class Project extends AbstractSpecification { - /** - * @param {object} parameters Project parameters - * @param {string} parameters.id Unique ID for the project - * @param {string} parameters.version Version of the project - * @param {string} parameters.modulePath File System path to access the projects resources - * @param {module:@ui5/project.specifications.Configuration} parameters.configuration - * Configuration instance for the project - */ constructor(parameters) { super(parameters); - if (this.getKind() !== "project") { - throw new Error(`Could not create project: Supplied configuration must be of kind project but is ` + - this.getKind()); + if (new.target === Project) { + throw new TypeError("Class 'Project' is abstract"); } + this._frameworkName = null; + this._frameworkVersion = null; + this._frameworkDependencies = null; } + /* === Attributes === */ /** - * Configuration + * @public */ + getFrameworkName() { + return this._frameworkName; + } + /** + * @public + */ + getFrameworkVersion() { + return this._frameworkVersion; + } /** * @public */ - getFrameworkConfiguration() { + getFrameworkDependencies() { // TODO: Clone or freeze object before exposing? - return this._getConfiguration().getObject().framework; + return this._frameworkDependencies || []; } isFrameworkProject() { return this.__id.startsWith("@openui5/") || this.__id.startsWith("@sapui5/"); } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + + if (config.framework) { + if (config.framework.name) { + this._frameworkName = config.framework.name; + } + if (config.framework.version) { + this._frameworkVersion = config.framework.version; + } + if (config.framework.libraries) { + this._frameworkDependencies = JSON.parse(JSON.stringify(config.framework.libraries)); + } + } + } + + async _validate() { + await super._validate(); + if (this.getKind() !== "project") { + throw new Error( + `Configuration missmatch: Supplied configuration must be of kind 'project' but ` + + `is of kind '${this.getKind()}'`); + } + } + + /* === Helper === */ + /** + * Checks whether a given string contains a maven placeholder. + * E.g. ${appId}. + * + * @param {string} value String to check + * @returns {boolean} True if given string contains a maven placeholder + */ + _hasMavenPlaceholder(value) { + return !!value.match(/^\$\{(.*)\}$/); + } + + /** + * Resolves a maven placeholder in a given string using the projects pom.xml + * + * @param {string} value String containing a maven placeholder + * @returns {Promise} Resolved string + */ + async _resolveMavenPlaceholder(value) { + const parts = value && value.match(/^\$\{(.*)\}$/); + if (parts) { + this._log.verbose( + `"${value} contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); + const pom = await this.getPom(); + let mvnValue; + if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { + mvnValue = pom.project.properties[parts[1]]; + } else { + let obj = pom; + parts[1].split(".").forEach((part) => { + obj = obj && obj[part]; + }); + mvnValue = obj; + } + if (!mvnValue) { + throw new Error(`"${value}" couldn't be resolved from maven property ` + + `"${parts[1]}" of pom.xml of project ${this._project.metadata.name}`); + } + return mvnValue; + } else { + throw new Error(`"${value}" is not a maven placeholder`); + } + } + + /** + * Reads the projects pom.xml file + * + * @returns {Promise} Resolves with a JSON representation of the content + */ + async _getPom() { + if (this._pPom) { + return this._pPom; + } + const fsPath = path.join(this._project.path, "pom.xml"); + return this._pPom = readFile(fsPath).then(async (content) => { + const xml2js = require("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + ignoreAttrs: true + }); + const readXML = promisify(parser.parseString); + return readXML(content); + }).catch((err) => { + throw new Error( + `Failed to read pom.xml for project ${this._project.metadata.name}: ${err.message}`); + }); + } } module.exports = Project; diff --git a/lib/specifications/configurations/AbstractConfiguration.js b/lib/specifications/configurations/AbstractConfiguration.js new file mode 100644 index 000000000..c1f197d2a --- /dev/null +++ b/lib/specifications/configurations/AbstractConfiguration.js @@ -0,0 +1,38 @@ +/* +* Private configuration class for use in specifications +*/ + +class AbstractConfiguration { + /** + * @param {object} parameters + * @param {object} parameters.specification Specification instance + * @param {object} parameters.configObject Configuration object + */ + constructor({configObject, specification}) { + this._config = configObject; + this._specification = specification; + } + + async init() { + /* to be implemented by subclasses where needed*/ + return this; + } + + getName() { + return this._config.metadata.name; + } + + getKind() { + return this._config.kind; + } + + getType() { + return this._config.type; + } + + getObject() { + return this._config; + } +} + +module.exports = AbstractConfiguration; diff --git a/lib/specifications/configurations/Application.js b/lib/specifications/configurations/Application.js new file mode 100644 index 000000000..1f2b14c21 --- /dev/null +++ b/lib/specifications/configurations/Application.js @@ -0,0 +1,13 @@ +const AbstractConfiguration = require("./AbstractConfiguration"); + +/* +* Private configuration class for use in Module and specifications +*/ + +class Application extends AbstractConfiguration { + async init() { + + } +} + +module.exports = Application; diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js new file mode 100644 index 000000000..f65adb455 --- /dev/null +++ b/lib/specifications/types/Application.js @@ -0,0 +1,204 @@ +const path = require("path"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const Project = require("../Project"); + +/* +* Private configuration class for use in Module and specifications +*/ + +class Application extends Project { + constructor(parameters) { + super(parameters); + + this._pManifests = {}; + this._webappPath = "webapp"; + this._propertiesFilesSourceEncoding = "UTF-8"; + } + + /* === Attributes === */ + /** + * @public + */ + getPropertiesFileSourceEncoding() { + return this._propertiesFilesSourceEncoding; + } + + getNamespace() { + return this._namespace; + } + + /* === Resource Access === */ + /** + * @public + */ + getRuntimeReader() { + return resourceFactory.createReader({ + fsBasePath: path.join(this.getPath(), this._webappPath), + virBasePath: "/", // Applications are served at "/" + name: `Source reader for ${this.getType()} ${this.getKind()} ${this.getName()}` + }); + } + + /** + * @public + */ + getBuildtimeReader() { + return resourceFactory.createReader({ + fsBasePath: path.join(this.getPath(), this._webappPath), + virBasePath: "/resources" + this.getNamespace(), + name: `Source reader for ${this.getType()} ${this.getKind()} ${this.getName()}` + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + + if (config.resources && config.resources.configuration) { + if (config.resources.configuration.paths && + config.resources.configuration.paths.webapp) { + // "webapp" path mapping + this._webappPath = config.resources.configuration.paths.webapp; + } + if (config.resources.configuration.propertiesFileSourceEncoding) { + // .properties files encoding + this._propertiesFilesSourceEncoding = config.resources.configuration.propertiesFileSourceEncoding; + } + } + + this._namespace = await this._getNamespace(); + } + + + async _validate() { + await super._validate(); + if (this.getType() !== "application") { + throw new Error( + `Configuration missmatch: Supplied configuration must be of type 'application' but ` + + `is of type '${this.getType()}'`); + } + if (!await this._dirExists("/" + this._webappPath)) { + throw new Error( + `Unable to find directory '${this._webappPath}' in application project ${this.getName()}`); + } + } + + /** + * Determine application namespace either based on a project`s + * manifest.json or manifest.appdescr_variant (fallback if present) + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespace() { + try { + return await this._getNamespaceFromManifestJson(); + } catch (manifestJsonError) { + if (manifestJsonError.code !== "ENOENT") { + throw manifestJsonError; + } + // No manifest.json present + // => attempt fallback to manifest.appdescr_variant (typical for App Variants) + try { + return await this._getNamespaceFromManifestAppDescVariant(); + } catch (appDescVarError) { + if (appDescVarError.code === "ENOENT") { + // Fallback not possible: No manifest.appdescr_variant present + // => Throw error indicating missing manifest.json + // (do not mention manifest.appdescr_variant since it is only + // relevant for the rather "uncommon" App Variants) + throw new Error( + `Could not find required manifest.json for project ` + + `${this.getName()}: ${manifestJsonError.message}`); + } + throw appDescVarError; + } + } + } + + /** + * Determine application namespace by checking manifest.json. + * Any maven placeholders are resolved from the projects pom.xml + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestJson() { + const manifest = await this._getManifest("/manifest.json"); + let appId; + // check for a proper sap.app/id in manifest.json to determine namespace + if (manifest["sap.app"] && manifest["sap.app"].id) { + appId = manifest["sap.app"].id; + } else { + throw new Error( + `No sap.app/id configuration found in manifest.json of project ${this.getName()}`); + } + + if (this._hasMavenPlaceholder(appId)) { + try { + appId = await this.resolveMavenPlaceholder(appId); + } catch (err) { + throw new Error( + `Failed to resolve namespace of project ${this.getName()}: ${err.message}`); + } + } + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`); + return namespace; + } + + /** + * Determine application namespace by checking manifest.appdescr_variant. + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestAppDescVariant() { + const manifest = await this._getManifest("/manifest.appdescr_variant"); + let appId; + // check for the id property in manifest.appdescr_variant to determine namespace + if (manifest && manifest.id) { + appId = manifest.id; + } else { + throw new Error( + `No "id" property found in manifest.appdescr_variant of project ${this.getName()}`); + } + + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`); + return namespace; + } + + /** + * Reads and parses a JSON file with the provided name from the projects source directory + * + * @param {string} filePath Name of the JSON file to read. Typically "manifest.json" or "manifest.appdescr_variant" + * @returns {Promise} resolves with an object containing the content requested manifest file + */ + async _getManifest(filePath) { + if (this._pManifests[filePath]) { + return this._pManifests[filePath]; + } + return this._pManifests[filePath] = this.getRuntimeReader().byPath(filePath) + .then(async (resource) => { + if (!resource) { + throw new Error( + `Could not find resource ${filePath} in project ${this.getName()}`); + } + return JSON.parse(await resource.getString()); + }) + .catch((err) => { + throw new Error( + `Failed to read ${filePath} for project ` + + `${this.getName()}: ${err.message}`); + }); + } +} + +module.exports = Application; diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index a4683addb..0f5ef5765 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -5,32 +5,36 @@ const Configuration = require("../../../lib/specifications/Configuration"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const basicConfiguration = new Configuration({ - specVersion: "2.3", - kind: "project", - metadata: {name: "application.a"} +test.beforeEach(async (t) => { + t.context.basicConfiguration = new Configuration({ + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + }); + + t.context.basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: t.context.basicConfiguration + }; }); -const basicProjectInput = { - id: "application.a.id", - version: "1.0.0", - modulePath: applicationAPath, - configuration: basicConfiguration -}; test("Instantiate a basic project", async (t) => { - const project = new Project(basicProjectInput); + const project = new Project(t.context.basicProjectInput); t.is(project.getName(), "application.a", "Returned correct name"); t.is(project.getVersion(), "1.0.0", "Returned correct version"); t.is(project.getPath(), applicationAPath, "Returned correct project path"); }); test("_getConfiguration", async (t) => { - const project = new Project(basicProjectInput); - t.is(await project._getConfiguration(), basicConfiguration, "Returned correct configuration instance"); + const project = new Project(t.context.basicProjectInput); + t.is(await project._getConfiguration(), t.context.basicConfiguration, "Returned correct configuration instance"); }); test("Access project root resources via reader", async (t) => { - const project = new Project(basicProjectInput); + const project = new Project(t.context.basicProjectInput); const rootReader = await project.getRootReader(); const packageJsonResource = await rootReader.byPath("/package.json"); t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); From 70f00cba6669f4b5bff73b64c8ab3e74a61e4248 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 18 Feb 2021 20:02:13 +0100 Subject: [PATCH 27/99] Cleanup --- lib/Specification.js | 29 ++--- lib/graph/Module.js | 121 ++---------------- lib/graph/ShimCollection.js | 10 +- lib/graph/projectGraphBuilder.js | 2 +- .../configurations/AbstractConfiguration.js | 38 ------ .../configurations/Application.js | 13 -- lib/specifications/types/Application.js | 2 +- test/lib/specifications/Project.js | 32 ++--- 8 files changed, 41 insertions(+), 206 deletions(-) delete mode 100644 lib/specifications/configurations/AbstractConfiguration.js delete mode 100644 lib/specifications/configurations/Application.js diff --git a/lib/Specification.js b/lib/Specification.js index 4dd3820f6..fc4ffe90e 100644 --- a/lib/Specification.js +++ b/lib/Specification.js @@ -6,28 +6,17 @@ * @param {object} parameters.configuration Configuration object to use */ module.exports = { - // async create(specParams) { - // if (!specParams.configuration) { - // throw new Error(`Unable to create Specification: No configuration provided`); - // } - // switch (specParams.configuration.kind) { - // case "project": - // return Project.create(specParams); - // case "extension": - // return Extension.create(specParams); - // default: - // throw new Error( - // `Encountered unexpected specification configuration of kind ${specParams.configuration.kind} ` + - // `Supported kinds are 'project' and 'extension'`); - // } - // } - async create(params) { - if (!["project", "extension"].includes(params.configuration.kind)) { - throw new Error(`Unable to create Specification instance: Unknown kind '${params.configuration.kind}'`); + if (!params.configuration) { + throw new Error( + `Unable to create Specification instance: Missing configuration parameter`); + } + const {kind, type} = params.configuration; + if (!["project", "extension"].includes(kind)) { + throw new Error(`Unable to create Specification instance: Unknown kind '${kind}'`); } - switch (params.configuration.type) { + switch (type) { case "application": { return createAndInitializeSpec("Application", params); } @@ -51,7 +40,7 @@ module.exports = { } default: throw new Error( - `Unable to create Specification instance: Unknown specification type '${params.configuration.type}'`); + `Unable to create Specification instance: Unknown specification type '${type}'`); } } }; diff --git a/lib/graph/Module.js b/lib/graph/Module.js index e70adafa5..d48845b89 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -61,9 +61,9 @@ class Module { if (shimCollection) { // Retrieve and clone shims in constructor // Shims added to the collection at a later point in time should not be applied in this module - const shims = shimCollection.getConfigurationShims(this.getId()); + const shims = shimCollection.getProjectConfigurationShims(this.getId()); if (shims && shims.length) { - this._configShims = clone(shims); + this._projectConfigShims = clone(shims); } } } @@ -91,8 +91,6 @@ class Module { async _getSpecifications() { const configs = await this._getConfigurations(); - // let project; - // const extensions = []; const specs = await Promise.all(configs.map(async (configuration) => { const spec = await Specification.create({ id: this.getId(), @@ -103,36 +101,6 @@ class Module { log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); return spec; - - // switch (configuration.kind) { - // case "project": - // if (project) { - // throw new Error( - // `Invalid configuration for module ${this.getId()}: Per module there ` + - // `must be no more than one configuration of kind 'project'`); - // } - // log.verbose(`Module ${this.getId()} contains project ${configuration.getName()}`); - // project = await Project.create({ - // id: this.getId(), - // version: this.getVersion(), - // modulePath: this.getPath(), - // configuration - // }); - // break; - // case "extension": - // log.verbose(`Module ${this.getId()} contains extension ${configuration.getName()}`); - // extensions.push(new Extension({ - // id: this.getId(), - // version: this.getVersion(), - // modulePath: this.getPath(), - // configuration - // })); - // break; - // default: - // throw new Error( - // `Encountered unexpected specification configuration of kind ${configuration.kind} ` + - // `Supported kinds are 'project' and 'extension'`); - // } })); const projects = specs.filter((spec) => { @@ -172,29 +140,28 @@ class Module { return configurations || []; } - async _createConfigurationObject(config) { + async _normalizeAndApplyShims(config) { this._normalizeConfig(config); - if (config.kind === "project") { - this._applyShims(config); + + if (config.kind !== "project") { + this._applyProjectShims(config); } - // await this._validateConfig(config); return config; } async _createConfigurationFromShim() { - const config = this._applyShims(); + const config = this._applyProjectShims(); if (config) { this._normalizeConfig(config); - // await this._validateConfig(config); return config; } } - _applyShims(config = {}) { - if (!this._configShims) { + _applyProjectShims(config = {}) { + if (!this._projectConfigShims) { return; } - this._configShims.forEach(({name, shim}) => { + this._projectConfigShims.forEach(({name, shim}) => { log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); Object.assign(config, shim); }); @@ -205,7 +172,7 @@ class Module { if (this._suppliedConfigs.length) { log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); return await Promise.all(this._suppliedConfigs.map(async (config) => { - return this._createConfigurationObject(config); + return this._normalizeAndApplyShims(config); })); } } @@ -228,44 +195,8 @@ class Module { return []; } - // for (let i = configs.length - 1; i >= 0; i--) { - // this._normalizeConfig(configs[i]); - // } - - // const projectConfigs = configs.filter((config) => { - // return config.kind === "project"; - // }); - - // const extensionConfigs = configs.filter((config) => { - // return config.kind === "extension"; - // }); - - // // While a project can contain multiple configurations, - // // from a dependency tree perspective it is always a single project - // // This means it can represent one "project", plus multiple extensions or - // // one extension, plus multiple extensions - - // if (projectConfigs.length > 1) { - // throw new Error( - // `Found ${projectConfigs.length} configurations of kind 'project' for ` + - // `project ${this.getId()}. There is only one project per configuration allowed.`); - // } else if (projectConfigs.length === 0 && extensionConfigs.length === 0) { - // throw new Error( - // `Found ${configs.length} configurations for ` + - // `project ${this.getId()}. However, none of them are of kind 'project' or 'extension'.`); - // } - - // const configurations = []; - // if (projectConfigs.length) { - // configurations.push(await this._createConfigurationObject(projectConfigs[0])); - // } - - // await Promise.all(extensionConfigs.map(async (config) => { - // configurations.push(await this._createConfigurationObject(config)); - // })); - return await Promise.all(configs.map((config) => { - return this._createConfigurationObject(config); + return this._normalizeAndApplyShims(config); })); } @@ -361,34 +292,6 @@ class Module { return config; } - // async _validateConfig(config) { - // const moduleId = this.getId(); - // if (!moduleId.startsWith("@openui5/") && !moduleId.startsWith("@sapui5/")) { - // if (config.specVersion === "0.1" || config.specVersion === "1.0" || - // config.specVersion === "1.1") { - // throw new Error( - // `Unsupported specification version ${config.specVersion} defined in module ` + - // `${this.getId()}. The new Module API can only be used with specification versions >= 2.0. ` + - // `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - // } - // if (config.specVersion !== "2.0" && - // config.specVersion !== "2.1" && config.specVersion !== "2.2" && - // config.specVersion !== "2.3") { - // throw new Error( - // `Unsupported specification version ${config.specVersion} defined in module ` + - // `${this.getId()}. Your UI5 CLI installation might be outdated. ` + - // `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - // } - // } - - // await validate({ - // config, - // project: { - // id: moduleId - // } - // }); - // } - _isConfigValid(project) { if (!project.type) { if (project._isRoot) { diff --git a/lib/graph/ShimCollection.js b/lib/graph/ShimCollection.js index 5fb0e87f0..63ec2ca4b 100644 --- a/lib/graph/ShimCollection.js +++ b/lib/graph/ShimCollection.js @@ -14,19 +14,19 @@ function addToMap(name, fromMap, toMap) { class ShimCollection { constructor() { - this._configShims = {}; + this._projectConfigShims = {}; this._dependencyShims = {}; this._collectionShims = {}; } - addShim(shimExtension) { + addProjectShim(shimExtension) { const name = shimExtension.getName(); log.verbose(`Adding new shim ${name}...`); // TODO: Move this into a dedicated ShimConfiguration class? const config = shimExtension.getConfigurationObject(); const {configurations, dependencies, collections} = config.shims; if (configurations) { - addToMap(name, configurations, this._configShims); + addToMap(name, configurations, this._projectConfigShims); } if (dependencies) { addToMap(name, dependencies, this._dependencyShims); @@ -36,8 +36,8 @@ class ShimCollection { } } - getConfigurationShims(moduleId) { - return this._configShims[moduleId]; + getProjectConfigurationShims(moduleId) { + return this._projectConfigShims[moduleId]; } getAllDependencyShims() { diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index 0495b546b..0826cccca 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -9,7 +9,7 @@ function _handleExtensions(graph, shimCollection, extensions) { const type = extension.getType(); switch (type) { case "project-shim": - shimCollection.addShim(extension); + shimCollection.addProjectShim(extension); break; case "task": case "server-middleware": diff --git a/lib/specifications/configurations/AbstractConfiguration.js b/lib/specifications/configurations/AbstractConfiguration.js deleted file mode 100644 index c1f197d2a..000000000 --- a/lib/specifications/configurations/AbstractConfiguration.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -* Private configuration class for use in specifications -*/ - -class AbstractConfiguration { - /** - * @param {object} parameters - * @param {object} parameters.specification Specification instance - * @param {object} parameters.configObject Configuration object - */ - constructor({configObject, specification}) { - this._config = configObject; - this._specification = specification; - } - - async init() { - /* to be implemented by subclasses where needed*/ - return this; - } - - getName() { - return this._config.metadata.name; - } - - getKind() { - return this._config.kind; - } - - getType() { - return this._config.type; - } - - getObject() { - return this._config; - } -} - -module.exports = AbstractConfiguration; diff --git a/lib/specifications/configurations/Application.js b/lib/specifications/configurations/Application.js deleted file mode 100644 index 1f2b14c21..000000000 --- a/lib/specifications/configurations/Application.js +++ /dev/null @@ -1,13 +0,0 @@ -const AbstractConfiguration = require("./AbstractConfiguration"); - -/* -* Private configuration class for use in Module and specifications -*/ - -class Application extends AbstractConfiguration { - async init() { - - } -} - -module.exports = Application; diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index f65adb455..d218ca57e 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -45,7 +45,7 @@ class Application extends Project { getBuildtimeReader() { return resourceFactory.createReader({ fsBasePath: path.join(this.getPath(), this._webappPath), - virBasePath: "/resources" + this.getNamespace(), + virBasePath: `/resources/${this.getNamespace()}/`, name: `Source reader for ${this.getType()} ${this.getKind()} ${this.getName()}` }); } diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 0f5ef5765..4eee1d6fa 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -1,40 +1,34 @@ const test = require("ava"); const path = require("path"); -const Project = require("../../../lib/specifications/Project"); -const Configuration = require("../../../lib/specifications/Configuration"); +const Specification = require("../../../lib/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); - -test.beforeEach(async (t) => { - t.context.basicConfiguration = new Configuration({ +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { specVersion: "2.3", kind: "project", type: "application", metadata: {name: "application.a"} - }); - - t.context.basicProjectInput = { - id: "application.a.id", - version: "1.0.0", - modulePath: applicationAPath, - configuration: t.context.basicConfiguration - }; -}); + } +}; test("Instantiate a basic project", async (t) => { - const project = new Project(t.context.basicProjectInput); + const project = await Specification.create(basicProjectInput); t.is(project.getName(), "application.a", "Returned correct name"); t.is(project.getVersion(), "1.0.0", "Returned correct version"); t.is(project.getPath(), applicationAPath, "Returned correct project path"); }); -test("_getConfiguration", async (t) => { - const project = new Project(t.context.basicProjectInput); - t.is(await project._getConfiguration(), t.context.basicConfiguration, "Returned correct configuration instance"); +test("Configurations", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getKind(), "project", "Returned correct kind configuration"); }); test("Access project root resources via reader", async (t) => { - const project = new Project(t.context.basicProjectInput); + const project = await Specification.create(basicProjectInput); const rootReader = await project.getRootReader(); const packageJsonResource = await rootReader.byPath("/package.json"); t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); From 386b5684d29c1beed636bb36d9ccb95a11643fc9 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 20 Feb 2021 00:54:54 +0100 Subject: [PATCH 28/99] Add all missing types and adopt most tests --- lib/graph/Module.js | 2 +- lib/graph/ShimCollection.js | 3 +- lib/specifications/ComponentProject.js | 151 ++++++ lib/specifications/Extension.js | 6 +- lib/specifications/Project.js | 133 ++--- ...tractSpecification.js => Specification.js} | 106 +++- lib/specifications/types/Application.js | 84 ++- lib/specifications/types/Library.js | 508 ++++++++++++++++++ lib/specifications/types/Module.js | 97 ++++ lib/specifications/types/ProjectShim.js | 31 ++ lib/specifications/types/ServerMiddleware.js | 22 + lib/specifications/types/Task.js | 22 + lib/specifications/types/ThemeLibrary.js | 100 ++++ .../library.e/src/library/e/.library | 2 +- .../library.e/node_modules/library.d/ui5.yaml | 5 - .../library.e/src/library/e/.library | 2 +- .../src/library/{d => d-depender}/.library | 2 +- .../src/library/{d => d-depender}/some.js | 0 .../library.e/src/library/e/.library | 2 +- .../library.e/node_modules/library.d/ui5.yaml | 5 - .../library.e/src/library/e/.library | 2 +- .../library.e/src/library/e/.library | 2 +- .../application.cycle.c/webapp/manifest.json | 13 + .../application.cycle.d/webapp/manifest.json | 13 + .../application.cycle.e/webapp/manifest.json | 13 + .../application.cycle.f/webapp/manifest.json | 13 + test/lib/graph/ProjectGraph.js | 259 +++++---- test/lib/graph/projectGraphFromTree.js | 43 +- .../providers/ui5Framework.integration.js | 2 +- test/lib/specifications/Project.js | 2 +- 30 files changed, 1313 insertions(+), 332 deletions(-) create mode 100644 lib/specifications/ComponentProject.js rename lib/specifications/{AbstractSpecification.js => Specification.js} (51%) create mode 100644 lib/specifications/types/Library.js create mode 100644 lib/specifications/types/Module.js create mode 100644 lib/specifications/types/ProjectShim.js create mode 100644 lib/specifications/types/ServerMiddleware.js create mode 100644 lib/specifications/types/Task.js create mode 100644 lib/specifications/types/ThemeLibrary.js rename test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/{d => d-depender}/.library (88%) rename test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/{d => d-depender}/some.js (100%) create mode 100644 test/fixtures/cyclic-deps/node_modules/application.cycle.c/webapp/manifest.json create mode 100644 test/fixtures/cyclic-deps/node_modules/application.cycle.d/webapp/manifest.json create mode 100644 test/fixtures/cyclic-deps/node_modules/application.cycle.e/webapp/manifest.json create mode 100644 test/fixtures/cyclic-deps/node_modules/application.cycle.f/webapp/manifest.json diff --git a/lib/graph/Module.js b/lib/graph/Module.js index d48845b89..b8add5335 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -4,7 +4,7 @@ const {promisify} = require("util"); const readFile = promisify(fs.readFile); const jsyaml = require("js-yaml"); const resourceFactory = require("@ui5/fs").resourceFactory; -const Specification = require("../Specification"); +const Specification = require("../specifications/Specification"); const {validate} = require("../validation/validator"); const log = require("@ui5/logger").getLogger("graph:Module"); diff --git a/lib/graph/ShimCollection.js b/lib/graph/ShimCollection.js index 63ec2ca4b..817cfd618 100644 --- a/lib/graph/ShimCollection.js +++ b/lib/graph/ShimCollection.js @@ -23,8 +23,7 @@ class ShimCollection { const name = shimExtension.getName(); log.verbose(`Adding new shim ${name}...`); // TODO: Move this into a dedicated ShimConfiguration class? - const config = shimExtension.getConfigurationObject(); - const {configurations, dependencies, collections} = config.shims; + const {configurations, dependencies, collections} = shimExtension.getShimConfiguration(); if (configurations) { addToMap(name, configurations, this._projectConfigShims); } diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js new file mode 100644 index 000000000..f92ff9fa7 --- /dev/null +++ b/lib/specifications/ComponentProject.js @@ -0,0 +1,151 @@ +const {promisify} = require("util"); +const Project = require("./Project"); + +/* +* Private configuration class for use in Module and specifications +*/ + +class ComponentProject extends Project { + constructor(parameters) { + super(parameters); + if (new.target === ComponentProject) { + throw new TypeError("Class 'ComponentProject' is abstract. Please use one of the 'types' subclasses"); + } + + this._pPom = null; + + this._namespace = null; + } + + /* === Attributes === */ + /** + * @public + */ + getNamespace() { + return this._namespace; + } + + /** + * @public + */ + getCopyright() { + return this._config.metadata.copyright; + } + + /** + * @public + */ + getComponentPreloadPaths() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.paths || []; + } + + /** + * @public + */ + getComponentPreloadNamespaces() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.namespaces || []; + } + + getJsdocExcludes() { + return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; + } + + /** + * @public + */ + getBundles() { + return this._config.builder && this._config.builder.bundles || []; + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + + this._namespace = await this._getNamespace(); + } + + async _getNamespace() { + throw new Error(`_getNamespace must be implemented by subclass ${this.constructor.name}`); + } + + /* === Helper === */ + /** + * Checks whether a given string contains a maven placeholder. + * E.g. ${appId}. + * + * @param {string} value String to check + * @returns {boolean} True if given string contains a maven placeholder + */ + _hasMavenPlaceholder(value) { + return !!value.match(/^\$\{(.*)\}$/); + } + + /** + * Resolves a maven placeholder in a given string using the projects pom.xml + * + * @param {string} value String containing a maven placeholder + * @returns {Promise} Resolved string + */ + async _resolveMavenPlaceholder(value) { + const parts = value && value.match(/^\$\{(.*)\}$/); + if (parts) { + this._log.verbose( + `"${value} contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); + const pom = await this.getPom(); + let mvnValue; + if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { + mvnValue = pom.project.properties[parts[1]]; + } else { + let obj = pom; + parts[1].split(".").forEach((part) => { + obj = obj && obj[part]; + }); + mvnValue = obj; + } + if (!mvnValue) { + throw new Error(`"${value}" couldn't be resolved from maven property ` + + `"${parts[1]}" of pom.xml of project ${this._project.metadata.name}`); + } + return mvnValue; + } else { + throw new Error(`"${value}" is not a maven placeholder`); + } + } + + /** + * Reads the projects pom.xml file + * + * @returns {Promise} Resolves with a JSON representation of the content + */ + async _getPom() { + if (this._pPom) { + return this._pPom; + } + return this._pPom = this.getRootReader().byPath("/pom.xml") + .then(async (resource) => { + if (!resource) { + throw new Error( + `Could not find pom.xml in project ${this.getName()}`); + } + const content = await resource.getString(); + const xml2js = require("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + ignoreAttrs: true + }); + const readXML = promisify(parser.parseString); + return readXML(content); + }).catch((err) => { + throw new Error( + `Failed to read pom.xml for project ${this.getName()}: ${err.message}`); + }); + } +} + +module.exports = ComponentProject; diff --git a/lib/specifications/Extension.js b/lib/specifications/Extension.js index f09e1f672..4df68c060 100644 --- a/lib/specifications/Extension.js +++ b/lib/specifications/Extension.js @@ -1,10 +1,10 @@ -const AbstractSpecification = require("./AbstractSpecification"); +const Specification = require("./Specification"); -class Extension extends AbstractSpecification { +class Extension extends Specification { constructor(parameters) { super(parameters); if (new.target === Extension) { - throw new TypeError("Class 'Project' is abstract"); + throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); } } diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index c594aaaa9..18118e7fc 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -1,14 +1,11 @@ -const AbstractSpecification = require("./AbstractSpecification"); +const Specification = require("./Specification"); -class Project extends AbstractSpecification { +class Project extends Specification { constructor(parameters) { super(parameters); if (new.target === Project) { - throw new TypeError("Class 'Project' is abstract"); + throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); } - this._frameworkName = null; - this._frameworkVersion = null; - this._frameworkDependencies = null; } /* === Attributes === */ @@ -16,26 +13,64 @@ class Project extends AbstractSpecification { * @public */ getFrameworkName() { - return this._frameworkName; + return this._config.framework && this._config.framework.name; } /** * @public */ getFrameworkVersion() { - return this._frameworkVersion; + return this._config.framework && this._config.framework.version; } /** * @public */ getFrameworkDependencies() { // TODO: Clone or freeze object before exposing? - return this._frameworkDependencies || []; + return this._config.framework && this._config.framework.libraries || []; } isFrameworkProject() { return this.__id.startsWith("@openui5/") || this.__id.startsWith("@sapui5/"); } + getCustomConfiguration() { + return this._config.customConfiguration; + } + + getBuilderResourceExcludes() { + return this._config.builder && this._config.builder.resources && this._config.builder.resources.excludes || []; + } + + getCustomTasks() { + return this._config.builder && this._config.builder.customTasks || []; + } + + getServerSettings() { + return this._config.server && this._config.server.settings; + } + + /* === Resource Access === */ + /** + * @public + */ + getSourceReader() { + throw new Error(`getSourceReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * @public + */ + getRuntimeReader() { + throw new Error(`getRuntimeReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * @public + */ + getBuildtimeReader() { + throw new Error(`getBuildtimeReader must be implemented by subclass ${this.constructor.name}`); + } + /* === Internals === */ /** * @private @@ -43,18 +78,6 @@ class Project extends AbstractSpecification { */ async _parseConfiguration(config) { await super._parseConfiguration(config); - - if (config.framework) { - if (config.framework.name) { - this._frameworkName = config.framework.name; - } - if (config.framework.version) { - this._frameworkVersion = config.framework.version; - } - if (config.framework.libraries) { - this._frameworkDependencies = JSON.parse(JSON.stringify(config.framework.libraries)); - } - } } async _validate() { @@ -65,74 +88,6 @@ class Project extends AbstractSpecification { `is of kind '${this.getKind()}'`); } } - - /* === Helper === */ - /** - * Checks whether a given string contains a maven placeholder. - * E.g. ${appId}. - * - * @param {string} value String to check - * @returns {boolean} True if given string contains a maven placeholder - */ - _hasMavenPlaceholder(value) { - return !!value.match(/^\$\{(.*)\}$/); - } - - /** - * Resolves a maven placeholder in a given string using the projects pom.xml - * - * @param {string} value String containing a maven placeholder - * @returns {Promise} Resolved string - */ - async _resolveMavenPlaceholder(value) { - const parts = value && value.match(/^\$\{(.*)\}$/); - if (parts) { - this._log.verbose( - `"${value} contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); - const pom = await this.getPom(); - let mvnValue; - if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { - mvnValue = pom.project.properties[parts[1]]; - } else { - let obj = pom; - parts[1].split(".").forEach((part) => { - obj = obj && obj[part]; - }); - mvnValue = obj; - } - if (!mvnValue) { - throw new Error(`"${value}" couldn't be resolved from maven property ` + - `"${parts[1]}" of pom.xml of project ${this._project.metadata.name}`); - } - return mvnValue; - } else { - throw new Error(`"${value}" is not a maven placeholder`); - } - } - - /** - * Reads the projects pom.xml file - * - * @returns {Promise} Resolves with a JSON representation of the content - */ - async _getPom() { - if (this._pPom) { - return this._pPom; - } - const fsPath = path.join(this._project.path, "pom.xml"); - return this._pPom = readFile(fsPath).then(async (content) => { - const xml2js = require("xml2js"); - const parser = new xml2js.Parser({ - explicitArray: false, - ignoreAttrs: true - }); - const readXML = promisify(parser.parseString); - return readXML(content); - }).catch((err) => { - throw new Error( - `Failed to read pom.xml for project ${this._project.metadata.name}: ${err.message}`); - }); - } } module.exports = Project; diff --git a/lib/specifications/AbstractSpecification.js b/lib/specifications/Specification.js similarity index 51% rename from lib/specifications/AbstractSpecification.js rename to lib/specifications/Specification.js index 8fcd0fb09..a2311804f 100644 --- a/lib/specifications/AbstractSpecification.js +++ b/lib/specifications/Specification.js @@ -1,9 +1,9 @@ const resourceFactory = require("@ui5/fs").resourceFactory; -class AbstractSpecification { +class Specification { constructor() { - if (new.target === AbstractSpecification) { - throw new TypeError("Class 'AbstractSpecification' is abstract"); + if (new.target === Specification) { + throw new TypeError("Class 'Specification' is abstract. Please use one of the 'types' subclasses"); } this._log = require("@ui5/logger").getLogger(`specifications:types:${this.constructor.name}`); } @@ -36,34 +36,50 @@ class AbstractSpecification { // The ID property as supplied by the translators is only here for debugging and potential tracing purposes this.__id = id; + const config = JSON.parse(JSON.stringify(configuration)); + + // Deep clone config to prevent changes by reference const {validate} = require("../validation/validator"); await validate({ - config: configuration, + config, project: { id } }); + // Check whether the given configuration matches the class by guessing the type name from the class name + if (config.type.replace("-", "") !== this.constructor.name.toLowerCase()) { + throw new Error( + `Configuration missmatch: Supplied configuration of type '${config.type}' does not match with ` + + `specification class ${this.constructor.name}`); + } + if (!id.startsWith("@openui5/") && !id.startsWith("@sapui5/")) { - if (configuration.specVersion === "0.1" || configuration.specVersion === "1.0" || - configuration.specVersion === "1.1") { + if (config.specVersion === "0.1" || config.specVersion === "1.0" || + config.specVersion === "1.1") { throw new Error( - `Unsupported specification version ${configuration.specVersion} defined for ` + - `${configuration.kind} ${configuration.metadata.name}. The new Specification API can only be ` + + `Unsupported specification version ${config.specVersion} defined for ` + + `${config.kind} ${config.metadata.name}. The new Specification API can only be ` + `used with specification versions >= 2.0. For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); } - if (configuration.specVersion !== "2.0" && - configuration.specVersion !== "2.1" && configuration.specVersion !== "2.2" && - configuration.specVersion !== "2.3") { + if (config.specVersion !== "2.0" && + config.specVersion !== "2.1" && config.specVersion !== "2.2" && + config.specVersion !== "2.3") { throw new Error( - `Unsupported specification version ${configuration.specVersion} defined in ${configuration.kind} ` + - `${configuration.metadata.name}. Your UI5 CLI installation might be outdated. ` + + `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + + `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); } } - await this._parseConfiguration(configuration); - await this._validate(); + this._name = config.metadata.name; + this._kind = config.kind; + this._type = config.type; + this._specVersion = config.specVersion; + + await this._configureAndValidatePaths(config); + await this._parseConfiguration(config); + this._config = config; return this; } @@ -104,6 +120,8 @@ class AbstractSpecification { } /** + * Might not be POSIX + * * @private */ getPath() { @@ -127,14 +145,13 @@ class AbstractSpecification { * @private * @param {object} config Configuration object */ - async _parseConfiguration(config) { - this._name = config.metadata.name; - this._kind = config.kind; - this._type = config.type; - this._specVersion = config.specVersion; - } + async _configureAndValidatePaths(config) {} - async _validate() {} + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) {} /* === Helper === */ /** @@ -147,6 +164,49 @@ class AbstractSpecification { } return false; } + + static async create(params) { + if (!params.configuration) { + throw new Error( + `Unable to create Specification instance: Missing configuration parameter`); + } + const {kind, type} = params.configuration; + if (!["project", "extension"].includes(kind)) { + throw new Error(`Unable to create Specification instance: Unknown kind '${kind}'`); + } + + switch (type) { + case "application": { + return createAndInitializeSpec("Application", params); + } + case "library": { + return createAndInitializeSpec("Library", params); + } + case "theme-library": { + return createAndInitializeSpec("ThemeLibrary", params); + } + case "module": { + return createAndInitializeSpec("Module", params); + } + case "task": { + return createAndInitializeSpec("Task", params); + } + case "server-middleware": { + return createAndInitializeSpec("ServerMiddleware", params); + } + case "project-shim": { + return createAndInitializeSpec("ProjectShim", params); + } + default: + throw new Error( + `Unable to create Specification instance: Unknown specification type '${type}'`); + } + } +} + +function createAndInitializeSpec(moduleName, params) { + const Spec = require(`./types/${moduleName}`); + return new Spec().init(params); } -module.exports = AbstractSpecification; +module.exports = Specification; diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index d218ca57e..5669eeb59 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -1,18 +1,14 @@ -const path = require("path"); +const fsPath = require("path"); const resourceFactory = require("@ui5/fs").resourceFactory; -const Project = require("../Project"); +const ComponentProject = require("../ComponentProject"); -/* -* Private configuration class for use in Module and specifications -*/ - -class Application extends Project { +class Application extends ComponentProject { constructor(parameters) { super(parameters); this._pManifests = {}; + this._webappPath = "webapp"; - this._propertiesFilesSourceEncoding = "UTF-8"; } /* === Attributes === */ @@ -20,22 +16,35 @@ class Application extends Project { * @public */ getPropertiesFileSourceEncoding() { - return this._propertiesFilesSourceEncoding; + return this._config.resources && this._config.resources.configuration && + this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; } - getNamespace() { - return this._namespace; + getCachebusterSignatureType() { + return this._config.builder && this._config.builder.cachebuster && + this._config.builder.cachebuster.signatureType || "time"; } /* === Resource Access === */ + /** + * @public + */ + getSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._webappPath), + virBasePath: `/resources/${this.getNamespace()}/`, + name: `Source reader for application project ${this.getName()}` + }); + } + /** * @public */ getRuntimeReader() { return resourceFactory.createReader({ - fsBasePath: path.join(this.getPath(), this._webappPath), + fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath: "/", // Applications are served at "/" - name: `Source reader for ${this.getType()} ${this.getKind()} ${this.getName()}` + name: `Source reader for application project ${this.getName()}` }); } @@ -43,11 +52,7 @@ class Application extends Project { * @public */ getBuildtimeReader() { - return resourceFactory.createReader({ - fsBasePath: path.join(this.getPath(), this._webappPath), - virBasePath: `/resources/${this.getNamespace()}/`, - name: `Source reader for ${this.getType()} ${this.getKind()} ${this.getName()}` - }); + return this.getSourceReader(); } /* === Internals === */ @@ -55,38 +60,32 @@ class Application extends Project { * @private * @param {object} config Configuration object */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); - if (config.resources && config.resources.configuration) { - if (config.resources.configuration.paths && - config.resources.configuration.paths.webapp) { - // "webapp" path mapping - this._webappPath = config.resources.configuration.paths.webapp; - } - if (config.resources.configuration.propertiesFileSourceEncoding) { - // .properties files encoding - this._propertiesFilesSourceEncoding = config.resources.configuration.propertiesFileSourceEncoding; - } + if (config.resources && config.resources.configuration && + config.resources.configuration.paths && config.resources.configuration.paths.webapp) { + this._webappPath = config.resources.configuration.paths.webapp; } - this._namespace = await this._getNamespace(); - } - + this._log.verbose(`Path mapping for application project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getPath()}`); + this._log.verbose(` Mapped to: ${this._srcPath}`); - async _validate() { - await super._validate(); - if (this.getType() !== "application") { - throw new Error( - `Configuration missmatch: Supplied configuration must be of type 'application' but ` + - `is of type '${this.getType()}'`); - } if (!await this._dirExists("/" + this._webappPath)) { throw new Error( `Unable to find directory '${this._webappPath}' in application project ${this.getName()}`); } } + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + /** * Determine application namespace either based on a project`s * manifest.json or manifest.appdescr_variant (fallback if present) @@ -140,7 +139,7 @@ class Application extends Project { if (this._hasMavenPlaceholder(appId)) { try { - appId = await this.resolveMavenPlaceholder(appId); + appId = await this._resolveMavenPlaceholder(appId); } catch (err) { throw new Error( `Failed to resolve namespace of project ${this.getName()}: ${err.message}`); @@ -192,8 +191,7 @@ class Application extends Project { `Could not find resource ${filePath} in project ${this.getName()}`); } return JSON.parse(await resource.getString()); - }) - .catch((err) => { + }).catch((err) => { throw new Error( `Failed to read ${filePath} for project ` + `${this.getName()}: ${err.message}`); diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js new file mode 100644 index 000000000..52b223c3f --- /dev/null +++ b/lib/specifications/types/Library.js @@ -0,0 +1,508 @@ +const fsPath = require("path"); +const posixPath = require("path").posix; +const {promisify} = require("util"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const ComponentProject = require("../ComponentProject"); + +const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; + + +class Library extends ComponentProject { + constructor(parameters) { + super(parameters); + + this._pManifest = null; + this._pDotLibrary = null; + this._pLibraryJs = null; + + this._srcPath = "src"; + this._testPath = "test"; + this._testPathExists = false; + this._propertiesFilesSourceEncoding = "UTF-8"; + } + + /* === Attributes === */ + /** + * @public + */ + getPropertiesFileSourceEncoding() { + return this._config.resources && this._config.resources.configuration && + this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; + } + + /** + * @public + */ + getLibraryPreloadExcludes() { + return this._config.builder && this._config.builder.libraryPreload && + this._config.builder.libraryPreload.excludes || []; + } + + /* === Resource Access === */ + /** + * @public + */ + getSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/resources/", + name: `Source reader for library project ${this.getName()}` + }); + } + + /** + * @public + */ + getRuntimeReader() { + let reader = this.getSourceReader(); + if (this._testPathExists) { + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._testPath), + virBasePath: "/test-resources/", + name: `Test reader for library project ${this.getName()}` + }); + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for library project ${this.getName()}`, + readers: [reader, testReader] + }); + } + return reader; + } + + /** + * @public + */ + getBuildtimeReader() { + // Same as runtime + return this.getRuntimeReader(); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources.configuration.paths.src) { + this._srcPath = config.resources.configuration.paths.src; + } + if (config.resources.configuration.paths.test) { + this._testPath = config.resources.configuration.paths.test; + } + } + + this._log.verbose(`Path mapping for library project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + this._log.verbose(` /test-resources/ => ${this._testPath}`); + + if (!await this._dirExists("/" + this._srcPath)) { + throw new Error( + `Unable to find directory '${this._srcPath}' in library project ${this.getName()}`); + } + if (!await this._dirExists("/" + this._testPath)) { + this._log.verbose(` (/test-resources/ target does not exist)`); + } else { + this._testPathExists = true; + } + } + + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + + try { + this._namespace = await this.getNamespace(); + } catch (err) { + if (SAP_THEMES_NS_EXEMPTIONS.includes(this.getName())) { + // Exceptional handling for SAP theme libraries which used to be of type "library" + // (today they use "theme-library"). + // To allow use of OpenUI5 theme libraries in versions lower than 1.75 we must ignore + // namespace detection errors. + this._log.verbose(`Ignoring failed namespace detection for exempted SAP theme library ` + + `${this.getName()}: ${err.message}`); + } else { + throw err; + } + } + + if (!config.metadata.copyright) { + try { + config.metadata.copyrigh = await this.getCopyright(); + } catch (err) { + // Catch error because copyright is optional + // TODO: Make copyright mandatory? + this._log.verbose(err.message); + } + } + + if (this.isFrameworkProject()) { + if (config.builder && config.builder.libraryPreload && config.builder.libraryPreload.excludes) { + this._log.verbose( + `Using preload excludes for framework library ${this.getName()} from project configuration`); + } else { + this._log.verbose( + `No preload excludes defined in project configuration of framework library ` + + `${this.getName()}. Falling back to .library...`); + const excludes = await this._getPreloadExcludesFromDotLibrary(); + if (excludes) { + if (!config.builder) { + config.builder = {}; + } + if (!config.builder.libraryPreload) { + config.builder.libraryPreload = {}; + } + config.builder.libraryPreload.excludes = excludes; + } + } + } + } + + /** + * Determine library namespace by checking manifest.json with fallback to .library. + * Any maven placeholders are resolved from the projects pom.xml + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespace() { + // Trigger both reads asynchronously + const [{ + namespace: manifestNs, + filePath: manifestPath + }, { + namespace: dotLibraryNs, + filePath: dotLibraryPath + }] = await Promise.all([ + this._getNamespaceFromManifest(), + this._getNamespaceFromDotLibrary() + ]); + + let libraryNs; + let namespacePath; + if (manifestNs && dotLibraryNs) { + // Both files present + // => check whether they are on the same level + const manifestDepth = manifestPath.split("/").length; + const dotLibraryDepth = dotLibraryPath.split("/").length; + + if (manifestDepth < dotLibraryDepth) { + // We see the .library file as the "leading" file of a library + // Therefore, a manifest.json on a higher level is something we do not except + throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + + `Found a manifest.json on a higher directory level than the .library file. ` + + `It should be on the same or a lower level. ` + + `Note that a manifest.json on a lower level will be ignored.\n` + + ` manifest.json path: ${manifestPath}\n` + + ` is higher than\n` + + ` .library path: ${dotLibraryPath}`); + } + if (manifestDepth === dotLibraryDepth) { + if (posixPath.dirname(manifestPath) !== posixPath.dirname(dotLibraryPath)) { + // This just should not happen in your project + throw new Error(`Failed to detect namespace for project ${this.getName()}: ` + + `Found a manifest.json on the same directory level but in a different directory ` + + `than the .library file. They should be in the same directory.\n` + + ` manifest.json path: ${manifestPath}\n` + + ` is different to\n` + + ` .library path: ${dotLibraryPath}`); + } + // Typical scenario if both files are present + this._log.verbose( + `Found a manifest.json and a .library file on the same level for ` + + `project ${this.getName()}.`); + this._log.verbose( + `Resolving namespace of project ${this.getName()} from manifest.json...`); + libraryNs = manifestNs; + namespacePath = posixPath.dirname(manifestPath); + } else { + // Typical scenario: Some nested component has a manifest.json but the library itself only + // features a .library. => Ignore the manifest.json + this._log.verbose( + `Ignoring manifest.json found on a lower level than the .library file of ` + + `project ${this.getName()}.`); + this._log.verbose( + `Resolving namespace of project ${this.getName()} from .library...`); + libraryNs = dotLibraryNs; + namespacePath = posixPath.dirname(dotLibraryPath); + } + } else if (manifestNs) { + // Only manifest available + this._log.verbose( + `Resolving namespace of project ${this.getName()} from manifest.json...`); + libraryNs = manifestNs; + namespacePath = posixPath.dirname(manifestPath); + } else if (dotLibraryNs) { + // Only .library available + this._log.verbose( + `Resolving namespace of project ${this.getName()} from .library...`); + libraryNs = dotLibraryNs; + namespacePath = posixPath.dirname(dotLibraryPath); + } else { + this._log.verbose( + `Failed to resolve namespace of project ${this.getName()} from manifest.json ` + + `or .library file. Falling back to library.js file path...`); + } + + let namespace; + if (libraryNs) { + // Maven placeholders can only exist in manifest.json or .library configuration + if (this._hasMavenPlaceholder(libraryNs)) { + try { + libraryNs = await this._resolveMavenPlaceholder(libraryNs); + } catch (err) { + throw new Error( + `Failed to resolve namespace maven placeholder of project ` + + `${this.getName()}: ${err.message}`); + } + } + + namespace = libraryNs.replace(/\./g, "/"); + + const namespaceFromPath = this._getNamespaceFromDirPath(namespacePath); + if (namespaceFromPath !== namespace) { + throw new Error( + `Detected namespace "${namespace}" does not match detected directory ` + + `structure "${namespaceFromPath}" for project ${this.getName()}`); + } + } else { + try { + const libraryJsPath = await this._getLibraryJsPath(); + namespace = this._getNamespaceFromDirPath(posixPath.dirname(libraryJsPath)); + if (!namespace || namespace === "/") { + throw new Error(`Found library.js file in root directory. ` + + `Expected it to be in namespace directory.`); + } + this._log.verbose( + `Deriving namespace for project ${this.getName()} from ` + + `path of library.js file`); + } catch (err) { + this._log.verbose( + `Namespace resolution from library.js file path failed for project ` + + `${this.getName()}: ${err.message}`); + } + } + + if (!namespace) { + throw new Error(`Failed to detect namespace or namespace is empty for ` + + `project ${this.getName()}. Check verbose log for details.`); + } + + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace}`); + return namespace; + } + + async _getNamespaceFromManifest() { + try { + const {content: manifest, filePath} = await this._getManifest(); + // check for a proper sap.app/id in manifest.json to determine namespace + if (manifest["sap.app"] && manifest["sap.app"].id) { + const namespace = manifest["sap.app"].id; + this._log.verbose( + `Found namespace ${namespace} in manifest.json of project ${this.getName()} ` + + `at ${filePath}`); + return { + namespace, + filePath + }; + } else { + this._log.verbose( + `No sap.app/id configuration found in manifest.json of project ${this.getName()} ` + + `at ${filePath}`); + } + } catch (err) { + this._log.verbose( + `Namespace resolution from manifest.json failed for project ` + + `${this.getName()}: ${err.message}`); + } + return {}; + } + + async _getNamespaceFromDotLibrary() { + try { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + if (dotLibrary && dotLibrary.library && dotLibrary.library.name) { + const namespace = dotLibrary.library.name._; + this._log.verbose( + `Found namespace ${namespace} in .library file of project ${this.getName()} ` + + `at ${filePath}`); + return { + namespace, + filePath + }; + } else { + throw new Error( + `No library name found in .library of project ${this.getName()} ` + + `at ${filePath}`); + } + } catch (err) { + this._log.verbose( + `Namespace resolution from .library failed for project ` + + `${this.getName()}: ${err.message}`); + } + return {}; + } + + _getNamespaceFromDirPath(dirPath) { + const virtualBasePath = "/resources/"; + + if (!dirPath.startsWith(virtualBasePath)) { + if (virtualBasePath === dirPath + "/") { + // The given file path does not contain a namespace path + // It is equal to the source base path + // Therefore return an empty namespace + return ""; + } + throw new Error(`Given directory path ${dirPath} is not based on the ` + + `expected base path ${virtualBasePath}.`); + } + return dirPath.replace(virtualBasePath, ""); + } + + /** + * Determines library copyright from given project configuration with fallback to .library. + * + * @returns {string} Copyright of the project + * @throws {Error} if copyright can not be determined + */ + async _getCopyright() { + // If no copyright replacement was provided by ui5.yaml, + // check if the .library file has a valid copyright replacement + const {content: dotLibrary} = await this._getDotLibrary(); + if (dotLibrary && dotLibrary.library && dotLibrary.library.copyright) { + this._log.verbose( + `Using copyright from .library for project ${this.getName()}...`); + return dotLibrary.library.copyright._; + } else { + throw new Error(`No copyright configuration found in .library ` + + `of project ${this.getName()}`); + } + } + + async _getPreloadExcludesFromDotLibrary() { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + if (dotLibrary && dotLibrary.library && dotLibrary.library.appData && + dotLibrary.library.appData.packaging && + dotLibrary.library.appData.packaging["all-in-one"] && + dotLibrary.library.appData.packaging["all-in-one"].exclude + ) { + let excludes = dotLibrary.library.appData.packaging["all-in-one"].exclude; + if (!Array.isArray(excludes)) { + excludes = [excludes]; + } + this._log.verbose( + `Found ${excludes.length} preload excludes in .library file of ` + + `project ${this.getName()} at ${filePath}`); + return excludes.map((exclude) => { + return exclude.$.name; + }); + } else { + this._log.verbose( + + `No preload excludes found in .library of project ${this.getName()} ` + + `at ${filePath}`); + return null; + } + } + + /** + * Reads the projects manifest.json + * + * @returns {Promise} resolves with an object containing the content (as JSON) and + * fsPath (as string) of the manifest.json file + */ + async _getManifest() { + if (this._pManifest) { + return this._pManifest; + } + return this._pManifest = this.getSourceReader().byGlob("**/manifest.json") + .then(async (manifestResources) => { + if (!manifestResources.length) { + throw new Error(`Could not find manifest.json file for project ${this.getName()}`); + } + if (manifestResources.length > 1) { + throw new Error(`Found multiple (${manifestResources.length}) manifest.json files ` + + `for project ${this.getName()}`); + } + const resource = manifestResources[0]; + try { + return { + content: JSON.parse(await resource.getString()), + filePath: resource.getPath() + }; + } catch (err) { + throw new Error( + `Failed to read manifest.json for project ${this.getName()}: ${err.message}`); + } + }); + } + + /** + * Reads the .library file + * + * @returns {Promise} resolves with an object containing the content (as JSON) and + * fsPath (as string) of the .library file + */ + async _getDotLibrary() { + if (this._pDotLibrary) { + return this._pDotLibrary; + } + return this._pDotLibrary = this.getSourceReader().byGlob("**/.library") + .then(async (dotLibraryResources) => { + if (!dotLibraryResources.length) { + throw new Error(`Could not find .library file for project ${this.getName()}`); + } + if (dotLibraryResources.length > 1) { + throw new Error(`Found multiple (${dotLibraryResources.length}) .library files ` + + `for project ${this.getName()}`); + } + const resource = dotLibraryResources[0]; + const content = await resource.getString(); + const xml2js = require("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + explicitCharkey: true + }); + const readXML = promisify(parser.parseString); + const contentJson = await readXML(content); + return { + content: contentJson, + filePath: resource.getPath() + }; + }); + } + + /** + * Determines the path of the library.js file + * + * @returns {Promise} resolves with an a string containing the file system path + * of the library.js file + */ + async _getLibraryJsPath() { + if (this._pLibraryJs) { + return this._pLibraryJs; + } + return this._pLibraryJs = this.getSourceReader().byGlob("**/library.js") + .then(async (libraryJsResources) => { + if (!libraryJsResources.length) { + throw new Error(`Could not find library.js file for project ${this.getName()}`); + } + if (libraryJsResources.length > 1) { + throw new Error(`Found multiple (${libraryJsResources.length}) library.js files ` + + `for project ${this.getName()}`); + } + // Content is not yet relevant, so don't read it + return libraryJsResources[0].getPath(); + }); + } +} + +module.exports = Library; diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js new file mode 100644 index 000000000..7d0238946 --- /dev/null +++ b/lib/specifications/types/Module.js @@ -0,0 +1,97 @@ +const fsPath = require("path"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const Project = require("../Project"); + +class Module extends Project { + constructor(parameters) { + super(parameters); + + this._paths = null; + } + + /* === Attributes === */ + /** + * @public + */ + + /* === Resource Access === */ + /** + * @public + */ + getSourceReader() { + const readers = this._paths.map((readerArgs) => resourceFactory.createReader(readerArgs)); + if (readers.length === 1) { + return readers[0]; + } + return resourceFactory.createReaderCollection({ + name: `Reader collection for module project ${this.getName()}`, + readers + }); + } + + /** + * @public + */ + getRuntimeReader() { + return this.getSourceReader(); + } + + /** + * @public + */ + getBuildtimeReader() { + return this.getSourceReader(); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + + this._log.verbose(`Path mapping for library project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + this._paths = await Promise.all(Object.entries(config.resources.configuration.paths) + .map(async ([virBasePath, relFsPath]) => { + this._log.verbose(` ${virBasePath} => ${relFsPath}`); + if (!await this._dirExists("/" + relFsPath)) { + throw new Error( + `Unable to find directory '${relFsPath}' in module project ${this.getName()}`); + } + return { + name: `'${relFsPath}'' reader for moduleproject ${this.getName()}`, + virBasePath, + fsBasePath: fsPath.join(this.getPath(), relFsPath) + }; + })); + } else { + if (!await this._dirExists("/")) { + throw new Error( + `Unable to find root directory of module project ${this.getName()}`); + } + this._log.verbose(` / => `); + this._paths = [{ + name: `Root reader for module project ${this.getName()}`, + virBasePath: "/", + fsBasePath: this.getPath() + }]; + } + } + + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } +} + +module.exports = Module; diff --git a/lib/specifications/types/ProjectShim.js b/lib/specifications/types/ProjectShim.js new file mode 100644 index 000000000..b2b17aa2b --- /dev/null +++ b/lib/specifications/types/ProjectShim.js @@ -0,0 +1,31 @@ +const Extension = require("../Extension"); + +class ProjectShim extends Extension { + constructor(parameters) { + super(parameters); + } + + + /* === Attributes === */ + /** + * @public + */ + getShimConfiguration() { + return this._config.shims; + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + + async _validate() { + await super._validate(); + } +} + +module.exports = ProjectShim; diff --git a/lib/specifications/types/ServerMiddleware.js b/lib/specifications/types/ServerMiddleware.js new file mode 100644 index 000000000..36dc4b35c --- /dev/null +++ b/lib/specifications/types/ServerMiddleware.js @@ -0,0 +1,22 @@ +const Extension = require("../Extension"); + +class ServerMiddleware extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + + async _validate() { + await super._validate(); + } +} + +module.exports = ServerMiddleware; diff --git a/lib/specifications/types/Task.js b/lib/specifications/types/Task.js new file mode 100644 index 000000000..56fef3716 --- /dev/null +++ b/lib/specifications/types/Task.js @@ -0,0 +1,22 @@ +const Extension = require("../Extension"); + +class Task extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + + async _validate() { + await super._validate(); + } +} + +module.exports = Task; diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 000000000..511257528 --- /dev/null +++ b/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,100 @@ +const fsPath = require("path"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const Project = require("../Project"); + +class ThemeLibrary extends Project { + constructor(parameters) { + super(parameters); + + this._srcPath = "src"; + this._testPath = "test"; + } + + /* === Attributes === */ + /** + * @public + */ + + /* === Resource Access === */ + /** + * @public + */ + getSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/resources/", + name: `Source reader for theme-library project ${this.getName()}` + }); + } + + /** + * @public + */ + getRuntimeReader() { + let reader = this.getSourceReader(); + if (this._testPathExists) { + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._testPath), + virBasePath: "/test-resources/", + name: `Test reader for theme-library project ${this.getName()}` + }); + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for theme-library project ${this.getName()}`, + readers: [reader, testReader] + }); + } + return reader; + } + + /** + * @public + */ + getBuildtimeReader() { + // Same as runtime + return this.getRuntimeReader(); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources.configuration.paths.src) { + this._srcPath = config.resources.configuration.paths.src; + } + if (config.resources.configuration.paths.test) { + this._testPath = config.resources.configuration.paths.test; + } + } + + this._log.verbose(`Path mapping for theme-library project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + this._log.verbose(` /test-resources/ => ${this._testPath}`); + + if (!await this._dirExists("/" + this._srcPath)) { + throw new Error( + `Unable to find directory '${this._srcPath}' in theme-library project ${this.getName()}`); + } + if (!await this._dirExists("/" + this._testPath)) { + this._log.verbose(` (/test-resources/ target does not exist)`); + } else { + this._testPathExists = true; + } + } + + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } +} + +module.exports = ThemeLibrary; diff --git a/test/fixtures/application.c/node_modules/library.e/src/library/e/.library b/test/fixtures/application.c/node_modules/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/application.c/node_modules/library.e/src/library/e/.library +++ b/test/fixtures/application.c/node_modules/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml b/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml index a47c1f64c..e05b61880 100644 --- a/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -3,8 +3,3 @@ specVersion: "2.3" type: library metadata: name: library.d -resources: - configuration: - paths: - src: main/src - test: main/test diff --git a/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library b/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library +++ b/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d/.library b/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library similarity index 88% rename from test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d/.library rename to test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library index 53c2d14c9..42efe5f9d 100644 --- a/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d/.library +++ b/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library @@ -1,7 +1,7 @@ - library.d + library.d-depender SAP SE Some fancy copyright ${version} diff --git a/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d/some.js b/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js similarity index 100% rename from test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d/some.js rename to test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js diff --git a/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library b/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library +++ b/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml b/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml index a47c1f64c..e05b61880 100644 --- a/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml +++ b/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml @@ -3,8 +3,3 @@ specVersion: "2.3" type: library metadata: name: library.d -resources: - configuration: - paths: - src: main/src - test: main/test diff --git a/test/fixtures/application.d/node_modules/library.e/src/library/e/.library b/test/fixtures/application.d/node_modules/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/application.d/node_modules/library.e/src/library/e/.library +++ b/test/fixtures/application.d/node_modules/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/application.f/node_modules/library.e/src/library/e/.library b/test/fixtures/application.f/node_modules/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/application.f/node_modules/library.e/src/library/e/.library +++ b/test/fixtures/application.f/node_modules/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.c/webapp/manifest.json b/test/fixtures/cyclic-deps/node_modules/application.cycle.c/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.c/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.d/webapp/manifest.json b/test/fixtures/cyclic-deps/node_modules/application.cycle.d/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.d/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.e/webapp/manifest.json b/test/fixtures/cyclic-deps/node_modules/application.cycle.e/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.e/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/fixtures/cyclic-deps/node_modules/application.cycle.f/webapp/manifest.json b/test/fixtures/cyclic-deps/node_modules/application.cycle.f/webapp/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/cyclic-deps/node_modules/application.cycle.f/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 2db9aff8b..e32ad5ade 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -1,40 +1,37 @@ +const path = require("path"); const test = require("ava"); const sinonGlobal = require("sinon"); const mock = require("mock-require"); const logger = require("@ui5/logger"); -const Configuration = require("../../../lib/specifications/Configuration"); -const Project = require("../../../lib/specifications/Project"); -const Extension = require("../../../lib/specifications/Extension"); +const Specification = require("../../../lib/specifications/Specification"); +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -function createProject(name) { - const basicConfiguration = new Configuration({ - specVersion: "2.3", - kind: "project", - type: "application", - metadata: {name} - }); - - return new Project({ +async function createProject(name) { + return await Specification.create({ id: "application.a.id", version: "1.0.0", - modulePath: "some path", - configuration: basicConfiguration + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name} + } }); } -function createExtension(name) { - const basicConfiguration = new Configuration({ - specVersion: "2.3", - kind: "extension", - type: "task", - metadata: {name} - }); - - return new Extension({ - id: "application.a.id", +async function createExtension(name) { + return await Specification.create({ + id: "extension.a.id", version: "1.0.0", - modulePath: "some path", - configuration: basicConfiguration + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "extension", + type: "task", + task: {}, + metadata: {name} + } }); } @@ -107,7 +104,7 @@ test("getRoot", async (t) => { const graph = new ProjectGraph({ rootProjectName: "application.a" }); - const project = createProject("application.a"); + const project = await createProject("application.a"); graph.addProject(project); const res = graph.getRoot(); t.is(res, project, "Should return correct root project"); @@ -132,7 +129,7 @@ test("add-/getProject", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const project = createProject("application.a"); + const project = await createProject("application.a"); graph.addProject(project); const res = graph.getProject("application.a"); t.is(res, project, "Should return correct project"); @@ -143,10 +140,10 @@ test("addProject: Add duplicate", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const project1 = createProject("application.a"); + const project1 = await createProject("application.a"); graph.addProject(project1); - const project2 = createProject("application.a"); + const project2 = await createProject("application.a"); const error = t.throws(() => { graph.addProject(project2); }); @@ -163,10 +160,10 @@ test("addProject: Add duplicate with ignoreDuplicates", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const project1 = createProject("application.a"); + const project1 = await createProject("application.a"); graph.addProject(project1); - const project2 = createProject("application.a"); + const project2 = await createProject("application.a"); t.notThrows(() => { graph.addProject(project2, true); }, "Should not throw when adding duplicates"); @@ -180,7 +177,7 @@ test("addProject: Add project with integer-like name", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const project = createProject("1337"); + const project = await createProject("1337"); const error = t.throws(() => { graph.addProject(project); @@ -204,7 +201,7 @@ test("add-/getExtension", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const extension = createExtension("extension.a"); + const extension = await createExtension("extension.a"); graph.addExtension(extension); const res = graph.getExtension("extension.a"); t.is(res, extension, "Should return correct extension"); @@ -215,10 +212,10 @@ test("addExtension: Add duplicate", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const extension1 = createExtension("extension.a"); + const extension1 = await createExtension("extension.a"); graph.addExtension(extension1); - const extension2 = createExtension("extension.a"); + const extension2 = await createExtension("extension.a"); const error = t.throws(() => { graph.addExtension(extension2); }); @@ -235,7 +232,7 @@ test("addExtension: Add extension with integer-like name", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const extension = createExtension("1337"); + const extension = await createExtension("1337"); const error = t.throws(() => { graph.addExtension(extension); @@ -259,10 +256,10 @@ test("getAllExtensions", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - const extension1 = createExtension("extension.a"); + const extension1 = await createExtension("extension.a"); graph.addExtension(extension1); - const extension2 = createExtension("extension.b"); + const extension2 = await createExtension("extension.b"); graph.addExtension(extension2); const res = graph.getAllExtensions(); t.deepEqual(res, { @@ -276,8 +273,8 @@ test("declareDependency / getDependencies", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); t.deepEqual(graph.getDependencies("library.a"), [ @@ -307,7 +304,7 @@ test("declareDependency: Unknown source", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.b")); const error = t.throws(() => { graph.declareDependency("library.a", "library.b"); @@ -323,7 +320,7 @@ test("declareDependency: Unknown target", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); + graph.addProject(await createProject("library.a")); const error = t.throws(() => { graph.declareDependency("library.a", "library.b"); @@ -339,8 +336,8 @@ test("declareDependency: Same target as source", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); const error = t.throws(() => { graph.declareDependency("library.a", "library.a"); @@ -356,8 +353,8 @@ test("declareDependency: Already declared", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.b"); @@ -373,8 +370,8 @@ test("declareDependency: Already declared as optional", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareOptionalDependency("library.a", "library.b"); graph.declareOptionalDependency("library.a", "library.b"); @@ -393,8 +390,8 @@ test("declareDependency: Already declared as non-optional", async (t) => { const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); @@ -411,8 +408,8 @@ test("declareDependency: Already declared as optional, now non-optional", async const graph = new ProjectGraph({ rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareOptionalDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.b"); @@ -430,7 +427,7 @@ test("getDependencies: Project without dependencies", async (t) => { rootProjectName: "my root project" }); - graph.addProject(createProject("library.a")); + graph.addProject(await createProject("library.a")); t.deepEqual(graph.getDependencies("library.a"), [], "Should return an empty array for project without dependencies"); @@ -455,10 +452,10 @@ test("resolveOptionalDependencies", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); - graph.addProject(createProject("library.d")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); graph.declareOptionalDependency("library.a", "library.b"); graph.declareOptionalDependency("library.a", "library.c"); @@ -487,10 +484,10 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); - graph.addProject(createProject("library.d")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); graph.declareOptionalDependency("library.a", "library.b"); graph.declareOptionalDependency("library.a", "library.c"); @@ -516,8 +513,8 @@ test("traverseBreadthFirst", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); @@ -532,9 +529,9 @@ test("traverseBreadthFirst: No project visited twice", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -552,8 +549,8 @@ test("traverseBreadthFirst: Detect cycle", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.b", "library.a"); @@ -569,9 +566,9 @@ test("traverseBreadthFirst: No cycle when visited breadth first", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -602,9 +599,9 @@ test("traverseBreadthFirst: Custom start node", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.b", "library.c"); @@ -627,9 +624,9 @@ test("traverseBreadthFirst: getDependencies callback", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -665,10 +662,10 @@ test("traverseBreadthFirst: Dependency declaration order is followed", async (t) const graph1 = new ProjectGraph({ rootProjectName: "library.a" }); - graph1.addProject(createProject("library.a")); - graph1.addProject(createProject("library.b")); - graph1.addProject(createProject("library.c")); - graph1.addProject(createProject("library.d")); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); graph1.declareDependency("library.a", "library.b"); graph1.declareDependency("library.a", "library.c"); @@ -684,10 +681,10 @@ test("traverseBreadthFirst: Dependency declaration order is followed", async (t) const graph2 = new ProjectGraph({ rootProjectName: "library.a" }); - graph2.addProject(createProject("library.a")); - graph2.addProject(createProject("library.b")); - graph2.addProject(createProject("library.c")); - graph2.addProject(createProject("library.d")); + graph2.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.b")); + graph2.addProject(await createProject("library.c")); + graph2.addProject(await createProject("library.d")); graph2.declareDependency("library.a", "library.d"); graph2.declareDependency("library.a", "library.c"); @@ -706,8 +703,8 @@ test("traverseDepthFirst", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); @@ -722,9 +719,9 @@ test("traverseDepthFirst: No project visited twice", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -742,8 +739,8 @@ test("traverseDepthFirst: Detect cycle", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.b", "library.a"); @@ -759,9 +756,9 @@ test("traverseDepthFirst: Cycle which does not occur in BFS", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -791,9 +788,9 @@ test("traverseDepthFirst: Custom start node", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.b", "library.c"); @@ -816,9 +813,9 @@ test("traverseDepthFirst: getDependencies callback", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); @@ -854,10 +851,10 @@ test("traverseDepthFirst: Dependency declaration order is followed", async (t) = const graph1 = new ProjectGraph({ rootProjectName: "library.a" }); - graph1.addProject(createProject("library.a")); - graph1.addProject(createProject("library.b")); - graph1.addProject(createProject("library.c")); - graph1.addProject(createProject("library.d")); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); graph1.declareDependency("library.a", "library.b"); graph1.declareDependency("library.a", "library.c"); @@ -873,10 +870,10 @@ test("traverseDepthFirst: Dependency declaration order is followed", async (t) = const graph2 = new ProjectGraph({ rootProjectName: "library.a" }); - graph2.addProject(createProject("library.a")); - graph2.addProject(createProject("library.b")); - graph2.addProject(createProject("library.c")); - graph2.addProject(createProject("library.d")); + graph2.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.b")); + graph2.addProject(await createProject("library.c")); + graph2.addProject(await createProject("library.d")); graph2.declareDependency("library.a", "library.d"); graph2.declareDependency("library.a", "library.c"); @@ -898,28 +895,28 @@ test("join", async (t) => { const graph2 = new ProjectGraph({ rootProjectName: "theme.a" }); - graph1.addProject(createProject("library.a")); - graph1.addProject(createProject("library.b")); - graph1.addProject(createProject("library.c")); - graph1.addProject(createProject("library.d")); + graph1.addProject(await createProject("library.a")); + graph1.addProject(await createProject("library.b")); + graph1.addProject(await createProject("library.c")); + graph1.addProject(await createProject("library.d")); graph1.declareDependency("library.a", "library.b"); graph1.declareDependency("library.a", "library.c"); graph1.declareDependency("library.a", "library.d"); - const extensionA = createExtension("extension.a"); + const extensionA = await createExtension("extension.a"); graph1.addExtension(extensionA); - graph2.addProject(createProject("theme.a")); - graph2.addProject(createProject("theme.b")); - graph2.addProject(createProject("theme.c")); - graph2.addProject(createProject("theme.d")); + graph2.addProject(await createProject("theme.a")); + graph2.addProject(await createProject("theme.b")); + graph2.addProject(await createProject("theme.c")); + graph2.addProject(await createProject("theme.d")); graph2.declareDependency("theme.a", "theme.d"); graph2.declareDependency("theme.a", "theme.c"); graph2.declareDependency("theme.b", "theme.a"); // This causes theme.b to not appear - const extensionB = createExtension("extension.b"); + const extensionB = await createExtension("extension.b"); graph2.addExtension(extensionB); graph1.join(graph2); @@ -979,8 +976,8 @@ test("join: Unexpected project intersection", async (t) => { const graph2 = new ProjectGraph({ rootProjectName: "😼" }); - graph1.addProject(createProject("library.a")); - graph2.addProject(createProject("library.a")); + graph1.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.a")); const error = t.throws(() => { @@ -1000,8 +997,8 @@ test("join: Unexpected extension intersection", async (t) => { const graph2 = new ProjectGraph({ rootProjectName: "😼" }); - graph1.addExtension(createExtension("extension.a")); - graph2.addExtension(createExtension("extension.a")); + graph1.addExtension(await createExtension("extension.a")); + graph2.addExtension(await createExtension("extension.a")); const error = t.throws(() => { @@ -1019,16 +1016,16 @@ test("Seal/isSealed", async (t) => { const graph = new ProjectGraph({ rootProjectName: "library.a" }); - graph.addProject(createProject("library.a")); - graph.addProject(createProject("library.b")); - graph.addProject(createProject("library.c")); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); graph.declareDependency("library.a", "library.b"); graph.declareDependency("library.a", "library.c"); graph.declareDependency("library.b", "library.c"); graph.declareOptionalDependency("library.c", "library.a"); - graph.addExtension(createExtension("extension.a")); + graph.addExtension(await createExtension("extension.a")); t.is(graph.isSealed(), false, "Graph should not be sealed"); // Seal it @@ -1037,8 +1034,9 @@ test("Seal/isSealed", async (t) => { const expectedSealMsg = "Project graph with root node library.a has been sealed"; + const libX = await createProject("library.x"); t.throws(() => { - graph.addProject(createProject("library.x")); + graph.addProject(libX); }, { message: expectedSealMsg }); @@ -1052,8 +1050,9 @@ test("Seal/isSealed", async (t) => { }, { message: expectedSealMsg }); + const extB = await createExtension("extension.b"); t.throws(() => { - graph.addExtension(createExtension("extension.b")); + graph.addExtension(extB); }, { message: expectedSealMsg }); diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/graph/projectGraphFromTree.js index 07d19fc60..8edc5c9f8 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/graph/projectGraphFromTree.js @@ -14,9 +14,9 @@ const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); -const legacyLibraryAPath = path.join(__dirname, "..", "fixtures", "legacy.library.a"); -const legacyLibraryBPath = path.join(__dirname, "..", "fixtures", "legacy.library.b"); -const legacyCollectionAPath = path.join(__dirname, "..", "fixtures", "legacy.collection.a"); +const legacyLibraryAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.a"); +const legacyLibraryBPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.b"); +const legacyCollectionAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.collection.a"); test.beforeEach((t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); @@ -243,7 +243,7 @@ test("Project with inline configuration for two projects", async (t) => { } }, { specVersion: "2.3", - type: "library", + type: "application", metadata: { name: "yz" } @@ -254,8 +254,8 @@ test("Project with inline configuration for two projects", async (t) => { await t.throwsAsync(projectGraphFromTree(tree), { message: - "Invalid configuration for module application.a.id: Per module there " + - "must be no more than one configuration of kind 'project'" + `Found 2 configurations of kind 'project' for module application.a.id. ` + + `There must be only one project per module.` }, "Rejected with error"); }); @@ -344,10 +344,7 @@ test("No type configured for root project", async (t) => { }; const error = await t.throwsAsync(projectGraphFromTree(tree)); - t.is(error.message, `Invalid ui5.yaml configuration for project application.a.id - -Configuration must have required property 'type'`, - "Rejected with expected error"); + t.is(error.message, `Unable to create Specification instance: Unknown specification type 'undefined'`); }); test("Missing dependencies", async (t) => { @@ -819,13 +816,13 @@ const applicationATreeWithInlineConfigs = { metadata: { name: "library.d", }, - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } } } }, @@ -1444,7 +1441,8 @@ test("Project with project-shim extension with collection", async (t) => { t.is(log.info.callCount, 0, "log.info should not have been called"); }); -test("Project with project-shim extension with self-containing collection shim", async (t) => { +// TODO: Fixme +test.skip("Project with project-shim extension with self-containing collection shim", async (t) => { const tree = { id: "application.a.id", path: applicationAPath, @@ -1560,11 +1558,10 @@ test("Project with unknown extension dependency inline configuration", async (t) }], }; const {projectGraphFromTree} = t.context; - const validationError = await t.throwsAsync(projectGraphFromTree(tree), { - instanceOf: ValidationError - }); - t.true(validationError.message.includes("Configuration type must be equal to one of the allowed value"), - "ValidationError should contain error about missing metadata configuration"); + const validationError = await t.throwsAsync(projectGraphFromTree(tree)); + t.is(validationError.message, + `Unable to create Specification instance: Unknown specification type 'phony-pony'`, + "Should throw with expected error message"); }); test("Project with task extension dependency", async (t) => { diff --git a/test/lib/graph/providers/ui5Framework.integration.js b/test/lib/graph/providers/ui5Framework.integration.js index 453d4fef9..66b7857c1 100644 --- a/test/lib/graph/providers/ui5Framework.integration.js +++ b/test/lib/graph/providers/ui5Framework.integration.js @@ -104,7 +104,7 @@ function defineTest(testName, { } }; - test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + test.serial.skip(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { // Enable verbose logging if (verbose) { logger.setLevel("verbose"); diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 4eee1d6fa..3d4ee3ae0 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -1,6 +1,6 @@ const test = require("ava"); const path = require("path"); -const Specification = require("../../../lib/Specification"); +const Specification = require("../../../lib/specifications/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const basicProjectInput = { From 0ddf69589d71a24f8b7199432c26d778640c7740 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 22:49:48 +0100 Subject: [PATCH 29/99] JSDoc cleanup --- lib/graph/Module.js | 15 ++++++++ lib/graph/ProjectGraph.js | 9 +++++ lib/graph/projectGraphBuilder.js | 34 +++++++++++++++--- lib/specifications/Configuration.js | 54 ----------------------------- lib/specifications/Project.js | 9 +++++ lib/specifications/Specification.js | 3 ++ 6 files changed, 66 insertions(+), 58 deletions(-) delete mode 100644 lib/specifications/Configuration.js diff --git a/lib/graph/Module.js b/lib/graph/Module.js index b8add5335..1d4a67fa6 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -80,6 +80,21 @@ class Module { return this._modulePath; } + /** + * Specifications found in the module + * + * @public + * @typedef {object} SpecificationsResult + * @property {@ui5/project.specifications.Project|undefined} Project found in the module (if one is found) + * @property {@ui5/project.specifications.Extension[]} Array of extensions found in the module + * + */ + + /** + * Get any available project and extensions of the module + * + * @returns {SpecificationsResult} Project and extensions found in the module + */ async getSpecifications() { if (this._pGetSpecifications) { return this._pGetSpecifications; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index f389b0b0d..d34b9d087 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -27,6 +27,10 @@ class ProjectGraph { this._shouldResolveOptionalDependencies = false; // Performance optimization flag } + /** + * @public + * @returns {module:@ui5/project.specification.Project} Root project + */ getRoot() { const rootProject = this._projects[this._rootProjectName]; if (!rootProject) { @@ -224,9 +228,14 @@ class ProjectGraph { } /** + * Transforms any optional dependencies in the graph for which the target is referenced by + * at least one non-optional project + * into a non-optional dependency. + * * @public */ resolveOptionalDependencies() { + // TODO: If a project is referenced as non-optional by an *optional* dependency, it should still be optional if (!this._shouldResolveOptionalDependencies) { log.verbose(`Skipping resolution of optional dependencies since none have been declared`); return; diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index 0826cccca..ad9f4825c 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -24,17 +24,43 @@ function _handleExtensions(graph, shimCollection, extensions) { } /** - * Tree node + * Dependency graph node representing a module * * @public - * @typedef {object} TreeNode + * @typedef {object} Node * @property {string} node.id Unique ID for the project * @property {string} node.version Version of the project * @property {string} node.path File System path to access the projects resources * @property {object|object[]} [node.configuration] * Configuration object or array of objects to use instead of reading from a configuration file * @property {string} [node.configPath] Configuration file to use instead the default ui5.yaml - * @property {TreeNode[]} dependencies + * @property {boolean} [node.optional] + * Whether the node is an optional dependency of the parent it has been requested for + * @property {*} * Additional attributes are allowed but ignored. + * These can be used to pass information internally in the provider. + */ + +/** + * Node Provider interface + * + * @interface NodeProvider + */ + +/** + * Retrieve information on the root module + * + * @function + * @name NodeProvider#getRootNode + * @returns {Node} The root node of the dependency graph + */ + +/** + * Retrieve information on given a nodes dependencies + * + * @function + * @name NodeProvider#getDependencies + * @param {Node} The root node of the dependency graph + * @returns {Node[]} Array of nodes which are direct dependencies of the given node */ /** @@ -43,7 +69,7 @@ function _handleExtensions(graph, shimCollection, extensions) { * * @public * @alias module:@ui5/project.graph.projectGraphBuilder - * @param {ModuleProvider} nodeProvider Dependency tree as returned by a translator + * @param {NodeProvider} nodeProvider * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance */ module.exports = async function(nodeProvider) { diff --git a/lib/specifications/Configuration.js b/lib/specifications/Configuration.js deleted file mode 100644 index c5f80f19e..000000000 --- a/lib/specifications/Configuration.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -* Private configuration class for use in Module and specifications -*/ - -module.exports = { - /** - * @param {object} parameters - * @param {object} parameters.specification Specification instance - * @param {object} parameters.configObject Configuration object - */ - async create({specification, configObject}) { - if (specification) { - throw new Error(`Unable to create Configuration: No specification provided`); - } - - if (!configObject) { - throw new Error(`Unable to create Configuration: No configuration provided`); - } - if (!configObject.kind.includes(["project", "extension"])) { - throw new Error(`Unable to create Configuration: Unknown kind '${configObject.kind}'`); - } - - switch (configObject.type) { - case "application": { - return createConfig("Application", {configObject, specification}); - } - case "library": { - return createConfig("Library", {configObject, specification}); - } - case "theme-library": { - return createConfig("ThemeLibrary", {configObject, specification}); - } - case "module": { - return createConfig("Module", {configObject, specification}); - } - case "task": { - return createConfig("Task", {configObject, specification}); - } - case "middleware": { - return createConfig("Middleware", {configObject, specification}); - } - case "project-shim": { - return createConfig("ProjectShim", {configObject, specification}); - } - default: - throw new Error(`Unable to create Configuration: Unknown specification type '${configObject.type}'`); - } - } -}; - -function createConfig(moduleName, params) { - const Configuration = require(`./configurations/${moduleName}`); - return new Configuration(params).init(); -} diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 18118e7fc..40e213499 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -51,21 +51,30 @@ class Project extends Specification { /* === Resource Access === */ /** + * Get a resource reader for the sources of the project (excluding any test resources) + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getSourceReader() { throw new Error(`getSourceReader must be implemented by subclass ${this.constructor.name}`); } /** + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getRuntimeReader() { throw new Error(`getRuntimeReader must be implemented by subclass ${this.constructor.name}`); } /** + * Get a resource reader for accessing the project resources during the build process + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getBuildtimeReader() { throw new Error(`getBuildtimeReader must be implemented by subclass ${this.constructor.name}`); diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index a2311804f..7fd1af180 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -130,7 +130,10 @@ class Specification { /* === Resource Access === */ /** + * Get a resource reader for the root directory of the project + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getRootReader() { return resourceFactory.createReader({ From e827c75f8ac71712dff731722db6e409897a67c3 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 23:05:23 +0100 Subject: [PATCH 30/99] Specifications: Align reader getters --- lib/specifications/types/Application.js | 21 ++++++++--- lib/specifications/types/Library.js | 44 +++++++++++------------- lib/specifications/types/Module.js | 27 ++++++++++----- lib/specifications/types/ThemeLibrary.js | 20 +++++++++-- 4 files changed, 73 insertions(+), 39 deletions(-) diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 5669eeb59..881aa7167 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -27,32 +27,45 @@ class Application extends ComponentProject { /* === Resource Access === */ /** + * Get a resource reader for the sources of the project (excluding any test resources) + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), - virBasePath: `/resources/${this.getNamespace()}/`, + virBasePath: "/", name: `Source reader for application project ${this.getName()}` }); } /** + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getRuntimeReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath: "/", // Applications are served at "/" - name: `Source reader for application project ${this.getName()}` + name: `Runtime reader for application project ${this.getName()}` }); } /** + * Get a resource reader for accessing the project resources during the build process + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getBuildtimeReader() { - return this.getSourceReader(); + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._webappPath), + virBasePath: `/resources/${this.getNamespace()}/`, + name: `Buildtime reader for application project ${this.getName()}` + }); } /* === Internals === */ @@ -184,7 +197,7 @@ class Application extends ComponentProject { if (this._pManifests[filePath]) { return this._pManifests[filePath]; } - return this._pManifests[filePath] = this.getRuntimeReader().byPath(filePath) + return this._pManifests[filePath] = this.getSourceReader().byPath(filePath) .then(async (resource) => { if (!resource) { throw new Error( diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 52b223c3f..05b627825 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -40,26 +40,36 @@ class Library extends ComponentProject { /* === Resource Access === */ /** + * Get a resource reader for the sources of the project (excluding any test resources) + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/resources/", + virBasePath: "/", name: `Source reader for library project ${this.getName()}` }); } /** + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getRuntimeReader() { - let reader = this.getSourceReader(); + let reader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/resources/", + name: `Runtime resources reader for library project ${this.getName()}` + }); if (this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", - name: `Test reader for library project ${this.getName()}` + name: `Runtime test-resources reader for library project ${this.getName()}` }); reader = resourceFactory.createReaderCollection({ name: `Reader collection for library project ${this.getName()}`, @@ -70,7 +80,10 @@ class Library extends ComponentProject { } /** + * Get a resource reader for accessing the project resources during the build process + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getBuildtimeReader() { // Same as runtime @@ -265,17 +278,16 @@ class Library extends ComponentProject { } namespace = libraryNs.replace(/\./g, "/"); - - const namespaceFromPath = this._getNamespaceFromDirPath(namespacePath); - if (namespaceFromPath !== namespace) { + namespacePath = namespacePath.replace("/", ""); // remove leading slash + if (namespacePath !== namespace) { throw new Error( `Detected namespace "${namespace}" does not match detected directory ` + - `structure "${namespaceFromPath}" for project ${this.getName()}`); + `structure "${namespacePath}" for project ${this.getName()}`); } } else { try { const libraryJsPath = await this._getLibraryJsPath(); - namespace = this._getNamespaceFromDirPath(posixPath.dirname(libraryJsPath)); + namespace = posixPath.dirname(libraryJsPath); if (!namespace || namespace === "/") { throw new Error(`Found library.js file in root directory. ` + `Expected it to be in namespace directory.`); @@ -351,22 +363,6 @@ class Library extends ComponentProject { return {}; } - _getNamespaceFromDirPath(dirPath) { - const virtualBasePath = "/resources/"; - - if (!dirPath.startsWith(virtualBasePath)) { - if (virtualBasePath === dirPath + "/") { - // The given file path does not contain a namespace path - // It is equal to the source base path - // Therefore return an empty namespace - return ""; - } - throw new Error(`Given directory path ${dirPath} is not based on the ` + - `expected base path ${virtualBasePath}.`); - } - return dirPath.replace(virtualBasePath, ""); - } - /** * Determines library copyright from given project configuration with fallback to .library. * diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 7d0238946..c1553b3c1 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -16,9 +16,23 @@ class Module extends Project { /* === Resource Access === */ /** + * Get a resource reader for the sources of the project (excluding any test resources) + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getSourceReader() { + // TODO + throw new Error("Not sure what is expected here"); + } + + /** + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + getRuntimeReader() { const readers = this._paths.map((readerArgs) => resourceFactory.createReader(readerArgs)); if (readers.length === 1) { return readers[0]; @@ -30,17 +44,14 @@ class Module extends Project { } /** + * Get a resource reader for accessing the project resources during the build process + * * @public - */ - getRuntimeReader() { - return this.getSourceReader(); - } - - /** - * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getBuildtimeReader() { - return this.getSourceReader(); + // Same as runtime + return this.getRuntimeReader(); } /* === Internals === */ diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index 511257528..40bc34296 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -17,26 +17,37 @@ class ThemeLibrary extends Project { /* === Resource Access === */ /** + * Get a resource reader for the sources of the project (excluding any test resources) + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/resources/", + virBasePath: "/", name: `Source reader for theme-library project ${this.getName()}` }); } + /** + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getRuntimeReader() { - let reader = this.getSourceReader(); + let reader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/resources/", + name: `Runtime resources reader for theme-library project ${this.getName()}` + }); if (this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", - name: `Test reader for theme-library project ${this.getName()}` + name: `Runtime test-resources reader for theme-library project ${this.getName()}` }); reader = resourceFactory.createReaderCollection({ name: `Reader collection for theme-library project ${this.getName()}`, @@ -47,7 +58,10 @@ class ThemeLibrary extends Project { } /** + * Get a resource reader for accessing the project resources during the build process + * * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getBuildtimeReader() { // Same as runtime From aa832dd8cc1eb7905fdefefc10ecfaaa07fe5e5f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 23:06:51 +0100 Subject: [PATCH 31/99] ui5Framework provider: Fix framework dep collection --- lib/graph/providers/ui5Framework.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graph/providers/ui5Framework.js b/lib/graph/providers/ui5Framework.js index a71cdd00e..da339c878 100644 --- a/lib/graph/providers/ui5Framework.js +++ b/lib/graph/providers/ui5Framework.js @@ -76,7 +76,7 @@ const utils = { return; } // No need to check for specVersion since Specification API is >= 2.0 anyways - const frameworkDependencies = rootProject.getFrameworkDependencies(); + const frameworkDependencies = project.getFrameworkDependencies(); if (!frameworkDependencies.length) { log.verbose(`Project ${project.getName()} has no framework dependencies`); // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json @@ -206,7 +206,7 @@ module.exports = { }); const frameworkGraph = new ProjectGraph({ - rootProjectName: "sonic-rainboom" + rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` }); await Promise.all(referencedLibraries.map(async (libName) => { await projectProcessor.addProjectToGraph(libName, frameworkGraph); From 55242dcf2d954bbbbf67da580655eb1d4db9c97d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 23:09:47 +0100 Subject: [PATCH 32/99] projectGraphBuilder: Fix check for already qualified application projects If the already qualified project is visited again, it should not be skipped but handled like any other duplicate. This change is mainly to prevent the irritating log message indicating that the same project already qualified as the application project but is ignored. --- lib/graph/ProjectGraph.js | 1 + lib/graph/projectGraphBuilder.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index d34b9d087..72f8a3edf 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -60,6 +60,7 @@ class ProjectGraph { throw new Error( `Failed to add project ${projectName} to graph: Project name must not be integer-like`); } + log.verbose(`Adding project: ${projectName}`); this._projects[projectName] = project; this._adjList[projectName] = []; this._optAdjList[projectName] = []; diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index ad9f4825c..8e3a1930a 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -199,12 +199,12 @@ module.exports = async function(nodeProvider) { if (!qualifiedApplicationProject) { log.verbose(`Project ${projectName} qualified as application project for project graph`); qualifiedApplicationProject = project; - } else if (!(qualifiedApplicationProject.getName() === projectName && node.deduped)) { + } else if (qualifiedApplicationProject.getName() !== projectName) { // Project is not a duplicate of an already qualified project (which should // still be processed below), but a unique, additional application project // TODO: Should this rather be a verbose logging? - // projectPreprocessor handled this like any project that got ignored for some reason and did a + // projectPreprocessor handled this like any project that got ignored and did a // (in this case misleading) general verbose logging: // "Ignoring project with missing configuration" log.info( From 0bf6e0f1ac5422da21989832f51355d4cbb4a3c4 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 23:19:36 +0100 Subject: [PATCH 33/99] Remove obsolete lib/Specifications.js --- lib/Specification.js | 52 -------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 lib/Specification.js diff --git a/lib/Specification.js b/lib/Specification.js deleted file mode 100644 index fc4ffe90e..000000000 --- a/lib/Specification.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @param {object} parameters Specification parameters - * @param {string} parameters.id Unique ID - * @param {string} parameters.version Version - * @param {string} parameters.modulePath File System path to access resources - * @param {object} parameters.configuration Configuration object to use - */ -module.exports = { - async create(params) { - if (!params.configuration) { - throw new Error( - `Unable to create Specification instance: Missing configuration parameter`); - } - const {kind, type} = params.configuration; - if (!["project", "extension"].includes(kind)) { - throw new Error(`Unable to create Specification instance: Unknown kind '${kind}'`); - } - - switch (type) { - case "application": { - return createAndInitializeSpec("Application", params); - } - case "library": { - return createAndInitializeSpec("Library", params); - } - case "theme-library": { - return createAndInitializeSpec("ThemeLibrary", params); - } - case "module": { - return createAndInitializeSpec("Module", params); - } - case "task": { - return createAndInitializeSpec("Task", params); - } - case "middleware": { - return createAndInitializeSpec("Middleware", params); - } - case "project-shim": { - return createAndInitializeSpec("ProjectShim", params); - } - default: - throw new Error( - `Unable to create Specification instance: Unknown specification type '${type}'`); - } - } -}; - -function createAndInitializeSpec(moduleName, params) { - const Spec = require(`./specifications/types/${moduleName}`); - const bla = new Spec().init(params); - return bla; -} From 1e471bdd2dd8f6ea9f174ff256afe715ced617c1 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 23 Feb 2021 23:34:53 +0100 Subject: [PATCH 34/99] Collection handling: Ignore projects of collections To be compatible with the current behavior of the projectPreprocessor. Apparently the open.fe setup relies on that. --- lib/graph/Module.js | 23 ++++++++++++++++++++++- lib/graph/ShimCollection.js | 6 ++++-- lib/specifications/types/ProjectShim.js | 18 ++++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 1d4a67fa6..e4ca8c798 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -104,7 +104,28 @@ class Module { } async _getSpecifications() { - const configs = await this._getConfigurations(); + let configs = await this._getConfigurations(); + + // I'm using hacks here: + // Filter out project-shims to check whether the this module is a collection + const isCollection = configs.find((configuration) => { + if (configuration.kind === "extension" && configuration.type === "project-shim") { + // TODO create Specification instance and ask it for the configuration + if (configuration.shims && configuration.shims.collections && + configuration.shims.collections[this.getId()]) { + return true; + } + } + }); + + if (isCollection) { + // This module is configured as a collection + // For compatibility reasons with the behavior of projectPreprocessor, + // the project contained in this module must be ignored + configs = configs.filter((configuration) => { + return configuration.kind !== "project"; + }); + } const specs = await Promise.all(configs.map(async (configuration) => { const spec = await Specification.create({ diff --git a/lib/graph/ShimCollection.js b/lib/graph/ShimCollection.js index 817cfd618..fa78c5be6 100644 --- a/lib/graph/ShimCollection.js +++ b/lib/graph/ShimCollection.js @@ -22,14 +22,16 @@ class ShimCollection { addProjectShim(shimExtension) { const name = shimExtension.getName(); log.verbose(`Adding new shim ${name}...`); - // TODO: Move this into a dedicated ShimConfiguration class? - const {configurations, dependencies, collections} = shimExtension.getShimConfiguration(); + + const configurations = shimExtension.getConfigurationShims(); if (configurations) { addToMap(name, configurations, this._projectConfigShims); } + const dependencies = shimExtension.getDependencyShims(); if (dependencies) { addToMap(name, dependencies, this._dependencyShims); } + const collections = shimExtension.getCollectionShims(); if (collections) { addToMap(name, collections, this._collectionShims); } diff --git a/lib/specifications/types/ProjectShim.js b/lib/specifications/types/ProjectShim.js index b2b17aa2b..e61b12104 100644 --- a/lib/specifications/types/ProjectShim.js +++ b/lib/specifications/types/ProjectShim.js @@ -10,8 +10,22 @@ class ProjectShim extends Extension { /** * @public */ - getShimConfiguration() { - return this._config.shims; + getDependencyShims() { + return this._config.shims.dependencies; + } + + /** + * @public + */ + getConfigurationShims() { + return this._config.shims.configurations; + } + + /** + * @public + */ + getCollectionShims() { + return this._config.shims.collections; } /* === Internals === */ From 9f1ca5a99e68c611e061f8c86907b511671a80c5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 26 Feb 2021 11:43:16 +0100 Subject: [PATCH 35/99] ProjectGraph: Only resolve optional dependencies if the target can be reached from the root project Checking for a non-optional dependency is not sufficient since that can originate from a project which is optional itself (i.e. has no non-optional dependency leading to it). --- lib/graph/ProjectGraph.js | 22 ++++----- test/lib/graph/ProjectGraph.js | 89 ++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 72f8a3edf..9dd777ff7 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -229,30 +229,30 @@ class ProjectGraph { } /** - * Transforms any optional dependencies in the graph for which the target is referenced by - * at least one non-optional project - * into a non-optional dependency. + * Transforms any optional dependencies declared in the graph to non-optional dependency, if the target + * can already be reached from the root project. * * @public */ - resolveOptionalDependencies() { - // TODO: If a project is referenced as non-optional by an *optional* dependency, it should still be optional + async resolveOptionalDependencies() { if (!this._shouldResolveOptionalDependencies) { log.verbose(`Skipping resolution of optional dependencies since none have been declared`); return; } log.verbose(`Resolving optional dependencies...`); + + // First collect all projects that are currently reachable from the root project (=all non-optional projects) const resolvedProjects = new Set; - for (const [, dependencies] of Object.entries(this._adjList)) { - for (let i = dependencies.length - 1; i >= 0; i--) { - resolvedProjects.add(dependencies[i]); - } - } + await this.traverseBreadthFirst(({project}) => { + resolvedProjects.add(project.getName()); + }); + for (const [projectName, dependencies] of Object.entries(this._optAdjList)) { for (let i = dependencies.length - 1; i >= 0; i--) { const targetProjectName = dependencies[i]; if (resolvedProjects.has(targetProjectName)) { - // Resolve optional dependency + // Target node is already reachable in the graph + // => Resolve optional dependency log.verbose(`Resolving optional dependency from ${projectName} to ${targetProjectName}...`); if (this._adjList[targetProjectName].includes(projectName)) { diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index e32ad5ade..a4bb8e7e1 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -463,7 +463,7 @@ test("resolveOptionalDependencies", async (t) => { graph.declareDependency("library.d", "library.b"); graph.declareDependency("library.d", "library.c"); - graph.resolveOptionalDependencies(); + await graph.resolveOptionalDependencies(); t.is(graph.isOptionalDependency("library.a", "library.b"), false, "library.a should have no optional dependency to library.b anymore"); @@ -478,7 +478,6 @@ test("resolveOptionalDependencies", async (t) => { ]); }); - test("resolveOptionalDependencies: Optional dependency has not been resolved", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ @@ -493,7 +492,7 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a graph.declareOptionalDependency("library.a", "library.c"); graph.declareDependency("library.a", "library.d"); - graph.resolveOptionalDependencies(); + await graph.resolveOptionalDependencies(); t.is(graph.isOptionalDependency("library.a", "library.b"), true, "Dependency from library.a to library.b should still be optional"); @@ -507,6 +506,90 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a ]); }); +test("resolveOptionalDependencies: Dependency of optional dependency has not been resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareOptionalDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.b"), true, + "Dependency from library.a to library.b should still be optional"); + + t.is(graph.isOptionalDependency("library.a", "library.c"), true, + "Dependency from library.a to library.c should still be optional"); + + await traverseDepthFirst(t, graph, [ + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Cyclic optional dependency is not resolved", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + + graph.declareDependency("library.a", "library.b"); + graph.declareDependency("library.a", "library.c"); + graph.declareDependency("library.c", "library.b"); + graph.declareOptionalDependency("library.b", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.b", "library.c"), true, + "Dependency from library.b to library.c should still be optional"); + + await traverseDepthFirst(t, graph, [ + "library.b", + "library.c", + "library.a" + ]); +}); + +test("resolveOptionalDependencies: Resolves transitive optional dependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + graph.addProject(await createProject("library.c")); + graph.addProject(await createProject("library.d")); + + graph.declareDependency("library.a", "library.b"); + graph.declareOptionalDependency("library.a", "library.c"); + graph.declareDependency("library.b", "library.c"); + graph.declareDependency("library.c", "library.d"); + graph.declareOptionalDependency("library.a", "library.d"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.c"), false, + "Dependency from library.a to library.c should not be optional anymore"); + + t.is(graph.isOptionalDependency("library.a", "library.d"), false, + "Dependency from library.a to library.d should not be optional anymore"); + + await traverseDepthFirst(t, graph, [ + "library.d", + "library.c", + "library.b", + "library.a" + ]); +}); test("traverseBreadthFirst", async (t) => { const {ProjectGraph} = t.context; From aa6b5aeeb2b5c134b7624e9801f4defbdb1fad9e Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 12 Mar 2021 13:06:28 +0100 Subject: [PATCH 36/99] Move legacy OpenUI5 theme library handling to graph.Module --- lib/graph/Module.js | 15 ++++++++++++--- lib/specifications/types/Library.js | 18 +----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index e4ca8c798..e559e4137 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -9,7 +9,8 @@ const {validate} = require("../validation/validator"); const log = require("@ui5/logger").getLogger("graph:Module"); -const defaultConfigPath = "ui5.yaml"; +const DEFAULT_CONFIG_PATH = "ui5.yaml"; +const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; function clone(obj) { return JSON.parse(JSON.stringify(obj)); @@ -36,7 +37,7 @@ class Module { * @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection] * Collection of shims that might be relevant for this module */ - constructor({id, version, modulePath, configPath = defaultConfigPath, configuration = [], shimCollection}) { + constructor({id, version, modulePath, configPath = DEFAULT_CONFIG_PATH, configuration = [], shimCollection}) { if (!id) { throw new Error(`Could not create Module: Missing or empty parameter 'id'`); } @@ -127,6 +128,14 @@ class Module { }); } + configs.forEach((configuration) => { + if (configuration.kind === "project" && configuration.type === "library" && + configuration.metadata && configuration.metadata.name && + SAP_THEMES_NS_EXEMPTIONS.includes(configuration.metadata.name)) { + configuration.type = "theme-library"; + } + }); + const specs = await Promise.all(configs.map(async (configuration) => { const spec = await Specification.create({ id: this.getId(), @@ -258,7 +267,7 @@ class Module { `${this.getId()} at "${configPath}". Error: ${err.message}`); } if (!configResource) { - if (configPath !== defaultConfigPath) { + if (configPath !== DEFAULT_CONFIG_PATH) { throw new Error("Failed to read configuration for module " + `${this.getId()}: Could not find configuration file in module at path '${configPath}'`); } diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 05b627825..c315c3a64 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -4,9 +4,6 @@ const {promisify} = require("util"); const resourceFactory = require("@ui5/fs").resourceFactory; const ComponentProject = require("../ComponentProject"); -const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; - - class Library extends ComponentProject { constructor(parameters) { super(parameters); @@ -131,20 +128,7 @@ class Library extends ComponentProject { async _parseConfiguration(config) { await super._parseConfiguration(config); - try { - this._namespace = await this.getNamespace(); - } catch (err) { - if (SAP_THEMES_NS_EXEMPTIONS.includes(this.getName())) { - // Exceptional handling for SAP theme libraries which used to be of type "library" - // (today they use "theme-library"). - // To allow use of OpenUI5 theme libraries in versions lower than 1.75 we must ignore - // namespace detection errors. - this._log.verbose(`Ignoring failed namespace detection for exempted SAP theme library ` + - `${this.getName()}: ${err.message}`); - } else { - throw err; - } - } + this._namespace = await this.getNamespace(); if (!config.metadata.copyright) { try { From 134315045794f00f6f561dc26725fd450e5e6399 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 12 Mar 2021 13:28:48 +0100 Subject: [PATCH 37/99] projectGraphBuilder: Wait for async resolution of optional dependencies --- lib/graph/projectGraphBuilder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index 8e3a1930a..5fc4f8d30 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -217,7 +217,7 @@ module.exports = async function(nodeProvider) { if (projectGraph.getProject(projectName)) { log.verbose( `Project ${projectName} has already been added to the graph. ` + - `Skipping repeated dependency resolution..`); + `Skipping dependency resolution...`); skipDependencies = true; } else { projectGraph.addProject(project); @@ -295,7 +295,7 @@ module.exports = async function(nodeProvider) { } } } - projectGraph.resolveOptionalDependencies(); + await projectGraph.resolveOptionalDependencies(); return projectGraph; }; From 96317124bac696f696b77d6ffd26dbd2886e35ac Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 23 Apr 2022 11:27:43 +0200 Subject: [PATCH 38/99] Refactor graph module structure --- index.js | 8 +-- ...omDirectory.js => generateProjectGraph.js} | 61 ++++++++++++----- .../{providers => helpers}/ui5Framework.js | 2 +- lib/graph/projectGraphFromTree.js | 7 -- lib/graph/providers/DependencyTree.js | 13 ++-- .../{npm.js => NodePackageDependencies.js} | 6 +- .../{ => providers}/projectGraphBuilder.js | 12 ++-- lib/normalizer.js | 28 -------- ...js => generateProjectGraph.usingObject.js} | 67 ++++++++++--------- .../ui5Framework.integration.js | 4 +- ...=> NodePackageDependencies.integration.js} | 32 ++++----- 11 files changed, 117 insertions(+), 123 deletions(-) rename lib/{graph/projectGraphFromDirectory.js => generateProjectGraph.js} (50%) rename lib/graph/{providers => helpers}/ui5Framework.js (98%) delete mode 100644 lib/graph/projectGraphFromTree.js rename lib/graph/providers/{npm.js => NodePackageDependencies.js} (96%) rename lib/graph/{ => providers}/projectGraphBuilder.js (95%) rename test/lib/{graph/projectGraphFromTree.js => generateProjectGraph.usingObject.js} (93%) rename test/lib/graph/{providers => helpers}/ui5Framework.integration.js (98%) rename test/lib/graph/providers/{npm.integration.js => NodePackageDependencies.integration.js} (86%) diff --git a/index.js b/index.js index 33d786896..f7b882dfb 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,10 @@ module.exports = { * @type {import('./lib/projectPreprocessor')} */ projectPreprocessor: "./lib/projectPreprocessor", + /** + * @type {import('./lib/generateProjectGraph')} + */ + generateProjectGraph: "./lib/generateProjectGraph", /** * @public * @alias module:@ui5/project.ui5Framework @@ -66,10 +70,6 @@ module.exports = { * @type {typeof import('./lib/graph/ProjectGraph')} */ ProjectGraph: "./lib/graph/ProjectGraph", - /** - * @type {typeof import('./lib/graph/projectGraphFromTree')} - */ - projectGraphFromTree: "./lib/graph/projectGraphFromTree" }, }; diff --git a/lib/graph/projectGraphFromDirectory.js b/lib/generateProjectGraph.js similarity index 50% rename from lib/graph/projectGraphFromDirectory.js rename to lib/generateProjectGraph.js index 112c1d84d..58880ad5e 100644 --- a/lib/graph/projectGraphFromDirectory.js +++ b/lib/generateProjectGraph.js @@ -1,24 +1,25 @@ -const projectGraphBuilder = require("./projectGraphBuilder"); -const ui5Framework = require("./providers/ui5Framework"); -const log = require("@ui5/logger").getLogger("graph:projectGraphFromDirectory"); +const path = require("path"); +const projectGraphBuilder = require("./graph/providers/projectGraphBuilder"); +const ui5Framework = require("./graph/helpers/ui5Framework"); +const log = require("@ui5/logger").getLogger("generateProjectGraph"); /** * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} * from a directory * * @public - * @alias module:@ui5/project.graph.projectGraphFromTree + * @alias module:@ui5/project.generateProjectGraph * @param {TreeNode} tree Dependency tree as returned by a translator * @returns {module:@ui5/project.graph.ProjectGraph} A new project graph instance */ -const projectGraphFromDirectory = { +const generateProjectGraph = { /** * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} by resolving * dependencies from package.json files and configuring projects from ui5.yaml files * * @public * @param {object} [options] - * @param {string} [options.cwd=.] Directory to start searching for the root module + * @param {string} [options.cwd=process.cwd()] Directory to start searching for the root module * @param {object} [options.rootConfiguration] * Configuration object to use for the root module instead of reading from a configuration file * @param {string} [options.rootConfigPath] @@ -26,12 +27,12 @@ const projectGraphFromDirectory = { * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @returns {Promise} Promise resolving to a Project Graph instance */ - usingNpm: async function({cwd = ".", rootConfiguration, rootConfigPath, versionOverride}) { + usingNodePackageDependencies: async function({cwd, rootConfiguration, rootConfigPath, versionOverride}) { log.verbose(`Creating project graph using npm provider...`); - const NpmProvider = require("./providers/npm"); + const NpmProvider = require("./graph/providers/NodePackageDependencies"); const provider = new NpmProvider({ - cwd: cwd, + cwd: cwd ? path.resolve(cwd) : process.cwd(), rootConfiguration, rootConfigPath }); @@ -44,36 +45,62 @@ const projectGraphFromDirectory = { }, /** - * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} from on a + * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} from a * YAML file following the structure of the * [@ui5/project.graph.projectGraphFromTree]{@link module:@ui5/project.graph.projectGraphFromTree} API * * @public * @param {object} options * @param {object} options.filePath Path to the file dependency configuration file - * @param {string} [options.cwd=.] Directory to start searching for the root module + * @param {string} [options.cwd=process.cwd()] Directory to start searching for the root module * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @returns {Promise} Promise resolving to a Project Graph instance */ - usingStaticFile: async function({cwd = ".", filePath, versionOverride}) { + usingStaticFile: async function({cwd, filePath, versionOverride}) { log.verbose(`Creating project graph using static file...`); - const staticTranslator = require("../translators/static"); - const DependencyTreeProvider = require("./providers/DependencyTree"); + const staticTranslator = require("./translators/static"); - const tree = await staticTranslator.generateDependencyTree(null, { + const dependencyTree = await staticTranslator.generateDependencyTree(cwd ? path.resolve(cwd) : process.cwd(), { parameters: [filePath] // *sigh* }); + const DependencyTreeProvider = require("./graph/providers/DependencyTree"); const provider = new DependencyTreeProvider({ - tree + dependencyTree }); const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + return projectGraph; + }, + + /** + * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} from a + * YAML file following the structure of the + * [@ui5/project.graph.projectGraphFromTree]{@link module:@ui5/project.graph.projectGraphFromTree} API + * + * @public + * @param {object} options + * @param {module:@ui5/project.graph.providers.DependencyTree.TreeNode} options.dependencyTree + * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @returns {Promise} Promise resolving to a Project Graph instance + */ + usingObject: async function({dependencyTree, versionOverride}) { + log.verbose(`Creating project graph using object...`); + + const DependencyTreeProvider = require("./graph/providers/DependencyTree"); + const dependencyTreeProvider = new DependencyTreeProvider({ + dependencyTree + }); + + const projectGraph = await projectGraphBuilder(dependencyTreeProvider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + return projectGraph; } }; -module.exports = projectGraphFromDirectory; +module.exports = generateProjectGraph; diff --git a/lib/graph/providers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js similarity index 98% rename from lib/graph/providers/ui5Framework.js rename to lib/graph/helpers/ui5Framework.js index da339c878..86f29e5d5 100644 --- a/lib/graph/providers/ui5Framework.js +++ b/lib/graph/helpers/ui5Framework.js @@ -1,6 +1,6 @@ const Module = require("../Module"); const ProjectGraph = require("../ProjectGraph"); -const log = require("@ui5/logger").getLogger("graph:providers:ui5Framework"); +const log = require("@ui5/logger").getLogger("graph:helpers:ui5Framework"); class ProjectProcessor { constructor({libraryMetadata}) { diff --git a/lib/graph/projectGraphFromTree.js b/lib/graph/projectGraphFromTree.js deleted file mode 100644 index 08490c209..000000000 --- a/lib/graph/projectGraphFromTree.js +++ /dev/null @@ -1,7 +0,0 @@ -const projectGraphBuilder = require("./projectGraphBuilder"); -const DependencyTreeProvider = require("./providers/DependencyTree"); - -module.exports = async function(tree) { - const dependencyTreeProvider = new DependencyTreeProvider(tree); - return projectGraphBuilder(dependencyTreeProvider); -}; diff --git a/lib/graph/providers/DependencyTree.js b/lib/graph/providers/DependencyTree.js index 633c34016..abd7106a1 100644 --- a/lib/graph/providers/DependencyTree.js +++ b/lib/graph/providers/DependencyTree.js @@ -17,14 +17,15 @@ class DependencyTree { * from a dependency tree as returned by translators. * * @public - * @alias module:@ui5/project.graph.projectGraphFromTree - * @param {TreeNode} tree Dependency tree as returned by a translator + * @alias module:@ui5/project.graph.providers.DependencyTree + * @param {object} parameters + * @param {TreeNode} parameters.dependencyTree Dependency tree as returned by a translator */ - constructor(tree) { - if (!tree) { - throw new Error(`Failed to instantiate DependencyTree provider: Missing parameter 'tree'`); + constructor({dependencyTree}) { + if (!dependencyTree) { + throw new Error(`Failed to instantiate DependencyTree provider: Missing parameter 'dependencyTree'`); } - this._tree= tree; + this._tree = dependencyTree; } async getRootNode() { diff --git a/lib/graph/providers/npm.js b/lib/graph/providers/NodePackageDependencies.js similarity index 96% rename from lib/graph/providers/npm.js rename to lib/graph/providers/NodePackageDependencies.js index 0559de69b..8bbdc86df 100644 --- a/lib/graph/providers/npm.js +++ b/lib/graph/providers/NodePackageDependencies.js @@ -5,14 +5,14 @@ const {promisify} = require("util"); const fs = require("graceful-fs"); const realpath = promisify(fs.realpath); const resolveModulePath = promisify(require("resolve")); -const log = require("@ui5/logger").getLogger("graph:providers:npm"); +const log = require("@ui5/logger").getLogger("graph:providers:NodePackageDependencies"); // Packages to consider: // * https://github.com/npm/read-package-json-fast // * https://github.com/npm/name-from-folder ? -class Npm { +class NodePackageDependencies { /** * Generates a project graph from npm modules * @@ -145,4 +145,4 @@ class Npm { } } -module.exports = Npm; +module.exports = NodePackageDependencies; diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/providers/projectGraphBuilder.js similarity index 95% rename from lib/graph/projectGraphBuilder.js rename to lib/graph/providers/projectGraphBuilder.js index 5fc4f8d30..254ae87a8 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/providers/projectGraphBuilder.js @@ -1,8 +1,8 @@ const path = require("path"); -const Module = require("./Module"); -const ProjectGraph = require("./ProjectGraph"); -const ShimCollection = require("./ShimCollection"); -const log = require("@ui5/logger").getLogger("graph:projectGraphBuilder"); +const Module = require("../Module"); +const ProjectGraph = require("../ProjectGraph"); +const ShimCollection = require("../ShimCollection"); +const log = require("@ui5/logger").getLogger("graph:providers:projectGraphBuilder"); function _handleExtensions(graph, shimCollection, extensions) { extensions.forEach((extension) => { @@ -64,8 +64,8 @@ function _handleExtensions(graph, shimCollection, extensions) { */ /** - * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} - * from a dependency tree as returned by translators. + * Generic helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph}. + * For example from a dependency tree as returned by the legacy "translators". * * @public * @alias module:@ui5/project.graph.projectGraphBuilder diff --git a/lib/normalizer.js b/lib/normalizer.js index cd2e8cf4e..b341ef321 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -45,34 +45,6 @@ const Normalizer = { return tree; }, - /** - * Generates a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} - * - * @public - * @param {object} [options] - * @param {string} [options.cwd] Current working directory - * @param {string} [options.configPath] Path to configuration file - * @param {string} [options.translatorName] Translator to use - * @param {object} [options.translatorOptions] Options to pass to translator - * @param {object} [options.frameworkOptions] Options to pass to the framework installer - * @param {string} [options.frameworkOptions.versionOverride] - * Framework version to use instead of the one defined in the root project - * framework - * @returns {Promise} Promise resolving to a Project Graph instance - */ - generateProjectGraph: async function(options = {}) { - // const projectGraphFromTree = require("./graph/projectGraphFromTree"); - // const tree = await Normalizer.generateDependencyTree(options); - const projectGraphFromDirectory = require("./graph/projectGraphFromDirectory"); - - const projectGraph = await projectGraphFromDirectory.usingNpm({ - rootConfigPath: options.configPath, - versionOverride: options.versionOverride - }); - - return projectGraph; - }, - /** * Generates a project and dependency tree via translators * diff --git a/test/lib/graph/projectGraphFromTree.js b/test/lib/generateProjectGraph.usingObject.js similarity index 93% rename from test/lib/graph/projectGraphFromTree.js rename to test/lib/generateProjectGraph.usingObject.js index 8edc5c9f8..6a3a7484f 100644 --- a/test/lib/graph/projectGraphFromTree.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -3,20 +3,20 @@ const path = require("path"); const sinonGlobal = require("sinon"); const mock = require("mock-require"); const logger = require("@ui5/logger"); -const ValidationError = require("../../../lib/validation/ValidationError"); +const ValidationError = require("../../lib/validation/ValidationError"); -const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b"); -const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); -const libraryAPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a"); -const libraryBPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b"); -const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); -const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); -const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule"); +const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); +const applicationBPath = path.join(__dirname, "..", "fixtures", "application.b"); +const applicationCPath = path.join(__dirname, "..", "fixtures", "application.c"); +const libraryAPath = path.join(__dirname, "..", "fixtures", "collection", "library.a"); +const libraryBPath = path.join(__dirname, "..", "fixtures", "collection", "library.b"); +const libraryDPath = path.join(__dirname, "..", "fixtures", "library.d"); +const cycleDepsBasePath = path.join(__dirname, "..", "fixtures", "cyclic-deps", "node_modules"); +const pathToInvalidModule = path.join(__dirname, "..", "fixtures", "invalidModule"); -const legacyLibraryAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.a"); -const legacyLibraryBPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.b"); -const legacyCollectionAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.collection.a"); +const legacyLibraryAPath = path.join(__dirname, "..", "fixtures", "legacy.library.a"); +const legacyLibraryBPath = path.join(__dirname, "..", "fixtures", "legacy.library.b"); +const legacyCollectionAPath = path.join(__dirname, "..", "fixtures", "legacy.collection.a"); test.beforeEach((t) => { const sinon = t.context.sinon = sinonGlobal.createSandbox(); @@ -29,9 +29,10 @@ test.beforeEach((t) => { isLevelEnabled: () => true }; sinon.stub(logger, "getLogger").callThrough() - .withArgs("graph:projectGraphBuilder").returns(t.context.log); - mock.reRequire("../../../lib/graph/projectGraphBuilder"); - t.context.projectGraphFromTree = mock.reRequire("../../../lib/graph/projectGraphFromTree"); + .withArgs("graph:providers:projectGraphBuilder").returns(t.context.log); + mock.reRequire("../../lib/graph/providers/projectGraphBuilder"); + + t.context.projectGraphFromTree = mock.reRequire("../../lib/generateProjectGraph").usingObject; logger.getLogger.restore(); // Immediately restore global stub for following tests }); @@ -41,14 +42,14 @@ test.afterEach.always((t) => { test("Application A", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationATree); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); const rootProject = projectGraph.getRoot(); t.is(rootProject.getName(), "application.a", "Returned correct root project"); }); test("Application A: Traverse project graph breadth first", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationATree); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); @@ -67,7 +68,7 @@ test("Application A: Traverse project graph breadth first", async (t) => { test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub)); @@ -89,7 +90,7 @@ test("Application Cycle A: Traverse project graph breadth first with cycles", as test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); @@ -108,7 +109,7 @@ test("Application Cycle B: Traverse project graph breadth first with cycles", as test("Application A: Traverse project graph depth first", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationATree); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseDepthFirst(callbackStub); @@ -129,7 +130,7 @@ test("Application A: Traverse project graph depth first", async (t) => { test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationCycleATreeIncDeduped); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); @@ -143,7 +144,7 @@ test("Application Cycle A: Traverse project graph depth first with cycles", asyn test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(applicationCycleBTreeIncDeduped); + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); const callbackStub = t.context.sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); @@ -172,7 +173,7 @@ async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) { throw new Error("Test error: Parameter 'bfs' must be specified"); } const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree(tree); + const projectGraph = await projectGraphFromTree({dependencyTree: tree}); const callbackStub = t.context.sinon.stub().resolves(); if (bfs) { await projectGraph.traverseBreadthFirst(callbackStub); @@ -251,7 +252,7 @@ test("Project with inline configuration for two projects", async (t) => { }; const {projectGraphFromTree} = t.context; - await t.throwsAsync(projectGraphFromTree(tree), + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { message: `Found 2 configurations of kind 'project' for module application.a.id. ` + @@ -308,7 +309,7 @@ test("Missing configuration file for root project", async (t) => { path: "non-existent", dependencies: [] }; - await t.throwsAsync(projectGraphFromTree(tree), + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { message: "Failed to crate a UI5 project from module application.a.id at non-existent. " + @@ -320,10 +321,10 @@ test("Missing configuration file for root project", async (t) => { test("Missing id for root project", async (t) => { const {projectGraphFromTree} = t.context; const tree = { - path: path.join(__dirname, "../fixtures/application.a"), + path: path.join(__dirname, "fixtures/application.a"), dependencies: [] }; - await t.throwsAsync(projectGraphFromTree(tree), + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), {message: "Could not create Module: Missing or empty parameter 'id'"}, "Rejected with error"); }); @@ -332,7 +333,7 @@ test("No type configured for root project", async (t) => { const tree = { id: "application.a.id", version: "1.0.0", - path: path.join(__dirname, "../fixtures/application.a"), + path: path.join(__dirname, "fixtures/application.a"), dependencies: [], configuration: { specVersion: "2.1", @@ -342,7 +343,7 @@ test("No type configured for root project", async (t) => { } } }; - const error = await t.throwsAsync(projectGraphFromTree(tree)); + const error = await t.throwsAsync(projectGraphFromTree({dependencyTree: tree})); t.is(error.message, `Unable to create Specification instance: Unknown specification type 'undefined'`); }); @@ -354,7 +355,7 @@ test("Missing dependencies", async (t) => { version: "1.0.0", path: applicationAPath }); - await t.notThrowsAsync(projectGraphFromTree(tree), + await t.notThrowsAsync(projectGraphFromTree({dependencyTree: tree}), "Gracefully accepted project with no dependencies attribute"); }); @@ -370,7 +371,7 @@ test("Missing second-level dependencies", async (t) => { path: path.join(applicationAPath, "node_modules", "library.d") }] }); - return t.notThrowsAsync(projectGraphFromTree(tree), + return t.notThrowsAsync(projectGraphFromTree({dependencyTree: tree}), "Gracefully accepted project with no dependencies attribute"); }); @@ -1275,7 +1276,7 @@ test("Project with project-shim extension with invalid dependency configuration" }] }; const {projectGraphFromTree} = t.context; - const validationError = await t.throwsAsync(projectGraphFromTree(tree), { + const validationError = await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { instanceOf: ValidationError }); t.true(validationError.message.includes("Configuration must have required property 'metadata'"), @@ -1558,7 +1559,7 @@ test("Project with unknown extension dependency inline configuration", async (t) }], }; const {projectGraphFromTree} = t.context; - const validationError = await t.throwsAsync(projectGraphFromTree(tree)); + const validationError = await t.throwsAsync(projectGraphFromTree({dependencyTree: tree})); t.is(validationError.message, `Unable to create Specification instance: Unknown specification type 'phony-pony'`, "Should throw with expected error message"); diff --git a/test/lib/graph/providers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js similarity index 98% rename from test/lib/graph/providers/ui5Framework.integration.js rename to test/lib/graph/helpers/ui5Framework.integration.js index 66b7857c1..178c82e38 100644 --- a/test/lib/graph/providers/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -11,7 +11,7 @@ const lockfile = require("lockfile"); const logger = require("@ui5/logger"); const Module = require("../../../../lib/graph/Module"); const DependencyTreeProvider = require("../../../../lib/graph/providers/DependencyTree"); -const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); +const projectGraphBuilder = require("../../../../lib/graph/providers/projectGraphBuilder"); let ui5Framework; let Installer; @@ -47,7 +47,7 @@ test.beforeEach((t) => { mock("mkdirp", sinon.stub().resolves()); // Re-require to ensure that mocked modules are used - ui5Framework = mock.reRequire("../../../../lib/graph/providers/ui5Framework"); + ui5Framework = mock.reRequire("../../../../lib/graph/helpers/ui5Framework"); Installer = require("../../../../lib/ui5Framework/npm/Installer"); }); diff --git a/test/lib/graph/providers/npm.integration.js b/test/lib/graph/providers/NodePackageDependencies.integration.js similarity index 86% rename from test/lib/graph/providers/npm.integration.js rename to test/lib/graph/providers/NodePackageDependencies.integration.js index 91a98cb22..96072b468 100644 --- a/test/lib/graph/providers/npm.integration.js +++ b/test/lib/graph/providers/NodePackageDependencies.integration.js @@ -12,8 +12,8 @@ const applicationGPath = path.join(__dirname, "..", "..", "..", "fixtures", "app const errApplicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "err.application.a"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "..", "fixtures", "cyclic-deps", "node_modules"); -const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); -const NpmProvider = require("../../../../lib/graph/providers/npm"); +const projectGraphBuilder = require("../../../../lib/graph/providers/projectGraphBuilder"); +const NodePackageDependenciesProvider = require("../../../../lib/graph/providers/NodePackageDependencies"); test.beforeEach((t) => { t.context.sinon = sinonGlobal.createSandbox(); @@ -52,7 +52,7 @@ async function _testGraphCreation(t, npmProvider, expectedOrder, bfs) { } test("AppA: project with collection dependency", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationAPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -65,7 +65,7 @@ test("AppA: project with collection dependency", async (t) => { }); test("AppC: project with dependency with optional dependency resolved through root project", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -76,7 +76,7 @@ test("AppC: project with dependency with optional dependency resolved through ro }); test("AppC2: project with dependency with optional dependency resolved through other project", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationC2Path }); await testGraphCreationDfs(t, npmProvider, [ @@ -89,7 +89,7 @@ test("AppC2: project with dependency with optional dependency resolved through o test("AppC3: project with dependency with optional dependency resolved " + "through other project (but got hoisted)", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationC3Path }); await testGraphCreationDfs(t, npmProvider, [ @@ -103,7 +103,7 @@ test("AppC3: project with dependency with optional dependency resolved " + test("AppD: project with dependency with unresolved optional dependency", async (t) => { // application.d`s dependency "library.e" has an optional dependency to "library.d" // which is already present in the node_modules directory of library.e - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationDPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -113,7 +113,7 @@ test("AppD: project with dependency with unresolved optional dependency", async }); test("AppF: UI5-dependencies in package.json are ignored", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationFPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -125,7 +125,7 @@ test("AppF: UI5-dependencies in package.json are ignored", async (t) => { test("AppG: project with npm 'optionalDependencies' should not fail if optional dependency cannot be resolved", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationGPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -137,7 +137,7 @@ test("AppG: project with npm 'optionalDependencies' should not fail if optional test("AppCycleA: cyclic dev deps", async (t) => { const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCycleAPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -150,7 +150,7 @@ test("AppCycleA: cyclic dev deps", async (t) => { test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCycleBPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -162,7 +162,7 @@ test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", asy test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => { const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCycleCPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -179,7 +179,7 @@ test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", asyn test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { const applicationCycleDPath = path.join(cycleDepsBasePath, "application.cycle.d"); - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCycleDPath }); @@ -190,7 +190,7 @@ test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { test("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: applicationCycleEPath }); await testGraphCreationDfs(t, npmProvider, [ @@ -202,7 +202,7 @@ test("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { test("Error: missing package.json", async (t) => { const dir = path.parse(__dirname).root; - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: dir }); const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); @@ -210,7 +210,7 @@ test("Error: missing package.json", async (t) => { }); test("Error: missing dependency", async (t) => { - const npmProvider = new NpmProvider({ + const npmProvider = new NodePackageDependenciesProvider({ cwd: errApplicationAPath }); const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); From 62714dae95fe69e91da22c945797d62fe0872968 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 23 Apr 2022 12:24:53 +0200 Subject: [PATCH 39/99] Fix typos, add comments --- lib/graph/Module.js | 5 +++-- lib/graph/providers/projectGraphBuilder.js | 10 +++++++--- test/lib/generateProjectGraph.usingObject.js | 2 +- test/lib/graph/helpers/ui5Framework.integration.js | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index e559e4137..83276c8d8 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -105,10 +105,11 @@ class Module { } async _getSpecifications() { + // Retrieve all configurations available for this module let configs = await this._getConfigurations(); - // I'm using hacks here: - // Filter out project-shims to check whether the this module is a collection + // Edge case: + // Search for project-shims to check whether this module defines a collection for itself const isCollection = configs.find((configuration) => { if (configuration.kind === "extension" && configuration.type === "project-shim") { // TODO create Specification instance and ask it for the configuration diff --git a/lib/graph/providers/projectGraphBuilder.js b/lib/graph/providers/projectGraphBuilder.js index 254ae87a8..64abe16d9 100644 --- a/lib/graph/providers/projectGraphBuilder.js +++ b/lib/graph/providers/projectGraphBuilder.js @@ -156,9 +156,13 @@ module.exports = async function(nodeProvider) { })); // Keep this out of the async map function to ensure - // all projects and extensions are applied a deterministic order + // all projects and extensions are applied in a deterministic order for (let i = 0; i < res.length; i++) { - const {node, project, extensions} = res[i]; + const { + node, // Tree "raw" dependency tree node + project, // The project found for this node, if any + extensions // Any extensions found for this node + } = res[i]; handleExtensions(extensions); @@ -260,7 +264,7 @@ module.exports = async function(nodeProvider) { } } - // Appply dependency shims + // Apply dependency shims for (const [shimmedModuleId, moduleDepShims] of Object.entries(shimCollection.getAllDependencyShims())) { const sourceModule = moduleCollection[shimmedModuleId]; diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index 6a3a7484f..54c50dd90 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -1526,7 +1526,7 @@ test.skip("Project with project-shim extension with self-containing collection s t.is(log.info.callCount, 0, "log.info should not have been called"); const libraryY = graph.getProject("legacy.library.y"); - t.deepEqual(libraryY.getConfigurationObject().framework, { + t.deepEqual(libraryY.getFrameworkName(), { name: "OpenUI5" }, "Configuration from collection project should be taken over into shimmed project"); }); diff --git a/test/lib/graph/helpers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js index 178c82e38..93a80e827 100644 --- a/test/lib/graph/helpers/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -312,7 +312,7 @@ function defineTest(testName, { .resolves(distributionMetadata); } - const provider = new DependencyTreeProvider(translatorTree); + const provider = new DependencyTreeProvider({dependencyTree: translatorTree}); const projectGraph = await projectGraphBuilder(provider); From 4920de01be583691d7905203997fad02bbc655f3 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 26 Apr 2022 15:03:50 +0200 Subject: [PATCH 40/99] First draft for improved resource access --- lib/specifications/ComponentProject.js | 142 ++++++++++++++++++- lib/specifications/Project.js | 59 ++++++-- lib/specifications/types/Application.js | 63 +++++--- lib/specifications/types/Library.js | 128 +++++++++++------ lib/specifications/types/ProjectShim.js | 1 + test/lib/specifications/Project.js | 19 +++ test/lib/specifications/types/Application.js | 76 ++++++++++ test/lib/specifications/types/Library.js | 109 ++++++++++++++ 8 files changed, 524 insertions(+), 73 deletions(-) create mode 100644 test/lib/specifications/types/Application.js create mode 100644 test/lib/specifications/types/Library.js diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index f92ff9fa7..a4812fcf1 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -1,8 +1,9 @@ const {promisify} = require("util"); const Project = require("./Project"); +const resourceFactory = require("@ui5/fs").resourceFactory; /* -* Private configuration class for use in Module and specifications +* Subclass for projects potentially containing Components */ class ComponentProject extends Project { @@ -59,6 +60,143 @@ class ComponentProject extends Project { return this._config.builder && this._config.builder.bundles || []; } + _isRuntimeNamespaced() { + return true; + } + + /* === Resource Access === */ + + /** + * Get a resource reader for accessing the project resources in a given style. + * If project resources have been changed through the means of a workspace, those changes + * are reflected in the provided reader too. + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. Can be "buildtime", "runtime" or "flat" + * TODO: describe styles + * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime", includeTestResources = false} = {}) { + // TODO: Additional parameter 'includeWorkspace' to include reader to relevant Memory Adapter? + // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? + let reader; + switch (style) { + case "buildtime": + reader = this._getSourceReaderFlat(`/resources/${this._namespace}/`); + if (includeTestResources) { + const testReader = this._getTestReaderFlat(`/test-resources/${this._namespace}/`); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); + } + } + break; + case "runtime": + if (includeTestResources) { + throw new Error(`Readers of style "runtime" can't include test resources`); + } + if (this._isRuntimeNamespaced()) { + // Same as buildtime + return this.getReader({style: "buildtime", includeTestResources: false}); + } + reader = this._getSourceReaderFlat("/"); + break; + case "flat": + if (includeTestResources) { + throw new Error(`Readers of style "flat" can't include test resources`); + } + reader = this._getSourceReaderFlat("/"); + break; + default: + throw new Error(`Unknown path mapping style ${style}`); + } + + const writer = this._getWriter({ + style, includeTestResources + }); + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [writer, reader] + }); + } + + getWorkspace({includeTestResources = false} = {}) { + // Workspace is always of style "buildtime" + const reader = this.getReader({ + style: "buildtime", + includeTestResources + }); + + const writer = this._getWriter({ + style: "buildtime", + includeTestResources + }); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter({style = "buildtime", includeTestResources = false} = {}) { + if (!this._writer) { + // writer is always of style "buildtime" and may always include test resources + this._writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + } + + let writer = this._writer; + if (!includeTestResources) { + // If no test-resources are requested, filter them out + writer = writer.filter((resource) => { + return !this._isTestResource(resource); + }); + } + + switch (style) { + case "buildtime": { + // Writer already uses buildtime style + return writer; + } + case "runtime": { + if (includeTestResources) { + throw new Error(`Readers of style "runtime" can't include test resources`); + } + if (this._isRuntimeNamespaced()) { + // Same as buildtime + return this._getWriter({style: "buildtime", includeTestResources}); + } + + // Rewrite paths from "runtime" to "buildtime" + writer = writer.link({ + linkPath: `/`, + targetPath: `/resources/${this._namespace}/` + }); + return writer; + } + case "flat": { + if (includeTestResources) { + throw new Error(`Readers of style "flat" can't include test resources`); + } + // Rewrite paths from "flat" to "buildtime" + return writer.link({ + linkPath: `/`, + targetPath: `/resources/${this._namespace}/` + }); + } + default: + throw new Error(`Unknown path mapping style ${style}`); + } + } + + _isTestResource(resource) { + throw new Error(`_isTestResource must be implemented by subclass ${this.constructor.name}`); + } + /* === Internals === */ /** * @private @@ -66,8 +204,6 @@ class ComponentProject extends Project { */ async _parseConfiguration(config) { await super._parseConfiguration(config); - - this._namespace = await this._getNamespace(); } async _getNamespace() { diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 40e213499..86e673c87 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -1,5 +1,12 @@ const Specification = require("./Specification"); +/** + * Project + * + * @public + * @memberof module:@ui5/project.specifications + * @augments module:@ui5/project.specifications.Specification + */ class Project extends Specification { constructor(parameters) { super(parameters); @@ -9,6 +16,15 @@ class Project extends Specification { } /* === Attributes === */ + /** + * @public + */ + getNamespace() { + // Default namespace for general Projects: + // Their resources should be structured with globally unique paths, hence their namespace is undefined + return null; + } + /** * @public */ @@ -49,6 +65,10 @@ class Project extends Specification { return this._config.server && this._config.server.settings; } + getBuilderSettings() { + return this._config.builder && this._config.builder.settings; + } + /* === Resource Access === */ /** * Get a resource reader for the sources of the project (excluding any test resources) @@ -56,28 +76,51 @@ class Project extends Specification { * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getSourceReader() { - throw new Error(`getSourceReader must be implemented by subclass ${this.constructor.name}`); + _getSourceReader() { + throw new Error(`_getSourceReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * TODO + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getSourceReaderFlat() { + throw new Error(`_getSourceReaderFlat must be implemented by subclass ${this.constructor.name}`); } /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * TODO * * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getRuntimeReader() { - throw new Error(`getRuntimeReader must be implemented by subclass ${this.constructor.name}`); + _getTestReaderFlat() { + throw new Error(`_getTestReaderFlat must be implemented by subclass ${this.constructor.name}`); + } + + _getDesignatedPath({namespace, isFlattable, isTestResources, isFramework}) { + } /** - * Get a resource reader for accessing the project resources during the build process + * TODO * * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getBuildtimeReader() { - throw new Error(`getBuildtimeReader must be implemented by subclass ${this.constructor.name}`); + getReader() { + throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); + } + /** + * TODO + * + * @public + * @returns {module:@ui5/fs.DuplexCollection} DuplexCollection + */ + getWorkspace() { + throw new Error(`getWorkspace must be implemented by subclass ${this.constructor.name}`); } /* === Internals === */ diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 881aa7167..40769a41d 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -29,44 +29,63 @@ class Application extends ComponentProject { /** * Get a resource reader for the sources of the project (excluding any test resources) * - * @public + * @param {string} virBasePath * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getSourceReader() { + _getSourceReaderFlat(virBasePath = "/") { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), - virBasePath: "/", + virBasePath, name: `Source reader for application project ${this.getName()}` }); } - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getRuntimeReader() { + _getTestReaderFlat(virBasePath = "/") { + return null; + // TODO allow test path for applications + // return resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._testPath), + // virBasePath, + // name: `Source reader for application project ${this.getName()}` + // }); + } + + _getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), - virBasePath: "/", // Applications are served at "/" - name: `Runtime reader for application project ${this.getName()}` + virBasePath: "/", + name: `Source reader for application project ${this.getName()}` }); } + /** - * Get a resource reader for accessing the project resources during the build process + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do * * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getBuildtimeReader() { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._webappPath), - virBasePath: `/resources/${this.getNamespace()}/`, - name: `Buildtime reader for application project ${this.getName()}` - }); - } + // getRuntimeReader() { + // return resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._webappPath), + // virBasePath: "/", // Applications are served at "/" + // name: `Runtime reader for application project ${this.getName()}` + // }); + // } + + // * + // * Get a resource reader for accessing the project resources during the build process + // * + // * @public + // * @returns {module:@ui5/fs.ReaderCollection} Reader collection + + // getBuildtimeReader() { + // return resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._webappPath), + // virBasePath: `/resources/${this.getNamespace()}/`, + // name: `Buildtime reader for application project ${this.getName()}` + // }); + // } /* === Internals === */ /** @@ -97,6 +116,8 @@ class Application extends ComponentProject { */ async _parseConfiguration(config) { await super._parseConfiguration(config); + + this._namespace = await this._getNamespace(); } /** @@ -197,7 +218,7 @@ class Application extends ComponentProject { if (this._pManifests[filePath]) { return this._pManifests[filePath]; } - return this._pManifests[filePath] = this.getSourceReader().byPath(filePath) + return this._pManifests[filePath] = this._getSourceReader().byPath(filePath) .then(async (resource) => { if (!resource) { throw new Error( diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index c315c3a64..a04e1201f 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -19,6 +19,7 @@ class Library extends ComponentProject { } /* === Attributes === */ + /** * @public */ @@ -36,13 +37,33 @@ class Library extends ComponentProject { } /* === Resource Access === */ - /** - * Get a resource reader for the sources of the project (excluding any test resources) - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection + + // getReader({style = "namespaced", includeTestResources = false} = {}) { + // // if source is namespaced, create fs reader not for src/ but for src/name/space/ + // // If there are other files or directories on the top level (like in sap.ui.core): + // // libraries: map to src/ but throw if flat is requested. Same for test/ + // // applications: throw on creation. This should not be allowed. Same for test/ + + // return resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._srcPath), + // virBasePath: "/", + // name: `Source reader for library project ${this.getName()}` + // }); + // } + + // getWorkspace(readerOptions) { + + // } + + /* + * + * Get a resource reader for the sources of the project (excluding any test resources) + * In the future the path structure can be flat or namespaced depending on the project + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getSourceReader() { + _getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/", @@ -50,43 +71,68 @@ class Library extends ComponentProject { }); } - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getRuntimeReader() { - let reader = resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/resources/", - name: `Runtime resources reader for library project ${this.getName()}` + _getSourceReaderFlat(virBasePath = "/") { + // TODO: Throw for libraries with additional namespaces like sap.ui.core? + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath, ...this._namespace.split("/")), + virBasePath, + name: `Source reader for library project ${this.getName()}` }); - if (this._testPathExists) { - const testReader = resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._testPath), - virBasePath: "/test-resources/", - name: `Runtime test-resources reader for library project ${this.getName()}` - }); - reader = resourceFactory.createReaderCollection({ - name: `Reader collection for library project ${this.getName()}`, - readers: [reader, testReader] - }); + } + + _getTestReaderFlat(virBasePath = "/") { + if (!this._testPathExists) { + return null; } - return reader; + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._testPath, ...this._namespace.split("/")), + virBasePath, + name: `Runtime test-resources reader for library project ${this.getName()}` + }); + return testReader; } - /** - * Get a resource reader for accessing the project resources during the build process - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getBuildtimeReader() { - // Same as runtime - return this.getRuntimeReader(); + _isTestResource(resource) { + return resource.getPath().startsWith("/test-resources/"); } + // /** + // * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + // * + // * @public + // * @returns {module:@ui5/fs.ReaderCollection} Reader collection + // */ + // getRuntimeReader() { + // let reader = resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._srcPath), + // virBasePath: "/resources/", + // name: `Runtime resources reader for library project ${this.getName()}` + // }); + // if (this._testPathExists) { + // const testReader = resourceFactory.createReader({ + // fsBasePath: fsPath.join(this.getPath(), this._testPath), + // virBasePath: "/test-resources/", + // name: `Runtime test-resources reader for library project ${this.getName()}` + // }); + // reader = resourceFactory.createReaderCollection({ + // name: `Reader collection for library project ${this.getName()}`, + // readers: [reader, testReader] + // }); + // } + // return reader; + // } + + // /** + // * Get a resource reader for accessing the project resources during the build process + // * + // * @public + // * @returns {module:@ui5/fs.ReaderCollection} Reader collection + // */ + // getBuildtimeReader() { + // // Same as runtime + // return this.getRuntimeReader(); + // } + /* === Internals === */ /** * @private @@ -128,7 +174,7 @@ class Library extends ComponentProject { async _parseConfiguration(config) { await super._parseConfiguration(config); - this._namespace = await this.getNamespace(); + this._namespace = await this._getNamespace(); if (!config.metadata.copyright) { try { @@ -403,7 +449,7 @@ class Library extends ComponentProject { if (this._pManifest) { return this._pManifest; } - return this._pManifest = this.getSourceReader().byGlob("**/manifest.json") + return this._pManifest = this._getSourceReader().byGlob("**/manifest.json") .then(async (manifestResources) => { if (!manifestResources.length) { throw new Error(`Could not find manifest.json file for project ${this.getName()}`); @@ -435,7 +481,7 @@ class Library extends ComponentProject { if (this._pDotLibrary) { return this._pDotLibrary; } - return this._pDotLibrary = this.getSourceReader().byGlob("**/.library") + return this._pDotLibrary = this._getSourceReader().byGlob("**/.library") .then(async (dotLibraryResources) => { if (!dotLibraryResources.length) { throw new Error(`Could not find .library file for project ${this.getName()}`); @@ -470,7 +516,7 @@ class Library extends ComponentProject { if (this._pLibraryJs) { return this._pLibraryJs; } - return this._pLibraryJs = this.getSourceReader().byGlob("**/library.js") + return this._pLibraryJs = this._getSourceReader().byGlob("**/library.js") .then(async (libraryJsResources) => { if (!libraryJsResources.length) { throw new Error(`Could not find library.js file for project ${this.getName()}`); diff --git a/lib/specifications/types/ProjectShim.js b/lib/specifications/types/ProjectShim.js index e61b12104..f299c9643 100644 --- a/lib/specifications/types/ProjectShim.js +++ b/lib/specifications/types/ProjectShim.js @@ -1,3 +1,4 @@ +// TODO: Move to "extensions" dir const Extension = require("../Extension"); class ProjectShim extends Extension { diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 3d4ee3ae0..6baf2755d 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -2,6 +2,10 @@ const test = require("ava"); const path = require("path"); const Specification = require("../../../lib/specifications/Specification"); +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const basicProjectInput = { id: "application.a.id", @@ -25,6 +29,21 @@ test("Instantiate a basic project", async (t) => { test("Configurations", async (t) => { const project = await Specification.create(basicProjectInput); t.is(project.getKind(), "project", "Returned correct kind configuration"); + t.is(project.getType(), "application", "Returned correct type configuration"); +}); + +test("Invalid configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources = { + configuration: { + propertiesFileSourceEncoding: "Ponycode" + } + }; + const error = await t.throwsAsync(Specification.create(customProjectInput)); + t.is(error.message, `Invalid ui5.yaml configuration for project application.a.id + +Configuration resources/configuration/propertiesFileSourceEncoding must be equal to one of the allowed values +Allowed values: UTF-8, ISO-8859-1`, "Threw with validation error"); }); test("Access project root resources via reader", async (t) => { diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js new file mode 100644 index 000000000..3bfcfd053 --- /dev/null +++ b/test/lib/specifications/types/Application.js @@ -0,0 +1,76 @@ +const test = require("ava"); +const path = require("path"); +const Specification = require("../../../../lib/specifications/Specification"); +const Application = require("../../../../lib/specifications/types/Application"); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test("Correct class", async (t) => { + const project = await Specification.create(basicProjectInput); + t.true(project instanceof Application, `Is an instance of the Application class`); +}); + +test("getPropertiesFileSourceEncoding: Default", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("getPropertiesFileSourceEncoding: Configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources = { + configuration: { + propertiesFileSourceEncoding: "ISO-8859-1" + } + }; + const project = await Specification.create(customProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("Access project resources via reader: namespace style, no test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/resources/id1/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/id1/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style, no test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader({style: "flat"}); + const resource = await reader.byPath("/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); +}); + +// test("Access project resources via reader: namespace style, including test resources", async (t) => { +// const project = await Specification.create(basicProjectInput); +// const reader = await project.getReader({style: "namespace", includeTestResources: true}); +// const resource = await reader.byPath("/test-resources/library/d/Test.html"); +// t.truthy(resource, "Found the requested resource"); +// t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); +// }); + +// test("Access project resources via reader: flat style, including test resources", async (t) => { +// const project = await Specification.create(basicProjectInput); +// const reader = await project.getReader({style: "flat", includeTestResources: true}); +// const resource = await reader.byPath("/test/Test.html"); +// t.truthy(resource, "Found the requested resource"); +// t.is(resource.getPath(), "/test/Test.html", "Resource has correct path"); +// }); diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js new file mode 100644 index 000000000..604bcecdd --- /dev/null +++ b/test/lib/specifications/types/Library.js @@ -0,0 +1,109 @@ +const test = require("ava"); +const path = require("path"); +const Specification = require("../../../../lib/specifications/Specification"); +const Library = require("../../../../lib/specifications/types/Library"); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const basicProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "main/src", + test: "main/test" + } + } + }, + } +}; + +test("Correct class", async (t) => { + const project = await Specification.create(basicProjectInput); + t.true(project instanceof Library, `Is an instance of the Library class`); +}); + +test("getNamespace", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getNamespace(), "library/d", + "Returned correct namespace"); +}); + +test("getPropertiesFileSourceEncoding: Default", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("getPropertiesFileSourceEncoding: Configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + const project = await Specification.create(customProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("Access project resources via reader: buildtime style, no test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/resources/library/d/.library"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/library/d/.library", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style, no test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader({style: "flat"}); + const resource = await reader.byPath("/.library"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/.library", "Resource has correct path"); +}); + +test("Access project resources via reader: buildtime style, including test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader({style: "buildtime", includeTestResources: true}); + const resource = await reader.byPath("/test-resources/library/d/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style, including test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const error = t.throws(() => { + project.getReader({style: "flat", includeTestResources: true}); + }); + t.is(error.message, `Readers of style "flat" can't include test resources`, "Correct error message"); +}); + +test("Modify project resources via workspace and access via flat reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace({includeTestResources: true}); + const workspaceResource = await workspace.byPath("/resources/library/d/.library"); + + const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = await project.getReader({style: "flat", includeTestResources: false}); + const readerResource = await reader.byPath("/.library"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/.library", "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const globResult = await reader.byGlob("**/.library"); + t.is(globResult.length, 1, "Found the requested resource byGlob"); + t.is(globResult[0].getPath(), "/.library", "Resource (byGlob) has correct path"); + t.is(await globResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); +}); From 5eda2bd8a48cac8c087a4b1bd49c4a7a058d99b2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 26 Apr 2022 15:56:56 +0200 Subject: [PATCH 41/99] Align Module and ThemeLibrary with resource access changes, cleanup --- lib/specifications/ComponentProject.js | 67 ++++++++++++++----- lib/specifications/Project.js | 51 ++++---------- lib/specifications/types/Application.js | 44 ++---------- lib/specifications/types/Library.js | 63 +----------------- lib/specifications/types/Module.js | 53 +++++++++------ lib/specifications/types/ThemeLibrary.js | 70 +++++++++++++++----- test/lib/specifications/types/Application.js | 24 +++---- 7 files changed, 163 insertions(+), 209 deletions(-) diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index a4812fcf1..4833c90d5 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -14,8 +14,8 @@ class ComponentProject extends Project { } this._pPom = null; - this._namespace = null; + this._isRuntimeNamespaced = true; } /* === Attributes === */ @@ -60,10 +60,6 @@ class ComponentProject extends Project { return this._config.builder && this._config.builder.bundles || []; } - _isRuntimeNamespaced() { - return true; - } - /* === Resource Access === */ /** @@ -84,9 +80,9 @@ class ComponentProject extends Project { let reader; switch (style) { case "buildtime": - reader = this._getSourceReaderFlat(`/resources/${this._namespace}/`); + reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); if (includeTestResources) { - const testReader = this._getTestReaderFlat(`/test-resources/${this._namespace}/`); + const testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); if (testReader) { reader = resourceFactory.createReaderCollection({ name: `Reader collection for project ${this.getName()}`, @@ -99,17 +95,17 @@ class ComponentProject extends Project { if (includeTestResources) { throw new Error(`Readers of style "runtime" can't include test resources`); } - if (this._isRuntimeNamespaced()) { + if (this._isRuntimeNamespaced) { // Same as buildtime return this.getReader({style: "buildtime", includeTestResources: false}); } - reader = this._getSourceReaderFlat("/"); + reader = this._getFlatSourceReader("/"); break; case "flat": if (includeTestResources) { throw new Error(`Readers of style "flat" can't include test resources`); } - reader = this._getSourceReaderFlat("/"); + reader = this._getFlatSourceReader("/"); break; default: throw new Error(`Unknown path mapping style ${style}`); @@ -124,6 +120,49 @@ class ComponentProject extends Project { }); } + /** + * Get a resource reader for the sources of the project (not including any test resources) + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getFullSourceReader() { + throw new Error(`_getFullSourceReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * TODO + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getFlatSourceReader() { + throw new Error(`_getFlatSourceReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get a resource reader for the test-sources of the project + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getFullTestReader() { + throw new Error(`_getFullTestReader must be implemented by subclass ${this.constructor.name}`); + } + /** + * TODO + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getFlatTestReader() { + throw new Error(`_getFlatTestReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @param {object} [options] + * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ getWorkspace({includeTestResources = false} = {}) { // Workspace is always of style "buildtime" const reader = this.getReader({ @@ -153,7 +192,7 @@ class ComponentProject extends Project { if (!includeTestResources) { // If no test-resources are requested, filter them out writer = writer.filter((resource) => { - return !this._isTestResource(resource); + return !resource.getPath().startsWith("/test-resources/"); }); } @@ -166,7 +205,7 @@ class ComponentProject extends Project { if (includeTestResources) { throw new Error(`Readers of style "runtime" can't include test resources`); } - if (this._isRuntimeNamespaced()) { + if (this._isRuntimeNamespaced) { // Same as buildtime return this._getWriter({style: "buildtime", includeTestResources}); } @@ -193,10 +232,6 @@ class ComponentProject extends Project { } } - _isTestResource(resource) { - throw new Error(`_isTestResource must be implemented by subclass ${this.constructor.name}`); - } - /* === Internals === */ /** * @private diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 86e673c87..e6bf1973f 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -71,48 +71,21 @@ class Project extends Specification { /* === Resource Access === */ /** - * Get a resource reader for the sources of the project (excluding any test resources) - * - * @public + * TODO + * + * @public + * @param {object} [options] + * @param {string} [options.style=buildtime] Path style to access resources. Can be "buildtime", "runtime" or "flat" + * TODO: describe styles + * This parameter might be ignored by some specifications + * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set + * This parameter might be ignored by some specifications * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getSourceReader() { - throw new Error(`_getSourceReader must be implemented by subclass ${this.constructor.name}`); - } - - /** - * TODO - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getSourceReaderFlat() { - throw new Error(`_getSourceReaderFlat must be implemented by subclass ${this.constructor.name}`); - } - - /** - * TODO - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getTestReaderFlat() { - throw new Error(`_getTestReaderFlat must be implemented by subclass ${this.constructor.name}`); - } - - _getDesignatedPath({namespace, isFlattable, isTestResources, isFramework}) { - - } - - /** - * TODO - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getReader() { + */ + getReader(options) { throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); } + /** * TODO * diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 40769a41d..b43ebdb95 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -9,8 +9,11 @@ class Application extends ComponentProject { this._pManifests = {}; this._webappPath = "webapp"; + + this._isRuntimeNamespaced = false; } + /* === Attributes === */ /** * @public @@ -32,7 +35,7 @@ class Application extends ComponentProject { * @param {string} virBasePath * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - _getSourceReaderFlat(virBasePath = "/") { + _getFlatSourceReader(virBasePath = "/") { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath, @@ -40,14 +43,8 @@ class Application extends ComponentProject { }); } - _getTestReaderFlat(virBasePath = "/") { - return null; - // TODO allow test path for applications - // return resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._testPath), - // virBasePath, - // name: `Source reader for application project ${this.getName()}` - // }); + _getFlatTestReader() { + return null; // Applications do not have a dedicated test directory } _getSourceReader() { @@ -58,35 +55,6 @@ class Application extends ComponentProject { }); } - - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - // getRuntimeReader() { - // return resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._webappPath), - // virBasePath: "/", // Applications are served at "/" - // name: `Runtime reader for application project ${this.getName()}` - // }); - // } - - // * - // * Get a resource reader for accessing the project resources during the build process - // * - // * @public - // * @returns {module:@ui5/fs.ReaderCollection} Reader collection - - // getBuildtimeReader() { - // return resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._webappPath), - // virBasePath: `/resources/${this.getNamespace()}/`, - // name: `Buildtime reader for application project ${this.getName()}` - // }); - // } - /* === Internals === */ /** * @private diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index a04e1201f..e6d53185d 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -37,24 +37,6 @@ class Library extends ComponentProject { } /* === Resource Access === */ - - // getReader({style = "namespaced", includeTestResources = false} = {}) { - // // if source is namespaced, create fs reader not for src/ but for src/name/space/ - // // If there are other files or directories on the top level (like in sap.ui.core): - // // libraries: map to src/ but throw if flat is requested. Same for test/ - // // applications: throw on creation. This should not be allowed. Same for test/ - - // return resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._srcPath), - // virBasePath: "/", - // name: `Source reader for library project ${this.getName()}` - // }); - // } - - // getWorkspace(readerOptions) { - - // } - /* * * Get a resource reader for the sources of the project (excluding any test resources) @@ -71,7 +53,7 @@ class Library extends ComponentProject { }); } - _getSourceReaderFlat(virBasePath = "/") { + _getFlatSourceReader(virBasePath = "/") { // TODO: Throw for libraries with additional namespaces like sap.ui.core? return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath, ...this._namespace.split("/")), @@ -80,7 +62,7 @@ class Library extends ComponentProject { }); } - _getTestReaderFlat(virBasePath = "/") { + _getFlatTestReader(virBasePath = "/") { if (!this._testPathExists) { return null; } @@ -92,47 +74,6 @@ class Library extends ComponentProject { return testReader; } - _isTestResource(resource) { - return resource.getPath().startsWith("/test-resources/"); - } - - // /** - // * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - // * - // * @public - // * @returns {module:@ui5/fs.ReaderCollection} Reader collection - // */ - // getRuntimeReader() { - // let reader = resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._srcPath), - // virBasePath: "/resources/", - // name: `Runtime resources reader for library project ${this.getName()}` - // }); - // if (this._testPathExists) { - // const testReader = resourceFactory.createReader({ - // fsBasePath: fsPath.join(this.getPath(), this._testPath), - // virBasePath: "/test-resources/", - // name: `Runtime test-resources reader for library project ${this.getName()}` - // }); - // reader = resourceFactory.createReaderCollection({ - // name: `Reader collection for library project ${this.getName()}`, - // readers: [reader, testReader] - // }); - // } - // return reader; - // } - - // /** - // * Get a resource reader for accessing the project resources during the build process - // * - // * @public - // * @returns {module:@ui5/fs.ReaderCollection} Reader collection - // */ - // getBuildtimeReader() { - // // Same as runtime - // return this.getRuntimeReader(); - // } - /* === Internals === */ /** * @private diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index c1553b3c1..1a78e9f3d 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -7,6 +7,7 @@ class Module extends Project { super(parameters); this._paths = null; + this._writer = null; } /* === Attributes === */ @@ -16,42 +17,50 @@ class Module extends Project { /* === Resource Access === */ /** - * Get a resource reader for the sources of the project (excluding any test resources) + * Get a resource reader for accessing the project resources * * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getSourceReader() { - // TODO - throw new Error("Not sure what is expected here"); - } - - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getRuntimeReader() { + getReader() { const readers = this._paths.map((readerArgs) => resourceFactory.createReader(readerArgs)); if (readers.length === 1) { return readers[0]; } - return resourceFactory.createReaderCollection({ + const readerCollection = resourceFactory.createReaderCollection({ name: `Reader collection for module project ${this.getName()}`, readers }); + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [this._getWriter(), readerCollection] + }); } /** - * Get a resource reader for accessing the project resources during the build process - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getBuildtimeReader() { - // Same as runtime - return this.getRuntimeReader(); + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ + getWorkspace() { + const reader = this.getReader(); + + const writer = this._getWriter(); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter({includeTestResources = false} = {}) { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + } + + return this._writer; } /* === Internals === */ diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index 40bc34296..0afc9ebad 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -8,6 +8,7 @@ class ThemeLibrary extends Project { this._srcPath = "src"; this._testPath = "test"; + this._writer = null; } /* === Attributes === */ @@ -30,20 +31,21 @@ class ThemeLibrary extends Project { }); } - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getRuntimeReader() { + * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * + * @public + * @param {object} [options] + * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + getReader({includeTestResources=false} = {}) { let reader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/resources/", name: `Runtime resources reader for theme-library project ${this.getName()}` }); - if (this._testPathExists) { + if (includeTestResources && this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", @@ -54,18 +56,52 @@ class ThemeLibrary extends Project { readers: [reader, testReader] }); } - return reader; + const writer = this._getWriter({ + includeTestResources + }); + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [writer, reader] + }); } /** - * Get a resource reader for accessing the project resources during the build process - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getBuildtimeReader() { - // Same as runtime - return this.getRuntimeReader(); + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @param {object} [options] + * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ + getWorkspace({includeTestResources = false} = {}) { + const reader = this.getReader({ + includeTestResources + }); + + const writer = this._getWriter({ + includeTestResources + }); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter({includeTestResources = false} = {}) { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + } + + let writer = this._writer; + if (!includeTestResources) { + // If no test-resources are requested, filter them out + writer = writer.filter((resource) => { + return !resource.getPath().startsWith("/test-resources/"); + }); + } + return writer; } /* === Internals === */ diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index 3bfcfd053..4361dbb70 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -43,7 +43,7 @@ test("getPropertiesFileSourceEncoding: Configuration", async (t) => { "Returned correct default propertiesFileSourceEncoding configuration"); }); -test("Access project resources via reader: namespace style, no test resources", async (t) => { +test("Access project resources via reader: buildtime style, no test resources", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader(); const resource = await reader.byPath("/resources/id1/manifest.json"); @@ -59,18 +59,10 @@ test("Access project resources via reader: flat style, no test resources", async t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); }); -// test("Access project resources via reader: namespace style, including test resources", async (t) => { -// const project = await Specification.create(basicProjectInput); -// const reader = await project.getReader({style: "namespace", includeTestResources: true}); -// const resource = await reader.byPath("/test-resources/library/d/Test.html"); -// t.truthy(resource, "Found the requested resource"); -// t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); -// }); - -// test("Access project resources via reader: flat style, including test resources", async (t) => { -// const project = await Specification.create(basicProjectInput); -// const reader = await project.getReader({style: "flat", includeTestResources: true}); -// const resource = await reader.byPath("/test/Test.html"); -// t.truthy(resource, "Found the requested resource"); -// t.is(resource.getPath(), "/test/Test.html", "Resource has correct path"); -// }); +test("Access project resources via reader: flat style, including test resources", async (t) => { + const project = await Specification.create(basicProjectInput); + const error = t.throws(() => { + project.getReader({style: "flat", includeTestResources: true}); + }); + t.is(error.message, `Readers of style "flat" can't include test resources`, "Correct error message"); +}); From 7a8d390cab112d7f5575f1880838375edbcac5b9 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 26 Apr 2022 16:00:52 +0200 Subject: [PATCH 42/99] Move extension types to separate sub-dir --- lib/specifications/Specification.js | 6 +++--- lib/specifications/types/{ => extensions}/ProjectShim.js | 3 +-- .../types/{ => extensions}/ServerMiddleware.js | 2 +- lib/specifications/types/{ => extensions}/Task.js | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) rename lib/specifications/types/{ => extensions}/ProjectShim.js (89%) rename lib/specifications/types/{ => extensions}/ServerMiddleware.js (88%) rename lib/specifications/types/{ => extensions}/Task.js (88%) diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 7fd1af180..5a51f7655 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -192,13 +192,13 @@ class Specification { return createAndInitializeSpec("Module", params); } case "task": { - return createAndInitializeSpec("Task", params); + return createAndInitializeSpec("extensions/Task", params); } case "server-middleware": { - return createAndInitializeSpec("ServerMiddleware", params); + return createAndInitializeSpec("extensions/ServerMiddleware", params); } case "project-shim": { - return createAndInitializeSpec("ProjectShim", params); + return createAndInitializeSpec("extensions/ProjectShim", params); } default: throw new Error( diff --git a/lib/specifications/types/ProjectShim.js b/lib/specifications/types/extensions/ProjectShim.js similarity index 89% rename from lib/specifications/types/ProjectShim.js rename to lib/specifications/types/extensions/ProjectShim.js index f299c9643..e9706a827 100644 --- a/lib/specifications/types/ProjectShim.js +++ b/lib/specifications/types/extensions/ProjectShim.js @@ -1,5 +1,4 @@ -// TODO: Move to "extensions" dir -const Extension = require("../Extension"); +const Extension = require("../../Extension"); class ProjectShim extends Extension { constructor(parameters) { diff --git a/lib/specifications/types/ServerMiddleware.js b/lib/specifications/types/extensions/ServerMiddleware.js similarity index 88% rename from lib/specifications/types/ServerMiddleware.js rename to lib/specifications/types/extensions/ServerMiddleware.js index 36dc4b35c..1134d342d 100644 --- a/lib/specifications/types/ServerMiddleware.js +++ b/lib/specifications/types/extensions/ServerMiddleware.js @@ -1,4 +1,4 @@ -const Extension = require("../Extension"); +const Extension = require("../../Extension"); class ServerMiddleware extends Extension { constructor(parameters) { diff --git a/lib/specifications/types/Task.js b/lib/specifications/types/extensions/Task.js similarity index 88% rename from lib/specifications/types/Task.js rename to lib/specifications/types/extensions/Task.js index 56fef3716..bc1f09007 100644 --- a/lib/specifications/types/Task.js +++ b/lib/specifications/types/extensions/Task.js @@ -1,4 +1,4 @@ -const Extension = require("../Extension"); +const Extension = require("../../Extension"); class Task extends Extension { constructor(parameters) { From 94f1d631bde61d088c83d38399774915b13816ed Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 27 Apr 2022 01:05:20 +0200 Subject: [PATCH 43/99] Implement build mechanics --- index.js | 4 + lib/buildDefinitions/AbstractBuilder.js | 283 ++++++++++++++++++++ lib/buildDefinitions/ApplicationBuilder.js | 197 ++++++++++++++ lib/buildDefinitions/LibraryBuilder.js | 219 +++++++++++++++ lib/buildDefinitions/ModuleBuilder.js | 7 + lib/buildDefinitions/ThemeLibraryBuilder.js | 63 +++++ lib/buildDefinitions/getInstance.js | 28 ++ lib/buildHelpers/BuildContext.js | 87 ++++++ lib/buildHelpers/ProjectBuildContext.js | 75 ++++++ lib/buildHelpers/composeTaskList.js | 126 +++++++++ lib/builder.js | 226 ++++++++++++++++ lib/graph/Module.js | 15 +- lib/graph/ProjectGraph.js | 19 +- lib/specifications/ComponentProject.js | 65 +++-- lib/specifications/Specification.js | 3 + lib/specifications/types/Application.js | 14 +- lib/specifications/types/LegacyLibrary.js | 60 +++++ lib/specifications/types/Library.js | 18 +- lib/specifications/types/Module.js | 6 +- lib/specifications/types/ThemeLibrary.js | 41 +-- lib/validation/validator.js | 4 + test/lib/graph/ProjectGraph.js | 28 +- 22 files changed, 1517 insertions(+), 71 deletions(-) create mode 100644 lib/buildDefinitions/AbstractBuilder.js create mode 100644 lib/buildDefinitions/ApplicationBuilder.js create mode 100644 lib/buildDefinitions/LibraryBuilder.js create mode 100644 lib/buildDefinitions/ModuleBuilder.js create mode 100644 lib/buildDefinitions/ThemeLibraryBuilder.js create mode 100644 lib/buildDefinitions/getInstance.js create mode 100644 lib/buildHelpers/BuildContext.js create mode 100644 lib/buildHelpers/ProjectBuildContext.js create mode 100644 lib/buildHelpers/composeTaskList.js create mode 100644 lib/builder.js create mode 100644 lib/specifications/types/LegacyLibrary.js diff --git a/index.js b/index.js index f7b882dfb..562547f5b 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,10 @@ module.exports = { * @type {import('./lib/generateProjectGraph')} */ generateProjectGraph: "./lib/generateProjectGraph", + /** + * @type {import('./lib/builder')} + */ + builder: "./lib/builder", /** * @public * @alias module:@ui5/project.ui5Framework diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js new file mode 100644 index 000000000..7ab728cf1 --- /dev/null +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -0,0 +1,283 @@ +const {getTask} = require("@ui5/builder").tasks.taskRepository; +const composeTaskList = require("../buildHelpers/composeTaskList"); + +/** + * Resource collections + * + * @public + * @typedef module:@ui5/builder.BuilderResourceCollections + * @property {module:@ui5/fs.DuplexCollection} workspace Workspace Resource + * @property {module:@ui5/fs.ReaderCollection} dependencies Workspace Resource + */ + +/** + * Base class for the builder implementation of a project type + * + * @abstract + */ +class AbstractBuilder { + /** + * Constructor + * + * @param {object} parameters + * @param {object} parameters.graph + * @param {object} parameters.project + * @param {GroupLogger} parameters.parentLogger Logger to use + * @param {object} parameters.taskUtil + * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections + */ + constructor({graph, project, parentLogger, taskUtil, resourceCollections}) { + if (new.target === AbstractBuilder) { + throw new TypeError("Class 'AbstractBuilder' is abstract"); + } + + this.project = project; + this.graph = graph; + + this.log = parentLogger.createSubLogger(project.type + " " + project.getName(), 0.2); + this.taskLog = this.log.createTaskLogger("🔨"); + + this.tasks = {}; + this.taskExecutionOrder = []; + + this.addStandardTasks({ + project, + log: this.log, + taskUtil, + getTask, + resourceCollections + }); + this.addCustomTasks({ + graph, + project, + taskUtil, + resourceCollections + }); + } + + /** + * Adds all standard tasks to execute + * + * @abstract + * @protected + * @param {object} parameters + * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections + * @param {object} parameters.taskUtil + * @param {object} parameters.project + * @param {object} parameters.log @ui5/logger logger instance + */ + addStandardTasks({project, log, taskUtil, resourceCollections}) { + throw new Error("Function 'addStandardTasks' is not implemented"); + } + + /** + * Adds custom tasks to execute + * + * @private + * @param {object} parameters + * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections + * @param {object} parameters.graph + * @param {object} parameters.project + * @param {object} parameters.taskUtil + */ + addCustomTasks({graph, project, taskUtil, resourceCollections}) { + const projectCustomTasks = project.getCustomTasks(); + if (!projectCustomTasks || projectCustomTasks.length === 0) { + return; // No custom tasks defined + } + 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 (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) { + // Iff there are tasks configured, beforeTask or afterTask must be given + 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 = `${taskDef.name}--${suffixCounter}`; + } + } + const task = graph.getExtension(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} parameters.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 + {string} [parameters.options.configuration] Task configuration if given in ui5.yaml + Returns: + {Promise} Promise resolving with undefined once data has been written + */ + const params = { + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + projectNamespace: project.getNamespace(), + configuration: taskDef.configuration + } + }; + + const taskUtilInterface = taskUtil.getInterface(project.getSpecVersion()); + // Interface is undefined if specVersion does not support taskUtil + if (taskUtilInterface) { + params.taskUtil = taskUtilInterface; + } + return task(params); + }; + + this.tasks[newTaskName] = execTask; + + if (this.taskExecutionOrder.length) { + // There is at least one task configured. Use before- and afterTask to add the custom task + 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); + } else { + // There is no task configured so far. Just add the custom task + this.taskExecutionOrder.push(newTaskName); + } + } + } + + /** + * Adds a executable task to the builder + * + * 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.tasks[taskName]) { + throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`); + } + if (this.taskExecutionOrder.includes(taskName)) { + throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` + + `It has already been scheduled for execution.`); + } + this.tasks[taskName] = taskFunction; + this.taskExecutionOrder.push(taskName); + } + + /** + * Check whether a task is defined + * + * @private + * @param {string} taskName + * @returns {boolean} + */ + hasTask(taskName) { + // TODO 3.0: Check whether this method is still required. + // Only usage within #build seems to be unnecessary as all tasks are also added to the taskExecutionOrder + return Object.prototype.hasOwnProperty.call(this.tasks, taskName); + } + + /** + * Takes a list of tasks which should be executed from the available task list of the current builder + * + * @param {object} parameters + * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks + * @param {boolean} parameters.selfContained + * True if a the build should be self-contained or false for prelead build bundles + * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed + * @param {Array} parameters.includedTasks Task list to be included from build + * @param {Array} parameters.excludedTasks Task list to be excluded from build + * @returns {Promise} Returns promise chain with tasks + */ + build(parameters) { + const tasksToRun = composeTaskList(Object.keys(this.tasks), parameters); + const allTasks = this.taskExecutionOrder.filter((taskName) => { + // There might be a numeric suffix in case a custom task is configured multiple times. + // The suffix needs to be removed in order to check against the list of tasks to run. + // + // Note: The 'tasksToRun' parameter only allows to specify the custom task name + // (without suffix), so it executes either all or nothing. + // It's currently not possible to just execute some occurrences of a custom task. + // This would require a more robust contract to identify task executions + // (e.g. via an 'id' that can be assigned to a specific execution in the configuration). + const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); + return this.hasTask(taskName) && tasksToRun.includes(taskWithoutSuffixCounter); + }); + + this.taskLog.addWork(allTasks.length); + + return allTasks.reduce((taskChain, taskName) => { + const taskFunction = this.tasks[taskName]; + + if (typeof taskFunction === "function") { + taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); + } + + return taskChain; + }, Promise.resolve()); + } + + /** + * Adds progress related functionality to task function. + * + * @private + * @param {string} taskName Name of the task + * @param {Function} taskFunction Function which executed the task + * @returns {Function} Wrapped task function + */ + wrapTask(taskName, taskFunction) { + return () => { + this.taskLog.startWork(`Running task ${taskName}...`); + return taskFunction().then(() => this.taskLog.completeWork(1)); + }; + } + + /** + * Appends the list of 'excludes' to the list of 'patterns'. To harmonize both lists, the 'excludes' + * are negated and the 'patternPrefix' is added to make them absolute. + * + * @private + * @param {string[]} patterns + * List of absolute default patterns. + * @param {string[]} excludes + * List of relative patterns to be excluded. Excludes with a leading "!" are meant to be re-included. + * @param {string} patternPrefix + * Prefix to be added to the excludes to make them absolute. The prefix must have a leading and a + * trailing "/". + */ + enhancePatternWithExcludes(patterns, excludes, patternPrefix) { + excludes.forEach((exclude) => { + if (exclude.startsWith("!")) { + patterns.push(`${patternPrefix}${exclude.slice(1)}`); + } else { + patterns.push(`!${patternPrefix}${exclude}`); + } + }); + } +} + +module.exports = AbstractBuilder; diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js new file mode 100644 index 000000000..4a0a94ace --- /dev/null +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -0,0 +1,197 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class ApplicationBuilder extends AbstractBuilder { + addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + this.addTask("escapeNonAsciiCharacters", async () => { + return getTask("escapeNonAsciiCharacters").task({ + workspace: resourceCollections.workspace, + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + }); + + this.addTask("replaceCopyright", async () => { + return getTask("replaceCopyright").task({ + workspace: resourceCollections.workspace, + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,json}" + } + }); + }); + + this.addTask("replaceVersion", async () => { + return getTask("replaceVersion").task({ + workspace: resourceCollections.workspace, + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json}" + } + }); + }); + + // Support rules should not be minified to have readable code in the Support Assistant + const minificationPattern = ["/**/*.js", "!**/*.support.js"]; + if (["2.6"].includes(project.getSpecVersion())) { + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + this.enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + } + this.addTask("minify", async () => { + return getTask("minify").task({ + workspace: resourceCollections.workspace, + taskUtil, + options: { + pattern: minificationPattern + } + }); + }); + + this.addTask("generateFlexChangesBundle", async () => { + const generateFlexChangesBundle = getTask("generateFlexChangesBundle").task; + return generateFlexChangesBundle({ + workspace: resourceCollections.workspace, + taskUtil, + options: { + namespace: project.getNamespace() + } + }); + }); + + if (project.getNamespace()) { + this.addTask("generateManifestBundle", async () => { + const generateManifestBundle = getTask("generateManifestBundle").task; + return generateManifestBundle({ + workspace: resourceCollections.workspace, + options: { + projectName: project.getName(), + namespace: project.getNamespace() + } + }); + }); + } + + const componentPreloadPaths = project.getComponentPreloadPaths(); + const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadNamespaces(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + this.addTask("generateComponentPreload", async () => { + return getTask("generateComponentPreload").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes + } + }); + }); + } else { + // Default component preload for application namespace + this.addTask("generateComponentPreload", async () => { + return getTask("generateComponentPreload").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + namespaces: [project.getNamespace()], + excludes: componentPreloadExcludes + } + }); + }); + } + + this.addTask("generateStandaloneAppBundle", async () => { + return getTask("generateStandaloneAppBundle").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + namespace: project.getNamespace() + } + }); + }); + + this.addTask("transformBootstrapHtml", async () => { + return getTask("transformBootstrapHtml").task({ + workspace: resourceCollections.workspace, + options: { + projectName: project.getName(), + namespace: project.getNamespace() + } + }); + }); + + const bundles = project.getBundles(); + if (bundles.length) { + this.addTask("generateBundle", async () => { + return Promise.all(bundles.map((bundle) => { + return getTask("generateBundle").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + })); + }); + } + + this.addTask("generateVersionInfo", async () => { + return getTask("generateVersionInfo").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }); + }); + + if (project.getNamespace()) { + this.addTask("generateCachebusterInfo", async () => { + return getTask("generateCachebusterInfo").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + namespace: project.getNamespace(), + signatureType: project.getCachebusterSignatureType(), + } + }); + }); + } + + this.addTask("generateApiIndex", async () => { + return getTask("generateApiIndex").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName() + } + }); + }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName() + } + }); + }); + } +} + +module.exports = ApplicationBuilder; diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js new file mode 100644 index 000000000..1455bb194 --- /dev/null +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -0,0 +1,219 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class LibraryBuilder extends AbstractBuilder { + addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + this.addTask("escapeNonAsciiCharacters", async () => { + return getTask("escapeNonAsciiCharacters").task({ + workspace: resourceCollections.workspace, + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + }); + + this.addTask("replaceCopyright", async () => { + return getTask("replaceCopyright").task({ + workspace: resourceCollections.workspace, + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }); + }); + + this.addTask("replaceVersion", async () => { + return getTask("replaceVersion").task({ + workspace: resourceCollections.workspace, + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }); + }); + + this.addTask("replaceBuildtime", async () => { + return getTask("replaceBuildtime").task({ + workspace: resourceCollections.workspace, + options: { + pattern: "/resources/sap/ui/Global.js" + } + }); + }); + + if (project.getNamespace()) { + this.addTask("generateJsdoc", async () => { + const patterns = ["/resources/**/*.js"]; + // Add excludes + const excludes = project.getJsdocExcludes(); + if (excludes.length) { + const excludes = excludes.map((pattern) => { + return `!/resources/${pattern}`; + }); + + patterns.push(...excludes); + } + + return getTask("generateJsdoc").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + namespace: project.getNamespace(), + version: project.getVersion(), + pattern: patterns + } + }); + }); + + this.addTask("executeJsdocSdkTransformation", async () => { + return getTask("executeJsdocSdkTransformation").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + dotLibraryPattern: "/resources/**/*.library", + } + }); + }); + } + + // Support rules should not be minified to have readable code in the Support Assistant + const minificationPattern = ["/resources/**/*.js", "!**/*.support.js"]; + if (["2.6"].includes(project.getSpecVersion())) { + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + this.enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + } + + this.addTask("minify", async () => { + return getTask("minify").task({ + workspace: resourceCollections.workspace, + taskUtil, + options: { + pattern: minificationPattern + } + }); + }); + + this.addTask("generateLibraryManifest", async () => { + return getTask("generateLibraryManifest").task({ + workspace: resourceCollections.workspace, + taskUtil, + options: { + projectName: project.getName() + } + }); + }); + + + if (project.getNamespace()) { + this.addTask("generateManifestBundle", async () => { + return getTask("generateManifestBundle").task({ + workspace: resourceCollections.workspace, + options: { + projectName: project.getName(), + namespace: project.getNamespace() + } + }); + }); + } + + const componentPreloadPaths = project.getComponentPreloadPaths(); + const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadNamespaces(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + this.addTask("generateComponentPreload", async () => { + return getTask("generateComponentPreload").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes + } + }); + }); + } + + this.addTask("generateLibraryPreload", async () => { + return getTask("generateLibraryPreload").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + project: project, + excludes: project.getLibraryPreloadExcludes() + } + }); + }); + + const bundles = project.getBundles(); + if (bundles.length) { + this.addTask("generateBundle", async () => { + return bundles.reduce(function(sequence, bundle) { + return sequence.then(function() { + return getTask("generateBundle").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName(), + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + }); + }, Promise.resolve()); + }); + } + + this.addTask("buildThemes", async () => { + // Only compile themes directly below the lib namespace to be in sync with the theme support at runtime + // which only loads themes from that folder. + // TODO 3.0: Remove fallback in case of missing namespace + const inputPattern = `/resources/${project.getNamespace() || "**"}/themes/*/library.source.less`; + + return getTask("buildThemes").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern, + cssVariables: taskUtil.getBuildOption("cssVariables") + } + }); + }); + + this.addTask("generateThemeDesignerResources", async () => { + return getTask("generateThemeDesignerResources").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + version: project.getVersion(), + namespace: project.getNamespace() + } + }); + }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName() + } + }); + }); + } +} + +module.exports = LibraryBuilder; diff --git a/lib/buildDefinitions/ModuleBuilder.js b/lib/buildDefinitions/ModuleBuilder.js new file mode 100644 index 000000000..01981c4dd --- /dev/null +++ b/lib/buildDefinitions/ModuleBuilder.js @@ -0,0 +1,7 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class ModuleBuilder extends AbstractBuilder { + addStandardTasks() {/* nothing to do*/} +} + +module.exports = ModuleBuilder; diff --git a/lib/buildDefinitions/ThemeLibraryBuilder.js b/lib/buildDefinitions/ThemeLibraryBuilder.js new file mode 100644 index 000000000..9e7e5f29d --- /dev/null +++ b/lib/buildDefinitions/ThemeLibraryBuilder.js @@ -0,0 +1,63 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class ThemeLibraryBuilder extends AbstractBuilder { + addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + this.addTask("replaceCopyright", async () => { + return getTask("replaceCopyright").task({ + workspace: resourceCollections.workspace, + options: { + copyright: project.getCopyright(), + pattern: "/resources/**/*.{less,theme}" + } + }); + }); + + this.addTask("replaceVersion", async () => { + return getTask("replaceVersion").task({ + workspace: resourceCollections.workspace, + options: { + version: project.getVersion(), + pattern: "/resources/**/*.{less,theme}" + } + }); + }); + + this.addTask("buildThemes", async () => { + return getTask("buildThemes").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: taskUtil.getBuildOption("cssVariables") + } + }); + }); + + this.addTask("generateThemeDesignerResources", async () => { + return getTask("generateThemeDesignerResources").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.getName(), + version: project.getVersion() + } + }); + }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + taskUtil, + options: { + projectName: project.getName() + } + }); + }); + } +} + +module.exports = ThemeLibraryBuilder; diff --git a/lib/buildDefinitions/getInstance.js b/lib/buildDefinitions/getInstance.js new file mode 100644 index 000000000..a55ade037 --- /dev/null +++ b/lib/buildDefinitions/getInstance.js @@ -0,0 +1,28 @@ + +function createInstance(moduleName, params) { + const BuildDefinition = require(`./${moduleName}`); + return new BuildDefinition(params); +} + +/** + * Get build definition instance + * + * @param {object} parameters + * @param {object} parameters.graph + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {GroupLogger} parameters.parentLogger Logger to use + */ +module.exports = function(parameters) { + switch (parameters.project.getType()) { + case "application": + return createInstance("ApplicationBuilder", parameters); + case "library": + case "legacy-library": + return createInstance("LibraryBuilder", parameters); + case "module": + return createInstance("ModuleBuilder", parameters); + case "theme-library": + return createInstance("ThemeLibraryBuilder", parameters); + } +}; diff --git a/lib/buildHelpers/BuildContext.js b/lib/buildHelpers/BuildContext.js new file mode 100644 index 000000000..88957b675 --- /dev/null +++ b/lib/buildHelpers/BuildContext.js @@ -0,0 +1,87 @@ +const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; +const resourceFactory = require("@ui5/fs").resourceFactory; +const ProjectBuildContext = require("./ProjectBuildContext"); + +// Note: When adding standard tags, always update the public documentation in TaskUtil +// (Type "module:@ui5/builder.tasks.TaskUtil~StandardBuildTags") +const GLOBAL_TAGS = Object.freeze({ + IsDebugVariant: "ui5:IsDebugVariant", + HasDebugVariant: "ui5:HasDebugVariant", +}); + +/** + * Context of a build process + * + * @private + * @memberof module:@ui5/builder.builder + */ +class BuildContext { + constructor({graph, options = {}}) { + if (!graph) { + throw new Error(`Missing parameter 'graph'`); + } + this._graph = graph; + this.projectBuildContexts = []; + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: Object.values(GLOBAL_TAGS) + }); + this.options = options; + + this._readerCollectionCache = {}; + this._readerCollectionCacheWithTestResources = {}; + } + + getRootProject() { + return this._graph.getRoot(); + } + + getOption(key) { + return this.options[key]; + } + + createProjectContext({project}) { + const projectBuildContext = new ProjectBuildContext({ + buildContext: this, + globalTags: GLOBAL_TAGS, + project + }); + this.projectBuildContexts.push(projectBuildContext); + return projectBuildContext; + } + + async executeCleanupTasks() { + await Promise.all(this.projectBuildContexts.map((ctx) => { + return ctx.executeCleanupTasks(); + })); + } + + getResourceTagCollection() { + return this._resourceTagCollection; + } + + async getDependenciesReader({projectName, includeTestResources}) { + if (!includeTestResources && this._readerCollectionCache[projectName]) { + return this._readerCollectionCache[projectName]; + } else if (includeTestResources && this._readerCollectionCacheWithTestResources[projectName]) { + return this._readerCollectionCacheWithTestResources[projectName]; + } + const readers = []; + await this._graph.traverseBreadthFirst(async function(dep) { + readers.push(dep.getReader({ + includeTestResources + })); + }, projectName); + const readerCollection = resourceFactory.createReaderCollection({ + name: `Dependency reader collection for project ${projectName}`, + readers + }); + + if (!includeTestResources) { + return this._readerCollectionCache[projectName] = readerCollection; + } else if (includeTestResources) { + return this._readerCollectionCacheWithTestResources[projectName] = readerCollection; + } + } +} + +module.exports = BuildContext; diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js new file mode 100644 index 000000000..797577e6d --- /dev/null +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -0,0 +1,75 @@ +const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; +const TaskUtil = require("@ui5/builder").tasks.TaskUtil; + +// Note: When adding standard tags, always update the public documentation in TaskUtil +// (Type "module:@ui5/builder.tasks.TaskUtil~StandardBuildTags") +const STANDARD_TAGS = { + OmitFromBuildResult: "ui5:OmitFromBuildResult", + IsBundle: "ui5:IsBundle", +}; + +/** + * Build context of a single project. Always part of an overall + * [Build Context]{@link module:@ui5/builder.builder.BuildContext} + * + * @private + * @memberof module:@ui5/builder.builder + */ +class ProjectBuildContext { + constructor({buildContext, globalTags, project}) { + if (!buildContext || !globalTags || !project) { + throw new Error(`One or more mandatory parameters are missing`); + } + this._buildContext = buildContext; + this._project = project; + this.queues = { + cleanup: [] + }; + + this.STANDARD_TAGS = Object.assign({}, STANDARD_TAGS, globalTags); + Object.freeze(this.STANDARD_TAGS); + + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: Object.values(this.STANDARD_TAGS), + superCollection: this._buildContext.getResourceTagCollection() + }); + } + + isRootProject() { + return this._project === this._buildContext.getRootProject(); + } + + getOption(key) { + return this._buildContext.getOption(key); + } + + registerCleanupTask(callback) { + this.queues.cleanup.push(callback); + } + + async executeCleanupTasks() { + await Promise.all(this.queues.cleanup.map((callback) => { + return callback(); + })); + } + + getResourceTagCollection() { + return this._resourceTagCollection; + } + + getTaskUtil() { + if (!this._taskUtil) { + this._taskUtil = new TaskUtil({ + projectBuildContext: this + }); + } + + return this._taskUtil; + } + + async getDependenciesReader({includeTestResources = false} = {}) { + return this._buildContest.getDependenciesReader({includeTestResources}); + } +} + +module.exports = ProjectBuildContext; diff --git a/lib/buildHelpers/composeTaskList.js b/lib/buildHelpers/composeTaskList.js new file mode 100644 index 000000000..52184c051 --- /dev/null +++ b/lib/buildHelpers/composeTaskList.js @@ -0,0 +1,126 @@ +const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); + +// Set of tasks for development +const devTasks = [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "buildThemes" +]; +/** + * Creates the list of tasks to be executed by the build process + * + * Sets specific tasks to be disabled by default, these tasks need to be included explicitly. + * Based on the selected build mode (dev|selfContained|preload), different tasks are enabled. + * Tasks can be enabled or disabled. The wildcard * is also supported and affects all tasks. + * + * @private + * @param {string[]} allTasks + * @param {object} parameters + * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks + * @param {boolean} parameters.selfContained + * True if a the build should be self-contained or false for prelead build bundles + * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed + * @param {Array} parameters.includedTasks Task list to be included from build + * @param {Array} parameters.excludedTasks Task list to be excluded from build + * @returns {Array} Return a task list for the builder + */ +module.exports = function composeTaskList(allTasks, {dev, selfContained, jsdoc, includedTasks, excludedTasks}) { + let selectedTasks = allTasks.reduce((list, key) => { + list[key] = true; + return list; + }, {}); + + // Exclude non default tasks + selectedTasks.generateManifestBundle = false; + selectedTasks.generateStandaloneAppBundle = false; + selectedTasks.transformBootstrapHtml = false; + selectedTasks.generateJsdoc = false; + selectedTasks.executeJsdocSdkTransformation = false; + selectedTasks.generateCachebusterInfo = false; + selectedTasks.generateApiIndex = false; + selectedTasks.generateThemeDesignerResources = false; + + // Disable generateResourcesJson due to performance. + // When executed it analyzes each module's AST and therefore + // takes up much time (~10% more) + selectedTasks.generateResourcesJson = false; + + if (selfContained) { + // No preloads, bundle only + selectedTasks.generateComponentPreload = false; + selectedTasks.generateStandaloneAppBundle = true; + selectedTasks.transformBootstrapHtml = true; + selectedTasks.generateLibraryPreload = false; + } + + // TODO 3.0: exclude generateVersionInfo if not --all is used + + if (jsdoc) { + // Include JSDoc tasks + selectedTasks.generateJsdoc = true; + selectedTasks.executeJsdocSdkTransformation = true; + selectedTasks.generateApiIndex = true; + + // Include theme build as required for SDK + selectedTasks.buildThemes = true; + + // Exclude all tasks not relevant to JSDoc generation + selectedTasks.replaceCopyright = false; + selectedTasks.replaceVersion = false; + selectedTasks.replaceBuildtime = false; + selectedTasks.generateComponentPreload = false; + selectedTasks.generateLibraryPreload = false; + selectedTasks.generateLibraryManifest = false; + selectedTasks.minify = false; + selectedTasks.generateFlexChangesBundle = false; + selectedTasks.generateManifestBundle = false; + } + + // Only run essential tasks in development mode, it is not desired to run time consuming tasks during development. + if (dev) { + // Overwrite all other tasks with noop promise + Object.keys(selectedTasks).forEach((key) => { + if (devTasks.indexOf(key) === -1) { + selectedTasks[key] = false; + } + }); + } + + // Exclude tasks + for (let i = 0; i < excludedTasks.length; i++) { + const taskName = excludedTasks[i]; + if (taskName === "*") { + Object.keys(selectedTasks).forEach((sKey) => { + selectedTasks[sKey] = false; + }); + break; + } + if (selectedTasks[taskName] === true) { + selectedTasks[taskName] = false; + } else if (typeof selectedTasks[taskName] === "undefined") { + log.warn(`Unable to exclude task '${taskName}': Task is unknown`); + } + } + + // Include tasks + for (let i = 0; i < includedTasks.length; i++) { + const taskName = includedTasks[i]; + if (taskName === "*") { + Object.keys(selectedTasks).forEach((sKey) => { + selectedTasks[sKey] = true; + }); + break; + } + if (selectedTasks[taskName] === false) { + selectedTasks[taskName] = true; + } else if (typeof selectedTasks[taskName] === "undefined") { + log.warn(`Unable to include task '${taskName}': Task is unknown`); + } + } + + // Filter only for tasks that will be executed + selectedTasks = Object.keys(selectedTasks).filter((task) => selectedTasks[task]); + + return selectedTasks; +}; diff --git a/lib/builder.js b/lib/builder.js new file mode 100644 index 000000000..96e7c7aab --- /dev/null +++ b/lib/builder.js @@ -0,0 +1,226 @@ +const {promisify} = require("util"); +const rimraf = promisify(require("rimraf")); +const resourceFactory = require("@ui5/fs").resourceFactory; +const log = require("@ui5/logger").getGroupLogger("builder"); +const BuildContext = require("./buildHelpers/BuildContext"); +const getBuildDefinitionInstance = require("./buildDefinitions/getInstance"); + +async function executeCleanupTasks(buildContext) { + log.info("Executing cleanup tasks..."); + await buildContext.executeCleanupTasks(); +} + +function registerCleanupSigHooks(buildContext) { + function createListener(exitCode) { + return function() { + // Asynchronously cleanup resources, then exit + executeCleanupTasks(buildContext).then(() => { + process.exit(exitCode); + }); + }; + } + + const processSignals = { + "SIGHUP": createListener(128 + 1), + "SIGINT": createListener(128 + 2), + "SIGTERM": createListener(128 + 15), + "SIGBREAK": createListener(128 + 21) + }; + + for (const signal of Object.keys(processSignals)) { + process.on(signal, processSignals[signal]); + } + + // == TO BE DISCUSSED: Also cleanup for unhandled rejections and exceptions? + // Add additional events like signals since they are registered on the process + // event emitter in a similar fashion + // processSignals["unhandledRejection"] = createListener(1); + // process.once("unhandledRejection", processSignals["unhandledRejection"]); + // processSignals["uncaughtException"] = function(err, origin) { + // const fs = require("fs"); + // fs.writeSync( + // process.stderr.fd, + // `Caught exception: ${err}\n` + + // `Exception origin: ${origin}` + // ); + // createListener(1)(); + // }; + // process.once("uncaughtException", processSignals["uncaughtException"]); + + return processSignals; +} + +function deregisterCleanupSigHooks(signals) { + for (const signal of Object.keys(signals)) { + process.removeListener(signal, signals[signal]); + } +} + +/** + * Calculates the elapsed build time and returns a prettified output + * + * @private + * @param {Array} startTime Array provided by process.hrtime() + * @returns {string} Difference between now and the provided time array as formatted string + */ +function getElapsedTime(startTime) { + const prettyHrtime = require("pretty-hrtime"); + const timeDiff = process.hrtime(startTime); + return prettyHrtime(timeDiff); +} + +/** + * Configures the project build and starts it. + * + * @public + * @param {object} parameters Parameters + * @param {module:@ui5/project.graph.ProjectGraph} parameters.graph Project graph as generated by the + * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @param {string} parameters.destPath Target path + * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build + * @param {boolean} [parameters.buildDependencies=false] Decides whether project dependencies are built as well + * @param {Array.} [parameters.includedDependencies=[]] + * List of build dependencies to be included if buildDependencies is true + * @param {Array.} [parameters.excludedDependencies=[]] + * List of build dependencies to be excluded if buildDependencies is true. + * If the wildcard '*' is provided, only the included dependencies will be built. + * @param {boolean} [parameters.dev=false] + * Decides whether a development build should be activated (skips non-essential and time-intensive tasks) + * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build + * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation + * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build + * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included + * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. + * If the wildcard '*' is provided, only the included tasks will be executed. + * @param {Array.} [parameters.devExcludeProject=[]] List of projects to be excluded from development build + * @returns {Promise} Promise resolving to undefined once build has finished + */ +module.exports = async function({ + graph, destPath, cleanDest = false, + buildDependencies = false, includedDependencies = [], excludedDependencies = [], + dev = false, selfContained = false, cssVariables = false, jsdoc = false, + includedTasks = [], excludedTasks = [], devExcludeProject = [] +}) { + if (graph.isSealed()) { + throw new Error( + `Can not build project graph with root node ${this._rootProjectName}: Graph has already been sealed`); + } + + const startTime = process.hrtime(); + log.info(`Starting build of project ${graph.getRoot().getName()}...`); + log.verbose(` Target directory: ${destPath}`); + log.verbose(` Including dependencies: ${buildDependencies}`); + + const buildParams = {dev, selfContained, jsdoc, includedTasks, excludedTasks}; + + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: destPath, + virBasePath: "/" + }); + + const buildContext = new BuildContext({ + graph, + options: { + cssVariables: cssVariables + } + }); + const cleanupSigHooks = registerCleanupSigHooks(buildContext); + function projectFilter(projectName) { + function projectMatchesAny(deps) { + return deps.some((dep) => dep instanceof RegExp ? + dep.test(projectName) : dep === projectName); + } + + // if everything is included, this overrules exclude lists + if (includedDependencies.includes("*")) return true; + let test = !excludedDependencies.includes("*"); // exclude everything? + + if (test && projectMatchesAny(excludedDependencies)) { + test = false; + } + if (!test && projectMatchesAny(includedDependencies)) { + test = true; + } + + return test; + } + + // Count total number of projects to build + const projectCount = graph.getAllProjects().filter(function([projectName]) { + return projectFilter(projectName); + }).length; + + const buildLogger = log.createTaskLogger("🛠 ", projectCount); + + try { + if (cleanDest) { + await rimraf(destPath); + } + + await graph.traverseDepthFirst(async function({project, getDependencies}) { + buildLogger.startWork(`Building project ${project.getName()}...`); + + const projectContext = buildContext.createProjectContext({ + project + }); + + const readers = []; + await graph.traverseBreadthFirst(async function({project: dep}) { + readers.push(dep.getReader({ + includeTestResources: true + })); + }, project.getName()); + + const dependencies = resourceFactory.createReaderCollection({ + name: `Dependency reader collection for project ${project.getName()}`, + readers + }); + + const builder = getBuildDefinitionInstance({ + graph, + project, + taskUtil: projectContext.getTaskUtil(), + parentLogger: log, + resourceCollections: { + workspace: project.getWorkspace(), + dependencies, + } + }); + await builder.build(buildParams); + log.verbose("Finished building project %s. Writing out files...", project.getName()); + buildLogger.completeWork(1); + + // let targetReader; + // if (projectContext.isRootProject() && project.getType() === "application") { + // targetReader = project.getReader({ + // style: "flat" + // }); + // } else { + // targetReader = project.getReader({ + // style: "buildtime" + // }); + // } + + const resources = await project.getReader({ + style: "runtime" + }).byGlob("/**/*"); + const tagCollection = projectContext.getResourceTagCollection(); + + await Promise.all(resources.map((resource) => { + if (tagCollection.getTag(resource, projectContext.STANDARD_TAGS.OmitFromBuildResult)) { + log.verbose(`Skipping write of resource tagged as "OmitFromBuildResult": ` + + resource.getPath()); + return; // Skip target write for this resource + } + return fsTarget.write(resource); + })); + }); + log.info(`Build succeeded in ${getElapsedTime(startTime)}`); + } catch (err) { + log.error(`Build failed in ${getElapsedTime(startTime)}`); + throw err; + } finally { + deregisterCleanupSigHooks(cleanupSigHooks); + await executeCleanupTasks(buildContext); + } +}; diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 83276c8d8..591420a31 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -11,6 +11,7 @@ const log = require("@ui5/logger").getLogger("graph:Module"); const DEFAULT_CONFIG_PATH = "ui5.yaml"; const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; +const SAP_LEGACY_LIBRARIES = ["sap.ui.core"]; function clone(obj) { return JSON.parse(JSON.stringify(obj)); @@ -129,11 +130,19 @@ class Module { }); } + // Patch configs configs.forEach((configuration) => { if (configuration.kind === "project" && configuration.type === "library" && - configuration.metadata && configuration.metadata.name && - SAP_THEMES_NS_EXEMPTIONS.includes(configuration.metadata.name)) { - configuration.type = "theme-library"; + configuration.metadata && configuration.metadata.name) { + const libraryName = configuration.metadata.name; + // Old theme-libraries where configured as type "library" + if (SAP_THEMES_NS_EXEMPTIONS.includes(libraryName)) { + configuration.type = "theme-library"; + } + // Old legacy-libraries where configured as type "library" + if (SAP_LEGACY_LIBRARIES.includes(libraryName)) { + configuration.type = "legacy-library"; + } } }); diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 9dd777ff7..1228aba17 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -29,7 +29,7 @@ class ProjectGraph { /** * @public - * @returns {module:@ui5/project.specification.Project} Root project + * @returns {module:@ui5/project.specifications.Project} Root project */ getRoot() { const rootProject = this._projects[this._rootProjectName]; @@ -41,7 +41,7 @@ class ProjectGraph { /** * @public - * @param {module:@ui5/project.specification.Project} project Project which should be added to the graph + * @param {module:@ui5/project.specifications.Project} project Project which should be added to the graph * @param {boolean} [ignoreDuplicates=false] Whether an error should be thrown when a duplicate project is added */ addProject(project, ignoreDuplicates) { @@ -69,13 +69,22 @@ class ProjectGraph { /** * @public * @param {string} projectName Name of the project to retrieve - * @returns {module:@ui5/project.specification.project|undefined} + * @returns {module:@ui5/project.specifications.Project|undefined} * project instance or undefined if the project is unknown to the graph */ getProject(projectName) { return this._projects[projectName]; } + /** + * @public + * @returns {object} + */ + getAllProjects() { + return Object.entries(this._projects); + } + + /** * @public * @param {module:@ui5/project.specification.Extension} extension Extension which should be available in the graph @@ -108,7 +117,7 @@ class ProjectGraph { } getAllExtensions() { - return this._extensions; + return Object.entries(this._extensions); } /** @@ -453,6 +462,8 @@ class ProjectGraph { throw new Error(`Project graph with root node ${this._rootProjectName} has been sealed`); } } + + // TODO: introduce function to check for dangling nodes/consistency in general? } function mergeMap(target, source) { diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index 4833c90d5..083a83eef 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -49,10 +49,29 @@ class ComponentProject extends Project { this._config.builder.componentPreload.namespaces || []; } + /** + * @public + */ + getComponentPreloadExcludes() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.excludes || []; + } + + /** + * @public + */ getJsdocExcludes() { return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; } + /** + * @public + */ + getMinificationExcludes() { + return this._config.builder && this._config.builder.minification && + this._config.builder.minification.excludes || []; + } + /** * @public */ @@ -60,6 +79,14 @@ class ComponentProject extends Project { return this._config.builder && this._config.builder.bundles || []; } + /** + * @public + */ + getPropertiesFileSourceEncoding() { + return this._config.resources && this._config.resources.configuration && + this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; + } + /* === Resource Access === */ /** @@ -97,7 +124,7 @@ class ComponentProject extends Project { } if (this._isRuntimeNamespaced) { // Same as buildtime - return this.getReader({style: "buildtime", includeTestResources: false}); + return this.getReader(); } reader = this._getFlatSourceReader("/"); break; @@ -111,13 +138,10 @@ class ComponentProject extends Project { throw new Error(`Unknown path mapping style ${style}`); } - const writer = this._getWriter({ + reader = this._addWriter(reader, { style, includeTestResources }); - return resourceFactory.createReaderCollectionPrioritized({ - name: `Reader/Writer collection for project ${this.getName()}`, - readers: [writer, reader] - }); + return reader; } /** @@ -170,25 +194,26 @@ class ComponentProject extends Project { includeTestResources }); - const writer = this._getWriter({ - style: "buildtime", - includeTestResources - }); + const writer = this._getWriter(); return resourceFactory.createWorkspace({ reader, writer }); } - _getWriter({style = "buildtime", includeTestResources = false} = {}) { + _getWriter() { if (!this._writer) { // writer is always of style "buildtime" and may always include test resources this._writer = resourceFactory.createAdapter({ - virBasePath: "/" + virBasePath: "/", + project: this }); } + return this._writer; + } - let writer = this._writer; + _addWriter(reader, {style = "buildtime", includeTestResources = false} = {}) { + let writer = this._getWriter(); if (!includeTestResources) { // If no test-resources are requested, filter them out writer = writer.filter((resource) => { @@ -199,7 +224,7 @@ class ComponentProject extends Project { switch (style) { case "buildtime": { // Writer already uses buildtime style - return writer; + break; } case "runtime": { if (includeTestResources) { @@ -207,7 +232,7 @@ class ComponentProject extends Project { } if (this._isRuntimeNamespaced) { // Same as buildtime - return this._getWriter({style: "buildtime", includeTestResources}); + return this._addWriter(reader, {includeTestResources}); } // Rewrite paths from "runtime" to "buildtime" @@ -215,21 +240,27 @@ class ComponentProject extends Project { linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); - return writer; + break; } case "flat": { if (includeTestResources) { throw new Error(`Readers of style "flat" can't include test resources`); } // Rewrite paths from "flat" to "buildtime" - return writer.link({ + writer = writer.link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); + break; } default: throw new Error(`Unknown path mapping style ${style}`); } + + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [writer, reader] + }); } /* === Internals === */ diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 5a51f7655..b1150e681 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -185,6 +185,9 @@ class Specification { case "library": { return createAndInitializeSpec("Library", params); } + case "legacy-library": { + return createAndInitializeSpec("LegacyLibrary", params); + } case "theme-library": { return createAndInitializeSpec("ThemeLibrary", params); } diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index b43ebdb95..ab4347ccb 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -15,14 +15,6 @@ class Application extends ComponentProject { /* === Attributes === */ - /** - * @public - */ - getPropertiesFileSourceEncoding() { - return this._config.resources && this._config.resources.configuration && - this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; - } - getCachebusterSignatureType() { return this._config.builder && this._config.builder.cachebuster && this._config.builder.cachebuster.signatureType || "time"; @@ -39,7 +31,8 @@ class Application extends ComponentProject { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath, - name: `Source reader for application project ${this.getName()}` + name: `Source reader for application project ${this.getName()}`, + project: this }); } @@ -51,7 +44,8 @@ class Application extends ComponentProject { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath: "/", - name: `Source reader for application project ${this.getName()}` + name: `Source reader for application project ${this.getName()}`, + project: this }); } diff --git a/lib/specifications/types/LegacyLibrary.js b/lib/specifications/types/LegacyLibrary.js new file mode 100644 index 000000000..2c3ec89ac --- /dev/null +++ b/lib/specifications/types/LegacyLibrary.js @@ -0,0 +1,60 @@ +const fsPath = require("path"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const Library = require("./Library"); + +/** + * Legacy UI5 library with resources outside its namespace + */ +class LegacyLibrary extends Library { + constructor(parameters) { + super(parameters); + } + + /* === Attributes === */ + + /* === Resource Access === */ + /* + * + * Get a resource reader for the sources of the project (excluding any test resources) + * In the future the path structure can be flat or namespaced depending on the project + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/", + name: `Source reader for library project ${this.getName()}`, + project: this + }); + } + + _getFlatSourceReader(virBasePath = "/") { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: this._stripNamespace(virBasePath), + name: `Source reader for library project ${this.getName()}`, + project: this + }); + } + + _getFlatTestReader(virBasePath = "/") { + if (!this._testPathExists) { + return null; + } + const testReader = resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._testPath), + virBasePath: this._stripNamespace(virBasePath), + name: `Runtime test-resources reader for library project ${this.getName()}`, + project: this + }); + return testReader; + } + + _stripNamespace(string) { + return string.replace(this._namespace + "/", ""); + } +} + +module.exports = LegacyLibrary; diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index e6d53185d..f080368ab 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -19,15 +19,6 @@ class Library extends ComponentProject { } /* === Attributes === */ - - /** - * @public - */ - getPropertiesFileSourceEncoding() { - return this._config.resources && this._config.resources.configuration && - this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; - } - /** * @public */ @@ -49,7 +40,8 @@ class Library extends ComponentProject { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/", - name: `Source reader for library project ${this.getName()}` + name: `Source reader for library project ${this.getName()}`, + project: this }); } @@ -58,7 +50,8 @@ class Library extends ComponentProject { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath, ...this._namespace.split("/")), virBasePath, - name: `Source reader for library project ${this.getName()}` + name: `Source reader for library project ${this.getName()}`, + project: this }); } @@ -69,7 +62,8 @@ class Library extends ComponentProject { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath, ...this._namespace.split("/")), virBasePath, - name: `Runtime test-resources reader for library project ${this.getName()}` + name: `Runtime test-resources reader for library project ${this.getName()}`, + project: this }); return testReader; } diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 1a78e9f3d..93dcd7b1d 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -88,7 +88,8 @@ class Module extends Project { return { name: `'${relFsPath}'' reader for moduleproject ${this.getName()}`, virBasePath, - fsBasePath: fsPath.join(this.getPath(), relFsPath) + fsBasePath: fsPath.join(this.getPath(), relFsPath), + project: this }; })); } else { @@ -100,7 +101,8 @@ class Module extends Project { this._paths = [{ name: `Root reader for module project ${this.getName()}`, virBasePath: "/", - fsBasePath: this.getPath() + fsBasePath: this.getPath(), + project: this }]; } } diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index 0afc9ebad..a8a481c82 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -15,6 +15,9 @@ class ThemeLibrary extends Project { /** * @public */ + getCopyright() { + return this._config.metadata.copyright; + } /* === Resource Access === */ /** @@ -27,7 +30,8 @@ class ThemeLibrary extends Project { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/", - name: `Source reader for theme-library project ${this.getName()}` + name: `Source reader for theme-library project ${this.getName()}`, + project: this }); } @@ -43,22 +47,29 @@ class ThemeLibrary extends Project { let reader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/resources/", - name: `Runtime resources reader for theme-library project ${this.getName()}` + name: `Runtime resources reader for theme-library project ${this.getName()}`, + project: this }); if (includeTestResources && this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", - name: `Runtime test-resources reader for theme-library project ${this.getName()}` + name: `Runtime test-resources reader for theme-library project ${this.getName()}`, + project: this }); reader = resourceFactory.createReaderCollection({ name: `Reader collection for theme-library project ${this.getName()}`, readers: [reader, testReader] }); } - const writer = this._getWriter({ - includeTestResources - }); + let writer = this._getWriter(); + + if (!includeTestResources) { + // If no test-resources are requested, filter them out + writer = writer.filter((resource) => { + return !resource.getPath().startsWith("/test-resources/"); + }); + } return resourceFactory.createReaderCollectionPrioritized({ name: `Reader/Writer collection for project ${this.getName()}`, readers: [writer, reader] @@ -78,30 +89,22 @@ class ThemeLibrary extends Project { includeTestResources }); - const writer = this._getWriter({ - includeTestResources - }); + const writer = this._getWriter(); return resourceFactory.createWorkspace({ reader, writer }); } - _getWriter({includeTestResources = false} = {}) { + _getWriter() { if (!this._writer) { this._writer = resourceFactory.createAdapter({ - virBasePath: "/" + virBasePath: "/", + project: this }); } - let writer = this._writer; - if (!includeTestResources) { - // If no test-resources are requested, filter them out - writer = writer.filter((resource) => { - return !resource.getPath().startsWith("/test-resources/"); - }); - } - return writer; + return this._writer; } /* === Internals === */ diff --git a/lib/validation/validator.js b/lib/validation/validator.js index bdee5de46..f0c4c7050 100644 --- a/lib/validation/validator.js +++ b/lib/validation/validator.js @@ -37,6 +37,10 @@ class Validator { async validate({config, project, yaml}) { const fnValidate = await this._compileSchema(); + if (config.type === "legacy-library") { + // TODO: Introduce legacy-library schema + return; + } const valid = fnValidate(config); if (!valid) { const ValidationError = require("./ValidationError"); diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index a4bb8e7e1..f28aef241 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -196,6 +196,25 @@ test("getProject: Project is not in graph", async (t) => { t.is(res, undefined, "Should return undefined"); }); +test("getAllProjects", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const project1 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.b"); + graph.addProject(project2); + + const res = graph.getAllProjects(); + t.deepEqual(res, [[ + "application.a", project1 + ], [ + "application.b", project2 + ]], "Should return all projects in a nested array"); +}); + test("add-/getExtension", async (t) => { const {ProjectGraph} = t.context; const graph = new ProjectGraph({ @@ -262,10 +281,11 @@ test("getAllExtensions", async (t) => { const extension2 = await createExtension("extension.b"); graph.addExtension(extension2); const res = graph.getAllExtensions(); - t.deepEqual(res, { - "extension.a": extension1, - "extension.b": extension2 - }, "Should return all extensions"); + t.deepEqual(res, [[ + "extension.a", extension1 + ], [ + "extension.b", extension2 + ]], "Should return all extensions in a nested array"); }); test("declareDependency / getDependencies", async (t) => { From eb95577cfd6729e76ebd55d3f4e221596d683b26 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 30 Apr 2022 11:55:10 +0200 Subject: [PATCH 44/99] Implement build mechanics II --- lib/buildDefinitions/ApplicationBuilder.js | 6 + lib/buildDefinitions/LibraryBuilder.js | 1 - lib/buildHelpers/BuildContext.js | 34 +- lib/buildHelpers/ProjectBuildContext.js | 14 +- lib/buildHelpers/composeProjectList.js | 192 ++++++++++++ lib/builder.js | 67 ++-- lib/graph/ProjectGraph.js | 22 +- lib/graph/helpers/ui5Framework.js | 9 +- lib/specifications/ComponentProject.js | 54 +--- lib/specifications/Project.js | 2 - lib/specifications/types/Library.js | 4 +- lib/specifications/types/ThemeLibrary.js | 22 +- lib/validation/validator.js | 2 +- .../fixtures/library.e/src/library/e/.library | 2 +- .../fixtures/library.f/src/library/f/.library | 11 + test/fixtures/library.f/src/library/f/some.js | 4 + .../fixtures/library.g/src/library/g/.library | 11 + test/fixtures/library.g/src/library/g/some.js | 4 + test/lib/buildHelpers/composeProjectList.js | 293 ++++++++++++++++++ test/lib/graph/ProjectGraph.js | 4 +- test/lib/specifications/types/Application.js | 12 +- test/lib/specifications/types/Library.js | 20 +- 22 files changed, 637 insertions(+), 153 deletions(-) create mode 100644 lib/buildHelpers/composeProjectList.js create mode 100644 test/fixtures/library.f/src/library/f/.library create mode 100644 test/fixtures/library.f/src/library/f/some.js create mode 100644 test/fixtures/library.g/src/library/g/.library create mode 100644 test/fixtures/library.g/src/library/g/some.js create mode 100644 test/lib/buildHelpers/composeProjectList.js diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index 4a0a94ace..06033496d 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -2,6 +2,12 @@ const AbstractBuilder = require("./AbstractBuilder"); class ApplicationBuilder extends AbstractBuilder { addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + // TODO: Refactor API to only addTasks() with actual task name and options + // Common parameters "workspace" and "taskUtil" are always passed to all tasks + // Additionally, tasks can request access to dependencies. In that case, a dependency + // reader is provided too and the build ensures that all relevant dependencies + // are built beforehand + this.addTask("escapeNonAsciiCharacters", async () => { return getTask("escapeNonAsciiCharacters").task({ workspace: resourceCollections.workspace, diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js index 1455bb194..762c1d43e 100644 --- a/lib/buildDefinitions/LibraryBuilder.js +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -146,7 +146,6 @@ class LibraryBuilder extends AbstractBuilder { dependencies: resourceCollections.dependencies, taskUtil, options: { - project: project, excludes: project.getLibraryPreloadExcludes() } }); diff --git a/lib/buildHelpers/BuildContext.js b/lib/buildHelpers/BuildContext.js index 88957b675..4b6301bb4 100644 --- a/lib/buildHelpers/BuildContext.js +++ b/lib/buildHelpers/BuildContext.js @@ -1,5 +1,4 @@ const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; -const resourceFactory = require("@ui5/fs").resourceFactory; const ProjectBuildContext = require("./ProjectBuildContext"); // Note: When adding standard tags, always update the public documentation in TaskUtil @@ -26,9 +25,6 @@ class BuildContext { allowedTags: Object.values(GLOBAL_TAGS) }); this.options = options; - - this._readerCollectionCache = {}; - this._readerCollectionCacheWithTestResources = {}; } getRootProject() { @@ -59,28 +55,16 @@ class BuildContext { return this._resourceTagCollection; } - async getDependenciesReader({projectName, includeTestResources}) { - if (!includeTestResources && this._readerCollectionCache[projectName]) { - return this._readerCollectionCache[projectName]; - } else if (includeTestResources && this._readerCollectionCacheWithTestResources[projectName]) { - return this._readerCollectionCacheWithTestResources[projectName]; - } - const readers = []; - await this._graph.traverseBreadthFirst(async function(dep) { - readers.push(dep.getReader({ - includeTestResources - })); - }, projectName); - const readerCollection = resourceFactory.createReaderCollection({ - name: `Dependency reader collection for project ${projectName}`, - readers - }); - if (!includeTestResources) { - return this._readerCollectionCache[projectName] = readerCollection; - } else if (includeTestResources) { - return this._readerCollectionCacheWithTestResources[projectName] = readerCollection; - } + /** + * Retrieve a single project from the dependency graph + * + * @param {string} projectName Name of the project to retrieve + * @returns {module:@ui5/project.specifications.Project|undefined} + * project instance or undefined if the project is unknown to the graph + */ + getProject(projectName) { + return this._graph.getProject(projectName); } } diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js index 797577e6d..c194acd1c 100644 --- a/lib/buildHelpers/ProjectBuildContext.js +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -67,8 +67,18 @@ class ProjectBuildContext { return this._taskUtil; } - async getDependenciesReader({includeTestResources = false} = {}) { - return this._buildContest.getDependenciesReader({includeTestResources}); + /** + * Retrieve a single project from the dependency graph + * + * @param {string} [projectName] Name of the project to retrieve. Defaults to the project currently being built + * @returns {module:@ui5/project.specifications.Project|undefined} + * project instance or undefined if the project is unknown to the graph + */ + getProject(projectName) { + if (projectName) { + return this._buildContext.getProject(projectName); + } + return this._project; } } diff --git a/lib/buildHelpers/composeProjectList.js b/lib/buildHelpers/composeProjectList.js new file mode 100644 index 000000000..30e4b1263 --- /dev/null +++ b/lib/buildHelpers/composeProjectList.js @@ -0,0 +1,192 @@ +const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); + +/** + * Creates an object containing the flattened project dependency tree. Each dependency is defined as an object key while + * its value is an array of all of its transitive dependencies. + * + * @param {module:@ui5/project.graph.ProjectGraph} graph + * @returns {Promise>} A promise resolving to an object with dependency names as + * key and each with an array of its transitive dependencies as value + */ +async function getFlattenedDependencyTree(graph) { + const dependencyMap = {}; + const rootName = graph.getRoot().getName(); + + await graph.traverseDepthFirst(({project, getDependencies}) => { + if (project.getName() === rootName) { + // Skip root project + return; + } + const projectDeps = []; + getDependencies().forEach((dep) => { + const depName = dep.getName(); + projectDeps.push(depName); + if (dependencyMap[depName]) { + projectDeps.push(...dependencyMap[depName]); + } + }); + dependencyMap[project.getName()] = projectDeps; + }); + return dependencyMap; +} + +/** + * Creates dependency lists for 'includedDependencies' and 'excludedDependencies'. Regular expressions are directly + * applied to a list of all project dependencies so that they don't need to be evaluated in later processing steps. + * Generally, includes are handled with a higher priority than excludes. Additionally, operations for processing + * transitive dependencies are handled with a lower priority than explicitly mentioned dependencies. The default + * dependencies set in the build settings are appended in the end. + * + * The priority of the various dependency lists is applied in the following order, but note that a later list can't + * overrule earlier ones: + *
    + *
  1. includeDependency, includeDependencyRegExp
  2. + *
  3. excludeDependency, excludeDependencyRegExp
  4. + *
  5. includeDependencyTree
  6. + *
  7. excludeDependencyTree
  8. + *
  9. defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree
  10. + *
+ * + * @param {object} parameters Parameters + * @param {object} parameters.graph Project tree as generated by the + * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @param {boolean} parameters.includeAllDependencies Whether all dependencies should be part of the build result + * This has the lowest priority and basically includes all remaining (not excluded) projects as include + * @param {string[]} parameters.includeDependency The dependencies to be considered in 'includedDependencies'; the + * "*" character can be used as wildcard for all dependencies and is an alias for the CLI option "--all" + * @param {string[]} parameters.includeDependencyRegExp Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in 'includedDependencies' + * @param {string[]} parameters.includeDependencyTree The dependencies to be considered in 'includedDependencies'; + * transitive dependencies are also appended + * @param {string[]} parameters.excludeDependency The dependencies to be considered in 'excludedDependencies' + * @param {string[]} parameters.excludeDependencyRegExp Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in 'excludedDependencies' + * @param {string[]} parameters.excludeDependencyTree The dependencies to be considered in 'excludedDependencies'; + * transitive dependencies are also appended + * @param {string[]} parameters.defaultIncludeDependency Same as 'includeDependency' parameter; used for build + * settings + * @param {string[]} parameters.defaultIncludeDependencyRegExp Same as 'includeDependencyRegExp' parameter; used + * for build settings + * @param {string[]} parameters.defaultIncludeDependencyTree Same as 'includeDependencyTree' parameter; used for + * build settings + * @returns {{includedDependencies:string[],excludedDependencies:string[]}} An object containing the + * 'includedDependencies' and 'excludedDependencies' + */ +async function createDependencyLists({ + graph, includeAllDependencies = false, + includeDependency = [], includeDependencyRegExp = [], includeDependencyTree = [], + excludeDependency = [], excludeDependencyRegExp = [], excludeDependencyTree = [], + defaultIncludeDependency = [], defaultIncludeDependencyRegExp = [], defaultIncludeDependencyTree = [] +}) { + if ( + !includeDependency.length && !includeDependencyRegExp.length && !includeDependencyTree.length && + !excludeDependency.length && !excludeDependencyRegExp.length && !excludeDependencyTree.length && + !defaultIncludeDependency.length && !defaultIncludeDependencyRegExp.length && + !defaultIncludeDependencyTree.length + ) { + return {includedDependencies: [], excludedDependencies: []}; + } + + const flattenedDependencyTree = await getFlattenedDependencyTree(graph); + + function isExcluded(excludeList, depName) { + return excludeList && excludeList.has(depName); + } + function processDependencies({targetList, dependencies, dependenciesRegExp = [], excludeList, handleSubtree}) { + if (handleSubtree && dependenciesRegExp.length) { + throw new Error("dependenciesRegExp can't be combined with handleSubtree:true option"); + } + dependencies.forEach((depName) => { + if (depName === "*") { + targetList.add(depName); + } else if (flattenedDependencyTree[depName]) { + if (!isExcluded(excludeList, depName)) { + targetList.add(depName); + } + if (handleSubtree) { + flattenedDependencyTree[depName].forEach((dep) => { + if (!isExcluded(excludeList, dep)) { + targetList.add(dep); + } + }); + } + } else { + log.warn( + `Could not find dependency "${depName}" for project ${graph.getRoot().getName()}. ` + + `Dependency filter is ignored`); + } + }); + dependenciesRegExp.map((exp) => new RegExp(exp)).forEach((regExp) => { + for (const depName in flattenedDependencyTree) { + if (regExp.test(depName) && !isExcluded(excludeList, depName)) { + targetList.add(depName); + } + } + }); + } + + const includedDependencies = new Set(); + const excludedDependencies = new Set(); + + // add dependencies defined in includeDependency and includeDependencyRegExp to the list of includedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependency, + dependenciesRegExp: includeDependencyRegExp + }); + // add dependencies defined in excludeDependency and excludeDependencyRegExp to the list of excludedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependency, + dependenciesRegExp: excludeDependencyRegExp + }); + // add dependencies defined in includeDependencyTree with their transitive dependencies to the list of + // includedDependencies; due to prioritization only those dependencies are added which are not excluded + // by excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + // add dependencies defined in excludeDependencyTree with their transitive dependencies to the list of + // excludedDependencies; due to prioritization only those dependencies are added which are not excluded + // by includedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependencyTree, + excludeList: includedDependencies, + handleSubtree: true + }); + // due to the lower priority only add the dependencies defined in build settings if they are not excluded + // by any other dependency defined in excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependency, + dependenciesRegExp: defaultIncludeDependencyRegExp, + excludeList: excludedDependencies + }); + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + + if (includeAllDependencies) { + // If requested, add all dependencies not excluded to include set + Object.keys(flattenedDependencyTree).forEach((depName) => { + if (!isExcluded(excludedDependencies, depName)) { + includedDependencies.add(depName); + } + }); + } + + return { + includedDependencies: Array.from(includedDependencies), + excludedDependencies: Array.from(excludedDependencies) + }; +} + +module.exports = createDependencyLists; +module.exports._getFlattenedDependencyTree = getFlattenedDependencyTree; diff --git a/lib/builder.js b/lib/builder.js index 96e7c7aab..dc80c29ed 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -3,6 +3,7 @@ const rimraf = promisify(require("rimraf")); const resourceFactory = require("@ui5/fs").resourceFactory; const log = require("@ui5/logger").getGroupLogger("builder"); const BuildContext = require("./buildHelpers/BuildContext"); +const composeProjectList = require("./buildHelpers/composeProjectList"); const getBuildDefinitionInstance = require("./buildDefinitions/getInstance"); async function executeCleanupTasks(buildContext) { @@ -78,40 +79,51 @@ function getElapsedTime(startTime) { * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} * @param {string} parameters.destPath Target path * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build - * @param {boolean} [parameters.buildDependencies=false] Decides whether project dependencies are built as well * @param {Array.} [parameters.includedDependencies=[]] - * List of build dependencies to be included if buildDependencies is true + * List of names of projects to include in the build result + * If the wildcard '*' is provided, all dependencies will be included in the build result. * @param {Array.} [parameters.excludedDependencies=[]] - * List of build dependencies to be excluded if buildDependencies is true. + * List of names of projects to exclude from the build result. * If the wildcard '*' is provided, only the included dependencies will be built. - * @param {boolean} [parameters.dev=false] - * Decides whether a development build should be activated (skips non-essential and time-intensive tasks) + * @param {object} [parameters.complexDependencyIncludes] TODO 3.0 * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. - * @param {Array.} [parameters.devExcludeProject=[]] List of projects to be excluded from development build * @returns {Promise} Promise resolving to undefined once build has finished */ module.exports = async function({ graph, destPath, cleanDest = false, - buildDependencies = false, includedDependencies = [], excludedDependencies = [], - dev = false, selfContained = false, cssVariables = false, jsdoc = false, - includedTasks = [], excludedTasks = [], devExcludeProject = [] + includedDependencies = [], excludedDependencies = [], + complexDependencyIncludes, + selfContained = false, cssVariables = false, jsdoc = false, + includedTasks = [], excludedTasks = [], }) { if (graph.isSealed()) { throw new Error( `Can not build project graph with root node ${this._rootProjectName}: Graph has already been sealed`); } + if (complexDependencyIncludes) { + if (includedDependencies.length || excludedDependencies.length) { + throw new Error( + "Parameter 'complexDependencyIncludes' can't be used in conjunction " + + "with parameters 'includedDependencies' or 'excludedDependencies"); + } + const deps = await composeProjectList(complexDependencyIncludes); + includedDependencies = deps.includedDependencies; + excludedDependencies = deps.excludedDependencies; + } + const startTime = process.hrtime(); log.info(`Starting build of project ${graph.getRoot().getName()}...`); log.verbose(` Target directory: ${destPath}`); - log.verbose(` Including dependencies: ${buildDependencies}`); + log.verbose(` Including dependencies: ${includedDependencies.join(", ")}`); + log.verbose(` Excluding dependencies: ${excludedDependencies.join(", ")}`); - const buildParams = {dev, selfContained, jsdoc, includedTasks, excludedTasks}; + const buildParams = {selfContained, jsdoc, includedTasks, excludedTasks}; const fsTarget = resourceFactory.createAdapter({ fsBasePath: destPath, @@ -158,18 +170,23 @@ module.exports = async function({ } await graph.traverseDepthFirst(async function({project, getDependencies}) { - buildLogger.startWork(`Building project ${project.getName()}...`); - const projectContext = buildContext.createProjectContext({ project }); + if (!buildDependencies && !projectContext.isRootProject()) { + return; + } + buildLogger.startWork(`Building project ${project.getName()}...`); const readers = []; - await graph.traverseBreadthFirst(async function({project: dep}) { + await graph.traverseBreadthFirst(project.getName(), async function({project: dep}) { + if (dep.getName() === project.getName()) { + return; + } readers.push(dep.getReader({ includeTestResources: true })); - }, project.getName()); + }); const dependencies = resourceFactory.createReaderCollection({ name: `Dependency reader collection for project ${project.getName()}`, @@ -190,20 +207,7 @@ module.exports = async function({ log.verbose("Finished building project %s. Writing out files...", project.getName()); buildLogger.completeWork(1); - // let targetReader; - // if (projectContext.isRootProject() && project.getType() === "application") { - // targetReader = project.getReader({ - // style: "flat" - // }); - // } else { - // targetReader = project.getReader({ - // style: "buildtime" - // }); - // } - - const resources = await project.getReader({ - style: "runtime" - }).byGlob("/**/*"); + const resources = await project.getReader({style: "runtime"}).byGlob("/**/*"); const tagCollection = projectContext.getResourceTagCollection(); await Promise.all(resources.map((resource) => { @@ -224,3 +228,8 @@ module.exports = async function({ await executeCleanupTasks(buildContext); } }; + +module.exports.composeProjectList = function (parameters) { + const composeProjectList = require("./buildHelpers/composeProjectList"); + return composeProjectList(parameters); +}; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 1228aba17..c2ba6bca9 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -67,6 +67,8 @@ class ProjectGraph { } /** + * Retrieve a single project from the dependency graph + * * @public * @param {string} projectName Name of the project to retrieve * @returns {module:@ui5/project.specifications.Project|undefined} @@ -313,10 +315,16 @@ class ProjectGraph { * In case a cycle is detected, an error is thrown * * @public - * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called */ - async traverseBreadthFirst(callback, startName = this._rootProjectName) { + async traverseBreadthFirst(startName, callback) { + if (!callback) { + // Default optional first parameter + callback = startName; + startName = this._rootProjectName; + } + if (!this.getProject(startName)) { throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } @@ -364,10 +372,16 @@ class ProjectGraph { * In case a cycle is detected, an error is thrown * * @public - * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called * @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project + * @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called */ - async traverseDepthFirst(callback, startName = this._rootProjectName) { + async traverseDepthFirst(startName, callback) { + if (!callback) { + // Default optional first parameter + callback = startName; + startName = this._rootProjectName; + } + if (!this.getProject(startName)) { throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js index 86f29e5d5..02ed380aa 100644 --- a/lib/graph/helpers/ui5Framework.js +++ b/lib/graph/helpers/ui5Framework.js @@ -59,9 +59,6 @@ class ProjectProcessor { } const utils = { - isFrameworkProject(project) { - return project.id.startsWith("@openui5/") || project.id.startsWith("@sapui5/"); - }, shouldIncludeDependency({optional, development}, root) { // Root project should include all dependencies // Otherwise only non-optional and non-development dependencies should be included @@ -138,6 +135,12 @@ module.exports = { */ enrichProjectGraph: async function(projectGraph, options = {}) { const rootProject = projectGraph.getRoot(); + + if (rootProject.isFrameworkProject()) { + // Ignoring UI5 Framework libraries in dependencies + return; + } + const frameworkName = rootProject.getFrameworkName(); const frameworkVersion = rootProject.getFrameworkVersion(); if (!frameworkName && !frameworkVersion) { diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index 083a83eef..407a85799 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -98,30 +98,25 @@ class ComponentProject extends Project { * @param {object} [options] * @param {string} [options.style=buildtime] Path style to access resources. Can be "buildtime", "runtime" or "flat" * TODO: describe styles - * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance */ - getReader({style = "buildtime", includeTestResources = false} = {}) { + getReader({style = "buildtime"} = {}) { // TODO: Additional parameter 'includeWorkspace' to include reader to relevant Memory Adapter? // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? let reader; + let testReader; switch (style) { case "buildtime": reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); - if (includeTestResources) { - const testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); - if (testReader) { - reader = resourceFactory.createReaderCollection({ - name: `Reader collection for project ${this.getName()}`, - readers: [reader, testReader] - }); - } + testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); } break; case "runtime": - if (includeTestResources) { - throw new Error(`Readers of style "runtime" can't include test resources`); - } if (this._isRuntimeNamespaced) { // Same as buildtime return this.getReader(); @@ -129,18 +124,13 @@ class ComponentProject extends Project { reader = this._getFlatSourceReader("/"); break; case "flat": - if (includeTestResources) { - throw new Error(`Readers of style "flat" can't include test resources`); - } reader = this._getFlatSourceReader("/"); break; default: throw new Error(`Unknown path mapping style ${style}`); } - reader = this._addWriter(reader, { - style, includeTestResources - }); + reader = this._addWriter(reader, style); return reader; } @@ -183,15 +173,12 @@ class ComponentProject extends Project { * Get a resource reader/writer for accessing and modifying a project's resources * * @public - * @param {object} [options] - * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance */ - getWorkspace({includeTestResources = false} = {}) { + getWorkspace() { // Workspace is always of style "buildtime" const reader = this.getReader({ - style: "buildtime", - includeTestResources + style: "buildtime" }); const writer = this._getWriter(); @@ -203,7 +190,7 @@ class ComponentProject extends Project { _getWriter() { if (!this._writer) { - // writer is always of style "buildtime" and may always include test resources + // writer is always of style "buildtime" this._writer = resourceFactory.createAdapter({ virBasePath: "/", project: this @@ -212,27 +199,17 @@ class ComponentProject extends Project { return this._writer; } - _addWriter(reader, {style = "buildtime", includeTestResources = false} = {}) { + _addWriter(reader, style = "buildtime") { let writer = this._getWriter(); - if (!includeTestResources) { - // If no test-resources are requested, filter them out - writer = writer.filter((resource) => { - return !resource.getPath().startsWith("/test-resources/"); - }); - } - switch (style) { case "buildtime": { // Writer already uses buildtime style break; } case "runtime": { - if (includeTestResources) { - throw new Error(`Readers of style "runtime" can't include test resources`); - } if (this._isRuntimeNamespaced) { // Same as buildtime - return this._addWriter(reader, {includeTestResources}); + return this._addWriter(reader); } // Rewrite paths from "runtime" to "buildtime" @@ -243,9 +220,6 @@ class ComponentProject extends Project { break; } case "flat": { - if (includeTestResources) { - throw new Error(`Readers of style "flat" can't include test resources`); - } // Rewrite paths from "flat" to "buildtime" writer = writer.link({ linkPath: `/`, diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index e6bf1973f..8dc55188a 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -78,8 +78,6 @@ class Project extends Specification { * @param {string} [options.style=buildtime] Path style to access resources. Can be "buildtime", "runtime" or "flat" * TODO: describe styles * This parameter might be ignored by some specifications - * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set - * This parameter might be ignored by some specifications * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ getReader(options) { diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index f080368ab..100790414 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -113,11 +113,11 @@ class Library extends ComponentProject { if (!config.metadata.copyright) { try { - config.metadata.copyrigh = await this.getCopyright(); + config.metadata.copyright = await this._getCopyright(); } catch (err) { // Catch error because copyright is optional // TODO: Make copyright mandatory? - this._log.verbose(err.message); + this._log.verbose(`Failed to get copyright: ${err.message}`); } } diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index a8a481c82..3a0bed094 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -39,18 +39,16 @@ class ThemeLibrary extends Project { * Get a resource reader for accessing the project resources the same way the UI5 runtime would do * * @public - * @param {object} [options] - * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - getReader({includeTestResources=false} = {}) { + getReader() { let reader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/resources/", name: `Runtime resources reader for theme-library project ${this.getName()}`, project: this }); - if (includeTestResources && this._testPathExists) { + if (this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", @@ -62,14 +60,8 @@ class ThemeLibrary extends Project { readers: [reader, testReader] }); } - let writer = this._getWriter(); + const writer = this._getWriter(); - if (!includeTestResources) { - // If no test-resources are requested, filter them out - writer = writer.filter((resource) => { - return !resource.getPath().startsWith("/test-resources/"); - }); - } return resourceFactory.createReaderCollectionPrioritized({ name: `Reader/Writer collection for project ${this.getName()}`, readers: [writer, reader] @@ -80,14 +72,10 @@ class ThemeLibrary extends Project { * Get a resource reader/writer for accessing and modifying a project's resources * * @public - * @param {object} [options] - * @param {boolean} [options.includeTestResources=false] Whether test resources should be included in the result set * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance */ - getWorkspace({includeTestResources = false} = {}) { - const reader = this.getReader({ - includeTestResources - }); + getWorkspace() { + const reader = this.getReader(); const writer = this._getWriter(); return resourceFactory.createWorkspace({ diff --git a/lib/validation/validator.js b/lib/validation/validator.js index f0c4c7050..b97a725e6 100644 --- a/lib/validation/validator.js +++ b/lib/validation/validator.js @@ -37,7 +37,7 @@ class Validator { async validate({config, project, yaml}) { const fnValidate = await this._compileSchema(); - if (config.type === "legacy-library") { + if (config && config.type === "legacy-library") { // TODO: Introduce legacy-library schema return; } diff --git a/test/fixtures/library.e/src/library/e/.library b/test/fixtures/library.e/src/library/e/.library index 20c990700..26ff954f7 100644 --- a/test/fixtures/library.e/src/library/e/.library +++ b/test/fixtures/library.e/src/library/e/.library @@ -1,7 +1,7 @@ - library.d + library.e SAP SE ${copyright} ${version} diff --git a/test/fixtures/library.f/src/library/f/.library b/test/fixtures/library.f/src/library/f/.library new file mode 100644 index 000000000..c45172d48 --- /dev/null +++ b/test/fixtures/library.f/src/library/f/.library @@ -0,0 +1,11 @@ + + + + library.f + SAP SE + ${copyright} + ${version} + + Library F + + diff --git a/test/fixtures/library.f/src/library/f/some.js b/test/fixtures/library.f/src/library/f/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/library.f/src/library/f/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/test/fixtures/library.g/src/library/g/.library b/test/fixtures/library.g/src/library/g/.library new file mode 100644 index 000000000..4d884278e --- /dev/null +++ b/test/fixtures/library.g/src/library/g/.library @@ -0,0 +1,11 @@ + + + + library.g + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/test/fixtures/library.g/src/library/g/some.js b/test/fixtures/library.g/src/library/g/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/library.g/src/library/g/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/test/lib/buildHelpers/composeProjectList.js b/test/lib/buildHelpers/composeProjectList.js new file mode 100644 index 000000000..f98f1b404 --- /dev/null +++ b/test/lib/buildHelpers/composeProjectList.js @@ -0,0 +1,293 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const path = require("path"); +const logger = require("@ui5/logger"); +const generateProjectGraph = require("../../../lib/generateProjectGraph"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); +const libraryFPath = path.join(__dirname, "..", "..", "fixtures", "library.f"); +const libraryGPath = path.join(__dirname, "..", "..", "fixtures", "library.g"); +const libraryDDependerPath = path.join(__dirname, "..", "..", "fixtures", "library.d-depender"); + +test.beforeEach((t) => { + t.context.log = { + warn: sinon.stub() + }; + sinon.stub(logger, "getLogger").callThrough() + .withArgs("buildHelpers:composeProjectList").returns(t.context.log); + t.context.composeProjectList = mock.reRequire("../../../lib/buildHelpers/composeProjectList"); +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test.serial("_getFlattenedDependencyTree", async (t) => { + const {_getFlattenedDependencyTree} = t.context.composeProjectList; + const tree = { // Does not reflect actual dependencies in fixtures + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.e.id", + version: "1.0.0", + path: libraryEPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [{ + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }] + }] + }, { + id: "library.f.id", + version: "1.0.0", + path: libraryFPath, + dependencies: [{ + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }] + }] + }; + const graph = await generateProjectGraph.usingObject({dependencyTree: tree}); + + t.deepEqual(await _getFlattenedDependencyTree(graph), { + "library.e": ["library.d", "library.a", "library.b", "library.c"], + "library.f": ["library.a", "library.b", "library.c"], + "library.d": ["library.a", "library.b", "library.c"], + "library.a": ["library.b", "library.c"], + "library.b": [], + "library.c": [] + }); +}); + +async function assertCreateDependencyLists(t, { + includeAllDependencies, + includeDependency, includeDependencyRegExp, includeDependencyTree, + excludeDependency, excludeDependencyRegExp, excludeDependencyTree, + defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree, + expectedIncludedDependencies, expectedExcludedDependencies +}) { + const tree = { // Does not reflect actual dependencies in fixtures + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [{ + id: "library.e.id", + version: "1.0.0", + path: libraryEPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [] + }, { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }] + }] + }, { + id: "library.f.id", + version: "1.0.0", + path: libraryFPath, + dependencies: [{ + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [] + }, { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [{ + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }] + }, { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + }] + }, { + id: "library.g.id", + version: "1.0.0", + path: libraryGPath, + dependencies: [{ + id: "library.d-depender.id", + version: "1.0.0", + path: libraryDDependerPath, + dependencies: [] + }] + }] + }; + + const graph = await generateProjectGraph.usingObject({dependencyTree: tree}); + + const {includedDependencies, excludedDependencies} = await t.context.composeProjectList({ + graph, + includeAllDependencies, + includeDependency, + includeDependencyRegExp, + includeDependencyTree, + excludeDependency, + excludeDependencyRegExp, + excludeDependencyTree, + defaultIncludeDependency, + defaultIncludeDependencyRegExp, + defaultIncludeDependencyTree + }); + t.deepEqual(includedDependencies, expectedIncludedDependencies, "Correct set of included dependencies"); + t.deepEqual(excludedDependencies, expectedExcludedDependencies, "Correct set of excluded dependencies"); +} + +test.serial("createDependencyLists: only includes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependency: ["library.f", "library.c"], + includeDependencyRegExp: ["^library\\.d$"], + includeDependencyTree: ["library.g"], + expectedIncludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"], + expectedExcludedDependencies: [] + }); +}); + +test.serial("createDependencyLists: only excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + excludeDependency: ["library.f", "library.c"], + excludeDependencyRegExp: ["^library\\.d$"], + excludeDependencyTree: ["library.g"], + expectedIncludedDependencies: [], + expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"] + }); +}); + +test.serial("createDependencyLists: include all + excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: [], + excludeDependency: ["library.f", "library.c"], + excludeDependencyRegExp: ["^library\\.d$"], + excludeDependencyTree: ["library.g"], + expectedIncludedDependencies: ["library.b", "library.a", "library.e"], + expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependencyTree: ["library.f"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd]$"], + expectedIncludedDependencies: ["library.b"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.a"] + }); +}); + +test.serial("createDependencyLists: excludeDependencyTree has lower priority than includes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependency: ["library.f"], + includeDependencyRegExp: ["^library\\.[acd]$"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: ["library.f", "library.d", "library.c", "library.a"], + expectedExcludedDependencies: ["library.b"] + }); +}); + +test.serial("createDependencyLists: include all, exclude tree and include single", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: ["library.f"], + includeDependencyRegExp: ["^library\\.[acd]$"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: [ + "library.f", "library.d", "library.c", "library.a", "library.d-depender", + "library.g", "library.e" + ], + expectedExcludedDependencies: ["library.b"] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has higher priority than excludeDependencyTree", + async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + includeDependencyTree: ["library.f"], + excludeDependencyTree: ["library.f"], + expectedIncludedDependencies: ["library.f", "library.d", "library.a", "library.b", "library.c"], + expectedExcludedDependencies: [] + }); + }); + +test.serial("createDependencyLists: defaultIncludeDependency/RegExp has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + defaultIncludeDependency: ["library.f", "library.c", "library.b"], + defaultIncludeDependencyRegExp: ["^library\\.d$"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], + expectedIncludedDependencies: ["library.b"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + }); +}); +test.serial("createDependencyLists: include all and defaultIncludeDependency/RegExp", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + defaultIncludeDependency: ["library.f", "library.c", "library.b"], + defaultIncludeDependencyRegExp: ["^library\\.d$"], + excludeDependency: ["library.f"], + excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"], + expectedIncludedDependencies: ["library.b", "library.g", "library.e"], + expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"] + }); +}); + +test.serial("createDependencyLists: defaultIncludeDependencyTree has lower priority than excludes", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: false, + defaultIncludeDependencyTree: ["library.f"], + excludeDependencyTree: ["library.a"], + expectedIncludedDependencies: ["library.f", "library.d", "library.c"], + expectedExcludedDependencies: ["library.a", "library.b"] + }); +}); diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index f28aef241..2f4610bad 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -710,7 +710,7 @@ test("traverseBreadthFirst: Custom start node", async (t) => { graph.declareDependency("library.b", "library.c"); const callbackStub = t.context.sinon.stub().resolves(); - await graph.traverseBreadthFirst(callbackStub, "library.b"); + await graph.traverseBreadthFirst("library.b", callbackStub); t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); @@ -899,7 +899,7 @@ test("traverseDepthFirst: Custom start node", async (t) => { graph.declareDependency("library.b", "library.c"); const callbackStub = t.context.sinon.stub().resolves(); - await graph.traverseDepthFirst(callbackStub, "library.b"); + await graph.traverseDepthFirst("library.b", callbackStub); t.is(callbackStub.callCount, 2, "Correct number of projects have been visited"); diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index 4361dbb70..fadadbde2 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -43,7 +43,7 @@ test("getPropertiesFileSourceEncoding: Configuration", async (t) => { "Returned correct default propertiesFileSourceEncoding configuration"); }); -test("Access project resources via reader: buildtime style, no test resources", async (t) => { +test("Access project resources via reader: buildtime style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader(); const resource = await reader.byPath("/resources/id1/manifest.json"); @@ -51,18 +51,10 @@ test("Access project resources via reader: buildtime style, no test resources", t.is(resource.getPath(), "/resources/id1/manifest.json", "Resource has correct path"); }); -test("Access project resources via reader: flat style, no test resources", async (t) => { +test("Access project resources via reader: flat style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader({style: "flat"}); const resource = await reader.byPath("/manifest.json"); t.truthy(resource, "Found the requested resource"); t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); }); - -test("Access project resources via reader: flat style, including test resources", async (t) => { - const project = await Specification.create(basicProjectInput); - const error = t.throws(() => { - project.getReader({style: "flat", includeTestResources: true}); - }); - t.is(error.message, `Readers of style "flat" can't include test resources`, "Correct error message"); -}); diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 604bcecdd..45539d076 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -55,7 +55,7 @@ test("getPropertiesFileSourceEncoding: Configuration", async (t) => { "Returned correct default propertiesFileSourceEncoding configuration"); }); -test("Access project resources via reader: buildtime style, no test resources", async (t) => { +test("Access project resources via reader: buildtime style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader(); const resource = await reader.byPath("/resources/library/d/.library"); @@ -63,7 +63,7 @@ test("Access project resources via reader: buildtime style, no test resources", t.is(resource.getPath(), "/resources/library/d/.library", "Resource has correct path"); }); -test("Access project resources via reader: flat style, no test resources", async (t) => { +test("Access project resources via reader: flat style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader({style: "flat"}); const resource = await reader.byPath("/.library"); @@ -71,32 +71,24 @@ test("Access project resources via reader: flat style, no test resources", async t.is(resource.getPath(), "/.library", "Resource has correct path"); }); -test("Access project resources via reader: buildtime style, including test resources", async (t) => { +test("Access project test-resources via reader: buildtime style, including test resources", async (t) => { const project = await Specification.create(basicProjectInput); - const reader = await project.getReader({style: "buildtime", includeTestResources: true}); + const reader = await project.getReader({style: "buildtime"}); const resource = await reader.byPath("/test-resources/library/d/Test.html"); t.truthy(resource, "Found the requested resource"); t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); }); -test("Access project resources via reader: flat style, including test resources", async (t) => { - const project = await Specification.create(basicProjectInput); - const error = t.throws(() => { - project.getReader({style: "flat", includeTestResources: true}); - }); - t.is(error.message, `Readers of style "flat" can't include test resources`, "Correct error message"); -}); - test("Modify project resources via workspace and access via flat reader", async (t) => { const project = await Specification.create(basicProjectInput); - const workspace = await project.getWorkspace({includeTestResources: true}); + const workspace = await project.getWorkspace(); const workspaceResource = await workspace.byPath("/resources/library/d/.library"); const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); workspaceResource.setString(newContent); await workspace.write(workspaceResource); - const reader = await project.getReader({style: "flat", includeTestResources: false}); + const reader = await project.getReader({style: "flat"}); const readerResource = await reader.byPath("/.library"); t.truthy(readerResource, "Found the requested resource byPath"); t.is(readerResource.getPath(), "/.library", "Resource (byPath) has correct path"); From 620e6d148b81b48b818cf98b40795f06b4d5929d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 3 May 2022 15:14:18 +0200 Subject: [PATCH 45/99] Refactor AbstractReader#addTask API --- lib/buildDefinitions/AbstractBuilder.js | 32 ++- lib/buildDefinitions/ApplicationBuilder.js | 219 ++++++------------- lib/buildDefinitions/LibraryBuilder.js | 229 +++++++------------- lib/buildDefinitions/ThemeLibraryBuilder.js | 71 ++---- lib/buildHelpers/BuildContext.js | 20 +- lib/buildHelpers/ProjectBuildContext.js | 38 +++- lib/buildHelpers/composeProjectList.js | 9 +- lib/builder.js | 46 ++-- lib/specifications/Project.js | 14 ++ test/lib/buildHelpers/composeProjectList.js | 16 ++ 10 files changed, 286 insertions(+), 408 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 7ab728cf1..ec8fbae14 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -33,6 +33,8 @@ class AbstractBuilder { this.project = project; this.graph = graph; + this.taskUtil = taskUtil; + this.resourceCollections = resourceCollections; this.log = parentLogger.createSubLogger(project.type + " " + project.getName(), 0.2); this.taskLog = this.log.createTaskLogger("🔨"); @@ -40,6 +42,7 @@ class AbstractBuilder { this.tasks = {}; this.taskExecutionOrder = []; + this.addStandardTasks({ project, log: this.log, @@ -174,9 +177,12 @@ class AbstractBuilder { * 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 + * @param {object} [parameters] + * @param {boolean} [parameters.requiresDependencies] + * @param {object} [parameters.options] + * @param {Function} [taskFunction] */ - addTask(taskName, taskFunction) { + addTask(taskName, {requiresDependencies = false, options = {}} = {}, taskFunction) { if (this.tasks[taskName]) { throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`); } @@ -184,7 +190,27 @@ class AbstractBuilder { throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` + `It has already been scheduled for execution.`); } - this.tasks[taskName] = taskFunction; + + options.projectName = this.project.getName(); + // TODO: Deprecate "namespace" in favor of "projectNamespace" as already used for custom tasks? + options.projectNamespace = this.project.getNamespace(); + options.namespace = this.project.getNamespace(); + + const params = { + workspace: this.resourceCollections.workspace, + dependencies: this.resourceCollections.dependencies, + taskUtil: this.taskUtil, + options + }; + if (taskFunction) { + this.tasks[taskName] = () => { + return taskFunction(params); + }; + } else { + this.tasks[taskName] = () => { + return getTask(taskName).task(params); + }; + } this.taskExecutionOrder.push(taskName); } diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index 06033496d..3171b15ae 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -2,40 +2,25 @@ const AbstractBuilder = require("./AbstractBuilder"); class ApplicationBuilder extends AbstractBuilder { addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { - // TODO: Refactor API to only addTasks() with actual task name and options - // Common parameters "workspace" and "taskUtil" are always passed to all tasks - // Additionally, tasks can request access to dependencies. In that case, a dependency - // reader is provided too and the build ensures that all relevant dependencies - // are built beforehand - - this.addTask("escapeNonAsciiCharacters", async () => { - return getTask("escapeNonAsciiCharacters").task({ - workspace: resourceCollections.workspace, - options: { - encoding: project.getPropertiesFileSourceEncoding(), - pattern: "/**/*.properties" - } - }); + this.addTask("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } }); - this.addTask("replaceCopyright", async () => { - return getTask("replaceCopyright").task({ - workspace: resourceCollections.workspace, - options: { - copyright: project.getCopyright(), - pattern: "/**/*.{js,json}" - } - }); + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,json}" + } }); - this.addTask("replaceVersion", async () => { - return getTask("replaceVersion").task({ - workspace: resourceCollections.workspace, - options: { - version: project.getVersion(), - pattern: "/**/*.{js,json}" - } - }); + this.addTask("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json}" + } }); // Support rules should not be minified to have readable code in the Support Assistant @@ -46,157 +31,75 @@ class ApplicationBuilder extends AbstractBuilder { this.enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); } } - this.addTask("minify", async () => { - return getTask("minify").task({ - workspace: resourceCollections.workspace, - taskUtil, - options: { - pattern: minificationPattern - } - }); - }); - - this.addTask("generateFlexChangesBundle", async () => { - const generateFlexChangesBundle = getTask("generateFlexChangesBundle").task; - return generateFlexChangesBundle({ - workspace: resourceCollections.workspace, - taskUtil, - options: { - namespace: project.getNamespace() - } - }); + this.addTask("minify", { + options: { + pattern: minificationPattern + } }); - if (project.getNamespace()) { - this.addTask("generateManifestBundle", async () => { - const generateManifestBundle = getTask("generateManifestBundle").task; - return generateManifestBundle({ - workspace: resourceCollections.workspace, - options: { - projectName: project.getName(), - namespace: project.getNamespace() - } - }); - }); - } + this.addTask("generateFlexChangesBundle"); + this.addTask("generateManifestBundle"); const componentPreloadPaths = project.getComponentPreloadPaths(); const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); const componentPreloadExcludes = project.getComponentPreloadNamespaces(); if (componentPreloadPaths.length || componentPreloadNamespaces.length) { - this.addTask("generateComponentPreload", async () => { - return getTask("generateComponentPreload").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName(), - paths: componentPreloadPaths, - namespaces: componentPreloadNamespaces, - excludes: componentPreloadExcludes - } - }); + this.addTask("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes + } }); } else { // Default component preload for application namespace - this.addTask("generateComponentPreload", async () => { - return getTask("generateComponentPreload").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName(), - namespaces: [project.getNamespace()], - excludes: componentPreloadExcludes - } - }); - }); - } - - this.addTask("generateStandaloneAppBundle", async () => { - return getTask("generateStandaloneAppBundle").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, + this.addTask("generateComponentPreload", { options: { - projectName: project.getName(), - namespace: project.getNamespace() + namespaces: [project.getNamespace()], + excludes: componentPreloadExcludes } }); - }); + } - this.addTask("transformBootstrapHtml", async () => { - return getTask("transformBootstrapHtml").task({ - workspace: resourceCollections.workspace, - options: { - projectName: project.getName(), - namespace: project.getNamespace() - } - }); - }); + this.addTask("generateStandaloneAppBundle", {requiresDependencies: true}); + + this.addTask("transformBootstrapHtml"); const bundles = project.getBundles(); if (bundles.length) { - this.addTask("generateBundle", async () => { - return Promise.all(bundles.map((bundle) => { - return getTask("generateBundle").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName(), - bundleDefinition: bundle.bundleDefinition, - bundleOptions: bundle.bundleOptions - } - }); - })); - }); - } - - this.addTask("generateVersionInfo", async () => { - return getTask("generateVersionInfo").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - rootProject: project, - pattern: "/resources/**/.library" - } - }); - }); - - if (project.getNamespace()) { - this.addTask("generateCachebusterInfo", async () => { - return getTask("generateCachebusterInfo").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - namespace: project.getNamespace(), - signatureType: project.getCachebusterSignatureType(), - } + this.addTask("generateBundle", {requiresDependencies: true}, + async ({workspace, dependencies, taskUtil, options}) => { + return Promise.all(bundles.map((bundle) => { + return getTask("generateBundle").task({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + })); }); - }); } - this.addTask("generateApiIndex", async () => { - return getTask("generateApiIndex").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName() - } - }); + this.addTask("generateVersionInfo", { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } }); - this.addTask("generateResourcesJson", () => { - return getTask("generateResourcesJson").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName() - } - }); + this.addTask("generateCachebusterInfo", { + options: { + signatureType: project.getCachebusterSignatureType(), + } }); + + this.addTask("generateApiIndex", {requiresDependencies: true}); + this.addTask("generateResourcesJson", {requiresDependencies: true}); } } diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js index 762c1d43e..f4852e89b 100644 --- a/lib/buildDefinitions/LibraryBuilder.js +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -2,47 +2,35 @@ const AbstractBuilder = require("./AbstractBuilder"); class LibraryBuilder extends AbstractBuilder { addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { - this.addTask("escapeNonAsciiCharacters", async () => { - return getTask("escapeNonAsciiCharacters").task({ - workspace: resourceCollections.workspace, - options: { - encoding: project.getPropertiesFileSourceEncoding(), - pattern: "/**/*.properties" - } - }); + this.addTask("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } }); - this.addTask("replaceCopyright", async () => { - return getTask("replaceCopyright").task({ - workspace: resourceCollections.workspace, - options: { - copyright: project.getCopyright(), - pattern: "/**/*.{js,library,css,less,theme,html}" - } - }); + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,library,css,less,theme,html}" + } }); - this.addTask("replaceVersion", async () => { - return getTask("replaceVersion").task({ - workspace: resourceCollections.workspace, - options: { - version: project.getVersion(), - pattern: "/**/*.{js,json,library,css,less,theme,html}" - } - }); + this.addTask("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } }); - this.addTask("replaceBuildtime", async () => { - return getTask("replaceBuildtime").task({ - workspace: resourceCollections.workspace, - options: { - pattern: "/resources/sap/ui/Global.js" - } - }); + this.addTask("replaceBuildtime", { + options: { + pattern: "/resources/sap/ui/Global.js" + } }); - if (project.getNamespace()) { - this.addTask("generateJsdoc", async () => { + this.addTask("generateJsdoc", {requiresDependencies: true}, + async ({workspace, dependencies, taskUtil, options}) => { const patterns = ["/resources/**/*.js"]; // Add excludes const excludes = project.getJsdocExcludes(); @@ -55,29 +43,24 @@ class LibraryBuilder extends AbstractBuilder { } return getTask("generateJsdoc").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, + workspace, + dependencies, taskUtil, options: { - projectName: project.getName(), - namespace: project.getNamespace(), + projectName: options.projectName, + namespace: project.projectNamespace, version: project.getVersion(), pattern: patterns } }); }); - this.addTask("executeJsdocSdkTransformation", async () => { - return getTask("executeJsdocSdkTransformation").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName(), - dotLibraryPattern: "/resources/**/*.library", - } - }); - }); - } + this.addTask("executeJsdocSdkTransformation", { + requiresDependencies: true, + options: { + dotLibraryPattern: "/resources/**/*.library", + } + }); // Support rules should not be minified to have readable code in the Support Assistant const minificationPattern = ["/resources/**/*.js", "!**/*.support.js"]; @@ -88,129 +71,75 @@ class LibraryBuilder extends AbstractBuilder { } } - this.addTask("minify", async () => { - return getTask("minify").task({ - workspace: resourceCollections.workspace, - taskUtil, - options: { - pattern: minificationPattern - } - }); - }); - - this.addTask("generateLibraryManifest", async () => { - return getTask("generateLibraryManifest").task({ - workspace: resourceCollections.workspace, - taskUtil, - options: { - projectName: project.getName() - } - }); + this.addTask("minify", { + options: { + pattern: minificationPattern + } }); - - if (project.getNamespace()) { - this.addTask("generateManifestBundle", async () => { - return getTask("generateManifestBundle").task({ - workspace: resourceCollections.workspace, - options: { - projectName: project.getName(), - namespace: project.getNamespace() - } - }); - }); - } + this.addTask("generateLibraryManifest"); + this.addTask("generateManifestBundle"); const componentPreloadPaths = project.getComponentPreloadPaths(); const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); const componentPreloadExcludes = project.getComponentPreloadNamespaces(); if (componentPreloadPaths.length || componentPreloadNamespaces.length) { - this.addTask("generateComponentPreload", async () => { - return getTask("generateComponentPreload").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName(), - paths: componentPreloadPaths, - namespaces: componentPreloadNamespaces, - excludes: componentPreloadExcludes - } - }); - }); - } - - this.addTask("generateLibraryPreload", async () => { - return getTask("generateLibraryPreload").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, + this.addTask("generateComponentPreload", { options: { - excludes: project.getLibraryPreloadExcludes() + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes } }); + } + + this.addTask("generateLibraryPreload", { + options: { + excludes: project.getLibraryPreloadExcludes() + } }); const bundles = project.getBundles(); if (bundles.length) { - this.addTask("generateBundle", async () => { - return bundles.reduce(function(sequence, bundle) { - return sequence.then(function() { - return getTask("generateBundle").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName(), - bundleDefinition: bundle.bundleDefinition, - bundleOptions: bundle.bundleOptions - } + this.addTask("generateBundle", {requiresDependencies: true}, + async ({workspace, dependencies, taskUtil, options}) => { + return bundles.reduce(function(sequence, bundle) { + return sequence.then(function() { + return getTask("generateBundle").task({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); }); - }); - }, Promise.resolve()); - }); + }, Promise.resolve()); + }); } - this.addTask("buildThemes", async () => { - // Only compile themes directly below the lib namespace to be in sync with the theme support at runtime - // which only loads themes from that folder. - // TODO 3.0: Remove fallback in case of missing namespace - const inputPattern = `/resources/${project.getNamespace() || "**"}/themes/*/library.source.less`; - - return getTask("buildThemes").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName(), - librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, - themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, - inputPattern, - cssVariables: taskUtil.getBuildOption("cssVariables") - } - }); + this.addTask("buildThemes", { + requiresDependencies: true, + options: { + projectName: project.getName(), + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern: `/resources/${project.getNamespace()}/themes/*/library.source.less`, + cssVariables: taskUtil.getBuildOption("cssVariables") + } }); - this.addTask("generateThemeDesignerResources", async () => { - return getTask("generateThemeDesignerResources").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName(), - version: project.getVersion(), - namespace: project.getNamespace() - } - }); + this.addTask("generateThemeDesignerResources", { + requiresDependencies: true, + options: { + version: project.getVersion() + } }); - this.addTask("generateResourcesJson", () => { - return getTask("generateResourcesJson").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName() - } - }); + this.addTask("generateResourcesJson", { + requiresDependencies: true }); } } diff --git a/lib/buildDefinitions/ThemeLibraryBuilder.js b/lib/buildDefinitions/ThemeLibraryBuilder.js index 9e7e5f29d..4ef14e74b 100644 --- a/lib/buildDefinitions/ThemeLibraryBuilder.js +++ b/lib/buildDefinitions/ThemeLibraryBuilder.js @@ -2,61 +2,38 @@ const AbstractBuilder = require("./AbstractBuilder"); class ThemeLibraryBuilder extends AbstractBuilder { addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { - this.addTask("replaceCopyright", async () => { - return getTask("replaceCopyright").task({ - workspace: resourceCollections.workspace, - options: { - copyright: project.getCopyright(), - pattern: "/resources/**/*.{less,theme}" - } - }); + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/resources/**/*.{less,theme}" + } }); - this.addTask("replaceVersion", async () => { - return getTask("replaceVersion").task({ - workspace: resourceCollections.workspace, - options: { - version: project.getVersion(), - pattern: "/resources/**/*.{less,theme}" - } - }); + this.addTask("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/resources/**/*.{less,theme}" + } }); - this.addTask("buildThemes", async () => { - return getTask("buildThemes").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName(), - librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, - themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, - inputPattern: "/resources/**/themes/*/library.source.less", - cssVariables: taskUtil.getBuildOption("cssVariables") - } - }); + this.addTask("buildThemes", { + requiresDependencies: true, + options: { + librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined, + themesPattern: !taskUtil.isRootProject() ? "/resources/sap/ui/core/themes/*" : undefined, + inputPattern: "/resources/**/themes/*/library.source.less", + cssVariables: taskUtil.getBuildOption("cssVariables") + } }); - this.addTask("generateThemeDesignerResources", async () => { - return getTask("generateThemeDesignerResources").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - options: { - projectName: project.getName(), - version: project.getVersion() - } - }); + this.addTask("generateThemeDesignerResources", { + requiresDependencies: true, + options: { + version: project.getVersion() + } }); - this.addTask("generateResourcesJson", () => { - return getTask("generateResourcesJson").task({ - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, - taskUtil, - options: { - projectName: project.getName() - } - }); - }); + this.addTask("generateResourcesJson", {requiresDependencies: true}); } } diff --git a/lib/buildHelpers/BuildContext.js b/lib/buildHelpers/BuildContext.js index 4b6301bb4..795626c60 100644 --- a/lib/buildHelpers/BuildContext.js +++ b/lib/buildHelpers/BuildContext.js @@ -1,4 +1,3 @@ -const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; const ProjectBuildContext = require("./ProjectBuildContext"); // Note: When adding standard tags, always update the public documentation in TaskUtil @@ -20,11 +19,8 @@ class BuildContext { throw new Error(`Missing parameter 'graph'`); } this._graph = graph; - this.projectBuildContexts = []; - this._resourceTagCollection = new ResourceTagCollection({ - allowedTags: Object.values(GLOBAL_TAGS) - }); - this.options = options; + this._projectBuildContexts = []; + this._options = options; } getRootProject() { @@ -32,21 +28,21 @@ class BuildContext { } getOption(key) { - return this.options[key]; + return this._options[key]; } - createProjectContext({project}) { + createProjectContext({project, log}) { const projectBuildContext = new ProjectBuildContext({ buildContext: this, - globalTags: GLOBAL_TAGS, - project + project, + log }); - this.projectBuildContexts.push(projectBuildContext); + this._projectBuildContexts.push(projectBuildContext); return projectBuildContext; } async executeCleanupTasks() { - await Promise.all(this.projectBuildContexts.map((ctx) => { + await Promise.all(this._projectBuildContexts.map((ctx) => { return ctx.executeCleanupTasks(); })); } diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js index c194acd1c..1bb02ab95 100644 --- a/lib/buildHelpers/ProjectBuildContext.js +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -3,10 +3,10 @@ const TaskUtil = require("@ui5/builder").tasks.TaskUtil; // Note: When adding standard tags, always update the public documentation in TaskUtil // (Type "module:@ui5/builder.tasks.TaskUtil~StandardBuildTags") -const STANDARD_TAGS = { +const STANDARD_TAGS = Object.freeze({ OmitFromBuildResult: "ui5:OmitFromBuildResult", IsBundle: "ui5:IsBundle", -}; +}); /** * Build context of a single project. Always part of an overall @@ -16,22 +16,22 @@ const STANDARD_TAGS = { * @memberof module:@ui5/builder.builder */ class ProjectBuildContext { - constructor({buildContext, globalTags, project}) { - if (!buildContext || !globalTags || !project) { + constructor({buildContext, log, project}) { + if (!buildContext || !log || !project) { throw new Error(`One or more mandatory parameters are missing`); } this._buildContext = buildContext; this._project = project; - this.queues = { + this._log = log; + this._queues = { cleanup: [] }; - this.STANDARD_TAGS = Object.assign({}, STANDARD_TAGS, globalTags); - Object.freeze(this.STANDARD_TAGS); + this.STANDARD_TAGS = STANDARD_TAGS; this._resourceTagCollection = new ResourceTagCollection({ allowedTags: Object.values(this.STANDARD_TAGS), - superCollection: this._buildContext.getResourceTagCollection() + allowedNamespaces: ["build"] }); } @@ -44,17 +44,31 @@ class ProjectBuildContext { } registerCleanupTask(callback) { - this.queues.cleanup.push(callback); + this._queues.cleanup.push(callback); } async executeCleanupTasks() { - await Promise.all(this.queues.cleanup.map((callback) => { + await Promise.all(this._queues.cleanup.map((callback) => { return callback(); })); } - getResourceTagCollection() { - return this._resourceTagCollection; + getResourceTagCollection(resource, tag) { + if (!resource.hasProject()) { + this._log.verbose(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`); + resource.setProject(this._project); + // throw new Error( + // `Unable to get tag collection for resource ${resource.getPath()}: ` + + // `Resource must be associated to a project`); + } + const projectCollection = resource.getProject().getResourceTagCollection(); + if (projectCollection.acceptsTag(tag)) { + return projectCollection; + } + if (this._resourceTagCollection.acceptsTag(tag)) { + return this._resourceTagCollection; + } + throw new Error(`Could not find collection for resource ${resource.getPath()} and tag ${tag}`); } getTaskUtil() { diff --git a/lib/buildHelpers/composeProjectList.js b/lib/buildHelpers/composeProjectList.js index 30e4b1263..06ae351f4 100644 --- a/lib/buildHelpers/composeProjectList.js +++ b/lib/buildHelpers/composeProjectList.js @@ -47,9 +47,9 @@ async function getFlattenedDependencyTree(graph) { *
  • defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree
  • * * - * @param {object} parameters Parameters - * @param {object} parameters.graph Project tree as generated by the + * @param {object} graph Project tree as generated by the * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @param {object} parameters Parameters * @param {boolean} parameters.includeAllDependencies Whether all dependencies should be part of the build result * This has the lowest priority and basically includes all remaining (not excluded) projects as include * @param {string[]} parameters.includeDependency The dependencies to be considered in 'includedDependencies'; the @@ -72,13 +72,14 @@ async function getFlattenedDependencyTree(graph) { * @returns {{includedDependencies:string[],excludedDependencies:string[]}} An object containing the * 'includedDependencies' and 'excludedDependencies' */ -async function createDependencyLists({ - graph, includeAllDependencies = false, +async function createDependencyLists(graph, { + includeAllDependencies = false, includeDependency = [], includeDependencyRegExp = [], includeDependencyTree = [], excludeDependency = [], excludeDependencyRegExp = [], excludeDependencyTree = [], defaultIncludeDependency = [], defaultIncludeDependencyRegExp = [], defaultIncludeDependencyTree = [] }) { if ( + !includeAllDependencies && !includeDependency.length && !includeDependencyRegExp.length && !includeDependencyTree.length && !excludeDependency.length && !excludeDependencyRegExp.length && !excludeDependencyTree.length && !defaultIncludeDependency.length && !defaultIncludeDependencyRegExp.length && diff --git a/lib/builder.js b/lib/builder.js index dc80c29ed..4f7390725 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -84,7 +84,6 @@ function getElapsedTime(startTime) { * If the wildcard '*' is provided, all dependencies will be included in the build result. * @param {Array.} [parameters.excludedDependencies=[]] * List of names of projects to exclude from the build result. - * If the wildcard '*' is provided, only the included dependencies will be built. * @param {object} [parameters.complexDependencyIncludes] TODO 3.0 * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation @@ -112,16 +111,17 @@ module.exports = async function({ "Parameter 'complexDependencyIncludes' can't be used in conjunction " + "with parameters 'includedDependencies' or 'excludedDependencies"); } - const deps = await composeProjectList(complexDependencyIncludes); + const deps = await composeProjectList(graph, complexDependencyIncludes); includedDependencies = deps.includedDependencies; excludedDependencies = deps.excludedDependencies; } const startTime = process.hrtime(); - log.info(`Starting build of project ${graph.getRoot().getName()}...`); - log.verbose(` Target directory: ${destPath}`); - log.verbose(` Including dependencies: ${includedDependencies.join(", ")}`); - log.verbose(` Excluding dependencies: ${excludedDependencies.join(", ")}`); + const rootProjectName = graph.getRoot().getName(); + log.info(`Starting build of project ${rootProjectName}...`); + log.info(` Target directory: ${destPath}`); + log.info(` Including dependencies: ${includedDependencies.join(", ")}`); + log.info(` Excluding dependencies: ${excludedDependencies.join(", ")}`); const buildParams = {selfContained, jsdoc, includedTasks, excludedTasks}; @@ -143,9 +143,14 @@ module.exports = async function({ dep.test(projectName) : dep === projectName); } + if (projectName === rootProjectName) { + // Always include the root project + return true; + } + // if everything is included, this overrules exclude lists if (includedDependencies.includes("*")) return true; - let test = !excludedDependencies.includes("*"); // exclude everything? + let test = false; if (test && projectMatchesAny(excludedDependencies)) { test = false; @@ -158,24 +163,26 @@ module.exports = async function({ } // Count total number of projects to build - const projectCount = graph.getAllProjects().filter(function([projectName]) { + const projectsToBuild = graph.getAllProjects().map((arr) => arr[0]).filter(function(projectName) { return projectFilter(projectName); - }).length; - - const buildLogger = log.createTaskLogger("🛠 ", projectCount); + }); + const buildLogger = log.createTaskLogger("🛠 ", projectsToBuild.length); + log.info(`Building projects: `); + log.info(` > ${projectsToBuild.join("\n > ")}`); try { if (cleanDest) { await rimraf(destPath); } await graph.traverseDepthFirst(async function({project, getDependencies}) { - const projectContext = buildContext.createProjectContext({ - project - }); - if (!buildDependencies && !projectContext.isRootProject()) { + if (!projectsToBuild.includes(project.getName())) { return; } + const projectContext = buildContext.createProjectContext({ + project, + log: buildLogger + }); buildLogger.startWork(`Building project ${project.getName()}...`); const readers = []; @@ -193,6 +200,7 @@ module.exports = async function({ readers }); + const taskUtil = projectContext.getTaskUtil(); const builder = getBuildDefinitionInstance({ graph, project, @@ -208,10 +216,9 @@ module.exports = async function({ buildLogger.completeWork(1); const resources = await project.getReader({style: "runtime"}).byGlob("/**/*"); - const tagCollection = projectContext.getResourceTagCollection(); await Promise.all(resources.map((resource) => { - if (tagCollection.getTag(resource, projectContext.STANDARD_TAGS.OmitFromBuildResult)) { + if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { log.verbose(`Skipping write of resource tagged as "OmitFromBuildResult": ` + resource.getPath()); return; // Skip target write for this resource @@ -228,8 +235,3 @@ module.exports = async function({ await executeCleanupTasks(buildContext); } }; - -module.exports.composeProjectList = function (parameters) { - const composeProjectList = require("./buildHelpers/composeProjectList"); - return composeProjectList(parameters); -}; diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 8dc55188a..9141cbaca 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -1,5 +1,6 @@ const Specification = require("./Specification"); + /** * Project * @@ -13,6 +14,8 @@ class Project extends Specification { if (new.target === Project) { throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); } + + this._resourceTagCollection = null; } /* === Attributes === */ @@ -84,6 +87,17 @@ class Project extends Specification { throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); } + getResourceTagCollection() { + if (!this._resourceTagCollection) { + const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], + allowedNamespaces: ["project"] + }); + } + return this._resourceTagCollection; + } + /** * TODO * diff --git a/test/lib/buildHelpers/composeProjectList.js b/test/lib/buildHelpers/composeProjectList.js index f98f1b404..32ec78425 100644 --- a/test/lib/buildHelpers/composeProjectList.js +++ b/test/lib/buildHelpers/composeProjectList.js @@ -212,6 +212,22 @@ test.serial("createDependencyLists: include all + excludes", async (t) => { }); }); +test.serial("createDependencyLists: include all", async (t) => { + await assertCreateDependencyLists(t, { + includeAllDependencies: true, + includeDependency: [], + excludeDependency: [], + excludeDependencyRegExp: [], + excludeDependencyTree: [], + expectedIncludedDependencies: [ + "library.d", "library.b", "library.c", + "library.d-depender", "library.a", "library.g", + "library.e", "library.f" + ], + expectedExcludedDependencies: [] + }); +}); + test.serial("createDependencyLists: includeDependencyTree has lower priority than excludes", async (t) => { await assertCreateDependencyLists(t, { includeAllDependencies: false, From 411a509dd0e7b3797d4e25a293bd0273b6bce332 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 4 May 2022 13:49:00 +0200 Subject: [PATCH 46/99] Dynamically decide which projects to build based on user and task demand --- lib/buildDefinitions/AbstractBuilder.js | 143 +++++++++++--------- lib/buildDefinitions/ApplicationBuilder.js | 2 +- lib/buildDefinitions/LibraryBuilder.js | 2 +- lib/buildDefinitions/ThemeLibraryBuilder.js | 2 +- lib/buildHelpers/composeTaskList.js | 26 +--- lib/builder.js | 92 +++++++++---- lib/graph/ProjectGraph.js | 2 +- 7 files changed, 156 insertions(+), 113 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index ec8fbae14..74b29f9db 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -24,9 +24,8 @@ class AbstractBuilder { * @param {object} parameters.project * @param {GroupLogger} parameters.parentLogger Logger to use * @param {object} parameters.taskUtil - * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections */ - constructor({graph, project, parentLogger, taskUtil, resourceCollections}) { + constructor({graph, project, parentLogger, taskUtil}) { if (new.target === AbstractBuilder) { throw new TypeError("Class 'AbstractBuilder' is abstract"); } @@ -34,9 +33,8 @@ class AbstractBuilder { this.project = project; this.graph = graph; this.taskUtil = taskUtil; - this.resourceCollections = resourceCollections; - this.log = parentLogger.createSubLogger(project.type + " " + project.getName(), 0.2); + this.log = parentLogger.createSubLogger(project.getType() + " " + project.getName(), 0.2); this.taskLog = this.log.createTaskLogger("🔨"); this.tasks = {}; @@ -45,16 +43,13 @@ class AbstractBuilder { this.addStandardTasks({ project, - log: this.log, taskUtil, - getTask, - resourceCollections + getTask }); this.addCustomTasks({ graph, project, - taskUtil, - resourceCollections + taskUtil }); } @@ -64,12 +59,10 @@ class AbstractBuilder { * @abstract * @protected * @param {object} parameters - * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections * @param {object} parameters.taskUtil * @param {object} parameters.project - * @param {object} parameters.log @ui5/logger logger instance */ - addStandardTasks({project, log, taskUtil, resourceCollections}) { + addStandardTasks({project, taskUtil}) { throw new Error("Function 'addStandardTasks' is not implemented"); } @@ -78,12 +71,11 @@ class AbstractBuilder { * * @private * @param {object} parameters - * @param {BuilderResourceCollections} parameters.resourceCollections Resource collections * @param {object} parameters.graph * @param {object} parameters.project * @param {object} parameters.taskUtil */ - addCustomTasks({graph, project, taskUtil, resourceCollections}) { + addCustomTasks({graph, project, taskUtil}) { const projectCustomTasks = project.getCustomTasks(); if (!projectCustomTasks || projectCustomTasks.length === 0) { return; // No custom tasks defined @@ -115,7 +107,10 @@ class AbstractBuilder { } } const task = graph.getExtension(taskDef.name); - const execTask = function() { + // TODO: Create callback for custom tasks to configure "requiresDependencies" and "enabled" + // Input: task "options" and build mode ("standalone", "preload", etc.) + const requiresDependencies = true; // Default to true for old spec versions + const execTask = function({workspace, dependencies}) { /* Custom Task Interface Parameters: {Object} parameters Parameters @@ -132,8 +127,7 @@ class AbstractBuilder { {Promise} Promise resolving with undefined once data has been written */ const params = { - workspace: resourceCollections.workspace, - dependencies: resourceCollections.dependencies, + workspace, options: { projectName: project.getName(), projectNamespace: project.getNamespace(), @@ -141,6 +135,10 @@ class AbstractBuilder { } }; + if (requiresDependencies) { + params.dependencies = dependencies; + } + const taskUtilInterface = taskUtil.getInterface(project.getSpecVersion()); // Interface is undefined if specVersion does not support taskUtil if (taskUtilInterface) { @@ -149,7 +147,10 @@ class AbstractBuilder { return task(params); }; - this.tasks[newTaskName] = execTask; + this.tasks[newTaskName] = { + task: execTask, + requiresDependencies + }; if (this.taskExecutionOrder.length) { // There is at least one task configured. Use before- and afterTask to add the custom task @@ -191,56 +192,50 @@ class AbstractBuilder { `It has already been scheduled for execution.`); } - options.projectName = this.project.getName(); - // TODO: Deprecate "namespace" in favor of "projectNamespace" as already used for custom tasks? - options.projectNamespace = this.project.getNamespace(); - options.namespace = this.project.getNamespace(); + const task = ({workspace, dependencies}) => { + options.projectName = this.project.getName(); + // TODO: Deprecate "namespace" in favor of "projectNamespace" as already used for custom tasks? + options.projectNamespace = this.project.getNamespace(); + options.namespace = this.project.getNamespace(); - const params = { - workspace: this.resourceCollections.workspace, - dependencies: this.resourceCollections.dependencies, - taskUtil: this.taskUtil, - options - }; - if (taskFunction) { - this.tasks[taskName] = () => { - return taskFunction(params); + const params = { + workspace, + taskUtil: this.taskUtil, + options }; - } else { - this.tasks[taskName] = () => { - return getTask(taskName).task(params); - }; - } - this.taskExecutionOrder.push(taskName); - } - /** - * Check whether a task is defined - * - * @private - * @param {string} taskName - * @returns {boolean} - */ - hasTask(taskName) { - // TODO 3.0: Check whether this method is still required. - // Only usage within #build seems to be unnecessary as all tasks are also added to the taskExecutionOrder - return Object.prototype.hasOwnProperty.call(this.tasks, taskName); + if (requiresDependencies) { + params.dependencies = dependencies; + } + + if (!taskFunction) { + taskFunction = getTask(taskName).task; + } + return taskFunction(params); + }; + this.tasks[taskName] = { + task, + requiresDependencies + }; + this.taskExecutionOrder.push(taskName); } /** * Takes a list of tasks which should be executed from the available task list of the current builder * - * @param {object} parameters - * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks - * @param {boolean} parameters.selfContained + * @param {object} buildConfig + * @param {boolean} buildConfig.selfContained * True if a the build should be self-contained or false for prelead build bundles - * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed - * @param {Array} parameters.includedTasks Task list to be included from build - * @param {Array} parameters.excludedTasks Task list to be excluded from build + * @param {boolean} buildConfig.jsdoc True if a JSDoc build should be executed + * @param {Array} buildConfig.includedTasks Task list to be included from build + * @param {Array} buildConfig.excludedTasks Task list to be excluded from build + * @param {object} buildParams + * @param {module:@ui5/fs.DuplexCollection} buildParams.workspace Workspace of the current project + * @param {module:@ui5/fs.ReaderCollection} buildParams.dependencies Dependencies reader collection * @returns {Promise} Returns promise chain with tasks */ - build(parameters) { - const tasksToRun = composeTaskList(Object.keys(this.tasks), parameters); + build(buildConfig, buildParams) { + const tasksToRun = composeTaskList(Object.keys(this.tasks), buildConfig); const allTasks = this.taskExecutionOrder.filter((taskName) => { // There might be a numeric suffix in case a custom task is configured multiple times. // The suffix needs to be removed in order to check against the list of tasks to run. @@ -251,34 +246,58 @@ class AbstractBuilder { // This would require a more robust contract to identify task executions // (e.g. via an 'id' that can be assigned to a specific execution in the configuration). const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); - return this.hasTask(taskName) && tasksToRun.includes(taskWithoutSuffixCounter); + return tasksToRun.includes(taskWithoutSuffixCounter); }); this.taskLog.addWork(allTasks.length); return allTasks.reduce((taskChain, taskName) => { - const taskFunction = this.tasks[taskName]; + const taskFunction = this.tasks[taskName].task; if (typeof taskFunction === "function") { - taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); + taskChain = taskChain.then(this.wrapTask(taskName, taskFunction, buildParams)); } return taskChain; }, Promise.resolve()); } + requiresDependencies(buildConfig) { + const tasksToRun = composeTaskList(Object.keys(this.tasks), buildConfig); + const allTasks = this.taskExecutionOrder.filter((taskName) => { + // There might be a numeric suffix in case a custom task is configured multiple times. + // The suffix needs to be removed in order to check against the list of tasks to run. + // + // Note: The 'tasksToRun' parameter only allows to specify the custom task name + // (without suffix), so it executes either all or nothing. + // It's currently not possible to just execute some occurrences of a custom task. + // This would require a more robust contract to identify task executions + // (e.g. via an 'id' that can be assigned to a specific execution in the configuration). + const taskWithoutSuffixCounter = taskName.replace(/--\d+$/, ""); + return tasksToRun.includes(taskWithoutSuffixCounter); + }); + return allTasks.some((taskName) => { + if (this.tasks[taskName].requiresDependencies) { + this.log.info(`Task ${taskName} for project ${this.project.getName()} requires dependencies`); + return true; + } + return false; + }); + } + /** * Adds progress related functionality to task function. * * @private * @param {string} taskName Name of the task * @param {Function} taskFunction Function which executed the task + * @param {object} taskParams Base parameters for all tasks * @returns {Function} Wrapped task function */ - wrapTask(taskName, taskFunction) { + wrapTask(taskName, taskFunction, taskParams) { return () => { this.taskLog.startWork(`Running task ${taskName}...`); - return taskFunction().then(() => this.taskLog.completeWork(1)); + return taskFunction(taskParams).then(() => this.taskLog.completeWork(1)); }; } diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index 3171b15ae..565867e03 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -1,7 +1,7 @@ const AbstractBuilder = require("./AbstractBuilder"); class ApplicationBuilder extends AbstractBuilder { - addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + addStandardTasks({project, taskUtil, getTask}) { this.addTask("escapeNonAsciiCharacters", { options: { encoding: project.getPropertiesFileSourceEncoding(), diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js index f4852e89b..a4b755816 100644 --- a/lib/buildDefinitions/LibraryBuilder.js +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -1,7 +1,7 @@ const AbstractBuilder = require("./AbstractBuilder"); class LibraryBuilder extends AbstractBuilder { - addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + addStandardTasks({project, taskUtil, getTask}) { this.addTask("escapeNonAsciiCharacters", { options: { encoding: project.getPropertiesFileSourceEncoding(), diff --git a/lib/buildDefinitions/ThemeLibraryBuilder.js b/lib/buildDefinitions/ThemeLibraryBuilder.js index 4ef14e74b..1225bbd19 100644 --- a/lib/buildDefinitions/ThemeLibraryBuilder.js +++ b/lib/buildDefinitions/ThemeLibraryBuilder.js @@ -1,7 +1,7 @@ const AbstractBuilder = require("./AbstractBuilder"); class ThemeLibraryBuilder extends AbstractBuilder { - addStandardTasks({resourceCollections, project, log, taskUtil, getTask}) { + addStandardTasks({project, taskUtil, getTask}) { this.addTask("replaceCopyright", { options: { copyright: project.getCopyright(), diff --git a/lib/buildHelpers/composeTaskList.js b/lib/buildHelpers/composeTaskList.js index 52184c051..09328d774 100644 --- a/lib/buildHelpers/composeTaskList.js +++ b/lib/buildHelpers/composeTaskList.js @@ -1,31 +1,24 @@ const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); -// Set of tasks for development -const devTasks = [ - "replaceCopyright", - "replaceVersion", - "replaceBuildtime", - "buildThemes" -]; /** * Creates the list of tasks to be executed by the build process * * Sets specific tasks to be disabled by default, these tasks need to be included explicitly. - * Based on the selected build mode (dev|selfContained|preload), different tasks are enabled. + * Based on the selected build mode (selfContained|preload), different tasks are enabled. * Tasks can be enabled or disabled. The wildcard * is also supported and affects all tasks. * * @private * @param {string[]} allTasks * @param {object} parameters - * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks * @param {boolean} parameters.selfContained * True if a the build should be self-contained or false for prelead build bundles * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed + * @param {boolean} parameters.archive True if an archive build should be executed * @param {Array} parameters.includedTasks Task list to be included from build * @param {Array} parameters.excludedTasks Task list to be excluded from build * @returns {Array} Return a task list for the builder */ -module.exports = function composeTaskList(allTasks, {dev, selfContained, jsdoc, includedTasks, excludedTasks}) { +module.exports = function composeTaskList(allTasks, {selfContained, jsdoc, archive, includedTasks, excludedTasks}) { let selectedTasks = allTasks.reduce((list, key) => { list[key] = true; return list; @@ -40,6 +33,7 @@ module.exports = function composeTaskList(allTasks, {dev, selfContained, jsdoc, selectedTasks.generateCachebusterInfo = false; selectedTasks.generateApiIndex = false; selectedTasks.generateThemeDesignerResources = false; + selectedTasks.generateVersionInfo = false; // Disable generateResourcesJson due to performance. // When executed it analyzes each module's AST and therefore @@ -54,8 +48,6 @@ module.exports = function composeTaskList(allTasks, {dev, selfContained, jsdoc, selectedTasks.generateLibraryPreload = false; } - // TODO 3.0: exclude generateVersionInfo if not --all is used - if (jsdoc) { // Include JSDoc tasks selectedTasks.generateJsdoc = true; @@ -77,16 +69,6 @@ module.exports = function composeTaskList(allTasks, {dev, selfContained, jsdoc, selectedTasks.generateManifestBundle = false; } - // Only run essential tasks in development mode, it is not desired to run time consuming tasks during development. - if (dev) { - // Overwrite all other tasks with noop promise - Object.keys(selectedTasks).forEach((key) => { - if (devTasks.indexOf(key) === -1) { - selectedTasks[key] = false; - } - }); - } - // Exclude tasks for (let i = 0; i < excludedTasks.length; i++) { const taskName = excludedTasks[i]; diff --git a/lib/builder.js b/lib/builder.js index 4f7390725..c04eca536 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -118,12 +118,18 @@ module.exports = async function({ const startTime = process.hrtime(); const rootProjectName = graph.getRoot().getName(); - log.info(`Starting build of project ${rootProjectName}...`); + log.info(`Building project ${rootProjectName}`); + if (includedDependencies.length) { + log.info(` Including dependencies:`); + log.info(` + ${includedDependencies.join("\n + ")}`); + } + if (excludedDependencies.length) { + log.info(` Excluding dependencies:`); + log.info(` - ${excludedDependencies.join("\n + ")}`); + } log.info(` Target directory: ${destPath}`); - log.info(` Including dependencies: ${includedDependencies.join(", ")}`); - log.info(` Excluding dependencies: ${excludedDependencies.join(", ")}`); - const buildParams = {selfContained, jsdoc, includedTasks, excludedTasks}; + const buildConfig = {selfContained, jsdoc, includedTasks, excludedTasks}; const fsTarget = resourceFactory.createAdapter({ fsBasePath: destPath, @@ -163,31 +169,70 @@ module.exports = async function({ } // Count total number of projects to build - const projectsToBuild = graph.getAllProjects().map((arr) => arr[0]).filter(function(projectName) { + const requestedProjects = graph.getAllProjects().map((arr) => arr[0]).filter(function(projectName) { return projectFilter(projectName); }); - const buildLogger = log.createTaskLogger("🛠 ", projectsToBuild.length); - log.info(`Building projects: `); - log.info(` > ${projectsToBuild.join("\n > ")}`); try { + const buildableProjects = {}; + // Copy list of requested projects. We might need to build more projects than requested to + // in order to satisfy tasks requiring dependencies to be built but we will still only write the + // resources of the requested projects to the build result + const projectsToBuild = [...requestedProjects]; + + const buildLogger = log.createTaskLogger("🛠 "); + await graph.traverseBreadthFirst(async function({project, getDependencies}) { + const projectName = project.getName(); + const projectContext = buildContext.createProjectContext({ + project, + log: buildLogger + }); + log.info(`Preparing project ${projectName}...`); + + const taskUtil = projectContext.getTaskUtil(); + const builder = getBuildDefinitionInstance({ + graph, + project, + taskUtil, + parentLogger: log + }); + buildableProjects[projectName] = { + projectContext, + builder + }; + + if (projectsToBuild.includes(projectName) && builder.requiresDependencies(buildConfig)) { + getDependencies().forEach((dep) => { + const depName = dep.getName(); + log.info(`Project ${projectName} requires dependency ${depName} to be built`); + if (!projectsToBuild.includes(depName)) { + projectsToBuild.push(depName); + } + }); + } + }); + + buildLogger.addWork(projectsToBuild.length); + log.info(`Building projects: `); + log.info(` > ${projectsToBuild.join("\n > ")}`); + if (cleanDest) { await rimraf(destPath); } await graph.traverseDepthFirst(async function({project, getDependencies}) { - if (!projectsToBuild.includes(project.getName())) { + const projectName = project.getName(); + if (!projectsToBuild.includes(projectName)) { return; } - const projectContext = buildContext.createProjectContext({ - project, - log: buildLogger - }); + const {projectContext, builder} = buildableProjects[projectName]; + buildLogger.startWork(`Building project ${project.getName()}...`); const readers = []; await graph.traverseBreadthFirst(project.getName(), async function({project: dep}) { if (dep.getName() === project.getName()) { + // Ignore project itself return; } readers.push(dep.getReader({ @@ -200,23 +245,20 @@ module.exports = async function({ readers }); - const taskUtil = projectContext.getTaskUtil(); - const builder = getBuildDefinitionInstance({ - graph, - project, - taskUtil: projectContext.getTaskUtil(), - parentLogger: log, - resourceCollections: { - workspace: project.getWorkspace(), - dependencies, - } + await builder.build(buildConfig, { + workspace: project.getWorkspace(), + dependencies, }); - await builder.build(buildParams); - log.verbose("Finished building project %s. Writing out files...", project.getName()); + log.verbose("Finished building project %s", project.getName()); buildLogger.completeWork(1); const resources = await project.getReader({style: "runtime"}).byGlob("/**/*"); + if (!requestedProjects.includes(projectName)) { + return; + } + log.verbose(`Writing out files...`); + const taskUtil = projectContext.getTaskUtil(); await Promise.all(resources.map((resource) => { if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { log.verbose(`Skipping write of resource tagged as "OmitFromBuildResult": ` + diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index c2ba6bca9..69793de60 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -200,7 +200,7 @@ class ProjectGraph { /** * @public * @param {string} projectName Name of the project to retrieve the dependencies of - * @returns {string[]} Project names of the given project's dependencies + * @returns {module:@ui5/project.specifications.Project[]} Project instances of the given project's dependencies */ getDependencies(projectName) { const adjacencies = this._adjList[projectName]; From 8acf7d42ab842987ee4e259e0b0fa31f34dc57d0 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 4 May 2022 13:50:57 +0200 Subject: [PATCH 47/99] Cleanup --- lib/builder.js | 7 +- lib/normalizer.js | 86 ---- lib/projectPreprocessor.js | 663 ----------------------------- lib/specifications/types/Module.js | 2 +- lib/translators/npm.js | 523 ----------------------- lib/translators/ui5Framework.js | 294 ------------- 6 files changed, 4 insertions(+), 1571 deletions(-) delete mode 100644 lib/normalizer.js delete mode 100644 lib/projectPreprocessor.js delete mode 100644 lib/translators/npm.js delete mode 100644 lib/translators/ui5Framework.js diff --git a/lib/builder.js b/lib/builder.js index c04eca536..325b49016 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -88,6 +88,7 @@ function getElapsedTime(startTime) { * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build + * @param {boolean} [parameters.archive=false] Whether to create an archive build * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. @@ -97,7 +98,7 @@ module.exports = async function({ graph, destPath, cleanDest = false, includedDependencies = [], excludedDependencies = [], complexDependencyIncludes, - selfContained = false, cssVariables = false, jsdoc = false, + selfContained = false, cssVariables = false, jsdoc = false, archive = false, includedTasks = [], excludedTasks = [], }) { if (graph.isSealed()) { @@ -235,9 +236,7 @@ module.exports = async function({ // Ignore project itself return; } - readers.push(dep.getReader({ - includeTestResources: true - })); + readers.push(dep.getReader()); }); const dependencies = resourceFactory.createReaderCollection({ diff --git a/lib/normalizer.js b/lib/normalizer.js deleted file mode 100644 index b341ef321..000000000 --- a/lib/normalizer.js +++ /dev/null @@ -1,86 +0,0 @@ -const log = require("@ui5/logger").getLogger("normalizer:normalizer"); - -/** - * Generate project and dependency trees via translators. - * Optionally configure all projects with the projectPreprocessor. - * - * @public - * @namespace - * @alias module:@ui5/project.normalizer - */ -const Normalizer = { - /** - * Generates a project and dependency tree via translators and configures all projects via the projectPreprocessor - * - * @public - * @param {object} [options] - * @param {string} [options.cwd] Current working directory - * @param {string} [options.configPath] Path to configuration file - * @param {string} [options.translatorName] Translator to use - * @param {object} [options.translatorOptions] Options to pass to translator - * @param {object} [options.frameworkOptions] Options to pass to the framework installer - * @param {string} [options.frameworkOptions.versionOverride] Framework version to use instead of the root projects - * framework - * @returns {Promise} Promise resolving to tree object - */ - generateProjectTree: async function(options = {}) { - const projectPreprocessor = require("./projectPreprocessor"); - let tree = await Normalizer.generateDependencyTree(options); - - if (options.configPath) { - tree.configPath = options.configPath; - } - tree = await projectPreprocessor.processTree(tree); - - if (tree.framework) { - const ui5Framework = require("./translators/ui5Framework"); - log.verbose(`Root project ${tree.metadata.name} defines framework ` + - `configuration. Installing UI5 dependencies...`); - let frameworkTree = await ui5Framework.generateDependencyTree(tree, options.frameworkOptions); - if (frameworkTree) { - frameworkTree = await projectPreprocessor.processTree(frameworkTree); - ui5Framework.mergeTrees(tree, frameworkTree); - } - } - return tree; - }, - - /** - * Generates a project and dependency tree via translators - * - * @public - * @param {object} [options] - * @param {string} [options.cwd=.] Current working directory - * @param {string} [options.translatorName=npm] Translator to use - * @param {object} [options.translatorOptions] Options to pass to translator - * @returns {Promise} Promise resolving to tree object - */ - generateDependencyTree: async function({cwd = ".", translatorName="npm", translatorOptions={}} = {}) { - log.verbose("Building dependency tree..."); - - let translatorParams = []; - let translator = translatorName; - if (translatorName.indexOf(":") !== -1) { - translatorParams = translatorName.split(":"); - translator = translatorParams[0]; - translatorParams = translatorParams.slice(1); - } - - let translatorModule; - switch (translator) { - case "static": - translatorModule = require("./translators/static"); - break; - case "npm": - translatorModule = require("./translators/npm"); - break; - default: - return Promise.reject(new Error(`Unknown translator ${translator}`)); - } - - translatorOptions.parameters = translatorParams; - return translatorModule.generateDependencyTree(cwd, translatorOptions); - } -}; - -module.exports = Normalizer; diff --git a/lib/projectPreprocessor.js b/lib/projectPreprocessor.js deleted file mode 100644 index f98022703..000000000 --- a/lib/projectPreprocessor.js +++ /dev/null @@ -1,663 +0,0 @@ -const log = require("@ui5/logger").getLogger("normalizer:projectPreprocessor"); -const fs = require("graceful-fs"); -const path = require("path"); -const {promisify} = require("util"); -const readFile = promisify(fs.readFile); -const jsyaml = require("js-yaml"); -const typeRepository = require("@ui5/builder").types.typeRepository; -const {validate} = require("./validation/validator"); - -class ProjectPreprocessor { - constructor({tree}) { - this.tree = tree; - this.processedProjects = {}; - this.configShims = {}; - this.collections = {}; - this.appliedExtensions = {}; - } - - /* - Adapt and enhance the project tree: - - Replace duplicate projects further away from the root with those closer to the root - - Add configuration to projects - */ - async processTree() { - const queue = [{ - projects: [this.tree], - parent: null, - level: 0 - }]; - const configPromises = []; - let startTime; - if (log.isLevelEnabled("verbose")) { - startTime = process.hrtime(); - } - - // Breadth-first search to prefer projects closer to root - while (queue.length) { - const {projects, parent, level} = queue.shift(); // Get and remove first entry from queue - - // Before processing all projects on a level concurrently, we need to set all of them as being processed. - // This prevents transitive dependencies pointing to the same projects from being processed first - // by the dependency lookahead - const projectsToProcess = projects.filter((project) => { - if (!project.id) { - const parentRefText = parent ? `(child of ${parent.id})` : `(root project)`; - throw new Error(`Encountered project with missing id ${parentRefText}`); - } - if (this.isBeingProcessed(parent, project)) { - return false; - } - // Flag this project as being processed - this.processedProjects[project.id] = { - project, - // If a project is referenced multiple times in the dependency tree it is replaced - // with the instance that is closest to the root. - // Here we track the parents referencing that project - parents: [parent] - }; - return true; - }); - - await Promise.all(projectsToProcess.map(async (project) => { - project._level = level; - if (level === 0) { - project._isRoot = true; - } - log.verbose(`Processing project ${project.id} on level ${project._level}...`); - - if (project.dependencies && project.dependencies.length) { - // Do a dependency lookahead to apply any extensions that might affect this project - await this.dependencyLookahead(project, project.dependencies); - } else { - // When using the static translator for instance, dependencies is not defined and will - // fail later access calls to it - project.dependencies = []; - } - - const {extensions} = await this.loadProjectConfiguration(project); - if (extensions && extensions.length) { - // Project contains additional extensions - // => apply them - // TODO: Check whether extensions get applied twice in case depLookahead already processed them - await Promise.all(extensions.map((extProject) => { - return this.applyExtension(extProject); - })); - } - await this.applyShims(project); - if (this.isConfigValid(project)) { - // Do not apply transparent projects. - // Their only purpose might be to have their dependencies processed - if (!project._transparentProject) { - await this.applyType(project); - this.checkProjectMetadata(parent, project); - } - queue.push({ - // copy array, so that the queue is stable while ignored project dependencies are removed - projects: [...project.dependencies], - parent: project, - level: level + 1 - }); - } else { - if (project === this.tree) { - throw new Error( - `Failed to configure root project "${project.id}". Please check verbose log for details.`); - } - // No config available - // => reject this project by removing it from its parents list of dependencies - log.verbose(`Ignoring project ${project.id} with missing configuration ` + - "(might be a non-UI5 dependency)"); - - const parents = this.processedProjects[project.id].parents; - for (let i = parents.length - 1; i >= 0; i--) { - parents[i].dependencies.splice(parents[i].dependencies.indexOf(project), 1); - } - this.processedProjects[project.id] = {ignored: true}; - } - })); - } - return Promise.all(configPromises).then(() => { - if (log.isLevelEnabled("verbose")) { - const prettyHrtime = require("pretty-hrtime"); - const timeDiff = process.hrtime(startTime); - log.verbose( - `Processed ${Object.keys(this.processedProjects).length} projects in ${prettyHrtime(timeDiff)}`); - } - return this.tree; - }); - } - - async dependencyLookahead(parent, dependencies) { - return Promise.all(dependencies.map(async (project) => { - if (this.isBeingProcessed(parent, project)) { - return; - } - log.verbose(`Processing dependency lookahead for ${parent.id}: ${project.id}`); - // Temporarily flag project as being processed - this.processedProjects[project.id] = { - project, - parents: [parent] - }; - const {extensions} = await this.loadProjectConfiguration(project); - if (extensions && extensions.length) { - // Project contains additional extensions - // => apply them - await Promise.all(extensions.map((extProject) => { - return this.applyExtension(extProject); - })); - } - - if (project.kind === "extension") { - // Not a project but an extension - // => remove it as from any known projects that depend on it - const parents = this.processedProjects[project.id].parents; - for (let i = parents.length - 1; i >= 0; i--) { - parents[i].dependencies.splice(parents[i].dependencies.indexOf(project), 1); - } - // Also ignore it from further processing by other projects depending on it - this.processedProjects[project.id] = {ignored: true}; - - if (this.isConfigValid(project)) { - // Finally apply the extension - await this.applyExtension(project); - } else { - log.verbose(`Ignoring extension ${project.id} with missing configuration`); - } - } else { - // Project is not an extension: Reset processing status of lookahead to allow the real processing - this.processedProjects[project.id] = null; - } - })); - } - - isBeingProcessed(parent, project) { // Check whether a project is currently being or has already been processed - const processedProject = this.processedProjects[project.id]; - if (project.deduped) { - // Ignore deduped modules - return true; - } - if (processedProject) { - if (processedProject.ignored) { - log.verbose(`Dependency of project ${parent.id}, "${project.id}" is flagged as ignored.`); - if (parent.dependencies.includes(project)) { - parent.dependencies.splice(parent.dependencies.indexOf(project), 1); - } - return true; - } - log.verbose( - `Dependency of project ${parent.id}, "${project.id}": ` + - `Distance to root of ${parent._level + 1}. Will be replaced `+ - `by project with same ID and distance to root of ${processedProject.project._level}.`); - - // Replace with the already processed project (closer to root -> preferred) - parent.dependencies[parent.dependencies.indexOf(project)] = processedProject.project; - processedProject.parents.push(parent); - - // No further processing needed - return true; - } - return false; - } - - async loadProjectConfiguration(project) { - if (project.specVersion) { // Project might already be configured - // Currently, specVersion is the indicator for configured projects - - if (project._transparentProject) { - // Assume that project is already processed - return {}; - } - - await this.validateAndNormalizeExistingProject(project); - - return {}; - } - - const configs = await this.readConfigFile(project); - - if (!configs || !configs.length) { - return {}; - } - - for (let i = configs.length - 1; i >= 0; i--) { - this.normalizeConfig(configs[i]); - } - - const projectConfigs = configs.filter((config) => { - return config.kind === "project"; - }); - - const extensionConfigs = configs.filter((config) => { - return config.kind === "extension"; - }); - - const projectClone = JSON.parse(JSON.stringify(project)); - - // While a project can contain multiple configurations, - // from a dependency tree perspective it is always a single project - // This means it can represent one "project", plus multiple extensions or - // one extension, plus multiple extensions - - if (projectConfigs.length === 1) { - // All well, this is the one. Merge config into project - Object.assign(project, projectConfigs[0]); - } else if (projectConfigs.length > 1) { - throw new Error(`Found ${projectConfigs.length} configurations of kind 'project' for ` + - `project ${project.id}. There is only one project per configuration allowed.`); - } else if (projectConfigs.length === 0 && extensionConfigs.length) { - // No project, but extensions - // => choose one to represent the project -> the first one - Object.assign(project, extensionConfigs.shift()); - } else { - throw new Error(`Found ${configs.length} configurations for ` + - `project ${project.id}. None are of valid kind.`); - } - - const extensionProjects = extensionConfigs.map((config) => { - // Clone original project - const configuredProject = JSON.parse(JSON.stringify(projectClone)); - - // Enhance project with its configuration - Object.assign(configuredProject, config); - return configuredProject; - }); - - return {extensions: extensionProjects}; - } - - normalizeConfig(config) { - if (!config.kind) { - config.kind = "project"; // default - } - } - - isConfigValid(project) { - if (!project.specVersion) { - if (project._isRoot) { - throw new Error(`No specification version defined for root project ${project.id}`); - } - log.verbose(`No specification version defined for project ${project.id}`); - return false; // ignore this project - } - - if (project.specVersion !== "0.1" && project.specVersion !== "1.0" && - project.specVersion !== "1.1" && project.specVersion !== "2.0" && - project.specVersion !== "2.1" && project.specVersion !== "2.2" && - project.specVersion !== "2.3" && project.specVersion !== "2.4" && - project.specVersion !== "2.5" && project.specVersion !== "2.6") { - throw new Error( - `Unsupported specification version ${project.specVersion} defined for project ` + - `${project.id}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - - if (!project.type) { - if (project._isRoot) { - throw new Error(`No type configured for root project ${project.id}`); - } - log.verbose(`No type configured for project ${project.id}`); - return false; // ignore this project - } - - if (project.kind !== "project" && project._isRoot) { - // This is arguable. It is not the concern of ui5-project to define the entry point of a project tree - // On the other hand, there is no known use case for anything else right now and failing early here - // makes sense in that regard - throw new Error(`Root project needs to be of kind "project". ${project.id} is of kind ${project.kind}`); - } - - if (project.kind === "project" && project.type === "application") { - // There must be exactly one application project per dependency tree - // If multiple are found, all but the one closest to the root are rejected (ignored) - // If there are two projects equally close to the root, an error is being thrown - if (!this.qualifiedApplicationProject) { - this.qualifiedApplicationProject = project; - } else if (this.qualifiedApplicationProject._level === project._level) { - throw new Error(`Found at least two projects ${this.qualifiedApplicationProject.id} and ` + - `${project.id} of type application with the same distance to the root project. ` + - "Only one project of type application can be used. Failed to decide which one to ignore."); - } else { - return false; // ignore this project - } - } - - return true; - } - - async applyType(project) { - let type; - try { - type = typeRepository.getType(project.type); - } catch (err) { - throw new Error(`Failed to retrieve type for project ${project.id}: ${err.message}`); - } - await type.format(project); - } - - checkProjectMetadata(parent, project) { - if (project.metadata.deprecated && parent && parent._isRoot) { - // Only warn for direct dependencies of the root project - log.warn(`Dependency ${project.metadata.name} is deprecated and should not be used for new projects!`); - } - - if (project.metadata.sapInternal && parent && parent._isRoot && !parent.metadata.allowSapInternal) { - // Only warn for direct dependencies of the root project, except it defines "allowSapInternal" - log.warn(`Dependency ${project.metadata.name} is restricted for use by SAP internal projects only! ` + - `If the project ${parent.metadata.name} is an SAP internal project, add the attribute ` + - `"allowSapInternal: true" to its metadata configuration`); - } - } - - async applyExtension(extension) { - if (!extension.metadata || !extension.metadata.name) { - throw new Error(`metadata.name configuration is missing for extension ${extension.id}`); - } - log.verbose(`Applying extension ${extension.metadata.name}...`); - - if (!extension.specVersion) { - throw new Error(`No specification version defined for extension ${extension.metadata.name}`); - } else if (extension.specVersion !== "0.1" && - extension.specVersion !== "1.0" && - extension.specVersion !== "1.1" && - extension.specVersion !== "2.0" && - extension.specVersion !== "2.1" && - extension.specVersion !== "2.2" && - extension.specVersion !== "2.3" && - extension.specVersion !== "2.4" && - extension.specVersion !== "2.5" && - extension.specVersion !== "2.6") { - throw new Error( - `Unsupported specification version ${extension.specVersion} defined for extension ` + - `${extension.metadata.name}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } else if (this.appliedExtensions[extension.metadata.name]) { - log.verbose(`Extension with the name ${extension.metadata.name} has already been applied. ` + - "This might have been done during dependency lookahead."); - log.verbose(`Already applied extension ID: ${this.appliedExtensions[extension.metadata.name].id}. ` + - `New extension ID: ${extension.id}`); - return; - } - this.appliedExtensions[extension.metadata.name] = extension; - - switch (extension.type) { - case "project-shim": - this.handleShim(extension); - break; - case "task": - this.handleTask(extension); - break; - case "server-middleware": - this.handleServerMiddleware(extension); - break; - default: - throw new Error(`Unknown extension type '${extension.type}' for ${extension.id}`); - } - } - - async readConfigFile(project) { - // A projects configPath property takes precedence over the default "/ui5.yaml" path - const configPath = project.configPath || path.join(project.path, "ui5.yaml"); - let configFile; - try { - configFile = await readFile(configPath, {encoding: "utf8"}); - } catch (err) { - const errorText = "Failed to read configuration for project " + - `${project.id} at "${configPath}". Error: ${err.message}`; - - // Something else than "File or directory does not exist" or root project - if (err.code !== "ENOENT" || project._isRoot) { - throw new Error(errorText); - } else { - log.verbose(errorText); - return null; - } - } - - let configs; - - try { - configs = jsyaml.loadAll(configFile, undefined, { - filename: configPath - }); - } catch (err) { - if (err.name === "YAMLException") { - throw new Error("Failed to parse configuration for project " + - `${project.id} at "${configPath}"\nError: ${err.message}`); - } else { - throw err; - } - } - - if (!configs || !configs.length) { - return configs; - } - - const validationResults = await Promise.all( - configs.map(async (config, documentIndex) => { - // Catch validation errors to ensure proper order of rejections within Promise.all - try { - await validate({ - config, - project: { - id: project.id - }, - yaml: { - path: configPath, - source: configFile, - documentIndex - } - }); - } catch (error) { - return error; - } - }) - ); - - const validationErrors = validationResults.filter(($) => $); - - if (validationErrors.length > 0) { - // For now just throw the error of the first invalid document - throw validationErrors[0]; - } - - return configs; - } - - handleShim(extension) { - if (!extension.shims) { - throw new Error(`Project shim extension ${extension.id} is missing 'shims' configuration`); - } - const {configurations, dependencies, collections} = extension.shims; - - if (configurations) { - log.verbose(`Project shim ${extension.id} contains ` + - `${Object.keys(configurations)} configuration(s)`); - for (const projectId of Object.keys(configurations)) { - this.normalizeConfig(configurations[projectId]); // TODO: Clone object beforehand? - if (this.configShims[projectId]) { - log.verbose(`Project shim ${extension.id}: A configuration shim for project ${projectId} `+ - "has already been applied. Skipping."); - } else if (this.isConfigValid(configurations[projectId])) { - log.verbose(`Project shim ${extension.id}: Adding project configuration for ${projectId}...`); - this.configShims[projectId] = configurations[projectId]; - } else { - log.verbose(`Project shim ${extension.id}: Ignoring invalid ` + - `configuration shim for project ${projectId}`); - } - } - } - - if (dependencies) { - // For the time being, shimmed dependencies only apply to shimmed project configurations - for (const projectId of Object.keys(dependencies)) { - if (this.configShims[projectId]) { - log.verbose(`Project shim ${extension.id}: Adding dependencies ` + - `to project shim '${projectId}'...`); - this.configShims[projectId].dependencies = dependencies[projectId]; - } else { - log.verbose(`Project shim ${extension.id}: No configuration shim found for ` + - `project ID '${projectId}'. Dependency shims currently only apply ` + - "to projects with configuration shims."); - } - } - } - - if (collections) { - log.verbose(`Project shim ${extension.id} contains ` + - `${Object.keys(collections).length} collection(s)`); - for (const projectId of Object.keys(collections)) { - if (this.collections[projectId]) { - log.verbose(`Project shim ${extension.id}: A collection with id '${projectId}' `+ - "is already known. Skipping."); - } else { - log.verbose(`Project shim ${extension.id}: Adding collection with id '${projectId}'...`); - this.collections[projectId] = collections[projectId]; - } - } - } - } - - async applyShims(project) { - const configShim = this.configShims[project.id]; - // Apply configuration shims - if (configShim) { - log.verbose(`Applying configuration shim for project ${project.id}...`); - - if (configShim.dependencies && configShim.dependencies.length) { - if (!configShim.shimDependenciesResolved) { - configShim.dependencies = configShim.dependencies.map((depId) => { - const depProject = this.processedProjects[depId].project; - if (!depProject) { - throw new Error( - `Failed to resolve shimmed dependency '${depId}' for project ${project.id}. ` + - `Is a dependency with ID '${depId}' part of the dependency tree?`); - } - return depProject; - }); - configShim.shimDependenciesResolved = true; - } - configShim.dependencies.forEach((depProject) => { - const parents = this.processedProjects[depProject.id].parents; - if (parents.indexOf(project) === -1) { - parents.push(project); - } else { - log.verbose(`Project ${project.id} is already parent of shimmed dependency ${depProject.id}`); - } - }); - } - - Object.assign(project, configShim); - delete project.shimDependenciesResolved; // Remove shim processing metadata from project - - await this.validateAndNormalizeExistingProject(project); - } - - // Apply collections - for (let i = project.dependencies.length - 1; i >= 0; i--) { - const depId = project.dependencies[i].id; - if (this.collections[depId]) { - log.verbose(`Project ${project.id} depends on collection ${depId}. Resolving...`); - // This project depends on a collection - // => replace collection dependency with first collection project. - const collectionDep = project.dependencies[i]; - const collectionModules = this.collections[depId].modules; - const projects = []; - for (const projectId of Object.keys(collectionModules)) { - // Clone and modify collection "project" - const project = JSON.parse(JSON.stringify(collectionDep)); - project.id = projectId; - project.path = path.join(project.path, collectionModules[projectId]); - projects.push(project); - } - - // Use first collection project to replace the collection dependency - project.dependencies[i] = projects.shift(); - // Add any additional collection projects to end of dependency array (already processed) - project.dependencies.push(...projects); - } - } - } - - handleTask(extension) { - if (!extension.metadata && !extension.metadata.name) { - throw new Error(`Task extension ${extension.id} is missing 'metadata.name' configuration`); - } - if (!extension.task) { - throw new Error(`Task extension ${extension.id} is missing 'task' configuration`); - } - const taskRepository = require("@ui5/builder").tasks.taskRepository; - - const taskPath = path.join(extension.path, extension.task.path); - - taskRepository.addTask({ - name: extension.metadata.name, - specVersion: extension.specVersion, - taskPath, - }); - } - - handleServerMiddleware(extension) { - if (!extension.metadata && !extension.metadata.name) { - throw new Error(`Middleware extension ${extension.id} is missing 'metadata.name' configuration`); - } - if (!extension.middleware) { - throw new Error(`Middleware extension ${extension.id} is missing 'middleware' configuration`); - } - const {middlewareRepository} = require("@ui5/server"); - - const middlewarePath = path.join(extension.path, extension.middleware.path); - middlewareRepository.addMiddleware({ - name: extension.metadata.name, - specVersion: extension.specVersion, - middlewarePath - }); - } - - async validateAndNormalizeExistingProject(project) { - // Validate project config, but exclude additional properties - const excludedProperties = [ - "id", - "version", - "path", - "dependencies", - "_level", - "_isRoot", - "configPath" - ]; - const config = {}; - for (const key of Object.keys(project)) { - if (!excludedProperties.includes(key)) { - config[key] = project[key]; - } - } - await validate({ - config, - project: { - id: project.id - } - }); - - this.normalizeConfig(project); - } -} - -/** - * The Project Preprocessor enriches the dependency information with project configuration - * - * @public - * @namespace - * @alias module:@ui5/project.projectPreprocessor - */ -module.exports = { - /** - * Collects project information and its dependencies to enrich it with project configuration - * - * @public - * @param {object} tree Dependency tree of the project - * @returns {Promise} Promise resolving with the dependency tree and enriched project configuration - */ - processTree: function(tree) { - return new ProjectPreprocessor({tree}).processTree(); - }, - _ProjectPreprocessor: ProjectPreprocessor -}; diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 93dcd7b1d..5aa6910b0 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -53,7 +53,7 @@ class Module extends Project { }); } - _getWriter({includeTestResources = false} = {}) { + _getWriter() { if (!this._writer) { this._writer = resourceFactory.createAdapter({ virBasePath: "/" diff --git a/lib/translators/npm.js b/lib/translators/npm.js deleted file mode 100644 index 7cb11556c..000000000 --- a/lib/translators/npm.js +++ /dev/null @@ -1,523 +0,0 @@ -const log = require("@ui5/logger").getLogger("normalizer:translators:npm"); -const path = require("path"); -const readPkgUp = require("read-pkg-up"); -const readPkg = require("read-pkg"); -const {promisify} = require("util"); -const fs = require("graceful-fs"); -const realpath = promisify(fs.realpath); -const resolveModulePath = promisify(require("resolve")); -const parentNameRegExp = new RegExp(/:([^:]+):$/i); - -class NpmTranslator { - constructor({includeDeduped}) { - this.projectCache = {}; - this.projectsWoUi5Deps = []; - this.pendingDeps = {}; - this.includeDeduped = includeDeduped; - this.debugUnresolvedProjects = {}; - } - - /* - Returns a promise with an array of projects - */ - async processPkg(data, parentPath) { - const cwd = data.path; - const moduleName = data.name; - const pkg = data.pkg; - const parentName = parentPath && this.getParentNameFromPath(parentPath) || "nothing - root project"; - - log.verbose("Analyzing %s (%s) (dependency of %s)", moduleName, cwd, parentName); - - if (!parentPath) { - parentPath = ":"; - } - parentPath += `${moduleName}:`; - - /* - * Inject collection definitions for some known projects - * until this is either not needed anymore or added to the actual project. - */ - this.shimCollection(moduleName, pkg); - - let dependencies = pkg.dependencies || {}; - let optDependencies = pkg.devDependencies || {}; - - const version = pkg.version; - - // Also look for "napa" dependencies (see https://github.com/shama/napa) - if (pkg.napa) { - Object.keys(pkg.napa).forEach((napaName) => { - dependencies[napaName] = pkg.napa[napaName]; - }); - } - - const ui5Deps = pkg.ui5 && pkg.ui5.dependencies; - if (ui5Deps && Array.isArray(ui5Deps)) { - for (let i = 0; i < ui5Deps.length; i++) { - const depName = ui5Deps[i]; - if (!dependencies[depName] && !optDependencies[depName]) { - throw new Error(`[npm translator] Module ${depName} is defined as UI5 dependency ` + - `but missing from npm dependencies of module ${moduleName}`); - } - } - // When UI5-dependencies are defined, we don't care whether an npm dependency is optional or not. - // All UI5-dependencies need to be there. - dependencies = Object.assign({}, dependencies, optDependencies); - optDependencies = {}; - - for (const depName of Object.keys(dependencies)) { - if (ui5Deps.indexOf(depName) === -1) { - log.verbose("Ignoring npm dependency %s. Not defined in UI5-dependency configuration.", depName); - delete dependencies[depName]; - } - } - } else { - this.projectsWoUi5Deps.push(moduleName); - } - - /* - It's either a project or a collection but never both! - - We don't care about dependencies of collections for now because we only remember dependencies - on projects. - Although we could add the collections dependencies as project dependencies to the related modules - */ - const isCollection = typeof pkg.collection === "object" && typeof pkg.collection.modules === "object"; - if (!isCollection) { - if (log.isLevelEnabled("silly")) { - this.debugUnresolvedProjects[cwd] = { - moduleName - }; - const logParentPath = parentPath.replace(":", "(root) ").replace(/([^:]*):$/, "(current) $1"); - log.silly(`Parent path: ${logParentPath.replace(/:/ig, " ➡️ ")}`); - log.silly(`Resolving dependencies of ${moduleName}...`); - } - return this.getDepProjects({ - cwd, - parentPath, - dependencies, - optionalDependencies: pkg.optionalDependencies - }).then((depProjects) => { - // Array needs to be flattened because: - // getDepProjects returns array * 2 = array with two arrays - const projects = Array.prototype.concat.apply([], depProjects); - if (log.isLevelEnabled("silly")) { - delete this.debugUnresolvedProjects[cwd]; - log.silly(`Resolved dependencies of ${moduleName}`); - const pendingModules = Object.keys(this.debugUnresolvedProjects).map((key) => { - return this.debugUnresolvedProjects[key].moduleName; - }); - if (pendingModules.length) { - log.silly(`${pendingModules.length} resolutions left: ${pendingModules.join(", ")}`); - } else { - log.silly("All modules resolved."); - } - } - - return [{ - id: moduleName, - version, - path: cwd, - dependencies: projects - }]; - }).then(([project]) => { - // Register optional dependencies as "pending" as we do not try to resolve them ourself. - // If we would, we would *always* resolve them for modules that are linked into monorepos. - // In such cases, dev-dependencies are typically always available in the node_modules directory. - // Therefore we register them as pending. And if any other project resolved them, we add them to - // our dependencies later on. - this.registerPendingDependencies({ - parentProject: project, - parentPath, - dependencies: optDependencies - }); - return [project]; - }); - } else { // collection - log.verbose("Found a collection: %s", moduleName); - const modules = pkg.collection.modules; - return Promise.all( - Object.keys(modules).map((depName) => { - const modulePath = path.join(cwd, modules[depName]); - if (depName === parentName) { // TODO improve recursion detection here - log.verbose("Ignoring module with same name as parent: " + parentName); - return null; - } - return this.readProject({modulePath, moduleName: depName, parentPath}); - }) - ).then((projects) => { - // Array needs to be flattened because: - // readProject returns an array + Promise.all returns an array = array filled with arrays - // Filter out null values of ignored packages - return Array.prototype.concat.apply([], projects.filter((p) => p !== null)); - }); - } - } - - getParentNameFromPath(parentPath) { - const parentNameMatch = parentPath.match(parentNameRegExp); - if (parentNameMatch) { - return parentNameMatch[1]; - } else { - log.error(`Failed to get parent name from path ${parentPath}`); - } - } - - getDepProjects({cwd, dependencies, optionalDependencies, parentPath}) { - return Promise.all( - Object.keys(dependencies).map((moduleName) => { - return this.findModulePath(cwd, moduleName).then((modulePath) => { - return this.readProject({modulePath, moduleName, parentPath}); - }, (err) => { - // Due to normalization done by by the "read-pkg-up" module the values - // in "optionalDependencies" get added to the modules "dependencies". Also described here: - // https://github.com/npm/normalize-package-data#what-normalization-currently-entails - // Ignore resolution errors for optional dependencies - if (optionalDependencies && optionalDependencies[moduleName]) { - return null; - } else { - throw err; - } - }); - }) - ).then((depProjects) => { - // Array needs to be flattened because: - // readProject returns an array + Promise.all returns an array = array filled with arrays - // Also filter out null values of ignored packages - return Array.prototype.concat.apply([], depProjects.filter((p) => p !== null)); - }); - } - - async readProject({modulePath, moduleName, parentPath}) { - let {pPkg} = this.projectCache[modulePath] || {}; - if (!pPkg) { - pPkg = readPkg({cwd: modulePath}).catch((err) => { - // Failed to read package - // If dependency shim is available, fake the package - - /* Disabled shimming until shim-plugin is available - const id = path.basename(modulePath); - if (pkgDependenciesShims[id]) { - const dependencies = JSON.parse(JSON.stringify(pkgDependenciesShims[id])); - return { // Fake package.json content - name: id, - dependencies, - version: "", - ui5: { - dependencies: Object.keys(dependencies) - } - }; - }*/ - throw err; - }); - this.projectCache[modulePath] = { - pPkg - }; - } - - // Check whether module has already been processed in the current subtree (indicates a loop) - if (parentPath.indexOf(`:${moduleName}:`) !== -1) { - log.verbose(`Deduping project ${moduleName} with parent path ${parentPath}`); - // This is a loop => abort further processing - if (!this.includeDeduped) { - // Ignore this dependency - return null; - } else { - // Create deduped project - const pkg = await pPkg; - return this.createDedupedProject({ - id: moduleName, - version: pkg.version, - path: modulePath - }); - } - } - - // Check whether project has already been processed - // Note: We can only cache already *processed* projects, not the promise waiting for the processing to complete - // Otherwise cyclic dependencies might wait for each other, emptying the event loop - // Note 2: Currently caching can't be used at all. If a cached dependency has an indirect dependency to the - // requesting module, a circular reference would be created - /* - if (cachedProject) { - if (log.isLevelEnabled("silly")) { - log.silly(`${parentPath.match(/([^:]*):$/)[1]} retrieved already ` + - `resolved project ${moduleName} from cache 🗄 `); - } - return cachedProject; - }*/ - if (log.isLevelEnabled("silly")) { - log.silly(`${parentPath.match(/([^:]*):$/)[1]} is waiting for ${moduleName}...`); - } - - return pPkg.then((pkg) => { - return this.processPkg({ - name: moduleName, - pkg, - path: modulePath - }, parentPath).then((projects) => { - // Flatten the array of project arrays (yes, because collections) - return Array.prototype.concat.apply([], projects.filter((p) => p !== null)); - })/* - // Currently no project caching, see above - .then((projects) => { - this.projectCache[modulePath].cachedProject = projects; - return projects; - })*/; - }, (err) => { - // Failed to read package. Create a project anyway - log.error(`Failed to read package.json of module ${moduleName} at ${modulePath} - Error: ${err.message}`); - log.error(`Ignoring module ${moduleName} due to errors.`); - return null; - }); - } - - /* Returns path to a module - */ - findModulePath(basePath, moduleName) { - return resolveModulePath(moduleName + "/package.json", { - basedir: basePath, - preserveSymlinks: false - }).then((pkgPath) => { - return realpath(pkgPath); - }).then((pkgPath) => { - return path.dirname(pkgPath); - }).catch((err) => { - // Fallback: Check for a collection above this module - return readPkgUp({ - cwd: path.dirname(basePath) - }).then((result) => { - if (result && result.packageJson) { - const pkg = result.packageJson; - - // As of today, collections only exist in shims - this.shimCollection(pkg.name, pkg); - if (pkg.collection) { - log.verbose(`Unable to locate module ${moduleName} via resolve logic, but found ` + - `a collection in parent hierarchy: ${pkg.name}`); - const modules = pkg.collection.modules || {}; - if (modules[moduleName]) { - const modulePath = path.join(path.dirname(result.path), modules[moduleName]); - log.verbose(`Found module ${moduleName} in that collection`); - return modulePath; - } - throw new Error( - `[npm translator] Could not find module ${moduleName} in collection ${pkg.name}`); - } - } - - throw new Error(`[npm translator] Could not locate module ${moduleName} via resolve logic ` + - `(error: ${err.message}) or in a collection`); - }, (err) => { - throw new Error( - `[npm translator] Failed to locate module ${moduleName} from ${basePath} - Error: ${err.message}`); - }); - }); - } - - registerPendingDependencies({dependencies, parentProject, parentPath}) { - Object.keys(dependencies).forEach((moduleName) => { - if (this.pendingDeps[moduleName]) { - // Register additional potential parent for pending dependency - this.pendingDeps[moduleName].parents.push({ - project: parentProject, - path: parentPath - }); - } else { - // Add new pending dependency - this.pendingDeps[moduleName] = { - parents: [{ - project: parentProject, - path: parentPath, - }] - }; - } - }); - } - - processPendingDeps(tree) { - if (Object.keys(this.pendingDeps).length === 0) { - // No pending deps => nothing to do - log.verbose("No pending (optional) dependencies to process"); - return tree; - } - const queue = [tree]; - const visited = new Set(); - - // Breadth-first search to prefer projects closer to root - while (queue.length) { - const project = queue.shift(); // Get and remove first entry from queue - if (!project.id) { - throw new Error("Encountered project with missing id"); - } - if (visited.has(project.id)) { - continue; - } - visited.add(project.id); - - if (this.pendingDeps[project.id]) { - for (let i = this.pendingDeps[project.id].parents.length - 1; i >= 0; i--) { - const parent = this.pendingDeps[project.id].parents[i]; - // Check whether module has already been processed in the current subtree (indicates a loop) - if (parent.path.indexOf(`:${project.id}:`) !== -1) { - // This is a loop - log.verbose(`Deduping pending dependency ${project.id} with parent path ${parent.path}`); - if (this.includeDeduped) { - // Create project marked as deduped - const dedupedProject = this.createDedupedProject({ - id: project.id, - version: project.version, - path: project.path - }); - parent.project.dependencies.push(dedupedProject); - } // else: do nothing - } else { - if (log.isLevelEnabled("silly")) { - log.silly(`Adding optional dependency ${project.id} to project ${parent.project.id} ` + - `(parent path: ${parent.path})...`); - } - const dedupedProject = this.dedupeTree(project, parent.path); - parent.project.dependencies.push(dedupedProject); - } - } - this.pendingDeps[project.id] = null; - - if (log.isLevelEnabled("silly")) { - log.silly(`${Object.keys(this.pendingDeps).length} pending dependencies left`); - } - } - - if (project.dependencies) { - queue.push(...project.dependencies); - } - } - return tree; - } - - generateDependencyTree(dirPath) { - return readPkgUp({ - cwd: dirPath - }).then((result) => { - if (!result || !result.packageJson) { - throw new Error( - `[npm translator] Failed to locate package.json for directory "${path.resolve(dirPath)}"`); - } - return { - // resolved path points to the package.json, but we want just the folder path - path: path.dirname(result.path), - name: result.packageJson.name, - pkg: result.packageJson - }; - }).then(this.processPkg.bind(this)).then((tree) => { - if (this.projectsWoUi5Deps.length) { - log.verbose( - "[PERF] Consider defining UI5-dependencies in the package.json files of the relevant modules " + - "from the following list to improve npm translator execution time: " + - this.projectsWoUi5Deps.join(", ")); - } - - /* - By default, there is just one root project in the tree, - but in case a collection is returned, there are multiple roots. - This can only happen: - 1. when running with a collection project as CWD - => This is not intended and will throw an error as no project will match the CWD - 2. when running in a project without a package.json within a collection project - => In case the CWD matches with a project from the collection, then that - project is picked as root, otherwise an error is thrown - */ - - for (let i = 0; i < tree.length; i++) { - const rootPackage = tree[i]; - if (path.resolve(rootPackage.path) === path.resolve(dirPath)) { - log.verbose("Treetop:"); - log.verbose(rootPackage); - return rootPackage; - } - } - - throw new Error("[npm translator] Could not identify root project."); - }).then(this.processPendingDeps.bind(this)); - } - - /* - * Inject collection definitions for some known projects - * until this is either not needed anymore or added to the actual project. - */ - shimCollection(moduleName, pkg) { - /* Disabled shimming until shim-plugin is available - if (!pkg.collection && pkgCollectionShims[moduleName]) { - pkg.collection = JSON.parse(JSON.stringify(pkgCollectionShims[moduleName])); - }*/ - } - - dedupeTree(tree, parentPath) { - const projectsToDedupe = new Set(parentPath.slice(1, -1).split(":")); - const clonedTree = JSON.parse(JSON.stringify(tree)); - const queue = [{project: clonedTree}]; - // BFS - while (queue.length) { - const {project, parent} = queue.shift(); // Get and remove first entry from queue - - if (parent && projectsToDedupe.has(project.id)) { - log.silly(`In tree "${tree.id}" (parent path "${parentPath}"): Deduplicating project ${project.id} `+ - `(child of ${parent.id})`); - - const idx = parent.dependencies.indexOf(project); - if (this.includeDeduped) { - const dedupedProject = this.createDedupedProject(project); - parent.dependencies.splice(idx, 1, dedupedProject); - } else { - parent.dependencies.splice(idx, 1); - } - } - - if (project.dependencies) { - queue.push(...project.dependencies.map((dependency) => { - return { - project: dependency, - parent: project - }; - })); - } - } - return clonedTree; - } - - createDedupedProject({id, version, path}) { - return { - id, - version, - path, - dependencies: [], - deduped: true - }; - } -} - -/** - * Translator for npm resources - * - * @private - * @namespace - * @alias module:@ui5/project.translators.npm - */ -module.exports = { - /** - * Generates a dependency tree for npm projects - * - * @public - * @param {string} dirPath Project path - * @param {object} [options] - * @param {boolean} [options.includeDeduped=false] - * @returns {Promise} Promise resolving with a dependency tree - */ - generateDependencyTree(dirPath, options = {includeDeduped: false}) { - return new NpmTranslator(options).generateDependencyTree(dirPath); - } -}; - -// Export NpmTranslator class for testing only -if (process.env.NODE_ENV === "test") { - module.exports._NpmTranslator = NpmTranslator; -} diff --git a/lib/translators/ui5Framework.js b/lib/translators/ui5Framework.js deleted file mode 100644 index de086bb10..000000000 --- a/lib/translators/ui5Framework.js +++ /dev/null @@ -1,294 +0,0 @@ -const log = require("@ui5/logger").getLogger("normalizer:translators:ui5Framework"); - -class ProjectProcessor { - constructor({libraryMetadata}) { - this._libraryMetadata = libraryMetadata; - this._projectCache = {}; - } - getProject(libName) { - log.verbose(`Creating project for library ${libName}...`); - - if (this._projectCache[libName]) { - log.verbose(`Returning cached project for library ${libName}`); - return this._projectCache[libName]; - } - - if (!this._libraryMetadata[libName]) { - throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); - } - - const depMetadata = this._libraryMetadata[libName]; - - const dependencies = []; - dependencies.push(...depMetadata.dependencies.map((depName) => { - return this.getProject(depName); - })); - - if (depMetadata.optionalDependencies) { - const resolvedOptionals = depMetadata.optionalDependencies.map((depName) => { - if (this._libraryMetadata[depName]) { - log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); - return this.getProject(depName); - } - }).filter(($)=>$); - - dependencies.push(...resolvedOptionals); - } - - this._projectCache[libName] = { - id: depMetadata.id, - version: depMetadata.version, - path: depMetadata.path, - dependencies - }; - return this._projectCache[libName]; - } -} - -const utils = { - getAllNodesOfTree(tree) { - const nodes = {}; - const queue = [...tree]; - while (queue.length) { - const project = queue.shift(); - if (!nodes[project.metadata.name]) { - nodes[project.metadata.name] = project; - queue.push(...project.dependencies); - } - } - return nodes; - }, - isFrameworkProject(project) { - return project.id.startsWith("@openui5/") || project.id.startsWith("@sapui5/"); - }, - shouldIncludeDependency({optional, development}, root) { - // Root project should include all dependencies - // Otherwise only non-optional and non-development dependencies should be included - return root || (optional !== true && development !== true); - }, - getFrameworkLibrariesFromTree(project, ui5Dependencies = [], root = true) { - if (utils.isFrameworkProject(project)) { - // Ignoring UI5 Framework libraries in dependencies - return ui5Dependencies; - } - - this._addFrameworkLibrariesFromProject(project, ui5Dependencies, root); - - project.dependencies.map((depProject) => { - utils.getFrameworkLibrariesFromTree(depProject, ui5Dependencies, false); - }); - return ui5Dependencies; - }, - _addFrameworkLibrariesFromProject(project, ui5Dependencies, root) { - if (!project.framework) { - return; - } - if ( - project.specVersion !== "2.0" && project.specVersion !== "2.1" && - project.specVersion !== "2.2" && project.specVersion !== "2.3" && - project.specVersion !== "2.4" && project.specVersion !== "2.5" && - project.specVersion !== "2.6" - ) { - log.warn(`Project ${project.metadata.name} defines invalid ` + - `specification version ${project.specVersion} for framework.libraries configuration`); - return; - } - - if (!project.framework.libraries || !project.framework.libraries.length) { - log.verbose(`Project ${project.metadata.name} defines no framework.libraries configuration`); - // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json - return; - } - - project.framework.libraries.forEach((dependency) => { - if (!ui5Dependencies.includes(dependency.name) && - utils.shouldIncludeDependency(dependency, root)) { - ui5Dependencies.push(dependency.name); - } - }); - }, - ProjectProcessor -}; - -/** - * - * - * @private - * @namespace - * @alias module:@ui5/project.translators.ui5Framework - */ -module.exports = { - /** - * - * - * @public - * @param {object} tree - * @param {object} [options] - * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework - * version from the provided tree - * @returns {Promise} Promise - */ - generateDependencyTree: async function(tree, options = {}) { - // Don't create a tree when root project doesn't have a framework configuration - if (!tree.framework) { - return null; - } - - // Ignoring UI5 Framework libraries - if (utils.isFrameworkProject(tree)) { - log.verbose(`UI5 framework dependency resolution is currently not supported ` + - `for framework libraries. Skipping project "${tree.id}"`); - return null; - } - - const frameworkName = tree.framework.name; - if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { - throw new Error( - `Unknown framework.name "${frameworkName}" for project ${tree.id}. Must be "OpenUI5" or "SAPUI5"` - ); - } - - let Resolver; - if (frameworkName === "OpenUI5") { - Resolver = require("../ui5Framework/Openui5Resolver"); - } else if (frameworkName === "SAPUI5") { - Resolver = require("../ui5Framework/Sapui5Resolver"); - } - - let version; - if (!tree.framework.version) { - throw new Error( - `framework.version is not defined for project ${tree.id}` - ); - } else if (options.versionOverride) { - version = await Resolver.resolveVersion(options.versionOverride, {cwd: tree.path}); - log.info( - `Overriding configured ${frameworkName} version ` + - `${tree.framework.version} with version ${version}` - ); - } else { - version = tree.framework.version; - } - - const referencedLibraries = utils.getFrameworkLibrariesFromTree(tree); - if (!referencedLibraries.length) { - log.verbose(`No ${frameworkName} libraries referenced in project ${tree.id} or its dependencies`); - return null; - } - - log.info(`Using ${frameworkName} version: ${version}`); - - const resolver = new Resolver({cwd: tree.path, version}); - - let startTime; - if (log.isLevelEnabled("verbose")) { - startTime = process.hrtime(); - } - - const {libraryMetadata} = await resolver.install(referencedLibraries); - - if (log.isLevelEnabled("verbose")) { - const timeDiff = process.hrtime(startTime); - const prettyHrtime = require("pretty-hrtime"); - log.verbose( - `${frameworkName} dependencies ${referencedLibraries.join(", ")} ` + - `resolved in ${prettyHrtime(timeDiff)}`); - } - - const projectProcessor = new utils.ProjectProcessor({ - libraryMetadata - }); - - const libraries = referencedLibraries.map((libName) => { - return projectProcessor.getProject(libName); - }); - - // Use root project (=requesting project) as root of framework tree - // For this we clone all properties of the root project, - // except the dependencies since they will be overwritten anyways - const frameworkTree = {}; - for (const attribute of Object.keys(tree)) { - if (attribute !== "dependencies") { - frameworkTree[attribute] = JSON.parse(JSON.stringify(tree[attribute])); - } - } - - // Overwrite dependencies to exclusively contain framework libraries - frameworkTree.dependencies = libraries; - // Flag as transparent so that the project type is not applied again - frameworkTree._transparentProject = true; - return frameworkTree; - }, - - mergeTrees: function(projectTree, frameworkTree) { - const frameworkLibs = utils.getAllNodesOfTree(frameworkTree.dependencies); - - log.verbose(`Merging framework tree into project tree "${projectTree.metadata.name}"`); - - const queue = [projectTree]; - const processedProjects = []; - while (queue.length) { - const project = queue.shift(); - if (project.deduped) { - // Deduped projects have certainly already been processed - // Note: Deduped dependencies don't have any metadata or other configuration. - continue; - } - if (processedProjects.includes(project.id)) { - // projectTree must be duplicate free. A second occurrence of the same project - // is always the same object. Therefore a single processing needs to be ensured. - // Otherwise the isFrameworkProject check would detect framework dependencies added - // at an earlier processing of the project and yield incorrect logging. - log.verbose(`Project ${project.metadata.name} (${project.id}) has already been processed`); - continue; - } - processedProjects.push(project.id); - - project.dependencies = project.dependencies.filter((depProject) => { - if (utils.isFrameworkProject(depProject)) { - log.verbose( - `A translator has already added the UI5 framework library ${depProject.metadata.name} ` + - `(id: ${depProject.id}) to the dependencies of project ${project.metadata.name}. ` + - `This dependency will be ignored.`); - log.info(`If project ${project.metadata.name} contains a package.json in which it defines a ` + - `dependency to the UI5 framework library ${depProject.id}, this dependency should be removed.`); - return false; - } - return true; - }); - queue.push(...project.dependencies); - - if ( - ( - project.specVersion === "2.0" || project.specVersion === "2.1" || - project.specVersion === "2.2" || project.specVersion === "2.3" || - project.specVersion === "2.4" || project.specVersion === "2.5" || - project.specVersion === "2.6" - ) && project.framework && project.framework.libraries) { - const frameworkDeps = project.framework.libraries - .filter((dependency) => { - if (dependency.optional && frameworkLibs[dependency.name]) { - // Resolved optional dependencies shall be used - return true; - } - // Filter out development and unresolved optional dependencies for non-root projects - return utils.shouldIncludeDependency(dependency, project._isRoot); - }) - .map((dependency) => { - if (!frameworkLibs[dependency.name]) { - throw new Error(`Missing framework library ${dependency.name} ` + - `required by project ${project.metadata.name}`); - } - return frameworkLibs[dependency.name]; - }); - if (frameworkDeps.length) { - project.dependencies.push(...frameworkDeps); - } - } - } - return projectTree; - }, - - // Export for testing only - _utils: process.env.NODE_ENV === "test" ? utils : undefined -}; From ba2404b196db9c9e4287ed2cd3701ab6d9f379e5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 4 May 2022 14:10:32 +0200 Subject: [PATCH 48/99] ApplicationBuilder: Build custom bundles in sequence To ensure reproducibility, especially if one bundle excludes another. --- lib/buildDefinitions/ApplicationBuilder.js | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index 565867e03..eecfafad2 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -69,18 +69,20 @@ class ApplicationBuilder extends AbstractBuilder { if (bundles.length) { this.addTask("generateBundle", {requiresDependencies: true}, async ({workspace, dependencies, taskUtil, options}) => { - return Promise.all(bundles.map((bundle) => { - return getTask("generateBundle").task({ - workspace, - dependencies, - taskUtil, - options: { - projectName: options.projectName, - bundleDefinition: bundle.bundleDefinition, - bundleOptions: bundle.bundleOptions - } + return bundles.reduce(function(sequence, bundle) { + return sequence.then(function() { + return getTask("generateBundle").task({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); }); - })); + }, Promise.resolve()); }); } From 453352d253b516dd2ac5f4ce5bf3af798e762cbb Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 5 May 2022 12:18:31 +0200 Subject: [PATCH 49/99] Implement archive metadata handling --- lib/buildHelpers/createArchiveMetadata.js | 58 +++++++++++++++++ lib/builder.js | 20 +++++- lib/graph/Module.js | 31 ++++++++- lib/specifications/Project.js | 7 +- lib/specifications/types/Application.js | 4 ++ lib/specifications/types/Library.js | 5 ++ .../application.a/.ui5/archive-metadata.json | 42 ++++++++++++ .../archives/application.a/package.json | 13 ++++ .../application.a/resources/id1/index.html | 9 +++ .../application.a/resources/id1/manifest.json | 13 ++++ .../application.a/resources/id1/test-dbg.js | 5 ++ .../application.a/resources/id1/test.js | 5 ++ test/lib/buildHelpers/archive.integration.js | 64 +++++++++++++++++++ .../lib/buildHelpers/createArchiveMetadata.js | 60 +++++++++++++++++ test/lib/graph/Module.js | 13 ++++ 15 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 lib/buildHelpers/createArchiveMetadata.js create mode 100644 test/fixtures/archives/application.a/.ui5/archive-metadata.json create mode 100644 test/fixtures/archives/application.a/package.json create mode 100644 test/fixtures/archives/application.a/resources/id1/index.html create mode 100644 test/fixtures/archives/application.a/resources/id1/manifest.json create mode 100644 test/fixtures/archives/application.a/resources/id1/test-dbg.js create mode 100644 test/fixtures/archives/application.a/resources/id1/test.js create mode 100644 test/lib/buildHelpers/archive.integration.js create mode 100644 test/lib/buildHelpers/createArchiveMetadata.js diff --git a/lib/buildHelpers/createArchiveMetadata.js b/lib/buildHelpers/createArchiveMetadata.js new file mode 100644 index 000000000..e453e722a --- /dev/null +++ b/lib/buildHelpers/createArchiveMetadata.js @@ -0,0 +1,58 @@ +function getVersion(pkg) { + const packageInfo = require(`${pkg}/package.json`); + return packageInfo.version; +} + +module.exports = async function(project, buildConfig) { + const projectName = project.getName(); + const type = project.getType(); + + let pathMappingSource; + switch (type) { + case "application": + pathMappingSource = "webapp"; + break; + case "library": + case "legacy-library": + case "theme-library": + pathMappingSource = "src"; + break; + default: + throw new Error( + `Unable to create archive metadata for project ${project.getName()}: ` + + `Project type ${type} is currently not supported`); + } + + + const metadata = { + specVersion: project.getSpecVersion(), + type, + metadata: { + name: projectName, + }, + customConfiguration: { // TODO 3.0: Make "_archive" a top-level property + _archive: { + archiveSpecVersion: "0.1", + timestamp: new Date().toISOString(), + versions: { + builderVersion: getVersion("@ui5/builder"), + projectVersion: getVersion("@ui5/project"), + fsVersion: getVersion("@ui5/fs"), + }, + buildConfig, + version: project.getVersion(), + namespace: project.getNamespace(), + tags: project.getResourceTagCollection().getAllTags() + } + }, + resources: { + configuration: { + paths: { + [pathMappingSource]: `resources/${project.getNamespace()}` + } + } + } + }; + + return metadata; +}; diff --git a/lib/builder.js b/lib/builder.js index 325b49016..f197421c0 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -251,12 +251,28 @@ module.exports = async function({ log.verbose("Finished building project %s", project.getName()); buildLogger.completeWork(1); - const resources = await project.getReader({style: "runtime"}).byGlob("/**/*"); - if (!requestedProjects.includes(projectName)) { + // This project shall not be part of the build result return; } + + const resources = await project.getReader({ + // Always use buildtime (=namespace) style when writing an archive + style: archive ? "buildtime" : "runtime" + }).byGlob("/**/*"); + log.verbose(`Writing out files...`); + + if (archive) { + // Create and write archive metadata file + const createArchiveMetadata = require("./buildHelpers/createArchiveMetadata"); + const metadata = await createArchiveMetadata(project, buildConfig); + await fsTarget.write(resourceFactory.createResource({ + path: `/.ui5/archive-metadata.json`, + string: JSON.stringify(metadata, null, "\t") // TODO 3.0: minify? + })); + } + const taskUtil = projectContext.getTaskUtil(); await Promise.all(resources.map((resource) => { if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 591420a31..5ab942cbf 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -186,6 +186,9 @@ class Module { configurations = await this._getSuppliedConfigurations(); + if (!configurations || !configurations.length) { + configurations = await this._getArchiveConfigurations(); + } if (!configurations || !configurations.length) { configurations = await this._getYamlConfigurations(); } @@ -195,10 +198,10 @@ class Module { return configurations || []; } - async _normalizeAndApplyShims(config) { + _normalizeAndApplyShims(config) { this._normalizeConfig(config); - if (config.kind !== "project") { + if (config.kind !== "project") { // TODO 3.0: Shouldn't this be '==='? this._applyProjectShims(config); } return config; @@ -259,6 +262,7 @@ class Module { const configPath = this._configPath; let configFile; if (path.isAbsolute(configPath)) { + // Handle absolute file paths with the native FS module try { configFile = await readFile(configPath, {encoding: "utf8"}); } catch (err) { @@ -268,6 +272,7 @@ class Module { `${this.getId()} at '${configPath}'. Error: ${err.message}`); } } else { + // Handle relative file paths with the @ui5/fs (virtual) file system const reader = await this.getReader(); let configResource; try { @@ -306,9 +311,11 @@ class Module { } if (!configs || !configs.length) { + // No configs found => exit here return configs; } + // Validate found configurations with schema const validationResults = await Promise.all( configs.map(async (config, documentIndex) => { // Catch validation errors to ensure proper order of rejections within Promise.all @@ -333,6 +340,7 @@ class Module { const validationErrors = validationResults.filter(($) => $); if (validationErrors.length > 0) { + // Throw any validation errors // For now just throw the error of the first invalid document throw validationErrors[0]; } @@ -340,6 +348,25 @@ class Module { return configs; } + async _getArchiveConfigurations() { + const config = await this._readArchiveMetadata(); + + if (!config) { + log.verbose(`Could not find any archive metadata files in module ${this.getId()}`); + return []; + } + + return [this._normalizeAndApplyShims(config)]; + } + + async _readArchiveMetadata() { + const reader = await this.getReader(); + const archiveMetadataResource = await reader.byPath("/.ui5/archive-metadata.json"); + if (archiveMetadataResource) { + return JSON.parse(await archiveMetadataResource.getString()); + } + } + _normalizeConfig(config) { if (!config.kind) { config.kind = "project"; // default diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 9141cbaca..185fc3925 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -72,6 +72,10 @@ class Project extends Specification { return this._config.builder && this._config.builder.settings; } + getArchiveMetadata() { + return this._config.customConfiguration?._archive; + } + /* === Resource Access === */ /** * TODO @@ -92,7 +96,8 @@ class Project extends Specification { const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; this._resourceTagCollection = new ResourceTagCollection({ allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], - allowedNamespaces: ["project"] + allowedNamespaces: ["project"], + tags: this.getArchiveMetadata()?.tags }); } return this._resourceTagCollection; diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index ab4347ccb..17e608d88 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -79,6 +79,10 @@ class Application extends ComponentProject { async _parseConfiguration(config) { await super._parseConfiguration(config); + if (config.customConfiguration?._archive) { + this._namespace = config.customConfiguration._archive.namespace; + return; + } this._namespace = await this._getNamespace(); } diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 100790414..4c6b09154 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -109,6 +109,11 @@ class Library extends ComponentProject { async _parseConfiguration(config) { await super._parseConfiguration(config); + if (config.customConfiguration?._archive) { + this._namespace = config.customConfiguration._archive.namespace; + return; + } + this._namespace = await this._getNamespace(); if (!config.metadata.copyright) { diff --git a/test/fixtures/archives/application.a/.ui5/archive-metadata.json b/test/fixtures/archives/application.a/.ui5/archive-metadata.json new file mode 100644 index 000000000..a644c0c6f --- /dev/null +++ b/test/fixtures/archives/application.a/.ui5/archive-metadata.json @@ -0,0 +1,42 @@ +{ + "specVersion": "2.3", + "type": "application", + "metadata": { + "name": "application.a" + }, + "customConfiguration": { + "_archive": { + "archiveSpecVersion": "0.1", + "timestamp": "2022-05-04T12:45:30.024Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "id1", + "version": "0.2.0", + "namespace": "id1", + "tags": { + "/resources/id1/test.js": { + "ui5:HasDebugVariant": true + }, + "/resources/id1/test-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } + }, + "resources": { + "configuration": { + "paths": { + "webapp": "resources/id1" + } + } + } +} diff --git a/test/fixtures/archives/application.a/package.json b/test/fixtures/archives/application.a/package.json new file mode 100644 index 000000000..b5401c1e6 --- /dev/null +++ b/test/fixtures/archives/application.a/package.json @@ -0,0 +1,13 @@ +{ + "name": "application.a-archive", + "version": "1.0.0", + "description": "Simple SAPUI5 based application", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/archives/application.a/resources/id1/index.html b/test/fixtures/archives/application.a/resources/id1/index.html new file mode 100644 index 000000000..77b0207cc --- /dev/null +++ b/test/fixtures/archives/application.a/resources/id1/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + \ No newline at end of file diff --git a/test/fixtures/archives/application.a/resources/id1/manifest.json b/test/fixtures/archives/application.a/resources/id1/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/archives/application.a/resources/id1/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/test/fixtures/archives/application.a/resources/id1/test-dbg.js b/test/fixtures/archives/application.a/resources/id1/test-dbg.js new file mode 100644 index 000000000..a3df410c3 --- /dev/null +++ b/test/fixtures/archives/application.a/resources/id1/test-dbg.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/test/fixtures/archives/application.a/resources/id1/test.js b/test/fixtures/archives/application.a/resources/id1/test.js new file mode 100644 index 000000000..a3df410c3 --- /dev/null +++ b/test/fixtures/archives/application.a/resources/id1/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/test/lib/buildHelpers/archive.integration.js b/test/lib/buildHelpers/archive.integration.js new file mode 100644 index 000000000..f162fc803 --- /dev/null +++ b/test/lib/buildHelpers/archive.integration.js @@ -0,0 +1,64 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const path = require("path"); +const logger = require("@ui5/logger"); +const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMetadata"); +const Module = require("../../../lib/graph/Module"); +const Specification = require("../../../lib/specifications/Specification"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const archiveApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "archives", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +const buildConfig = { + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] +}; + +test.beforeEach((t) => { + t.context.log = { + warn: sinon.stub() + }; + sinon.stub(logger, "getLogger").callThrough() + .withArgs("buildHelpers:composeProjectList").returns(t.context.log); + t.context.composeProjectList = mock.reRequire("../../../lib/buildHelpers/composeProjectList"); +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test("Create archive from project and compare to fixture", async (t) => { + const project = await Specification.create(basicProjectInput); + project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createArchiveMetadata(project, buildConfig); + const m = new Module({ + id: "archive-application.a.id", + version: "1.0.0", + modulePath: archiveApplicationAPath, + configuration: metadata + }); + + const {project: archiveProject} = await m.getSpecifications(); + t.truthy(archiveProject, "Module was able to create project from archive metadata"); + t.is(archiveProject.getName(), project.getName(), "Archive project has correct name"); + t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct name"); + t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct name"); + t.is(archiveProject.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); +}); diff --git a/test/lib/buildHelpers/createArchiveMetadata.js b/test/lib/buildHelpers/createArchiveMetadata.js new file mode 100644 index 000000000..5524d013d --- /dev/null +++ b/test/lib/buildHelpers/createArchiveMetadata.js @@ -0,0 +1,60 @@ +const test = require("ava"); +const path = require("path"); +const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMetadata"); +const Specification = require("../../../lib/specifications/Specification"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test("Create archive from project", async (t) => { + const project = await Specification.create(basicProjectInput); + project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createArchiveMetadata(project, "buildConfig"); + t.truthy(new Date(metadata.customConfiguration._archive.timestamp), "Timestamp is valid"); + metadata.customConfiguration._archive.timestamp = ""; + + t.deepEqual(metadata, { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + customConfiguration: { + _archive: { + archiveSpecVersion: "0.1", + buildConfig: "buildConfig", + namespace: "id1", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: require("@ui5/builder/package.json").version, + fsVersion: require("@ui5/fs/package.json").version, + projectVersion: require("@ui5/project/package.json").version, + }, + tags: { + "/resources/id1/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + }, + }, + resources: { + configuration: { + paths: { + webapp: "resources/id1", + }, + }, + } + }, "Returned correct metadata"); +}); diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index 02d1803eb..d8f974902 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -4,12 +4,18 @@ const path = require("path"); const Module = require("../../../lib/graph/Module"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const archiveApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "archives", "application.a"); const basicModuleInput = { id: "application.a.id", version: "1.0.0", modulePath: applicationAPath }; +const archiveProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: archiveApplicationAPath +}; // test.beforeEach((t) => { // }); @@ -38,3 +44,10 @@ test("Get specifications from module", async (t) => { t.is(project.getName(), "application.a", "Should return correct project"); t.is(extensions.length, 0, "Should return no extensions"); }); + +test.only("Get specifications from archive project", async (t) => { + const ui5Module = new Module(archiveProjectInput); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "application.a", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); From 55d6e44df3bdae1a71066ad2a4964412da7bd238 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 5 May 2022 21:50:44 +0200 Subject: [PATCH 50/99] Cleanup: Remove old modules, transfer tests, fix JSDoc --- index.js | 31 +- lib/buildHelpers/createArchiveMetadata.js | 17 +- lib/generateProjectGraph.js | 46 +- lib/graph/helpers/ui5Framework.js | 7 +- .../{providers => }/projectGraphBuilder.js | 8 +- lib/specifications/ComponentProject.js | 16 +- lib/specifications/Project.js | 20 +- lib/specifications/types/LegacyLibrary.js | 6 + lib/specifications/types/Library.js | 7 +- lib/specifications/types/ThemeLibrary.js | 2 +- lib/translators/static.js | 56 - .../application.h/projectDependencies.yaml | 15 +- test/lib/buildHelpers/composeProjectList.js | 3 +- .../lib/buildHelpers/createArchiveMetadata.js | 73 +- test/lib/extensions.js | 949 ------ test/lib/generateProjectGraph.usingObject.js | 36 +- .../generateProjectGraph.usingStaticFile.js | 43 + .../graph/helpers/ui5Framework.integration.js | 382 ++- test/lib/graph/helpers/ui5Framework.js | 485 ++++ .../NodePackageDependencies.integration.js | 3 +- test/lib/index.js | 8 +- test/lib/normalizer.js | 70 - test/lib/projectPreprocessor.js | 2534 ----------------- test/lib/specifications/types/Library.js | 4 +- test/lib/translators/npm.integration.js | 1014 ------- test/lib/translators/npm.js | 128 - test/lib/translators/static.js | 83 - .../translators/ui5Framework.integration.js | 948 ------ test/lib/translators/ui5Framework.js | 957 ------- 29 files changed, 1085 insertions(+), 6866 deletions(-) rename lib/graph/{providers => }/projectGraphBuilder.js (97%) delete mode 100644 lib/translators/static.js delete mode 100644 test/lib/extensions.js create mode 100644 test/lib/generateProjectGraph.usingStaticFile.js create mode 100644 test/lib/graph/helpers/ui5Framework.js delete mode 100644 test/lib/normalizer.js delete mode 100644 test/lib/projectPreprocessor.js delete mode 100644 test/lib/translators/npm.integration.js delete mode 100644 test/lib/translators/npm.js delete mode 100644 test/lib/translators/static.js delete mode 100644 test/lib/translators/ui5Framework.integration.js delete mode 100644 test/lib/translators/ui5Framework.js diff --git a/index.js b/index.js index 562547f5b..53a9134b1 100644 --- a/index.js +++ b/index.js @@ -4,21 +4,13 @@ */ module.exports = { /** - * @type {import('./lib/normalizer')} - */ - normalizer: "./lib/normalizer", - /** - * @type {import('./lib/projectPreprocessor')} + * @type {import('./lib/builder')} */ - projectPreprocessor: "./lib/projectPreprocessor", + builder: "./lib/builder", /** * @type {import('./lib/generateProjectGraph')} */ generateProjectGraph: "./lib/generateProjectGraph", - /** - * @type {import('./lib/builder')} - */ - builder: "./lib/builder", /** * @public * @alias module:@ui5/project.ui5Framework @@ -49,21 +41,6 @@ module.exports = { */ ValidationError: "./lib/validation/ValidationError" }, - /** - * @private - * @alias module:@ui5/project.translators - * @namespace - */ - translators: { - /** - * @type {import('./lib/translators/npm')} - */ - npm: "./lib/translators/npm", - /** - * @type {import('./lib/translators/static')} - */ - static: "./lib/translators/static" - }, /** * @public * @alias module:@ui5/project.graph @@ -74,6 +51,10 @@ module.exports = { * @type {typeof import('./lib/graph/ProjectGraph')} */ ProjectGraph: "./lib/graph/ProjectGraph", + /** + * @type {typeof import('./lib/graph/projectGraphBuilder')} + */ + projectGraphBuilder: "./lib/graph/projectGraphBuilder", }, }; diff --git a/lib/buildHelpers/createArchiveMetadata.js b/lib/buildHelpers/createArchiveMetadata.js index e453e722a..3b657089d 100644 --- a/lib/buildHelpers/createArchiveMetadata.js +++ b/lib/buildHelpers/createArchiveMetadata.js @@ -7,15 +7,19 @@ module.exports = async function(project, buildConfig) { const projectName = project.getName(); const type = project.getType(); - let pathMappingSource; + const pathMapping = {}; switch (type) { case "application": - pathMappingSource = "webapp"; + pathMapping.webapp = `resources/${project.getNamespace()}`; break; case "library": - case "legacy-library": case "theme-library": - pathMappingSource = "src"; + pathMapping.src = `resources/${project.getNamespace()}`; + pathMapping.test = `test-resources/${project.getNamespace()}`; + break; + case "legacy-library": + pathMapping.src = `resources`; + pathMapping.test = `test-resources`; break; default: throw new Error( @@ -23,7 +27,6 @@ module.exports = async function(project, buildConfig) { `Project type ${type} is currently not supported`); } - const metadata = { specVersion: project.getSpecVersion(), type, @@ -47,9 +50,7 @@ module.exports = async function(project, buildConfig) { }, resources: { configuration: { - paths: { - [pathMappingSource]: `resources/${project.getNamespace()}` - } + paths: pathMapping } } }; diff --git a/lib/generateProjectGraph.js b/lib/generateProjectGraph.js index 58880ad5e..831228f5e 100644 --- a/lib/generateProjectGraph.js +++ b/lib/generateProjectGraph.js @@ -1,8 +1,16 @@ const path = require("path"); -const projectGraphBuilder = require("./graph/providers/projectGraphBuilder"); +const projectGraphBuilder = require("./graph/projectGraphBuilder"); const ui5Framework = require("./graph/helpers/ui5Framework"); const log = require("@ui5/logger").getLogger("generateProjectGraph"); +function resolveProjectPaths(cwd, project) { + project.path = path.resolve(cwd, project.path); + if (project.dependencies) { + project.dependencies.forEach((project) => resolveProjectPaths(cwd, project)); + } + return project; +} + /** * Helper module to create a [@ui5/project.graph.ProjectGraph]{@link module:@ui5/project.graph.ProjectGraph} * from a directory @@ -51,18 +59,16 @@ const generateProjectGraph = { * * @public * @param {object} options - * @param {object} options.filePath Path to the file dependency configuration file - * @param {string} [options.cwd=process.cwd()] Directory to start searching for the root module + * @param {object} [options.filePath=projectDependencies.yaml] Path to the dependency configuration file + * @param {string} [options.cwd=process.cwd()] Directory to resolve relative paths to * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @returns {Promise} Promise resolving to a Project Graph instance */ - usingStaticFile: async function({cwd, filePath, versionOverride}) { + usingStaticFile: async function({cwd, filePath = "projectDependencies.yaml", versionOverride}) { log.verbose(`Creating project graph using static file...`); - const staticTranslator = require("./translators/static"); - const dependencyTree = await staticTranslator.generateDependencyTree(cwd ? path.resolve(cwd) : process.cwd(), { - parameters: [filePath] // *sigh* - }); + const dependencyTree = await generateProjectGraph + ._readDependencyConfigFile(cwd ? path.resolve(cwd) : process.cwd(), filePath); const DependencyTreeProvider = require("./graph/providers/DependencyTree"); const provider = new DependencyTreeProvider({ @@ -100,6 +106,30 @@ const generateProjectGraph = { await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); return projectGraph; + }, + + async _readDependencyConfigFile(cwd, filePath) { + const fs = require("graceful-fs"); + const {promisify} = require("util"); + const readFile = promisify(fs.readFile); + const parseYaml = require("js-yaml").load; + + if (!path.isAbsolute(filePath)) { + filePath = path.join(cwd, filePath); + } + + let dependencyTree; + try { + const contents = await readFile(filePath, {encoding: "utf-8"}); + dependencyTree = parseYaml(contents, { + filename: filePath + }); + } catch (err) { + throw new Error( + `Failed to load dependency tree configuration from path ${filePath}: ${err.message}`); + } + resolveProjectPaths(cwd, dependencyTree); + return dependencyTree; } }; diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js index 02ed380aa..86374243e 100644 --- a/lib/graph/helpers/ui5Framework.js +++ b/lib/graph/helpers/ui5Framework.js @@ -131,14 +131,15 @@ module.exports = { * @param {object} [options] * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework * version from the provided tree - * @returns {Promise} Promise + * @returns {Promise} + * Promise resolving with the given graph instance to allow method chaining */ enrichProjectGraph: async function(projectGraph, options = {}) { const rootProject = projectGraph.getRoot(); if (rootProject.isFrameworkProject()) { // Ignoring UI5 Framework libraries in dependencies - return; + return projectGraph; } const frameworkName = rootProject.getFrameworkName(); @@ -182,7 +183,7 @@ module.exports = { log.verbose( `No ${frameworkName} libraries referenced in project ${rootProject.getName()} ` + `or in any of its dependencies`); - return null; + return projectGraph; } log.info(`Using ${frameworkName} version: ${version}`); diff --git a/lib/graph/providers/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js similarity index 97% rename from lib/graph/providers/projectGraphBuilder.js rename to lib/graph/projectGraphBuilder.js index 64abe16d9..1a23ddb29 100644 --- a/lib/graph/providers/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -1,8 +1,8 @@ const path = require("path"); -const Module = require("../Module"); -const ProjectGraph = require("../ProjectGraph"); -const ShimCollection = require("../ShimCollection"); -const log = require("@ui5/logger").getLogger("graph:providers:projectGraphBuilder"); +const Module = require("./Module"); +const ProjectGraph = require("./ProjectGraph"); +const ShimCollection = require("./ShimCollection"); +const log = require("@ui5/logger").getLogger("graph:projectGraphBuilder"); function _handleExtensions(graph, shimCollection, extensions) { extensions.forEach((extension) => { diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index 407a85799..dbcbae986 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -27,14 +27,14 @@ class ComponentProject extends Project { } /** - * @public + * @private */ getCopyright() { return this._config.metadata.copyright; } /** - * @public + * @private */ getComponentPreloadPaths() { return this._config.builder && this._config.builder.componentPreload && @@ -42,7 +42,7 @@ class ComponentProject extends Project { } /** - * @public + * @private */ getComponentPreloadNamespaces() { return this._config.builder && this._config.builder.componentPreload && @@ -50,7 +50,7 @@ class ComponentProject extends Project { } /** - * @public + * @private */ getComponentPreloadExcludes() { return this._config.builder && this._config.builder.componentPreload && @@ -58,14 +58,14 @@ class ComponentProject extends Project { } /** - * @public + * @private */ getJsdocExcludes() { return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; } /** - * @public + * @private */ getMinificationExcludes() { return this._config.builder && this._config.builder.minification && @@ -73,14 +73,14 @@ class ComponentProject extends Project { } /** - * @public + * @private */ getBundles() { return this._config.builder && this._config.builder.bundles || []; } /** - * @public + * @private */ getPropertiesFileSourceEncoding() { return this._config.resources && this._config.resources.configuration && diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 185fc3925..0f1c07714 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -29,23 +29,23 @@ class Project extends Specification { } /** - * @public + * @private */ getFrameworkName() { - return this._config.framework && this._config.framework.name; + return this._config.framework?.name; } /** - * @public + * @private */ getFrameworkVersion() { - return this._config.framework && this._config.framework.version; + return this._config.framework?.version; } /** - * @public + * @private */ getFrameworkDependencies() { // TODO: Clone or freeze object before exposing? - return this._config.framework && this._config.framework.libraries || []; + return this._config.framework?.libraries || []; } isFrameworkProject() { @@ -57,19 +57,19 @@ class Project extends Specification { } getBuilderResourceExcludes() { - return this._config.builder && this._config.builder.resources && this._config.builder.resources.excludes || []; + return this._config.builder?.resources?.excludes || []; } getCustomTasks() { - return this._config.builder && this._config.builder.customTasks || []; + return this._config.builder?.customTasks || []; } getServerSettings() { - return this._config.server && this._config.server.settings; + return this._config.server?.settings; } getBuilderSettings() { - return this._config.builder && this._config.builder.settings; + return this._config.builder?.settings; } getArchiveMetadata() { diff --git a/lib/specifications/types/LegacyLibrary.js b/lib/specifications/types/LegacyLibrary.js index 2c3ec89ac..7f34e84d9 100644 --- a/lib/specifications/types/LegacyLibrary.js +++ b/lib/specifications/types/LegacyLibrary.js @@ -52,6 +52,12 @@ class LegacyLibrary extends Library { return testReader; } + /** + * Legacy libraries have resources outside their namespace or multiple namespaces + * Therefore it is necessary to remove the namespace form any virtual base paths + * + * @param {string} string Virtual base path to remove an eventual namespace from + */ _stripNamespace(string) { return string.replace(this._namespace + "/", ""); } diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 4c6b09154..f7e2b6f66 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -20,7 +20,8 @@ class Library extends ComponentProject { /* === Attributes === */ /** - * @public + * + * @private */ getLibraryPreloadExcludes() { return this._config.builder && this._config.builder.libraryPreload && @@ -28,12 +29,12 @@ class Library extends ComponentProject { } /* === Resource Access === */ - /* + + /** * * Get a resource reader for the sources of the project (excluding any test resources) * In the future the path structure can be flat or namespaced depending on the project * - * @public * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ _getSourceReader() { diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index 3a0bed094..ae84fc3e9 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -13,7 +13,7 @@ class ThemeLibrary extends Project { /* === Attributes === */ /** - * @public + * @private */ getCopyright() { return this._config.metadata.copyright; diff --git a/lib/translators/static.js b/lib/translators/static.js deleted file mode 100644 index 826ce1867..000000000 --- a/lib/translators/static.js +++ /dev/null @@ -1,56 +0,0 @@ -const path = require("path"); -const fs = require("graceful-fs"); -const {promisify} = require("util"); -const readFile = promisify(fs.readFile); -const parseYaml = require("js-yaml").load; - -function resolveProjectPaths(cwd, project) { - project.path = path.resolve(cwd, project.path); - if (project.dependencies) { - project.dependencies.forEach((project) => resolveProjectPaths(cwd, project)); - } - return project; -} - -/** - * Translator for static resources - * - * @private - * @namespace - * @alias module:@ui5/project.translators.static - */ -module.exports = { - /** - * Generates a dependency tree from static resources - * - * This feature is EXPERIMENTAL and used for testing purposes only. - * - * @public - * @param {string} dirPath Project path - * @param {object} [options] - * @param {Array} [options.parameters] CLI configuration options - * @param {object} [options.tree] Tree object to be used instead of reading a YAML - * @returns {Promise} Promise resolving with a dependency tree - */ - async generateDependencyTree(dirPath, options = {}) { - let tree = options.tree; - if (!tree) { - const depFilePath = options.parameters && options.parameters[0] || - path.join(dirPath, "projectDependencies.yaml"); - try { - const contents = await readFile(depFilePath, {encoding: "utf-8"}); - tree = parseYaml(contents, { - filename: depFilePath - }); - } catch (err) { - throw new Error( - `[static translator] Failed to load dependency tree from path ${depFilePath} `+ - `- Error: ${err.message}`); - } - } - - // Ensure that all project paths are absolute - resolveProjectPaths(dirPath, tree); - return tree; - } -}; diff --git a/test/fixtures/application.h/projectDependencies.yaml b/test/fixtures/application.h/projectDependencies.yaml index 5476e8a0c..b06c31213 100644 --- a/test/fixtures/application.h/projectDependencies.yaml +++ b/test/fixtures/application.h/projectDependencies.yaml @@ -1,13 +1,8 @@ --- -id: testsuite +id: static-application.a version: "0.0.1" -description: "Sample App" -main: "index.html" -path: "./" +path: "../application.a" dependencies: -- id: sap.f - version: "1.56.1" - path: "../sap.f" -- id: sap.m - version: "1.61.0" - path: "../sap.m" \ No newline at end of file +- id: static-library.e + version: "0.0.1" + path: "../library.e" diff --git a/test/lib/buildHelpers/composeProjectList.js b/test/lib/buildHelpers/composeProjectList.js index 32ec78425..e96040ec0 100644 --- a/test/lib/buildHelpers/composeProjectList.js +++ b/test/lib/buildHelpers/composeProjectList.js @@ -161,8 +161,7 @@ async function assertCreateDependencyLists(t, { const graph = await generateProjectGraph.usingObject({dependencyTree: tree}); - const {includedDependencies, excludedDependencies} = await t.context.composeProjectList({ - graph, + const {includedDependencies, excludedDependencies} = await t.context.composeProjectList(graph, { includeAllDependencies, includeDependency, includeDependencyRegExp, diff --git a/test/lib/buildHelpers/createArchiveMetadata.js b/test/lib/buildHelpers/createArchiveMetadata.js index 5524d013d..6efdcab24 100644 --- a/test/lib/buildHelpers/createArchiveMetadata.js +++ b/test/lib/buildHelpers/createArchiveMetadata.js @@ -4,7 +4,7 @@ const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMe const Specification = require("../../../lib/specifications/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const basicProjectInput = { +const applicationProjectInput = { id: "application.a.id", version: "1.0.0", modulePath: applicationAPath, @@ -16,8 +16,31 @@ const basicProjectInput = { } }; -test("Create archive from project", async (t) => { - const project = await Specification.create(basicProjectInput); +const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); +const libraryProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryDPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "main/src", + test: "main/test" + } + } + }, + } +}; + +test("Create application archive from project", async (t) => { + const project = await Specification.create(applicationProjectInput); project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); const metadata = await createArchiveMetadata(project, "buildConfig"); @@ -58,3 +81,47 @@ test("Create archive from project", async (t) => { } }, "Returned correct metadata"); }); + +test("Create library archive from project", async (t) => { + const project = await Specification.create(libraryProjectInput); + project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createArchiveMetadata(project, "buildConfig"); + t.truthy(new Date(metadata.customConfiguration._archive.timestamp), "Timestamp is valid"); + metadata.customConfiguration._archive.timestamp = ""; + + t.deepEqual(metadata, { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + customConfiguration: { + _archive: { + archiveSpecVersion: "0.1", + buildConfig: "buildConfig", + namespace: "library/d", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: require("@ui5/builder/package.json").version, + fsVersion: require("@ui5/fs/package.json").version, + projectVersion: require("@ui5/project/package.json").version, + }, + tags: { + "/resources/library/d/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + }, + }, + resources: { + configuration: { + paths: { + src: "resources/library/d", + test: "test-resources/library/d", + }, + }, + } + }, "Returned correct metadata"); +}); diff --git a/test/lib/extensions.js b/test/lib/extensions.js deleted file mode 100644 index 70527f992..000000000 --- a/test/lib/extensions.js +++ /dev/null @@ -1,949 +0,0 @@ -const test = require("ava"); -const path = require("path"); -const sinon = require("sinon"); -const ValidationError = require("../../lib/validation/ValidationError"); -const projectPreprocessor = require("../..").projectPreprocessor; -const Preprocessor = require("../..").projectPreprocessor._ProjectPreprocessor; -const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); -const legacyLibraryAPath = path.join(__dirname, "..", "fixtures", "legacy.library.a"); -const legacyLibraryBPath = path.join(__dirname, "..", "fixtures", "legacy.library.b"); -const legacyCollectionAPath = path.join(__dirname, "..", "fixtures", "legacy.collection.a"); -const legacyCollectionLibraryX = path.join(__dirname, "..", "fixtures", "legacy.collection.a", - "src", "legacy.library.x"); -const legacyCollectionLibraryY = path.join(__dirname, "..", "fixtures", "legacy.collection.a", - "src", "legacy.library.y"); - -test.afterEach.always((t) => { - sinon.restore(); -}); - -test("Project with project-shim extension with dependency configuration", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: { - configurations: { - "legacy.library.a": { - specVersion: "0.1", - type: "library", - metadata: { - name: "legacy.library.a", - } - } - } - } - }, { - id: "legacy.library.a", - version: "1.0.0", - path: legacyLibraryAPath, - dependencies: [] - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "xy", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [{ - id: "legacy.library.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyLibraryAPath, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.a", - copyright: "${copyright}", - namespace: "legacy/library/a", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - }], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Project with project-shim extension with invalid dependency configuration", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: { - configurations: { - "legacy.library.a": { - specVersion: "2.0", - type: "library" - } - } - } - }, { - id: "legacy.library.a", - version: "1.0.0", - path: legacyLibraryAPath, - dependencies: [] - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - - const validationError = await t.throwsAsync(projectPreprocessor.processTree(tree), { - instanceOf: ValidationError - }); - t.true(validationError.message.includes("Configuration must have required property 'metadata'"), - "ValidationError should contain error about missing metadata configuration"); -}); - -test("Project with project-shim extension with dependency declaration and configuration", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: { - configurations: { - "legacy.library.a": { - specVersion: "0.1", - type: "library", - metadata: { - name: "legacy.library.a", - } - }, - "legacy.library.b": { - specVersion: "0.1", - type: "library", - metadata: { - name: "legacy.library.b", - } - } - }, - dependencies: { - "legacy.library.a": [ - "legacy.library.b" - ] - } - } - }, { - id: "legacy.library.a", - version: "1.0.0", - path: legacyLibraryAPath, - dependencies: [] - }, { - id: "legacy.library.b", - version: "1.0.0", - path: legacyLibraryBPath, - dependencies: [] - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - // application.a and legacy.library.a will both have a dependency to legacy.library.b - // (one because it's the actual dependency and one because it's a shimmed dependency) - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "xy", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [{ - id: "legacy.library.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyLibraryAPath, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.a", - copyright: "${copyright}", - namespace: "legacy/library/a", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [{ - id: "legacy.library.b", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyLibraryBPath, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.b", - copyright: "${copyright}", - namespace: "legacy/library/b", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - }] - }, { - id: "legacy.library.b", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyLibraryBPath, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.b", - copyright: "${copyright}", - namespace: "legacy/library/b", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - }], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Project with project-shim extension with collection", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: { - configurations: { - "legacy.library.x": { - specVersion: "0.1", - type: "library", - metadata: { - name: "legacy.library.x", - } - }, - "legacy.library.y": { - specVersion: "0.1", - type: "library", - metadata: { - name: "legacy.library.y", - } - } - }, - dependencies: { - "application.a": [ - "legacy.library.x", - "legacy.library.y" - ], - "legacy.library.x": [ - "legacy.library.y" - ] - }, - collections: { - "legacy.collection.a": { - modules: { - "legacy.library.x": "src/legacy.library.x", - "legacy.library.y": "src/legacy.library.y" - } - } - } - } - }, { - id: "legacy.collection.a", - version: "1.0.0", - path: legacyCollectionAPath, - dependencies: [] - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "xy", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [{ - id: "legacy.library.x", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyCollectionLibraryX, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.x", - copyright: "${copyright}", - namespace: "legacy/library/x", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [{ - id: "legacy.library.y", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyCollectionLibraryY, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.y", - copyright: "${copyright}", - namespace: "legacy/library/y", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - }] - }, { - id: "legacy.library.y", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: legacyCollectionLibraryY, - _level: 1, - type: "library", - metadata: { - name: "legacy.library.y", - copyright: "${copyright}", - namespace: "legacy/library/y", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - }], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Project with project-type extension dependency inline configuration", (t) => { - // "project-type" extension handling not yet implemented => test currently checks for error - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-type", - metadata: { - name: "z" - } - }], - version: "1.0.0", - specVersion: "0.1", - type: "z", - metadata: { - name: "xy" - } - }; - return t.throwsAsync(projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "z", - metadata: { - name: "xy", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - root: "" - } - }, - pathMappings: { - "/": "", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: applicationAPath - }, "Parsed correctly"); - }), {message: "Unknown extension type 'project-type' for extension.a"}, "Rejected with error"); -}); - -test("Project with unknown extension dependency inline configuration", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "phony-pony", - metadata: { - name: "pinky.pie" - } - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - {message: "Unknown extension type 'phony-pony' for extension.a"}, "Rejected with error"); -}); - -test("Project with task extension dependency", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "ext.task.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "task", - metadata: { - name: "task.a" - }, - task: { - path: "task.a.js" - } - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree.dependencies.length, 0, "Application project has no dependencies"); - const taskRepository = require("@ui5/builder").tasks.taskRepository; - t.truthy(taskRepository.getTask("task.a"), "task.a has been added to the task repository"); - }); -}); - -test("Project with task extension dependency - does not throw for invalid task path", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "ext.task.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "task", - metadata: { - name: "task.b" - }, - task: { - path: "task.not.existing.js" - } - }], - version: "1.0.0", - specVersion: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - await t.notThrowsAsync(projectPreprocessor.processTree(tree)); -}); - - -test("Project with middleware extension dependency", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "ext.middleware.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "server-middleware", - metadata: { - name: "middleware.a" - }, - middleware: { - path: "middleware.a.js" - } - }], - version: "1.0.0", - specVersion: "1.0", - type: "application", - metadata: { - name: "xy" - } - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree.dependencies.length, 0, "Application project has no dependencies"); - const {middlewareRepository} = require("@ui5/server"); - t.truthy(middlewareRepository.getMiddleware("middleware.a"), - "middleware.a has been added to the middleware repository"); - }); -}); - -test("Project with middleware extension dependency - middleware is missing configuration", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [{ - id: "ext.middleware.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "server-middleware", - metadata: { - name: "middleware.a" - } - }], - version: "1.0.0", - specVersion: "1.0", - type: "application", - metadata: { - name: "xy" - } - }; - const error = await t.throwsAsync(projectPreprocessor.processTree(tree)); - t.deepEqual(error.message, `Middleware extension ext.middleware.a is missing 'middleware' configuration`, - "Rejected with error"); -}); - -test("specVersion: Missing version", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - await t.throwsAsync(preprocessor.applyExtension(extension), - {message: "No specification version defined for extension shims.a"}, - "Rejected with error"); -}); - -test("specVersion: Extension with invalid version", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.9", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - await t.throwsAsync(preprocessor.applyExtension(extension), {message: - "Unsupported specification version 0.9 defined for extension shims.a. " + - "Your UI5 CLI installation might be outdated. For details see " + - "https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions"}, - "Rejected with error"); -}); - -test("specVersion: Extension with valid version 0.1", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "0.1", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 1.0", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "1.0", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "1.0", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 1.1", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "1.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "1.1", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.0", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.0", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.0", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.1", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.1", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.1", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.2", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.2", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.2", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.3", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.3", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.3", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.4", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.4", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.4", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.5", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.5", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.5", "Correct spec version"); -}); - -test("specVersion: Extension with valid version 2.6", async (t) => { - const extension = { - id: "extension.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.6", - kind: "extension", - type: "project-shim", - metadata: { - name: "shims.a" - }, - shims: {} - }; - const preprocessor = new Preprocessor({}); - const handleShimStub = sinon.stub(preprocessor, "handleShim"); - await preprocessor.applyExtension(extension); - t.deepEqual(handleShimStub.getCall(0).args[0].specVersion, "2.6", "Correct spec version"); -}); diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index 54c50dd90..8d204f2da 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -28,10 +28,8 @@ test.beforeEach((t) => { info: sinon.stub(), isLevelEnabled: () => true }; - sinon.stub(logger, "getLogger").callThrough() - .withArgs("graph:providers:projectGraphBuilder").returns(t.context.log); - mock.reRequire("../../lib/graph/providers/projectGraphBuilder"); - + sinon.stub(logger, "getLogger").callThrough().withArgs("graph:projectGraphBuilder").returns(t.context.log); + mock.reRequire("../../lib/graph/projectGraphBuilder"); t.context.projectGraphFromTree = mock.reRequire("../../lib/generateProjectGraph").usingObject; logger.getLogger.restore(); // Immediately restore global stub for following tests }); @@ -67,9 +65,9 @@ test("Application A: Traverse project graph breadth first", async (t) => { }); test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => { - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub)); t.is(callbackStub.callCount, 4, "Four projects have been visited"); @@ -89,9 +87,9 @@ test("Application Cycle A: Traverse project graph breadth first with cycles", as }); test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => { - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); // TODO: Confirm this behavior with FW. BFS works fine since all modules have already been visited @@ -108,9 +106,9 @@ test("Application Cycle B: Traverse project graph breadth first with cycles", as }); test("Application A: Traverse project graph depth first", async (t) => { - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); await projectGraph.traverseDepthFirst(callbackStub); t.is(callbackStub.callCount, 5, "Five projects have been visited"); @@ -129,9 +127,9 @@ test("Application A: Traverse project graph depth first", async (t) => { test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => { - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); t.is(callbackStub.callCount, 0, "Zero projects have been visited"); @@ -143,9 +141,9 @@ test("Application Cycle A: Traverse project graph depth first with cycles", asyn }); test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => { - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub)); t.is(callbackStub.callCount, 0, "Zero projects have been visited"); @@ -172,9 +170,9 @@ async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) { if (bfs === undefined) { throw new Error("Test error: Parameter 'bfs' must be specified"); } - const {projectGraphFromTree} = t.context; + const {projectGraphFromTree, sinon} = t.context; const projectGraph = await projectGraphFromTree({dependencyTree: tree}); - const callbackStub = t.context.sinon.stub().resolves(); + const callbackStub = sinon.stub().resolves(); if (bfs) { await projectGraph.traverseBreadthFirst(callbackStub); } else { @@ -231,6 +229,7 @@ test("Project with inline configuration as array", async (t) => { }); test("Project with inline configuration for two projects", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { id: "application.a.id", path: applicationAPath, @@ -251,7 +250,6 @@ test("Project with inline configuration for two projects", async (t) => { }] }; - const {projectGraphFromTree} = t.context; await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { message: @@ -1242,6 +1240,7 @@ test("Project with project-shim extension dependency with dependency configurati }); test("Project with project-shim extension with invalid dependency configuration", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { id: "application.a.id", path: applicationAPath, @@ -1275,7 +1274,6 @@ test("Project with project-shim extension with invalid dependency configuration" dependencies: [] }] }; - const {projectGraphFromTree} = t.context; const validationError = await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { instanceOf: ValidationError }); @@ -1532,6 +1530,7 @@ test.skip("Project with project-shim extension with self-containing collection s }); test("Project with unknown extension dependency inline configuration", async (t) => { + const {projectGraphFromTree} = t.context; const tree = { id: "application.a", path: applicationAPath, @@ -1558,7 +1557,6 @@ test("Project with unknown extension dependency inline configuration", async (t) dependencies: [], }], }; - const {projectGraphFromTree} = t.context; const validationError = await t.throwsAsync(projectGraphFromTree({dependencyTree: tree})); t.is(validationError.message, `Unable to create Specification instance: Unknown specification type 'phony-pony'`, diff --git a/test/lib/generateProjectGraph.usingStaticFile.js b/test/lib/generateProjectGraph.usingStaticFile.js new file mode 100644 index 000000000..d61474c8e --- /dev/null +++ b/test/lib/generateProjectGraph.usingStaticFile.js @@ -0,0 +1,43 @@ +const test = require("ava"); +const path = require("path"); +const sinonGlobal = require("sinon"); + +const projectGraphFromStaticFile = require("../../lib/generateProjectGraph").usingStaticFile; + +const applicationHPath = path.join(__dirname, "..", "fixtures", "application.h"); +const notExistingPath = path.join(__dirname, "..", "fixtures", "does_not_exist"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Application H: Traverse project graph breadth first", async (t) => { + const projectGraph = await projectGraphFromStaticFile({ + cwd: applicationHPath + }); + const callbackStub = t.context.sinon.stub().resolves(); + await projectGraph.traverseBreadthFirst(callbackStub); + + t.is(callbackStub.callCount, 2, "Two projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + + t.deepEqual(callbackCalls, [ + "application.a", + "library.e", + ], "Traversed graph in correct order"); +}); + +test("Throws error if file not found", async (t) => { + const err = await t.throwsAsync(projectGraphFromStaticFile({ + cwd: notExistingPath + })); + t.is(err.message, + `Failed to load dependency tree configuration from path ${notExistingPath}/projectDependencies.yaml: ` + + `ENOENT: no such file or directory, open '${notExistingPath}/projectDependencies.yaml'`, + "Correct error message"); +}); diff --git a/test/lib/graph/helpers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js index 93a80e827..a311f665a 100644 --- a/test/lib/graph/helpers/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -10,10 +10,10 @@ const libnpmconfig = require("libnpmconfig"); const lockfile = require("lockfile"); const logger = require("@ui5/logger"); const Module = require("../../../../lib/graph/Module"); +const ApplicationType = require("../../../../lib/specifications/types/Application"); +const LibraryType = require("../../../../lib/specifications/types/Library"); const DependencyTreeProvider = require("../../../../lib/graph/providers/DependencyTree"); -const projectGraphBuilder = require("../../../../lib/graph/providers/projectGraphBuilder"); -let ui5Framework; -let Installer; +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); // Use path within project as mocking base directory to reduce chance of side effects // in case mocks/stubs do not work and real fs is used @@ -46,9 +46,16 @@ test.beforeEach((t) => { mock("mkdirp", sinon.stub().resolves()); + // Stub specification internal checks since none of the projects actually exist on disk + sinon.stub(ApplicationType.prototype, "_configureAndValidatePaths").resolves(); + sinon.stub(LibraryType.prototype, "_configureAndValidatePaths").resolves(); + sinon.stub(ApplicationType.prototype, "_parseConfiguration").resolves(); + sinon.stub(LibraryType.prototype, "_parseConfiguration").resolves(); + + // Re-require to ensure that mocked modules are used - ui5Framework = mock.reRequire("../../../../lib/graph/helpers/ui5Framework"); - Installer = require("../../../../lib/ui5Framework/npm/Installer"); + t.context.ui5Framework = mock.reRequire("../../../../lib/graph/helpers/ui5Framework"); + t.context.Installer = require("../../../../lib/ui5Framework/npm/Installer"); }); test.afterEach.always((t) => { @@ -104,11 +111,12 @@ function defineTest(testName, { } }; - test.serial.skip(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { // Enable verbose logging if (verbose) { logger.setLevel("verbose"); } + const {ui5Framework, Installer, logInfoSpy} = t.context; const testDependency = { id: "test-dependency-id", @@ -148,7 +156,7 @@ function defineTest(testName, { } } }; - const translatorTree = { + const dependencyTree = { id: "test-application-id", version: "1.2.3", path: path.join(fakeBaseDir, "project-test-application"), @@ -312,8 +320,7 @@ function defineTest(testName, { .resolves(distributionMetadata); } - const provider = new DependencyTreeProvider({dependencyTree: translatorTree}); - + const provider = new DependencyTreeProvider({dependencyTree}); const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph); @@ -335,29 +342,374 @@ function defineTest(testName, { "test-application", ], "Traversed graph in correct order"); - const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() + const frameworkLibAlreadyAddedInfoLogged = (logInfoSpy.getCalls() .map(($) => $.firstArg) .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); }); } -defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "SAPUI5" }); -defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "SAPUI5", verbose: true }); -defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "OpenUI5" }); -defineTest("ui5Framework translator should enhance project graph with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "OpenUI5", verbose: true }); -// TODO missing tests from non-graph ui5Framework.integration.js +function defineErrorTest(testName, { + frameworkName, + failExtract = false, + failMetadata = false, + expectedErrorMessage +}) { + test.serial(testName, async (t) => { + const {ui5Framework, Installer, logInfoSpy} = t.context; + + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] + } + } + }; + + const extractStub = sinon.stub(pacote, "extract"); + extractStub.callsFake(async (spec) => { + throw new Error("pacote.extract stub called with unknown spec: " + spec); + }); + + const manifestStub = sinon.stub(pacote, "manifest"); + manifestStub.callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }); + + if (frameworkName === "SAPUI5") { + if (failExtract) { + extractStub + .withArgs("@sapui5/sap.ui.lib1@1.75.1") + .rejects(new Error("404 - @sapui5/sap.ui.lib1")) + .withArgs("@openui5/sap.ui.lib4@1.75.4") + .rejects(new Error("404 - @openui5/sap.ui.lib4")); + } else { + extractStub + .withArgs("@sapui5/sap.ui.lib1@1.75.1").resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.4").resolves(); + } + if (failMetadata) { + extractStub + .withArgs("@sapui5/distribution-metadata@1.75.0") + .rejects(new Error("404 - @sapui5/distribution-metadata")); + } else { + extractStub + .withArgs("@sapui5/distribution-metadata@1.75.0") + .resolves(); + sinon.stub(Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + } + } else if (frameworkName === "OpenUI5") { + if (failExtract) { + extractStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("404 - @openui5/sap.ui.lib1")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("404 - @openui5/sap.ui.lib4")); + } else { + extractStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves(); + } + if (failMetadata) { + manifestStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib1@1.75.0")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib4@1.75.0")); + } else { + manifestStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0" + }); + } + } + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + await t.throwsAsync(async () => { + await ui5Framework.enrichProjectGraph(projectGraph); + }, {message: expectedErrorMessage}); + }); +} + +defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when metadata request fails", { + frameworkName: "SAPUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata +Failed to resolve library sap.ui.lib4: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata` // TODO: should only be returned once? +}); +defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when package extraction fails", { + frameworkName: "SAPUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/sap.ui.lib1@1.75.1: ` + +`404 - @sapui5/sap.ui.lib1 +Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.4: ` + +`404 - @openui5/sap.ui.lib4` +}); +defineErrorTest( + "SAPUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { + frameworkName: "SAPUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata +Failed to resolve library sap.ui.lib4: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + +`404 - @sapui5/distribution-metadata` + }); + + +defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when metadata request fails", { + frameworkName: "OpenUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 +Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` +}); +defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when package extraction fails", { + frameworkName: "OpenUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to extract package @openui5/sap.ui.lib1@1.75.0: ` + +`404 - @openui5/sap.ui.lib1 +Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.0: ` + +`404 - @openui5/sap.ui.lib4` +}); +defineErrorTest( + "OpenUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { + frameworkName: "OpenUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 +Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` + }); + +test.serial("ui5Framework helper should not fail when no framework configuration is given", async (t) => { + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + await t.context.ui5Framework.enrichProjectGraph(projectGraph); + + t.is(projectGraph, projectGraph, "Returned same graph without error"); +}); + +test.serial("ui5Framework translator should not try to install anything when no library is referenced", async (t) => { + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const extractStub = sinon.stub(pacote, "extract"); + const manifestStub = sinon.stub(pacote, "manifest"); + + await t.context.ui5Framework.enrichProjectGraph(projectGraph); + + t.is(extractStub.callCount, 0, "No package should be extracted"); + t.is(manifestStub.callCount, 0, "No manifest should be requested"); +}); + +test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => { + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await t.throwsAsync(async () => { + await t.context.ui5Framework.enrichProjectGraph(projectGraph); + }, {message: `No framework version defined for root project test-project`}, "Correct error message"); +}); + +test.serial( + "SAPUI5: ui5Framework translator should throw error when using a library that is not part of the dist metadata", + async (t) => { + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0", + libraries: [ + {name: "sap.ui.lib1"}, + {name: "does.not.exist"}, + {name: "sap.ui.lib4"}, + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + sinon.stub(pacote, "extract").resolves(); + + sinon.stub(t.context.Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + + await t.throwsAsync(async () => { + await t.context.ui5Framework.enrichProjectGraph(projectGraph); + }, { + message: `Resolution of framework libraries failed with errors: +Failed to resolve library does.not.exist: Could not find library "does.not.exist"`}); + }); // TODO test: Should not download packages again in case they are already installed diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js new file mode 100644 index 000000000..6497bb39e --- /dev/null +++ b/test/lib/graph/helpers/ui5Framework.js @@ -0,0 +1,485 @@ +const path = require("path"); +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +const DependencyTreeProvider = require("../../../../lib/graph/providers/DependencyTree"); +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); + +const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); + +test.beforeEach((t) => { + t.context.Sapui5ResolverStub = sinon.stub(); + t.context.Sapui5ResolverInstallStub = sinon.stub(); + t.context.Sapui5ResolverStub.callsFake(() => { + return { + install: t.context.Sapui5ResolverInstallStub + }; + }); + t.context.Sapui5ResolverResolveVersionStub = sinon.stub(); + t.context.Sapui5ResolverStub.resolveVersion = t.context.Sapui5ResolverResolveVersionStub; + mock("../../../../lib/ui5Framework/Sapui5Resolver", t.context.Sapui5ResolverStub); + + t.context.Openui5ResolverStub = sinon.stub(); + mock("../../../../lib/ui5Framework/Openui5Resolver", t.context.Openui5ResolverStub); + + // Re-require to ensure that mocked modules are used + t.context.ui5Framework = mock.reRequire("../../../../lib/graph/helpers/ui5Framework"); + t.context.utils = t.context.ui5Framework._utils; +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => { + const {ui5Framework, utils, Sapui5ResolverInstallStub} = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + const getFrameworkLibrariesFromGraphStub = sinon.stub(utils, "getFrameworkLibrariesFromGraph") + .resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + + const addProjectToGraphStub = sinon.stub(); + const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + + t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ + cwd: dependencyTree.path, + version: dependencyTree.configuration.framework.version + }], "Sapui5Resolver#constructor should be called with expected args"); + + t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once"); + t.deepEqual(t.context.Sapui5ResolverInstallStub.getCall(0).args, [ + referencedLibraries + ], "Sapui5Resolver#install should be called with expected args"); + + t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); + t.deepEqual(ProjectProcessorStub.getCall(0).args, [{libraryMetadata}], + "ProjectProcessor#constructor should be called with expected args"); + + t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); + t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 1)"); + t.deepEqual(addProjectToGraphStub.getCall(1).args[0], referencedLibraries[1], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 2)"); + t.deepEqual(addProjectToGraphStub.getCall(2).args[0], referencedLibraries[2], + "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 3)"); + + + const callbackStub = sinon.stub().resolves(); + await projectGraph.traverseDepthFirst(callbackStub); + + t.is(callbackStub.callCount, 1, "Correct number of projects have been visited"); + + const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName()); + t.deepEqual(callbackCalls, [ + "application.a" + ], "Traversed graph in correct order"); +}); + +test.serial("generateDependencyTree (with versionOverride)", async (t) => { + const { + ui5Framework, utils, + Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub + } = t.context; + + const dependencyTree = { + id: "test1", + version: "1.0.0", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries); + + Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + Sapui5ResolverResolveVersionStub.resolves("1.99.9"); + + const addProjectToGraphStub = sinon.stub(); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + addProjectToGraph: addProjectToGraphStub + }; + }); + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "1.99"}); + + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ + cwd: dependencyTree.path, + version: "1.99.9" + }], "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("generateDependencyTree should throw error when no framework version is provided", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5" + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await t.throwsAsync(async () => { + await ui5Framework.enrichProjectGraph(projectGraph); + }, {message: "No framework version defined for root project application.a"}); + + await t.throwsAsync(async () => { + await ui5Framework.enrichProjectGraph(projectGraph, { + versionOverride: "1.75.0" + }); + }, {message: "No framework version defined for root project application.a"}); +}); + +test.serial("generateDependencyTree should skip framework project without version", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); +}); + +test.serial("generateDependencyTree should skip framework project with version and framework config", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.2.3", + libraries: [ + { + name: "lib1" + } + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); +}); + +test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + await ui5Framework.enrichProjectGraph(projectGraph); + t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); +}); + +test.serial("utils.shouldIncludeDependency", (t) => { + const {utils} = t.context; + // root project dependency should always be included + t.true(utils.shouldIncludeDependency({}, true)); + t.true(utils.shouldIncludeDependency({optional: true}, true)); + t.true(utils.shouldIncludeDependency({optional: false}, true)); + t.true(utils.shouldIncludeDependency({optional: null}, true)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, true)); + t.true(utils.shouldIncludeDependency({development: true}, true)); + t.true(utils.shouldIncludeDependency({development: false}, true)); + t.true(utils.shouldIncludeDependency({development: null}, true)); + t.true(utils.shouldIncludeDependency({development: "abc"}, true)); + t.true(utils.shouldIncludeDependency({foo: true}, true)); + + t.true(utils.shouldIncludeDependency({}, false)); + t.false(utils.shouldIncludeDependency({optional: true}, false)); + t.true(utils.shouldIncludeDependency({optional: false}, false)); + t.true(utils.shouldIncludeDependency({optional: null}, false)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, false)); + t.false(utils.shouldIncludeDependency({development: true}, false)); + t.true(utils.shouldIncludeDependency({development: false}, false)); + t.true(utils.shouldIncludeDependency({development: null}, false)); + t.true(utils.shouldIncludeDependency({development: "abc"}, false)); + t.true(utils.shouldIncludeDependency({foo: true}, false)); + + // Having both optional and development should not be the case, but that should be validated beforehand + t.true(utils.shouldIncludeDependency({optional: true, development: true}, true)); + t.false(utils.shouldIncludeDependency({optional: true, development: true}, false)); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies", async (t) => { + const {utils} = t.context; + const dependencyTree = { + id: "test-id", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [] + } + } + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, []); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Framework project", async (t) => { + const {utils} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [ + { + name: "lib1" + } + ] + } + }, + dependencies: [{ + id: "@openui5/test1", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "library.d" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib2" + }] + } + } + }] + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, []); +}); + + +test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", async (t) => { + const {utils} = t.context; + const dependencyTree = { + id: "test-project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.100.0", + libraries: [{ + name: "lib1" + }, { + name: "lib2", + optional: true + }, { + name: "lib6", + development: true + }] + } + }, + dependencies: [{ + id: "test2", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test2" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib3" + }, { + name: "lib4", + optional: true + }] + } + }, + dependencies: [{ + id: "test3", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "test3" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "lib5" + }, { + name: "lib7", + optional: true + }] + } + } + }] + }, { + id: "@openui5/lib8", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib8" + }, + framework: { + name: "OpenUI5", + libraries: [{ + name: "should.be.ignored" + }] + } + } + }, { + id: "@openui5/lib9", + version: "1.2.3", + path: libraryEPath, + configuration: { + specVersion: "2.0", + type: "library", + metadata: { + name: "lib9" + } + } + }] + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph); + t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]); +}); + +// TODO test: ProjectProcessor diff --git a/test/lib/graph/providers/NodePackageDependencies.integration.js b/test/lib/graph/providers/NodePackageDependencies.integration.js index 96072b468..e1f59bfac 100644 --- a/test/lib/graph/providers/NodePackageDependencies.integration.js +++ b/test/lib/graph/providers/NodePackageDependencies.integration.js @@ -2,7 +2,6 @@ const test = require("ava"); const path = require("path"); const sinonGlobal = require("sinon"); const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); -// const applicationBPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.b"); const applicationCPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.c"); const applicationC2Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c2"); const applicationC3Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c3"); @@ -12,7 +11,7 @@ const applicationGPath = path.join(__dirname, "..", "..", "..", "fixtures", "app const errApplicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "err.application.a"); const cycleDepsBasePath = path.join(__dirname, "..", "..", "..", "fixtures", "cyclic-deps", "node_modules"); -const projectGraphBuilder = require("../../../../lib/graph/providers/projectGraphBuilder"); +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); const NodePackageDependenciesProvider = require("../../../../lib/graph/providers/NodePackageDependencies"); test.beforeEach((t) => { diff --git a/test/lib/index.js b/test/lib/index.js index 7c4d78f9f..a4b357b39 100644 --- a/test/lib/index.js +++ b/test/lib/index.js @@ -2,8 +2,8 @@ const test = require("ava"); const index = require("../../index"); test("index.js exports all expected modules", (t) => { - t.truthy(index.normalizer, "Module exported"); - t.truthy(index.projectPreprocessor, "Module exported"); + t.truthy(index.builder, "Module exported"); + t.truthy(index.generateProjectGraph, "Module exported"); t.truthy(index.ui5Framework.Openui5Resolver, "Module exported"); t.truthy(index.ui5Framework.Sapui5Resolver, "Module exported"); @@ -11,6 +11,6 @@ test("index.js exports all expected modules", (t) => { t.truthy(index.validation.validator, "Module exported"); t.truthy(index.validation.ValidationError, "Module exported"); - t.truthy(index.translators.npm, "Module exported"); - t.truthy(index.translators.static, "Module exported"); + t.truthy(index.graph.ProjectGraph, "Module exported"); + t.truthy(index.graph.projectGraphBuilder, "Module exported"); }); diff --git a/test/lib/normalizer.js b/test/lib/normalizer.js deleted file mode 100644 index e5d0d4d10..000000000 --- a/test/lib/normalizer.js +++ /dev/null @@ -1,70 +0,0 @@ -const test = require("ava"); -const sinon = require("sinon"); -const normalizer = require("../..").normalizer; -const projectPreprocessor = require("../../lib/projectPreprocessor"); -const ui5Framework = require("../../lib/translators/ui5Framework"); - -test.beforeEach((t) => { - t.context.npmTranslatorStub = sinon.stub(require("../..").translators.npm); - t.context.staticTranslatorStub = sinon.stub(require("../..").translators.static); -}); - -test.afterEach.always(() => { - sinon.restore(); -}); - -test.serial("Uses npm translator as default strategy", (t) => { - normalizer.generateDependencyTree(); - t.truthy(t.context.npmTranslatorStub.generateDependencyTree.called); -}); - -test.serial("Uses static translator as strategy", (t) => { - normalizer.generateDependencyTree({ - translatorName: "static" - }); - t.truthy(t.context.staticTranslatorStub.generateDependencyTree.called); -}); - -test.serial("Generate project tree using with overwritten config path", async (t) => { - sinon.stub(normalizer, "generateDependencyTree").resolves({configPath: "defaultPath/config.json"}); - const projectPreprocessorStub = sinon.stub(projectPreprocessor, "processTree").resolves(true); - await normalizer.generateProjectTree({configPath: "newPath/config.json"}); - t.deepEqual(projectPreprocessorStub.getCall(0).args[0], { - configPath: "newPath/config.json" - }, "Process tree with config loaded from custom path"); -}); - -test.serial("Pass frameworkOptions to ui5Framework translator", async (t) => { - const options = { - frameworkOptions: { - versionOverride: "1.2.3" - } - }; - const tree = { - metadata: { - name: "test" - }, - framework: {} - }; - - sinon.stub(normalizer, "generateDependencyTree").resolves({configPath: "defaultPath/config.json"}); - sinon.stub(projectPreprocessor, "processTree").resolves(tree); - - const ui5FrameworkGenerateDependencyTreeStub = sinon.stub(ui5Framework, "generateDependencyTree").resolves(null); - - await normalizer.generateProjectTree(options); - - t.is(ui5FrameworkGenerateDependencyTreeStub.callCount, 1, - "ui5Framework.generateDependencyTree should be called once"); - t.deepEqual(ui5FrameworkGenerateDependencyTreeStub.getCall(0).args, [tree, {versionOverride: "1.2.3"}], - "ui5Framework.generateDependencyTree should be called with expected args"); -}); - -test.serial("Error: Throws if unknown translator should be used as strategy", async (t) => { - const translatorName = "notExistingTranslator"; - return normalizer.generateDependencyTree({ - translatorName - }).catch((error) => { - t.is(error.message, `Unknown translator ${translatorName}`); - }); -}); diff --git a/test/lib/projectPreprocessor.js b/test/lib/projectPreprocessor.js deleted file mode 100644 index 93b9c40ae..000000000 --- a/test/lib/projectPreprocessor.js +++ /dev/null @@ -1,2534 +0,0 @@ -const test = require("ava"); -const sinon = require("sinon"); -const mock = require("mock-require"); -const path = require("path"); -const gracefulFs = require("graceful-fs"); -const validator = require("../../lib/validation/validator"); -const ValidationError = require("../../lib/validation/ValidationError"); -const projectPreprocessor = require("../../lib/projectPreprocessor"); -const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); -const applicationBPath = path.join(__dirname, "..", "fixtures", "application.b"); -const applicationCPath = path.join(__dirname, "..", "fixtures", "application.c"); -const libraryAPath = path.join(__dirname, "..", "fixtures", "collection", "library.a"); -const libraryBPath = path.join(__dirname, "..", "fixtures", "collection", "library.b"); -// const libraryCPath = path.join(__dirname, "..", "fixtures", "collection", "library.c"); -const libraryDPath = path.join(__dirname, "..", "fixtures", "library.d"); -const cycleDepsBasePath = path.join(__dirname, "..", "fixtures", "cyclic-deps", "node_modules"); -const pathToInvalidModule = path.join(__dirname, "..", "fixtures", "invalidModule"); - -test.afterEach.always((t) => { - mock.stopAll(); - sinon.restore(); -}); - -test("Project with inline configuration", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.3", - type: "application", - metadata: { - name: "xy" - } - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "xy", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Project with configPath", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - configPath: path.join(applicationBPath, "ui5.yaml"), // B, not A - just to have something different - dependencies: [], - version: "1.0.0" - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.b", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath, - configPath: path.join(applicationBPath, "ui5.yaml") - }, "Parsed correctly"); - }); -}); - -test("Project with ui5.yaml at default location", (t) => { - const tree = { - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [] - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.a", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Project with ui5.yaml at default location and some configuration", (t) => { - const tree = { - id: "application.c", - version: "1.0.0", - path: applicationCPath, - dependencies: [] - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.c", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "src" - } - }, - pathMappings: { - "/": "src", - } - }, - dependencies: [], - id: "application.c", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationCPath - }, "Parsed correctly"); - }); -}); - -test("Missing configuration for root project", async (t) => { - const tree = { - id: "application.a", - path: "non-existent", - version: "1.0.0", - dependencies: [] - }; - const exception = await t.throwsAsync(projectPreprocessor.processTree(tree)); - - t.true(exception.message.includes("Failed to read configuration for project application.a"), - "Error message should contain expected reason"); -}); - -test("Missing id for root project", (t) => { - const tree = { - path: path.join(__dirname, "../fixtures/application.a"), - dependencies: [] - }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - {message: "Encountered project with missing id (root project)"}, "Rejected with error"); -}); - -test("No type configured for root project", async (t) => { - const tree = { - id: "application.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(__dirname, "../fixtures/application.a"), - dependencies: [], - metadata: { - name: "application.a", - namespace: "id1" - } - }; - const error = await t.throwsAsync(projectPreprocessor.processTree(tree)); - - t.is(error.message, `Invalid ui5.yaml configuration for project application.a - -Configuration must have required property 'type'`, - "Rejected with expected error"); -}); - -test("Missing dependencies", (t) => { - const tree = ({ - id: "application.a", - version: "1.0.0", - path: applicationAPath - }); - return t.notThrowsAsync(projectPreprocessor.processTree(tree), - "Gracefully accepted project with no dependency attribute"); -}); - -test("Single non-root application-project", (t) => { - const tree = ({ - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [{ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [] - }] - }); - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree.id, "library.a", "Correct root project"); - t.deepEqual(parsedTree.dependencies.length, 1, "application-project dependency was not ignored"); - t.deepEqual(parsedTree.dependencies[0].id, "application.a", "application-project is on second level"); - }); -}); - -test("Multiple non-root application-projects on same level", (t) => { - const tree = ({ - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [{ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [] - }, { - id: "application.b", - version: "1.0.0", - path: applicationBPath, - dependencies: [] - }] - }); - return t.throwsAsync(projectPreprocessor.processTree(tree), {message: - "Found at least two projects application.a and application.b of type application with the same distance to " + - "the root project. Only one project of type application can be used. Failed to decide which one to ignore."}, - "Rejected with error"); -}); - -test("Multiple non-root application-projects on different levels", (t) => { - const tree = ({ - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [{ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [] - }, { - id: "library.b", - version: "1.0.0", - path: libraryBPath, - dependencies: [{ - id: "application.b", - version: "1.0.0", - path: applicationBPath, - dependencies: [] - }] - }] - }); - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree.id, "library.a", "Correct root project"); - t.deepEqual(parsedTree.dependencies.length, 2, "No dependency of the first level got ignored"); - t.deepEqual(parsedTree.dependencies[0].id, "application.a", "First application-project did not get ignored"); - t.deepEqual(parsedTree.dependencies[1].dependencies.length, 0, - "Second (deeper) application-project got ignored"); - }); -}); - -test("Root- and non-root application-projects", (t) => { - const tree = ({ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [{ - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [{ - id: "application.b", - version: "1.0.0", - path: applicationBPath, - dependencies: [] - }] - }] - }); - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree.id, "application.a", "Correct root project"); - t.deepEqual(parsedTree.dependencies[0].id, "library.a", "Correct library dependency"); - t.deepEqual(parsedTree.dependencies[0].dependencies[0], undefined, - "Second application-project dependency was ignored"); - }); -}); - -test("Ignores additional application-projects", (t) => { - const tree = ({ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [{ - id: "application.b", - version: "1.0.0", - path: applicationBPath, - dependencies: [] - }] - }); - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.a", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath - }, "Parsed correctly"); - }); -}); - -test("Inconsistent dependencies with same ID", (t) => { - // The one closer to the root should win - const tree = { - id: "application.a", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath, - type: "application", - metadata: { - name: "application.a" - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "2.3", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryBPath, // B, not A - inconsistency! - type: "library", - metadata: { - name: "library.XY", - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a", - }, - dependencies: [] - } - ] - }; - return projectPreprocessor.processTree(tree).then((parsedTree) => { - t.deepEqual(parsedTree, { - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath, - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.a", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp" - } - }, - dependencies: [ - { - id: "library.d", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: libraryDPath, - _level: 1, - type: "library", - metadata: { - name: "library.d", - namespace: "library/d", - copyright: "Some fancy copyright", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" - } - }, - pathMappings: { - "/resources/": "main/src", - "/test-resources/": "main/test" - } - }, - dependencies: [ - { - id: "library.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - _level: 1, - type: "library", - metadata: { - name: "library.a", - namespace: "library/a", - copyright: "Some fancy copyright ${currentYear}", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - kind: "project", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - _level: 1, - type: "library", - metadata: { - name: "library.a", - namespace: "library/a", - copyright: "Some fancy copyright ${currentYear}", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - } - ] - }, "Parsed correctly"); - }); -}); - -test("Project tree A with inline configs", (t) => { - return projectPreprocessor.processTree(treeAWithInlineConfigs).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeAWithInlineConfigs, "Parsed correctly"); - }); -}); - -test("Project tree A with configPaths", (t) => { - return projectPreprocessor.processTree(treeAWithConfigPaths).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeAWithConfigPaths, "Parsed correctly"); - }); -}); - -test("Project tree A with default YAMLs", (t) => { - return projectPreprocessor.processTree(treeAWithDefaultYamls).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeAWithDefaultYamls, "Parsed correctly"); - }); -}); - -test("Project tree B with inline configs", (t) => { - // Tree B depends on Library B which has a dependency to Library D - return projectPreprocessor.processTree(treeBWithInlineConfigs).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeBWithInlineConfigs, "Parsed correctly"); - }); -}); - -test("Project tree Cycle A with inline configs", (t) => { - // Tree B depends on Library B which has a dependency to Library D - return projectPreprocessor.processTree(treeApplicationCycleA).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeApplicationCycleA, "Parsed correctly"); - }); -}); - -test("Project tree Cycle B with inline configs", (t) => { - return projectPreprocessor.processTree(treeApplicationCycleB).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeApplicationCycleB, "Parsed correctly"); - }); -}); - -test("Project with nested invalid dependencies", (t) => { - return projectPreprocessor.processTree(treeWithInvalidModules).then((parsedTree) => { - t.deepEqual(parsedTree, expectedTreeWithInvalidModules); - }); -}); - -/* ========================= */ -/* ======= Test data ======= */ - -/* === Invalid Modules */ -const treeWithInvalidModules = { - id: "application.a", - path: applicationAPath, - dependencies: [ - // A - { - id: "library.a", - path: libraryAPath, - dependencies: [ - { - // C - invalid - should be missing in preprocessed tree - id: "module.c", - dependencies: [], - path: pathToInvalidModule, - version: "1.0.0" - }, - { - // D - invalid - should be missing in preprocessed tree - id: "module.d", - dependencies: [], - path: pathToInvalidModule, - version: "1.0.0" - } - ], - version: "1.0.0", - specVersion: "2.3", - type: "library", - metadata: {name: "library.a"} - }, - // B - { - id: "library.b", - path: libraryBPath, - dependencies: [ - { - // C - invalid - should be missing in preprocessed tree - id: "module.c", - dependencies: [], - path: pathToInvalidModule, - version: "1.0.0" - }, - { - // D - invalid - should be missing in preprocessed tree - id: "module.d", - dependencies: [], - path: pathToInvalidModule, - version: "1.0.0" - } - ], - version: "1.0.0", - specVersion: "2.3", - type: "library", - metadata: {name: "library.b"} - } - ], - version: "1.0.0", - specVersion: "2.3", - type: "application", - metadata: { - name: "application.a" - } -}; - -const expectedTreeWithInvalidModules = { - "id": "application.a", - "path": applicationAPath, - "dependencies": [{ - "id": "library.a", - "path": libraryAPath, - "dependencies": [], - "version": "1.0.0", - "specVersion": "2.3", - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}" - }, - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, { - "id": "library.b", - "path": libraryBPath, - "dependencies": [], - "version": "1.0.0", - "specVersion": "2.3", - "type": "library", - "metadata": { - "name": "library.b", - "namespace": "library/b", - "copyright": "Some fancy copyright ${currentYear}" - }, - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }], - "version": "1.0.0", - "specVersion": "2.3", - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "_level": 0, - "_isRoot": true, - "kind": "project", - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - } -}; - -/* === Tree A === */ -const treeAWithInlineConfigs = { - id: "application.a", - version: "1.0.0", - specVersion: "2.3", - path: applicationAPath, - type: "application", - metadata: { - name: "application.a", - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "2.3", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a", - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a" - }, - dependencies: [] - } - ] -}; - -const treeAWithConfigPaths = { - id: "application.a", - version: "1.0.0", - path: applicationAPath, - configPath: path.join(applicationAPath, "ui5.yaml"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: libraryDPath, - configPath: path.join(libraryDPath, "ui5.yaml"), - dependencies: [ - { - id: "library.a", - version: "1.0.0", - path: libraryAPath, - configPath: path.join(libraryAPath, "ui5.yaml"), - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - path: libraryAPath, - configPath: path.join(libraryAPath, "ui5.yaml"), - dependencies: [] - } - ] -}; - -const treeAWithDefaultYamls = { - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: libraryDPath, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - path: libraryAPath, - dependencies: [] - } - ] -}; - -const expectedTreeAWithInlineConfigs = { - "id": "application.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": applicationAPath, - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "main/src", - "test": "main/test" - } - }, - "pathMappings": { - "/resources/": "main/src", - "/test-resources/": "main/test" - } - }, - "dependencies": [ - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - }, - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] -}; -const expectedTreeAWithDefaultYamls = expectedTreeAWithInlineConfigs; - -// This is expectedTreeAWithInlineConfigs with added configPath attributes -const expectedTreeAWithConfigPaths = { - "id": "application.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": applicationAPath, - "configPath": path.join(applicationAPath, "ui5.yaml"), - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryDPath, - "configPath": path.join(libraryDPath, "ui5.yaml"), - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "main/src", - "test": "main/test" - } - }, - "pathMappings": { - "/resources/": "main/src", - "/test-resources/": "main/test" - } - }, - "dependencies": [ - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "configPath": path.join(libraryAPath, "ui5.yaml"), - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - }, - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "configPath": path.join(libraryAPath, "ui5.yaml"), - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] -}; - -/* === Tree B === */ -const treeBWithInlineConfigs = { - id: "application.b", - version: "1.0.0", - specVersion: "2.3", - path: applicationBPath, - type: "application", - metadata: { - name: "application.b" - }, - dependencies: [ - { - id: "library.b", - version: "1.0.0", - specVersion: "2.3", - path: libraryBPath, - type: "library", - metadata: { - name: "library.b", - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "2.3", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a" - }, - dependencies: [] - } - ] - } - ] - }, - { - id: "library.d", - version: "1.0.0", - specVersion: "2.3", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "2.3", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a" - }, - dependencies: [] - } - ] - } - ] -}; - -const expectedTreeBWithInlineConfigs = { - "id": "application.b", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": applicationBPath, - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.b", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.b", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryBPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.b", - "namespace": "library/b", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "main/src", - "test": "main/test" - } - }, - "pathMappings": { - "/resources/": "main/src", - "/test-resources/": "main/test" - } - }, - "dependencies": [ - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "_level": 2, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - } - ] - }, - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "main/src", - "test": "main/test" - } - }, - "pathMappings": { - "/resources/": "main/src", - "/test-resources/": "main/test" - } - }, - "dependencies": [ - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "2.3", - "path": libraryAPath, - "_level": 2, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - } - ] -}; - -const treeApplicationCycleA = { - id: "application.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "application.cycle.a"), - type: "application", - metadata: { - name: "application.cycle.a", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "component.cycle.a"), - type: "library", - metadata: { - name: "component.cycle.a", - }, - dependencies: [ - { - id: "library.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "library.cycle.a"), - type: "library", - metadata: { - name: "library.cycle.a", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "component.cycle.a"), - type: "library", - metadata: { - name: "component.cycle.a", - }, - dependencies: [], - deduped: true - } - ] - }, - { - id: "library.cycle.b", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "library.cycle.b"), - type: "library", - metadata: { - name: "library.cycle.b", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "component.cycle.a"), - type: "library", - metadata: { - name: "component.cycle.a", - }, - dependencies: [], - deduped: true - } - ] - }, - { - id: "application.cycle.a", - version: "1.0.0", - specVersion: "2.3", - path: path.join(cycleDepsBasePath, "application.cycle.a"), - type: "application", - metadata: { - name: "application.cycle.a", - }, - dependencies: [], - deduped: true - } - ] - } - ] -}; - -const expectedTreeApplicationCycleA = { - "id": "application.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "type": "application", - "metadata": { - "name": "application.cycle.a", - "namespace": "id1" - }, - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "type": "library", - "metadata": { - "name": "component.cycle.a", - "namespace": "component/cycle/a", - "copyright": "${copyright}" - }, - "dependencies": [ - { - "id": "library.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "library.cycle.a"), - "type": "library", - "metadata": { - "name": "library.cycle.a", - "namespace": "cycle/a", - "copyright": "${copyright}" - }, - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "type": "library", - "metadata": { - "name": "component.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 2, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, - { - "id": "library.cycle.b", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "library.cycle.b"), - "type": "library", - "metadata": { - "name": "library.cycle.b", - "namespace": "cycle/b", - "copyright": "${copyright}" - }, - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "type": "library", - "metadata": { - "name": "component.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 2, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, - { - "id": "application.cycle.a", - "version": "1.0.0", - "specVersion": "2.3", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "type": "application", - "metadata": { - "name": "application.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - } - ], - "_level": 0, - "_isRoot": true, - "kind": "project", - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "UTF-8", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - } -}; - - -const treeApplicationCycleB = { - id: "application.cycle.b", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "application.cycle.b"), - dependencies: [ - { - id: "module.d", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.d"), - dependencies: [ - { - id: "module.e", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.e"), - dependencies: [ - { - id: "module.d", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.d"), - dependencies: [], - deduped: true - } - ] - } - ] - }, - { - id: "module.e", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.e"), - dependencies: [ - { - id: "module.d", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.d"), - dependencies: [ - { - id: "module.e", - version: "1.0.0", - path: path.join(cycleDepsBasePath, "module.e"), - dependencies: [], - deduped: true - } - ] - } - ] - } - ] -}; - -const expectedTreeModuleCycleD = { - id: "module.d", - version: "1.0.0", - specVersion: "2.2", - kind: "project", - type: "module", - metadata: { - name: "module.d", - }, - path: path.join(cycleDepsBasePath, "module.d"), - resources: { - configuration: { - paths: { - "/": "" - } - }, - pathMappings: { - "/": "" - } - }, - _level: 1, - dependencies: [] -}; - -const expectedTreeModuleCycleE = { - id: "module.e", - version: "1.0.0", - specVersion: "2.2", - kind: "project", - type: "module", - metadata: { - name: "module.e", - }, - path: path.join(cycleDepsBasePath, "module.e"), - resources: { - configuration: { - paths: { - "/": "" - } - }, - pathMappings: { - "/": "" - } - }, - _level: 1, - dependencies: [expectedTreeModuleCycleD] -}; - -expectedTreeModuleCycleD.dependencies.push(expectedTreeModuleCycleE); - -const expectedTreeApplicationCycleB = { - id: "application.cycle.b", - version: "1.0.0", - specVersion: "2.2", - path: path.join(cycleDepsBasePath, "application.cycle.b"), - type: "application", - metadata: { - name: "application.cycle.b", - namespace: "id1" - }, - dependencies: [ - expectedTreeModuleCycleD, - expectedTreeModuleCycleE - ], - _level: 0, - _isRoot: true, - kind: "project", - resources: { - configuration: { - propertiesFileSourceEncoding: "UTF-8", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp" - } - } -}; - -/* ======= /Test data ======= */ -/* ========================= */ - -test("Application version in package.json data is missing", (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - type: "application", - metadata: { - name: "xy" - } - }; - return t.throwsAsync(projectPreprocessor.processTree(tree)).then((error) => { - t.is(error.message, "\"version\" is missing for project " + tree.id); - }); -}); - -test("Library version in package.json data is missing", (t) => { - const tree = { - id: "library.d", - path: libraryDPath, - dependencies: [], - type: "library", - metadata: { - name: "library.d" - } - }; - return t.throwsAsync(projectPreprocessor.processTree(tree)).then((error) => { - t.is(error.message, "\"version\" is missing for project " + tree.id); - }); -}); - -test("specVersion: Missing version", async (t) => { - const tree = { - id: "application.a", - path: "non-existent", - dependencies: [], - version: "1.0.0", - type: "application", - metadata: { - name: "xy" - } - }; - const exception = await t.throwsAsync(projectPreprocessor.processTree(tree)); - - t.true(exception.message.includes("Failed to read configuration for project application.a"), - "Error message should contain expected reason"); -}); - -test("specVersion: Project with invalid version", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "0.9", - type: "application", - metadata: { - name: "xy" - } - }; - const validationError = await t.throwsAsync(projectPreprocessor.processTree(tree), { - instanceOf: ValidationError - }); - - t.is(validationError.errors.length, 1, "ValidationError should have one error object"); - t.is(validationError.errors[0].dataPath, "/specVersion", "Error should be for the specVersion"); -}); - -test("specVersion: Project with valid version 0.1", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.3", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.3", "Correct spec version"); -}); - -test("specVersion: Project with valid version 1.0", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.3", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.3", "Correct spec version"); -}); - -test("specVersion: Project with valid version 1.1", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "1.1", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "1.1", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.0", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.0", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.0", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.1", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.1", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.1", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.2", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.2", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.2", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.3", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.3", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.3", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.4", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.4", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.4", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.5", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.5", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.5", "Correct spec version"); -}); - -test("specVersion: Project with valid version 2.6", async (t) => { - const tree = { - id: "application.a", - path: applicationAPath, - dependencies: [], - version: "1.0.0", - specVersion: "2.6", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "2.6", "Correct spec version"); -}); - -test("isBeingProcessed: Is not being processed", (t) => { - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - preprocessor.processedProjects = {}; - - const project = { - id: "some.id", - _level: 1337 - }; - const parent = { - dependencies: [project] - }; - const res = preprocessor.isBeingProcessed(parent, project); - t.deepEqual(res, false, "Project is not processed"); - t.deepEqual(parent.dependencies.length, 1, "Parent still has one dependency"); -}); - -test("isBeingProcessed: Is being processed", (t) => { - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const alreadyProcessedProject = { - project: { - id: "some.id", - _level: 42 - }, - parents: [] - }; - preprocessor.processedProjects = { - "some.id": alreadyProcessedProject - }; - - const project = { - id: "some.id", - _level: 1337 - }; - const parent = { - dependencies: [project] - }; - const res = preprocessor.isBeingProcessed(parent, project); - t.deepEqual(res, true, "Project is already processed"); - t.deepEqual(parent.dependencies.length, 1, "parent still has one dependency"); - t.deepEqual(parent.dependencies[0]._level, 42, "Parent dependency got replaced with already processed project"); - t.deepEqual(alreadyProcessedProject.parents.length, 1, "Already processed project now has one parent"); - t.is(alreadyProcessedProject.parents[0], parent, "Parent got added as parent of already processed project"); -}); - -test("isBeingProcessed: Processed project is ignored", (t) => { - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const alreadyProcessedProject = { - project: { - id: "some.id", - _level: 42 - }, - parents: [], - ignored: true - }; - preprocessor.processedProjects = { - "some.id": alreadyProcessedProject - }; - - const project = { - id: "some.id", - _level: 1337 - }; - const parent = { - dependencies: [project] - }; - const res = preprocessor.isBeingProcessed(parent, project); - t.deepEqual(res, true, "Project is already processed"); - t.deepEqual(parent.dependencies.length, 0, "Project got removed from parent dependencies"); - t.deepEqual(alreadyProcessedProject.parents.length, 0, "Already processed project still has no parents"); -}); - -test("isBeingProcessed: Processed project is ignored but already removed from parent", (t) => { - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const alreadyProcessedProject = { - project: { - id: "some.id", - _level: 42 - }, - parents: [], - ignored: true - }; - preprocessor.processedProjects = { - "some.id": alreadyProcessedProject - }; - - const project = { - id: "some.id", - _level: 1337 - }; - const otherProject = { - id: "some.other.id" - }; - const parent = { - dependencies: [otherProject] - }; - const res = preprocessor.isBeingProcessed(parent, project); - t.deepEqual(res, true, "Project is already processed"); - t.deepEqual(parent.dependencies.length, 1, "Parent still has one dependency"); - t.deepEqual(parent.dependencies[0].id, "some.other.id", - "Parent dependency to another project has not been removed"); - t.deepEqual(alreadyProcessedProject.parents.length, 0, "Already processed project still has no parents"); -}); - -test("isBeingProcessed: Deduped project is being ignored", (t) => { - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - preprocessor.processedProjects = {}; - - const project = { - deduped: true - }; - const parent = {}; - - const res = preprocessor.isBeingProcessed(parent, project); - t.deepEqual(res, true, "Project is being ignored"); -}); - - -test.serial("applyType", async (t) => { - const formatStub = sinon.stub(); - const getTypeStub = sinon.stub(require("@ui5/builder").types.typeRepository, "getType") - .returns({ - format: formatStub - }); - - const project = { - type: "pony", - metadata: {} - }; - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - await preprocessor.applyType(project); - - t.is(getTypeStub.callCount, 1, "getType got called once"); - t.deepEqual(getTypeStub.getCall(0).args[0], "pony", "getType got called with correct type"); - - t.is(formatStub.callCount, 1, "format got called once"); - t.is(formatStub.getCall(0).args[0], project, "format got called with correct project"); -}); - -test.serial("checkProjectMetadata: Warning logged for deprecated dependencies", async (t) => { - const log = require("@ui5/logger"); - const loggerInstance = log.getLogger("pony"); - mock("@ui5/logger", { - getLogger: () => loggerInstance - }); - const logWarnSpy = sinon.spy(loggerInstance, "warn"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const project1 = { - _level: 0, - _isRoot: true, - metadata: { - name: "root.project", - deprecated: true - } - }; - - // no warning should be logged for root level project - await preprocessor.checkProjectMetadata(null, project1); - - const project2 = { - _level: 1, - metadata: { - name: "my.project", - deprecated: true - } - }; - - // one warning should be logged for deprecated dependency - await preprocessor.checkProjectMetadata(project1, project2); - - t.is(logWarnSpy.callCount, 1, "One warning got logged"); - t.deepEqual(logWarnSpy.getCall(0).args[0], - "Dependency my.project is deprecated and should not be used for new projects!", - "Logged expected warning message"); -}); - -test.serial("checkProjectMetadata: No warning logged for nested deprecated libraries", async (t) => { - sinon.stub(require("@ui5/builder").types.typeRepository, "getType") - .returns({format: () => {}}); - - // Spying logger of processors/bootstrapHtmlTransformer - const log = require("@ui5/logger"); - const loggerInstance = log.getLogger("pony"); - mock("@ui5/logger", { - getLogger: () => loggerInstance - }); - const logWarnSpy = sinon.spy(loggerInstance, "warn"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const project1 = { - _level: 1, - metadata: { - name: "some.project", - deprecated: true - } - }; - - // no warning should be logged for nested project - await preprocessor.checkProjectMetadata(null, project1); - - const project2 = { - _level: 2, - metadata: { - name: "my.project", - deprecated: true - } - }; - await preprocessor.checkProjectMetadata(project1, project2); - - t.is(logWarnSpy.callCount, 0, "No warning got logged"); -}); - -test.serial("checkProjectMetadata: Warning logged for SAP internal dependencies", async (t) => { - const log = require("@ui5/logger"); - const loggerInstance = log.getLogger("pony"); - mock("@ui5/logger", { - getLogger: () => loggerInstance - }); - const logWarnSpy = sinon.spy(loggerInstance, "warn"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const project1 = { - _level: 0, - _isRoot: true, - metadata: { - name: "root.project", - sapInternal: true - } - }; - - // no warning should be logged for root level project - await preprocessor.checkProjectMetadata(null, project1); - - const project2 = { - _level: 1, - metadata: { - name: "my.project", - sapInternal: true - } - }; - - // one warning should be logged for internal dependency - await preprocessor.checkProjectMetadata(project1, project2); - - t.is(logWarnSpy.callCount, 1, "One warning got logged"); - t.deepEqual(logWarnSpy.getCall(0).args[0], - `Dependency my.project is restricted for use by SAP internal projects only! ` + - `If the project root.project is an SAP internal project, add the attribute ` + - `"allowSapInternal: true" to its metadata configuration`, - "Logged expected warning message"); -}); - -test.serial("checkProjectMetadata: No warning logged for allowed SAP internal libraries", async (t) => { - sinon.stub(require("@ui5/builder").types.typeRepository, "getType") - .returns({format: () => {}}); - - // Spying logger of processors/bootstrapHtmlTransformer - const log = require("@ui5/logger"); - const loggerInstance = log.getLogger("pony"); - mock("@ui5/logger", { - getLogger: () => loggerInstance - }); - const logWarnSpy = sinon.spy(loggerInstance, "warn"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const project1 = { - _level: 0, - _isRoot: true, - metadata: { - name: "root.project", - allowSapInternal: true // parent project (=root) allows sap internal project use - } - }; - - const project2 = { - _level: 1, - metadata: { - name: "my.project", - sapInternal: true - } - }; - - await preprocessor.checkProjectMetadata(project1, project2); - - t.is(logWarnSpy.callCount, 0, "No warning got logged"); -}); - -test.serial("checkProjectMetadata: No warning logged for nested SAP internal libraries", async (t) => { - sinon.stub(require("@ui5/builder").types.typeRepository, "getType") - .returns({format: () => {}}); - - // Spying logger of processors/bootstrapHtmlTransformer - const log = require("@ui5/logger"); - const loggerInstance = log.getLogger("pony"); - mock("@ui5/logger", { - getLogger: () => loggerInstance - }); - const logWarnSpy = sinon.spy(loggerInstance, "warn"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const project1 = { - _level: 1, - metadata: { - name: "some.project", - allowSapInternal: true // this flag doesn't matter for deeply nested internal dependency - } - }; - - const project2 = { - _level: 2, - metadata: { - name: "my.project", - sapInternal: true - } - }; - - await preprocessor.checkProjectMetadata(project1, project2); - - t.is(logWarnSpy.callCount, 0, "No warning got logged"); -}); - - -test.serial("readConfigFile: No exception for valid config", async (t) => { - const configPath = path.join("/application", "ui5.yaml"); - const ui5yaml = ` ---- -specVersion: "2.0" -type: application -metadata: - name: application.a -`; - - const validateSpy = sinon.spy(validator, "validate"); - - sinon.stub(gracefulFs, "readFile") - .callsFake((path) => { - throw new Error("readFileStub called with unexpected path: " + path); - }) - .withArgs(configPath).yieldsAsync(null, ui5yaml); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - await t.notThrowsAsync(async () => { - await preprocessor.readConfigFile({path: "/application", id: "id"}); - }); - - t.is(validateSpy.callCount, 1, "validate should be called once"); - t.deepEqual(validateSpy.getCall(0).args, [{ - config: { - specVersion: "2.0", - type: "application", - metadata: { - name: "application.a" - } - }, - project: { - id: "id", - }, - yaml: { - documentIndex: 0, - path: configPath, - source: ui5yaml - }, - - }], - "validate should be called with expected args"); -}); - -test.serial("readConfigFile: Exception for invalid config", async (t) => { - const configPath = path.join("/application", "ui5.yaml"); - const ui5yaml = ` ---- -specVersion: "2.0" -type: application -metadata: - name: application.a ---- -specVersion: "2.0" -kind: extension -type: task -metadata: - name: my-task ---- -specVersion: "2.0" -kind: extension -type: server-middleware -metadata: - name: my-middleware -`; - - const validateSpy = sinon.spy(validator, "validate"); - - sinon.stub(gracefulFs, "readFile") - .callsFake((path) => { - throw new Error("readFileStub called with unexpected path: " + path); - }) - .withArgs(configPath).yieldsAsync(null, ui5yaml); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const validationError = await t.throwsAsync(async () => { - await preprocessor.readConfigFile({path: "/application", id: "id"}); - }, { - instanceOf: ValidationError, - name: "ValidationError" - }); - - t.is(validationError.yaml.documentIndex, 1, "Error of first invalid document should be thrown"); - - t.is(validateSpy.callCount, 3, "validate should be called 3 times"); - t.deepEqual(validateSpy.getCall(0).args, [{ - config: { - specVersion: "2.0", - type: "application", - metadata: { - name: "application.a" - } - }, - project: { - id: "id", - }, - yaml: { - documentIndex: 0, - path: configPath, - source: ui5yaml, - }, - }], - "validate should be called first time with expected args"); - t.deepEqual(validateSpy.getCall(1).args, [{ - config: { - specVersion: "2.0", - kind: "extension", - type: "task", - metadata: { - name: "my-task" - } - }, - project: { - id: "id", - }, - yaml: { - documentIndex: 1, - path: configPath, - source: ui5yaml, - }, - }], - "validate should be called second time with expected args"); - t.deepEqual(validateSpy.getCall(2).args, [{ - config: { - specVersion: "2.0", - kind: "extension", - type: "server-middleware", - metadata: { - name: "my-middleware" - } - }, - project: { - id: "id", - }, - yaml: { - documentIndex: 2, - path: configPath, - source: ui5yaml, - }, - }], - "validate should be called third time with expected args"); -}); - -test.serial("readConfigFile: Exception for invalid YAML file", async (t) => { - const configPath = path.join("/application", "ui5.yaml"); - const ui5yaml = ` --- -specVersion: "2.0" -foo: bar -metadata: - name: application.a -`; - - const validateSpy = sinon.spy(validator, "validate"); - - sinon.stub(gracefulFs, "readFile") - .callsFake((path) => { - throw new Error("readFileStub called with unexpected path: " + path); - }) - .withArgs(configPath).yieldsAsync(null, ui5yaml); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const error = await t.throwsAsync(async () => { - await preprocessor.readConfigFile({path: "/application", id: "my-project"}); - }); - - t.true(error.message.includes("Failed to parse configuration for project my-project"), - "Error message should contain information about parsing error"); - - t.is(validateSpy.callCount, 0, "validate should not be called"); -}); - -test.serial("readConfigFile: Empty YAML", async (t) => { - const configPath = path.join("/application", "ui5.yaml"); - const ui5yaml = ""; - - const validateSpy = sinon.spy(validator, "validate"); - - sinon.stub(gracefulFs, "readFile") - .callsFake((path) => { - throw new Error("readFileStub called with unexpected path: " + path); - }) - .withArgs(configPath).yieldsAsync(null, ui5yaml); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - const configs = await preprocessor.readConfigFile({path: "/application", id: "my-project"}); - - t.deepEqual(configs, [], "Empty YAML should result in empty array"); - t.is(validateSpy.callCount, 0, "validate should not be called"); -}); - -test.serial("loadProjectConfiguration: Runs validation if specVersion already exists (error)", async (t) => { - const config = { - specVersion: "2.0", - foo: "bar", - metadata: { - name: "application.a" - }, - - id: "id", - version: "1.0.0", - path: "path", - dependencies: [] - }; - - const validateSpy = sinon.spy(validator, "validate"); - - // Re-require tested module - const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); - const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); - - await t.throwsAsync(async () => { - await preprocessor.loadProjectConfiguration(config); - }, { - instanceOf: ValidationError, - name: "ValidationError" - }); - - t.is(validateSpy.callCount, 1, "validate should be called once"); - t.deepEqual(validateSpy.getCall(0).args, [{ - config: { - specVersion: "2.0", - foo: "bar", - metadata: { - name: "application.a" - } - }, - project: { - id: "id" - } - }], - "validate should be called with expected args"); -}); diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 45539d076..f9fa05f3b 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -7,11 +7,11 @@ function clone(obj) { return JSON.parse(JSON.stringify(obj)); } -const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); const basicProjectInput = { id: "library.d.id", version: "1.0.0", - modulePath: applicationAPath, + modulePath: libraryDPath, configuration: { specVersion: "2.3", kind: "project", diff --git a/test/lib/translators/npm.integration.js b/test/lib/translators/npm.integration.js deleted file mode 100644 index 7bab081bf..000000000 --- a/test/lib/translators/npm.integration.js +++ /dev/null @@ -1,1014 +0,0 @@ -const test = require("ava"); -const path = require("path"); -const mock = require("mock-require"); -const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c"); -const applicationC2Path = path.join(__dirname, "..", "..", "fixtures", "application.c2"); -const applicationC3Path = path.join(__dirname, "..", "..", "fixtures", "application.c3"); -const applicationDPath = path.join(__dirname, "..", "..", "fixtures", "application.d"); -const applicationFPath = path.join(__dirname, "..", "..", "fixtures", "application.f"); -const applicationGPath = path.join(__dirname, "..", "..", "fixtures", "application.g"); -const errApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "err.application.a"); -const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules"); - -let npmTranslator = require("../../../lib/translators/npm"); - -test.serial("AppA: project with collection dependency", (t) => { - // Also cover log level based conditionals in this test - const logger = require("@ui5/logger"); - mock("@ui5/logger", { - getLogger: () => { - const log = logger.getLogger(); - log.isLevelEnabled = () => true; - return log; - } - }); - npmTranslator = mock.reRequire("../../../lib/translators/npm"); - return npmTranslator.generateDependencyTree(applicationAPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationATree, "Parsed correctly"); - mock.stop("@ui5/logger"); - }); -}); - -test("AppC: project with dependency with optional dependency resolved through root project", (t) => { - return npmTranslator.generateDependencyTree(applicationCPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCTree, "Parsed correctly"); - }); -}); - -test("AppC2: project with dependency with optional dependency resolved through other project", (t) => { - return npmTranslator.generateDependencyTree(applicationC2Path).then((parsedTree) => { - t.deepEqual(parsedTree, applicationC2Tree, "Parsed correctly"); - }); -}); - -test("AppC3: project with dependency with optional dependency resolved " + - "through other project (but got hoisted)", (t) => { - return npmTranslator.generateDependencyTree(applicationC3Path).then((parsedTree) => { - t.deepEqual(parsedTree, applicationC3Tree, "Parsed correctly"); - }); -}); - -test("AppD: project with dependency with unresolved optional dependency", (t) => { - // application.d`s dependency "library.e" has an optional dependency to "library.d" - // which is already present in the node_modules directory of library.e - return npmTranslator.generateDependencyTree(applicationDPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationDTree, "Parsed correctly. library.d is not in dependency tree."); - }); -}); - -test("AppF: project with UI5-dependencies", (t) => { - return npmTranslator.generateDependencyTree(applicationFPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationFTree, "Parsed correctly"); - }); -}); - -test("AppG: project with npm 'optionalDependencies' should not fail if optional dependency cannot be resolved", (t) => { - return npmTranslator.generateDependencyTree(applicationGPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationGTree, "Parsed correctly"); - }); -}); - -test("AppCycleA: cyclic dev deps", (t) => { - const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); - - return npmTranslator.generateDependencyTree(applicationCycleAPath).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleATree, "Parsed correctly"); - }); -}); - -test("AppCycleA: cyclic dev deps - include deduped", (t) => { - const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); - - return npmTranslator.generateDependencyTree(applicationCycleAPath, {includeDeduped: true}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleATreeIncDeduped, "Parsed correctly"); - }); -}); - -test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level - include deduped", (t) => { - const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); - return npmTranslator.generateDependencyTree(applicationCycleBPath, {includeDeduped: true}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleBTreeIncDeduped, "Parsed correctly"); - }); -}); - -test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", (t) => { - const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); - return npmTranslator.generateDependencyTree(applicationCycleBPath, {includeDeduped: false}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleBTree, "Parsed correctly"); - }); -}); - -test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", (t) => { - const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); - return npmTranslator.generateDependencyTree(applicationCycleCPath, {includeDeduped: false}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleCTree, "Parsed correctly"); - }); -}); - -test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection) - include deduped", (t) => { - const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); - return npmTranslator.generateDependencyTree(applicationCycleCPath, {includeDeduped: true}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleCTreeIncDeduped, "Parsed correctly"); - }); -}); - -test("AppCycleD: cyclic npm deps - Cycles everywhere", (t) => { - const applicationCycleDPath = path.join(cycleDepsBasePath, "application.cycle.d"); - return npmTranslator.generateDependencyTree(applicationCycleDPath, {includeDeduped: true}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleDTree, "Parsed correctly"); - }); -}); - -test("AppCycleE: cyclic npm deps - Cycle via devDependency - include deduped", (t) => { - const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); - return npmTranslator.generateDependencyTree(applicationCycleEPath, {includeDeduped: true}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleETreeIncDeduped, "Parsed correctly"); - }); -}); - -test("AppCycleE: cyclic npm deps - Cycle via devDependency", (t) => { - const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); - return npmTranslator.generateDependencyTree(applicationCycleEPath, {includeDeduped: false}).then((parsedTree) => { - t.deepEqual(parsedTree, applicationCycleETree, "Parsed correctly"); - }); -}); - -test("Error: missing package.json", async (t) => { - const dir = path.parse(__dirname).root; - const error = await t.throwsAsync(npmTranslator.generateDependencyTree(dir)); - t.is(error.message, `[npm translator] Failed to locate package.json for directory "${dir}"`); -}); - -test("Error: missing dependency", async (t) => { - const error = await t.throwsAsync(npmTranslator.generateDependencyTree(errApplicationAPath)); - t.is(error.message, "[npm translator] Could not locate " + - "module library.xx via resolve logic (error: Cannot find module 'library.xx/package.json' from '" + - errApplicationAPath + "') or in a collection"); -}); - -// TODO: Test for scenarios where a dependency is missing *and there is no package.json* in the path above -// the root module -// This should test whether the collection-fallback can handle not receiving a .pkg object from readPkgUp -// Currently tricky to test as there is always a package.json located above the test fixtures. - -/* ========================= */ -/* ======= Test data ======= */ - -const applicationATree = { - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "library.d"), - dependencies: [] - }, - { - id: "library.a", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.a"), - dependencies: [] - }, - { - id: "library.b", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.b"), - dependencies: [] - }, - { - id: "library.c", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.c"), - dependencies: [] - } - ] -}; - -const applicationCTree = { - id: "application.c", - version: "1.0.0", - path: applicationCPath, - dependencies: [ - { - id: "library.e", - version: "1.0.0", - path: path.join(applicationCPath, "node_modules", "library.e"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationCPath, "node_modules", "library.d"), - dependencies: [] - } - ] - }, - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationCPath, "node_modules", "library.d"), - dependencies: [] - } - ] -}; - - -const applicationC2Tree = { - id: "application.c2", - version: "1.0.0", - path: applicationC2Path, - dependencies: [ - { - id: "library.e", - version: "1.0.0", - path: path.join(applicationC2Path, "node_modules", "library.e"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationC2Path, "node_modules", "library.d-depender", - "node_modules", "library.d"), - dependencies: [] - } - ] - }, - { - id: "library.d-depender", - version: "1.0.0", - path: path.join(applicationC2Path, "node_modules", "library.d-depender"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationC2Path, "node_modules", "library.d-depender", - "node_modules", "library.d"), - dependencies: [] - } - ] - } - ] -}; - -const applicationC3Tree = { - id: "application.c3", - version: "1.0.0", - path: applicationC3Path, - dependencies: [ - { - id: "library.e", - version: "1.0.0", - path: path.join(applicationC3Path, "node_modules", "library.e"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationC3Path, "node_modules", "library.d"), - dependencies: [] - } - ] - }, - { - id: "library.d-depender", - version: "1.0.0", - path: path.join(applicationC3Path, "node_modules", "library.d-depender"), - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationC3Path, "node_modules", "library.d"), - dependencies: [] - } - ] - } - ] -}; - -const applicationDTree = { - id: "application.d", - version: "1.0.0", - path: applicationDPath, - dependencies: [ - { - id: "library.e", - version: "1.0.0", - path: path.join(applicationDPath, "node_modules", "library.e"), - dependencies: [] - } - ] -}; - -const applicationFTree = { - id: "application.f", - version: "1.0.0", - path: applicationFPath, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationFPath, "node_modules", "library.d"), - dependencies: [] - } - ] -}; - -const applicationGTree = { - id: "application.g", - version: "1.0.0", - path: applicationGPath, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - path: path.join(applicationGPath, "node_modules", "library.d"), - dependencies: [] - } - ] -}; - -const applicationCycleATree = { - "id": "application.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "dependencies": [ - { - "id": "library.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "library.cycle.a"), - "dependencies": [] - }, - { - "id": "library.cycle.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "library.cycle.b"), - "dependencies": [] - } - ] - } - ] -}; - -const applicationCycleATreeIncDeduped = { - "id": "application.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "dependencies": [ - { - "id": "library.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "library.cycle.a"), - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "dependencies": [], - "deduped": true - } - ] - }, - { - "id": "library.cycle.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "library.cycle.b"), - "dependencies": [ - { - "id": "component.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "dependencies": [], - "deduped": true - } - ] - }, - { - "id": "application.cycle.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "dependencies": [], - "deduped": true - } - ] - } - ] -}; - -const applicationCycleBTree = { - "id": "application.cycle.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.b"), - "dependencies": [ - { - "id": "module.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.d"), - "dependencies": [ - { - "id": "module.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.e"), - "dependencies": [] - } - ] - }, - { - "id": "module.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.e"), - "dependencies": [ - { - "id": "module.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.d"), - "dependencies": [] - } - ] - } - ] -}; - -const applicationCycleBTreeIncDeduped = { - "id": "application.cycle.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.b"), - "dependencies": [ - { - "id": "module.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.d"), - "dependencies": [ - { - "id": "module.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.e"), - "dependencies": [ - { - "id": "module.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.d"), - "dependencies": [], - "deduped": true - } - ] - } - ] - }, - { - "id": "module.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.e"), - "dependencies": [ - { - "id": "module.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.d"), - "dependencies": [ - { - "id": "module.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.e"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] -}; - -const applicationCycleCTree = { - "id": "application.cycle.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.c"), - "dependencies": [ - { - "id": "module.f", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.f"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [ - { - "id": "module.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.b"), - "dependencies": [ - { - "id": "module.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.c"), - "dependencies": [] - } - ] - } - ] - } - ] - }, - { - "id": "module.g", - "version": "1.0.0", "path": path.join(cycleDepsBasePath, "module.g"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [ - { - "id": "module.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.b"), - "dependencies": [ - { - "id": "module.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.c"), - "dependencies": [] - } - ] - } - ] - } - ] - } - ] -}; - -const applicationCycleCTreeIncDeduped = { - "id": "application.cycle.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.c"), - "dependencies": [ - { - "id": "module.f", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.f"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [ - { - "id": "module.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.b"), - "dependencies": [ - { - "id": "module.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.c"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - } - ] - }, - { - "id": "module.g", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.g"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [ - { - "id": "module.b", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.b"), - "dependencies": [ - { - "id": "module.c", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.c"), - "dependencies": [ - { - "id": "module.a", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.a"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - } - ] - } - ] -}; - -const applicationCycleDTree = { - "id": "application.cycle.d", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.d"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - } - ] - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - } - ] - } - ] - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [], - "deduped": true - } - ] - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [], - "deduped": true - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - }, - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [ - { - "id": "module.h", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.h"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [], - "deduped": true - } - ] - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - }, - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [], - "deduped": true - } - ] - }, - { - "id": "module.j", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.j"), - "dependencies": [ - { - "id": "module.i", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.i"), - "dependencies": [ - { - "id": "module.k", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.k"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] - } - ] -}; - -const applicationCycleETree = { - "id": "application.cycle.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.e"), - "dependencies": [ - { - "id": "module.l", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.l"), - "dependencies": [ - { - "id": "module.m", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.m"), - "dependencies": [] - } - ] - }, - { - "id": "module.m", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.m"), - "dependencies": [ - { - "id": "module.l", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.l"), - "dependencies": [] - } - ] - } - ] -}; - -const applicationCycleETreeIncDeduped = { - "id": "application.cycle.e", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "application.cycle.e"), - "dependencies": [ - { - "id": "module.l", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.l"), - "dependencies": [ - { - "id": "module.m", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.m"), - "dependencies": [ - { - "id": "module.l", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.l"), - "dependencies": [], - "deduped": true - } - ] - } - ] - }, - { - "id": "module.m", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.m"), - "dependencies": [ - { - "id": "module.l", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.l"), - "dependencies": [ - { - "id": "module.m", - "version": "1.0.0", - "path": path.join(cycleDepsBasePath, "module.m"), - "dependencies": [], - "deduped": true - } - ] - } - ] - } - ] -}; diff --git a/test/lib/translators/npm.js b/test/lib/translators/npm.js deleted file mode 100644 index c7fe9a454..000000000 --- a/test/lib/translators/npm.js +++ /dev/null @@ -1,128 +0,0 @@ -const test = require("ava"); -const sinon = require("sinon"); -const path = require("path"); - -const NpmTranslator = require("../../../lib/translators/npm")._NpmTranslator; - -test.afterEach.always((t) => { - sinon.restore(); -}); - -test.serial("processPkg - single package", async (t) => { - const npmTranslator = new NpmTranslator({ - includeDeduped: false - }); - const result = await npmTranslator.processPkg({ - path: path.join("/", "sample-package"), - name: "sample-package", - pkg: { - version: "1.2.3" - } - }, ":parent:"); - t.deepEqual(result, [{ - dependencies: [], - id: "sample-package", - path: path.join("/", "sample-package"), - version: "1.2.3" - }]); -}); - -test.serial("processPkg - collection", async (t) => { - const npmTranslator = new NpmTranslator({ - includeDeduped: false - }); - - const readProjectStub = sinon.stub(npmTranslator, "readProject").resolves({ - dependencies: [], - id: "other-package", - path: path.join("/", "sample-package", "packages", "other-package"), - version: "4.5.6" - }); - - const result = await npmTranslator.processPkg({ - path: path.join("/", "sample-package"), - name: "sample-package", - pkg: { - version: "1.2.3", - collection: { - modules: { - "other-package": "./packages/other-package" - } - } - } - }, ":parent:"); - - t.deepEqual(result, [{ - dependencies: [], - id: "other-package", - path: path.join("/", "sample-package", "packages", "other-package"), - version: "4.5.6" - }]); - - t.is(readProjectStub.callCount, 1, "readProject should be called once"); - t.deepEqual(readProjectStub.getCall(0).args, [ - { - moduleName: "other-package", - modulePath: path.join("/", "sample-package", "packages", "other-package"), - parentPath: ":parent:sample-package:", - }, - ], "readProject should be called with the expected args"); -}); - -test.serial("processPkg - pkg.collection (type string)", async (t) => { - const npmTranslator = new NpmTranslator({ - includeDeduped: false - }); - - const readProjectStub = sinon.stub(npmTranslator, "readProject").resolves(null); - - const result = await npmTranslator.processPkg({ - path: path.join("/", "sample-package"), - name: "sample-package", - pkg: { - version: "1.2.3", - - // collection of type string should not be detected as UI5 collection - collection: "foo" - } - }, ":parent:"); - - t.deepEqual(result, [{ - dependencies: [], - id: "sample-package", - path: path.join("/", "sample-package"), - version: "1.2.3" - }]); - - t.is(readProjectStub.callCount, 0, "readProject should not be called once"); -}); - -test.serial("processPkg - pkg.collection (without modules)", async (t) => { - const npmTranslator = new NpmTranslator({ - includeDeduped: false - }); - - const readProjectStub = sinon.stub(npmTranslator, "readProject").resolves(null); - - const result = await npmTranslator.processPkg({ - path: path.join("/", "sample-package"), - name: "sample-package", - pkg: { - version: "1.2.3", - - // collection without modules object should not be detected as UI5 collection - collection: { - modules: true - } - } - }, ":parent:"); - - t.deepEqual(result, [{ - dependencies: [], - id: "sample-package", - path: path.join("/", "sample-package"), - version: "1.2.3" - }]); - - t.is(readProjectStub.callCount, 0, "readProject should not be called once"); -}); diff --git a/test/lib/translators/static.js b/test/lib/translators/static.js deleted file mode 100644 index 7c83a7230..000000000 --- a/test/lib/translators/static.js +++ /dev/null @@ -1,83 +0,0 @@ -const test = require("ava"); -const path = require("path"); -const fs = require("graceful-fs"); -const sinon = require("sinon"); -const escapeStringRegexp = require("escape-string-regexp"); -const staticTranslator = require("../../..").translators.static; -const projectPath = path.join(__dirname, "..", "..", "fixtures", "application.h"); - -test("Generates dependency tree for project with projectDependencies.yaml", (t) => { - return staticTranslator.generateDependencyTree(projectPath) - .then((parsedTree) => { - t.deepEqual(parsedTree, expectedTree, "Parsed correctly"); - }); -}); - -test("Generates dependency tree for project with projectDependencies.yaml (via parameters)", (t) => { - return staticTranslator.generateDependencyTree(projectPath, { - parameters: [path.join(projectPath, "projectDependencies.yaml")] - }) - .then((parsedTree) => { - t.deepEqual(parsedTree, expectedTree, "Parsed correctly"); - }); -}); - -test("Generates dependency tree for project by passing tree object", (t) => { - return staticTranslator.generateDependencyTree(projectPath, { - tree: { - id: "testsuite", - version: "0.0.1", - description: "Sample App", - main: "index.html", - path: "./", - dependencies: [ - { - id: "sap.f", - version: "1.56.1", - path: "../sap.f" - }, - { - id: "sap.m", - version: "1.61.0", - path: "../sap.m" - } - ] - } - }) - .then((parsedTree) => { - t.deepEqual(parsedTree, expectedTree, "Parsed correctly"); - }); -}); - -test("Error: Throws if projectDependencies.yaml was not found", async (t) => { - const projectPath = "notExistingPath"; - const fsError = new Error("File not found"); - const fsStub = sinon.stub(fs, "readFile"); - fsStub.callsArgWith(1, fsError); - const error = await t.throwsAsync(staticTranslator.generateDependencyTree(projectPath)); - const yamlPath = path.join(projectPath, "projectDependencies.yaml"); - t.regex(error.message, - new RegExp(`\\[static translator\\] Failed to load dependency tree from path ` + - `${escapeStringRegexp(yamlPath)} - Error: ENOENT:`)); - fsStub.restore(); -}); - -const expectedTree = { - id: "testsuite", - version: "0.0.1", - description: "Sample App", - main: "index.html", - path: path.resolve(projectPath, "./"), - dependencies: [ - { - id: "sap.f", - version: "1.56.1", - path: path.resolve(projectPath, "../sap.f") - }, - { - id: "sap.m", - version: "1.61.0", - path: path.resolve(projectPath, "../sap.m") - } - ] -}; diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/translators/ui5Framework.integration.js deleted file mode 100644 index ad6842195..000000000 --- a/test/lib/translators/ui5Framework.integration.js +++ /dev/null @@ -1,948 +0,0 @@ -const test = require("ava"); -const sinon = require("sinon"); -const mock = require("mock-require"); -const path = require("path"); -const os = require("os"); -const fs = require("graceful-fs"); - -const pacote = require("pacote"); -const libnpmconfig = require("libnpmconfig"); -const lockfile = require("lockfile"); -const logger = require("@ui5/logger"); -const normalizer = require("../../../lib/normalizer"); -const projectPreprocessor = require("../../../lib/projectPreprocessor"); -let ui5Framework; -let Installer; - -// Use path within project as mocking base directory to reduce chance of side effects -// in case mocks/stubs do not work and real fs is used -const fakeBaseDir = path.join(__dirname, "fake-tmp"); -const ui5FrameworkBaseDir = path.join(fakeBaseDir, "homedir", ".ui5", "framework"); -const ui5PackagesBaseDir = path.join(ui5FrameworkBaseDir, "packages"); - -test.before((t) => { - sinon.stub(fs, "rename").yieldsAsync(); -}); - -test.beforeEach((t) => { - sinon.stub(libnpmconfig, "read").returns({ - toJSON: () => { - return { - registry: "https://registry.fake", - cache: path.join(ui5FrameworkBaseDir, "cacache"), - proxy: "" - }; - } - }); - sinon.stub(os, "homedir").returns(path.join(fakeBaseDir, "homedir")); - - sinon.stub(lockfile, "lock").yieldsAsync(); - sinon.stub(lockfile, "unlock").yieldsAsync(); - - const testLogger = logger.getLogger(); - sinon.stub(logger, "getLogger").returns(testLogger); - t.context.logInfoSpy = sinon.spy(testLogger, "info"); - - mock("mkdirp", sinon.stub().resolves()); - - // Re-require to ensure that mocked modules are used - ui5Framework = mock.reRequire("../../../lib/translators/ui5Framework"); - Installer = require("../../../lib/ui5Framework/npm/Installer"); -}); - -test.afterEach.always((t) => { - sinon.restore(); - mock.stopAll(); - logger.setLevel("info"); // default log level -}); - -function defineTest(testName, { - frameworkName, - verbose = false -}) { - const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; - - const distributionMetadata = { - libraries: { - "sap.ui.lib1": { - npmPackageName: "@sapui5/sap.ui.lib1", - version: "1.75.1", - dependencies: [], - optionalDependencies: [] - }, - "sap.ui.lib2": { - npmPackageName: "@sapui5/sap.ui.lib2", - version: "1.75.2", - dependencies: [ - "sap.ui.lib3" - ], - optionalDependencies: [] - }, - "sap.ui.lib3": { - npmPackageName: "@sapui5/sap.ui.lib3", - version: "1.75.3", - dependencies: [], - optionalDependencies: [ - "sap.ui.lib4" - ] - }, - "sap.ui.lib4": { - npmPackageName: "@openui5/sap.ui.lib4", - version: "1.75.4", - dependencies: [ - "sap.ui.lib1" - ], - optionalDependencies: [] - }, - "sap.ui.lib8": { - npmPackageName: "@sapui5/sap.ui.lib8", - version: "1.75.8", - dependencies: [], - optionalDependencies: [] - } - } - }; - - function project({name, version, type, specVersion = "2.0", framework, _level, dependencies = []}) { - const proj = { - _level, - id: name + "-id", - version, - path: path.join(fakeBaseDir, "project-" + name), - specVersion, - kind: "project", - type, - metadata: { - name - }, - dependencies - }; - if (_level === 0) { - proj._isRoot = true; - } - if (framework) { - proj.framework = framework; - } - return proj; - } - function frameworkProject({name, _level, dependencies = []}) { - const metadata = frameworkName === "SAPUI5" ? distributionMetadata.libraries[name] : null; - const id = frameworkName === "SAPUI5" ? metadata.npmPackageName : npmScope + "/" + name; - const version = frameworkName === "SAPUI5" ? metadata.version : "1.75.0"; - return { - _level, - id, - version, - path: path.join( - ui5PackagesBaseDir, - // sap.ui.lib4 is in @openui5 scope in SAPUI5 and OpenUI5 - name === "sap.ui.lib4" ? "@openui5" : npmScope, - name, version - ), - specVersion: "1.0", - kind: "project", - type: "library", - metadata: { - name - }, - framework: { - libraries: [] - }, - dependencies - }; - } - - test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { - // Enable verbose logging - if (verbose) { - logger.setLevel("verbose"); - } - - const testDependency = { - id: "test-dependency-id", - version: "4.5.6", - path: path.join(fakeBaseDir, "project-test-dependency"), - dependencies: [] - }; - const translatorTree = { - id: "test-application-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "project-test-application"), - dependencies: [ - testDependency, - { - id: "test-dependency-no-framework-id", - version: "7.8.9", - path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), - dependencies: [ - testDependency - ] - }, - { - id: "test-dependency-framework-old-spec-version-id", - version: "10.11.12", - path: path.join(fakeBaseDir, "project-test-dependency-framework-old-spec-version"), - dependencies: [] - } - ] - }; - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - - sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") - .callsFake(async (project) => { - switch (project.path) { - case path.join(fakeBaseDir, "project-test-application"): - return [{ - specVersion: "2.0", - type: "application", - metadata: { - name: "test-application" - }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib4", - optional: true - }, - { - name: "sap.ui.lib8", - development: true - } - ] - } - }]; - case path.join(fakeBaseDir, "project-test-dependency"): - return [{ - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency" - }, - framework: { - version: "1.99.0", - name: frameworkName, - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib2" - }, - { - name: "sap.ui.lib5", - optional: true - }, - { - name: "sap.ui.lib6", - development: true - }, - { - name: "sap.ui.lib8", - // optional dependency gets resolved by dev-dependency of root project - optional: true - } - ] - } - }]; - case path.join(fakeBaseDir, "project-test-dependency-no-framework"): - return [{ - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency-no-framework" - } - }]; - case path.join(fakeBaseDir, "project-test-dependency-framework-old-spec-version"): - return [{ - specVersion: "1.1", - type: "library", - metadata: { - name: "test-dependency-framework-old-spec-version" - }, - framework: { - libraries: [ - { - name: "sap.ui.lib5" - } - ] - } - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", - frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib1" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", - frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib2" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", - frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib3" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", - frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib4" - }, - framework: {libraries: []} - }]; - case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", - frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): - return [{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib8" - }, - framework: {libraries: []} - }]; - default: - throw new Error( - "ProjectPreprocessor#readConfigFile stub called with unknown project: " + - (project && project.path) - ); - } - }); - - // Prevent applying types as this would require a lot of mocking - sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); - - sinon.stub(pacote, "extract").resolves(); - - if (frameworkName === "OpenUI5") { - sinon.stub(pacote, "manifest") - .callsFake(async (spec) => { - throw new Error("pacote.manifest stub called with unknown spec: " + spec); - }) - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib1", - version: "1.75.0", - dependencies: {} - }) - .withArgs("@openui5/sap.ui.lib2@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib2", - version: "1.75.0", - dependencies: { - "@openui5/sap.ui.lib3": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib3@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib3", - version: "1.75.0", - devDependencies: { - "@openui5/sap.ui.lib4": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib4", - version: "1.75.0", - dependencies: { - "@openui5/sap.ui.lib1": "1.75.0" - } - }) - .withArgs("@openui5/sap.ui.lib8@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib8", - version: "1.75.0", - dependencies: {} - }); - } else if (frameworkName === "SAPUI5") { - sinon.stub(Installer.prototype, "readJson") - .callsFake(async (path) => { - throw new Error("Installer#readJson stub called with unknown path: " + path); - }) - .withArgs(path.join(fakeBaseDir, - "homedir", ".ui5", "framework", "packages", - "@sapui5", "distribution-metadata", "1.75.0", - "metadata.json")) - .resolves(distributionMetadata); - } - - const testDependencyProject = project({ - _level: 1, - name: "test-dependency", - version: "4.5.6", - type: "library", - framework: { - version: "1.99.0", - name: frameworkName, - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib2" - }, - { - name: "sap.ui.lib5", - optional: true - }, - { - name: "sap.ui.lib6", - development: true - }, - { - name: "sap.ui.lib8", - optional: true - } - ] - }, - dependencies: [ - frameworkProject({ - _level: 1, - name: "sap.ui.lib1", - }), - frameworkProject({ - _level: 1, - name: "sap.ui.lib2", - dependencies: [ - frameworkProject({ - _level: 2, - name: "sap.ui.lib3", - dependencies: [ - frameworkProject({ - name: "sap.ui.lib4", - _level: 1, - dependencies: [ - frameworkProject({ - _level: 1, - name: "sap.ui.lib1" - }) - ] - }) - ] - }) - ] - }), - frameworkProject({ - _level: 1, - name: "sap.ui.lib8", - }) - ] - }); - const expectedTree = project({ - _level: 0, - name: "test-application", - version: "1.2.3", - type: "application", - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib4", - optional: true - }, - { - name: "sap.ui.lib8", - development: true - } - ] - }, - dependencies: [ - testDependencyProject, - project({ - _level: 1, - name: "test-dependency-no-framework", - version: "7.8.9", - type: "library", - dependencies: [testDependencyProject] - }), - project({ - _level: 1, - name: "test-dependency-framework-old-spec-version", - specVersion: "1.1", - version: "10.11.12", - type: "library", - framework: { - libraries: [ - { - name: "sap.ui.lib5" - } - ] - }, - }), - frameworkProject({ - _level: 1, - name: "sap.ui.lib1", - }), - frameworkProject({ - name: "sap.ui.lib4", - _level: 1, - dependencies: [ - frameworkProject({ - _level: 1, - name: "sap.ui.lib1" - }) - ] - }), - frameworkProject({ - _level: 1, - name: "sap.ui.lib8", - }) - ] - }); - - const tree = await normalizer.generateProjectTree(); - - t.deepEqual(tree, expectedTree, "Returned tree should be correct"); - const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() - .map(($) => $.firstArg) - .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1); - t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged"); - }); -} - -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { - frameworkName: "SAPUI5" -}); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { - frameworkName: "SAPUI5", - verbose: true -}); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { - frameworkName: "OpenUI5" -}); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { - frameworkName: "OpenUI5", - verbose: true -}); - -function defineErrorTest(testName, { - frameworkName, - failExtract = false, - failMetadata = false, - expectedErrorMessage -}) { - test.serial(testName, async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - - sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") - .callsFake(async (project) => { - switch (project.path) { - case path.join(fakeBaseDir, "application-project"): - return [{ - specVersion: "2.0", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib4", - optional: true - } - ] - } - }]; - default: - throw new Error( - "ProjectPreprocessor#readConfigFile stub called with unknown project: " + - (project && project.path) - ); - } - }); - - // Prevent applying types as this would require a lot of mocking - sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); - - const extractStub = sinon.stub(pacote, "extract"); - extractStub.callsFake(async (spec) => { - throw new Error("pacote.extract stub called with unknown spec: " + spec); - }); - - const manifestStub = sinon.stub(pacote, "manifest"); - manifestStub.callsFake(async (spec) => { - throw new Error("pacote.manifest stub called with unknown spec: " + spec); - }); - - if (frameworkName === "SAPUI5") { - if (failExtract) { - extractStub - .withArgs("@sapui5/sap.ui.lib1@1.75.1") - .rejects(new Error("404 - @sapui5/sap.ui.lib1")) - .withArgs("@openui5/sap.ui.lib4@1.75.4") - .rejects(new Error("404 - @openui5/sap.ui.lib4")); - } else { - extractStub - .withArgs("@sapui5/sap.ui.lib1@1.75.1").resolves() - .withArgs("@openui5/sap.ui.lib4@1.75.4").resolves(); - } - if (failMetadata) { - extractStub - .withArgs("@sapui5/distribution-metadata@1.75.0") - .rejects(new Error("404 - @sapui5/distribution-metadata")); - } else { - extractStub - .withArgs("@sapui5/distribution-metadata@1.75.0") - .resolves(); - sinon.stub(Installer.prototype, "readJson") - .callThrough() - .withArgs(path.join(fakeBaseDir, - "homedir", ".ui5", "framework", "packages", - "@sapui5", "distribution-metadata", "1.75.0", - "metadata.json")) - .resolves({ - libraries: { - "sap.ui.lib1": { - npmPackageName: "@sapui5/sap.ui.lib1", - version: "1.75.1", - dependencies: [], - optionalDependencies: [] - }, - "sap.ui.lib2": { - npmPackageName: "@sapui5/sap.ui.lib2", - version: "1.75.2", - dependencies: [ - "sap.ui.lib3" - ], - optionalDependencies: [] - }, - "sap.ui.lib3": { - npmPackageName: "@sapui5/sap.ui.lib3", - version: "1.75.3", - dependencies: [], - optionalDependencies: [ - "sap.ui.lib4" - ] - }, - "sap.ui.lib4": { - npmPackageName: "@openui5/sap.ui.lib4", - version: "1.75.4", - dependencies: [ - "sap.ui.lib1" - ], - optionalDependencies: [] - } - } - }); - } - } else if (frameworkName === "OpenUI5") { - if (failExtract) { - extractStub - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .rejects(new Error("404 - @openui5/sap.ui.lib1")) - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .rejects(new Error("404 - @openui5/sap.ui.lib4")); - } else { - extractStub - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .resolves() - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .resolves(); - } - if (failMetadata) { - manifestStub - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib1@1.75.0")) - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib4@1.75.0")); - } else { - manifestStub - .withArgs("@openui5/sap.ui.lib1@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib1", - version: "1.75.0", - dependencies: {} - }) - .withArgs("@openui5/sap.ui.lib4@1.75.0") - .resolves({ - name: "@openui5/sap.ui.lib4", - version: "1.75.0" - }); - } - } - - await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); - }, {message: expectedErrorMessage}); - }); -} - -defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when metadata request fails", { - frameworkName: "SAPUI5", - failMetadata: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + -`404 - @sapui5/distribution-metadata -Failed to resolve library sap.ui.lib4: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + -`404 - @sapui5/distribution-metadata` // TODO: should only be returned once? -}); -defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when package extraction fails", { - frameworkName: "SAPUI5", - failExtract: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/sap.ui.lib1@1.75.1: ` + -`404 - @sapui5/sap.ui.lib1 -Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.4: ` + -`404 - @openui5/sap.ui.lib4` -}); -defineErrorTest( - "SAPUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { - frameworkName: "SAPUI5", - failMetadata: true, - failExtract: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + -`404 - @sapui5/distribution-metadata -Failed to resolve library sap.ui.lib4: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` + -`404 - @sapui5/distribution-metadata` - }); - - -defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when metadata request fails", { - frameworkName: "OpenUI5", - failMetadata: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 -Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` -}); -defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when package extraction fails", { - frameworkName: "OpenUI5", - failExtract: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to extract package @openui5/sap.ui.lib1@1.75.0: ` + -`404 - @openui5/sap.ui.lib1 -Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.0: ` + -`404 - @openui5/sap.ui.lib4` -}); -defineErrorTest( - "OpenUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { - frameworkName: "OpenUI5", - failMetadata: true, - failExtract: true, - expectedErrorMessage: `Resolution of framework libraries failed with errors: -Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 -Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` - }); - -test.serial("ui5Framework translator should not be called when no framework configuration is given", async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "1.1", - type: "application", - metadata: { - name: "test-project" - } - }); - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); - - const ui5FrameworkMock = sinon.mock(ui5Framework); - ui5FrameworkMock.expects("generateDependencyTree").never(); - - const expectedTree = projectPreprocessorTree; - - const tree = await normalizer.generateProjectTree(); - - t.deepEqual(tree, expectedTree, "Returned tree should be correct"); - ui5FrameworkMock.verify(); -}); - -test.serial("ui5Framework translator should not try to install anything when no library is referenced", async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "1.1", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: "SAPUI5", - version: "1.75.0" - } - }); - const frameworkTree = Object.assign({}, projectPreprocessorTree, { - _transparentProject: true - }); - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree") - .withArgs(translatorTree).resolves(projectPreprocessorTree) - .withArgs(frameworkTree).resolves(frameworkTree); - - const extractStub = sinon.stub(pacote, "extract"); - const manifestStub = sinon.stub(pacote, "manifest"); - - await normalizer.generateProjectTree(); - - t.is(extractStub.callCount, 0, "No package should be extracted"); - t.is(manifestStub.callCount, 0, "No manifest should be requested"); -}); - -test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "1.1", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: "SAPUI5" - } - }); - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); - - await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); - }, {message: `framework.version is not defined for project test-id`}); -}); - -test.serial("ui5Framework translator should throw an error when framework name is not supported", async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "1.1", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: "UI5" - } - }); - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); - - await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); - }, {message: `Unknown framework.name "UI5" for project test-id. Must be "OpenUI5" or "SAPUI5"`}); -}); - -test.serial( - "SAPUI5: ui5Framework translator should throw error when using a library that is not part of the dist metadata", - async (t) => { - const translatorTree = { - id: "test-id", - version: "1.2.3", - path: path.join(fakeBaseDir, "application-project"), - dependencies: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "2.0", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: "SAPUI5", - version: "1.75.0", - libraries: [ - {name: "sap.ui.lib1"}, - {name: "does.not.exist"}, - {name: "sap.ui.lib4"}, - ] - } - }); - - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); - - sinon.stub(pacote, "extract").resolves(); - - sinon.stub(Installer.prototype, "readJson") - .callThrough() - .withArgs(path.join(fakeBaseDir, - "homedir", ".ui5", "framework", "packages", - "@sapui5", "distribution-metadata", "1.75.0", - "metadata.json")) - .resolves({ - libraries: { - "sap.ui.lib1": { - npmPackageName: "@sapui5/sap.ui.lib1", - version: "1.75.1", - dependencies: [], - optionalDependencies: [] - }, - "sap.ui.lib4": { - npmPackageName: "@openui5/sap.ui.lib4", - version: "1.75.4", - dependencies: [ - "sap.ui.lib1" - ], - optionalDependencies: [] - } - } - }); - - await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); - }, { - message: `Resolution of framework libraries failed with errors: -Failed to resolve library does.not.exist: Could not find library "does.not.exist"`}); - }); - -// TODO test: Should not download packages again in case they are already installed - -// TODO test: Should ignore framework libraries in dependencies diff --git a/test/lib/translators/ui5Framework.js b/test/lib/translators/ui5Framework.js deleted file mode 100644 index 177646dbb..000000000 --- a/test/lib/translators/ui5Framework.js +++ /dev/null @@ -1,957 +0,0 @@ -const test = require("ava"); -const sinon = require("sinon"); -const mock = require("mock-require"); - -let ui5Framework; -let utils; - -test.beforeEach((t) => { - t.context.Sapui5ResolverStub = sinon.stub(); - t.context.Sapui5ResolverInstallStub = sinon.stub(); - t.context.Sapui5ResolverStub.callsFake(() => { - return { - install: t.context.Sapui5ResolverInstallStub - }; - }); - t.context.Sapui5ResolverResolveVersionStub = sinon.stub(); - t.context.Sapui5ResolverStub.resolveVersion = t.context.Sapui5ResolverResolveVersionStub; - mock("../../../lib/ui5Framework/Sapui5Resolver", t.context.Sapui5ResolverStub); - - t.context.Openui5ResolverStub = sinon.stub(); - mock("../../../lib/ui5Framework/Openui5Resolver", t.context.Openui5ResolverStub); - - ui5Framework = mock.reRequire("../../../lib/translators/ui5Framework"); - utils = ui5Framework._utils; -}); - -test.afterEach.always((t) => { - sinon.restore(); - mock.stopAll(); -}); - -test.serial("generateDependencyTree", async (t) => { - const tree = { - specVersion: "2.0", - id: "test1", - version: "1.0.0", - path: "/test-project/", - framework: { - name: "SAPUI5", - version: "1.75.0" - } - }; - - const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; - const libraryMetadata = {fake: "metadata"}; - - const getFrameworkLibrariesFromTreeStub = sinon.stub(utils, "getFrameworkLibrariesFromTree") - .returns(referencedLibraries); - - t.context.Sapui5ResolverInstallStub.resolves({libraryMetadata}); - - const getProjectStub = sinon.stub(); - getProjectStub.onFirstCall().returns({fake: "metadata-project-1"}); - getProjectStub.onSecondCall().returns({fake: "metadata-project-2"}); - getProjectStub.onThirdCall().returns({fake: "metadata-project-3"}); - const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor") - .callsFake(() => { - return { - getProject: getProjectStub - }; - }); - - const ui5FrameworkTree = await ui5Framework.generateDependencyTree(tree); - - t.is(getFrameworkLibrariesFromTreeStub.callCount, 1, "getFrameworkLibrariesFromTree should be called once"); - t.deepEqual(getFrameworkLibrariesFromTreeStub.getCall(0).args, [tree], - "getFrameworkLibrariesFromTree should be called with expected args"); - - t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); - t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{cwd: tree.path, version: tree.framework.version}], - "Sapui5Resolver#constructor should be called with expected args"); - - t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once"); - t.deepEqual(t.context.Sapui5ResolverInstallStub.getCall(0).args, [referencedLibraries], - "Sapui5Resolver#install should be called with expected args"); - - t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); - t.deepEqual(ProjectProcessorStub.getCall(0).args, [{libraryMetadata}], - "ProjectProcessor#constructor should be called with expected args"); - - t.is(getProjectStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); - t.deepEqual(getProjectStub.getCall(0).args, [referencedLibraries[0]], - "Sapui5Resolver#getProject should be called with expected args (call 1)"); - t.deepEqual(getProjectStub.getCall(1).args, [referencedLibraries[1]], - "Sapui5Resolver#getProject should be called with expected args (call 2)"); - t.deepEqual(getProjectStub.getCall(2).args, [referencedLibraries[2]], - "Sapui5Resolver#getProject should be called with expected args (call 3)"); - - t.deepEqual(ui5FrameworkTree, { - specVersion: "2.0", // specVersion must not be lost to prevent config re-loading in projectPreprocessor - id: "test1", - version: "1.0.0", - path: "/test-project/", - framework: { - name: "SAPUI5", - version: "1.75.0" - }, - dependencies: [ - {fake: "metadata-project-1"}, - {fake: "metadata-project-2"}, - {fake: "metadata-project-3"} - ], - _transparentProject: true - }); -}); - -test.serial("generateDependencyTree (with versionOverride)", async (t) => { - const tree = { - id: "test1", - version: "1.0.0", - path: "/test-project/", - framework: { - name: "SAPUI5", - version: "1.75.0" - } - }; - - const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; - const libraryMetadata = {fake: "metadata"}; - - sinon.stub(utils, "getFrameworkLibrariesFromTree").returns(referencedLibraries); - - t.context.Sapui5ResolverInstallStub.resolves({libraryMetadata}); - - t.context.Sapui5ResolverResolveVersionStub.resolves("1.99.9"); - - const getProjectStub = sinon.stub(); - getProjectStub.onFirstCall().returns({fake: "metadata-project-1"}); - getProjectStub.onSecondCall().returns({fake: "metadata-project-2"}); - getProjectStub.onThirdCall().returns({fake: "metadata-project-3"}); - sinon.stub(utils, "ProjectProcessor") - .callsFake(() => { - return { - getProject: getProjectStub - }; - }); - - await ui5Framework.generateDependencyTree(tree, {versionOverride: "1.99"}); - - t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); - t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{cwd: tree.path, version: "1.99.9"}], - "Sapui5Resolver#constructor should be called with expected args"); -}); - -test.serial("generateDependencyTree should throw error when no framework version is provided in tree", async (t) => { - const tree = { - id: "test-id", - version: "1.2.3", - path: "/test-project/", - metadata: { - name: "test-name" - }, - framework: { - name: "SAPUI5" - } - }; - - await t.throwsAsync(async () => { - await ui5Framework.generateDependencyTree(tree); - }, {message: "framework.version is not defined for project test-id"}); - - await t.throwsAsync(async () => { - await ui5Framework.generateDependencyTree(tree, { - versionOverride: "1.75.0" - }); - }, {message: "framework.version is not defined for project test-id"}); -}); - -test.serial("generateDependencyTree should skip framework project without version", async (t) => { - const tree = { - id: "@sapui5/project", - version: "1.2.3", - path: "/sapui5-project/", - metadata: { - name: "sapui5.project" - }, - framework: { - name: "SAPUI5" - } - }; - - const result = await ui5Framework.generateDependencyTree(tree); - t.is(result, null, "Framework projects should be skipped"); -}); - -test.serial("generateDependencyTree should skip framework project with version and framework config", async (t) => { - const tree = { - id: "@sapui5/project", - version: "1.2.3", - path: "/sapui5-project/", - metadata: { - name: "sapui5.project" - }, - framework: { - name: "SAPUI5", - version: "1.2.3", - libraries: [ - { - name: "lib1" - } - ] - } - }; - - const result = await ui5Framework.generateDependencyTree(tree); - t.is(result, null, "Framework projects should be skipped"); -}); - -test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => { - const tree = { - id: "test-id", - version: "1.2.3", - path: "/test-project/", - metadata: { - name: "test-name" - }, - dependencies: [] - }; - const ui5FrameworkTree = await ui5Framework.generateDependencyTree(tree); - - t.is(ui5FrameworkTree, null, "No framework tree should be returned"); -}); -test.serial("utils.isFrameworkProject", (t) => { - t.true(utils.isFrameworkProject({id: "@sapui5/foo"}), "@sapui5/foo"); - t.true(utils.isFrameworkProject({id: "@openui5/foo"}), "@openui5/foo"); - t.false(utils.isFrameworkProject({id: "sapui5"}), "sapui5"); - t.false(utils.isFrameworkProject({id: "openui5"}), "openui5"); -}); -test.serial("utils.shouldIncludeDependency", (t) => { - // root project dependency should always be included - t.true(utils.shouldIncludeDependency({}, true)); - t.true(utils.shouldIncludeDependency({optional: true}, true)); - t.true(utils.shouldIncludeDependency({optional: false}, true)); - t.true(utils.shouldIncludeDependency({optional: null}, true)); - t.true(utils.shouldIncludeDependency({optional: "abc"}, true)); - t.true(utils.shouldIncludeDependency({development: true}, true)); - t.true(utils.shouldIncludeDependency({development: false}, true)); - t.true(utils.shouldIncludeDependency({development: null}, true)); - t.true(utils.shouldIncludeDependency({development: "abc"}, true)); - t.true(utils.shouldIncludeDependency({foo: true}, true)); - - t.true(utils.shouldIncludeDependency({}, false)); - t.false(utils.shouldIncludeDependency({optional: true}, false)); - t.true(utils.shouldIncludeDependency({optional: false}, false)); - t.true(utils.shouldIncludeDependency({optional: null}, false)); - t.true(utils.shouldIncludeDependency({optional: "abc"}, false)); - t.false(utils.shouldIncludeDependency({development: true}, false)); - t.true(utils.shouldIncludeDependency({development: false}, false)); - t.true(utils.shouldIncludeDependency({development: null}, false)); - t.true(utils.shouldIncludeDependency({development: "abc"}, false)); - t.true(utils.shouldIncludeDependency({foo: true}, false)); - - // Having both optional and development should not be the case, but that should be validated beforehand - t.true(utils.shouldIncludeDependency({optional: true, development: true}, true)); - t.false(utils.shouldIncludeDependency({optional: true, development: true}, false)); -}); -test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies", (t) => { - const tree = { - id: "test", - metadata: { - name: "test" - }, - framework: { - libraries: [] - }, - dependencies: [] - }; - const ui5Dependencies = utils.getFrameworkLibrariesFromTree(tree); - t.deepEqual(ui5Dependencies, []); -}); - -test.serial("utils.getFrameworkLibrariesFromTree: Framework project", (t) => { - const tree = { - id: "@sapui5/project", - metadata: { - name: "project" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [ - { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - framework: { - libraries: [ - { - name: "lib2" - } - ] - } - } - ] - }; - const ui5Dependencies = utils.getFrameworkLibrariesFromTree(tree); - t.deepEqual(ui5Dependencies, []); // Framework projects should be skipped -}); - -test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", (t) => { - const tree = { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - }, - { - name: "lib2", - optional: true - }, - { - name: "lib6", - development: true - } - ] - }, - dependencies: [ - { - id: "test2", - specVersion: "2.0", - metadata: { - name: "test2" - }, - framework: { - libraries: [ - { - name: "lib3" - }, - { - name: "lib4", - optional: true - } - ] - }, - dependencies: [ - { - id: "test3", - specVersion: "2.0", - metadata: { - name: "test3" - }, - framework: { - libraries: [ - { - name: "lib5" - }, - { - name: "lib7", - development: true - } - ] - }, - dependencies: [] - } - ] - }, - { - id: "@sapui5/lib8", - specVersion: "2.0", - metadata: { - name: "lib8" - }, - framework: { - libraries: [ - { - name: "should.be.ignored" - } - ] - }, - dependencies: [] - }, - { - id: "@openui5/lib9", - specVersion: "1.1", - metadata: { - name: "lib9" - }, - dependencies: [] - }, - { - id: "@foo/library", - specVersion: "1.1", - metadata: { - name: "foo.library" - }, - framework: { - libraries: [ - { - name: "should.also.be.ignored" - } - ] - }, - dependencies: [] - } - ] - }; - const ui5Dependencies = utils.getFrameworkLibrariesFromTree(tree); - t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]); -}); - -test.serial("utils.mergeTrees", (t) => { - const projectTree = { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - }, - { - name: "lib2", - optional: true - }, - { - name: "lib6", - development: true - } - ] - }, - dependencies: [ - { - id: "test2", - specVersion: "2.0", - metadata: { - name: "test2" - }, - framework: { - libraries: [ - { - name: "lib3" - }, - { - name: "lib4", - optional: true - } - ] - }, - dependencies: [ - { - id: "test3", - specVersion: "2.0", - metadata: { - name: "test3" - }, - framework: { - libraries: [ - { - name: "lib5" - }, - { - name: "lib7", - development: true - } - ] - }, - dependencies: [] - } - ] - }, - { - id: "@openui5/lib9", - specVersion: "1.1", - metadata: { - name: "lib9" - }, - dependencies: [] - }, - { - id: "@foo/library", - specVersion: "1.1", - metadata: { - name: "foo.library" - }, - framework: { - libraries: [ - { - name: "should.also.be.ignored" - } - ] - }, - dependencies: [] - } - ] - }; - const frameworkTree = { - metadata: { - name: "test1" - }, - _transparentProject: true, - dependencies: [ - { - metadata: { - name: "lib1" - }, - dependencies: [] - }, - { - metadata: { - name: "lib2" - }, - dependencies: [] - }, - { - metadata: { - name: "lib3" - }, - dependencies: [ - { - metadata: { - name: "lib4" - }, - dependencies: [ - { - metadata: { - name: "lib6" - }, - dependencies: [] - } - ] - } - ] - }, - { - metadata: { - name: "lib4" - }, - dependencies: [ - { - metadata: { - name: "lib6" - }, - dependencies: [] - } - ] - }, - { - metadata: { - name: "lib5" - }, - dependencies: [] - }, - { - metadata: { - name: "lib6" - }, - dependencies: [] - }, - { - metadata: { - name: "lib7" - }, - dependencies: [] - } - ] - }; - const mergedProjectTree = ui5Framework.mergeTrees(projectTree, frameworkTree); - t.deepEqual(mergedProjectTree, { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - }, - { - name: "lib2", - optional: true - }, - { - name: "lib6", - development: true - } - ] - }, - dependencies: [ - { - id: "test2", - specVersion: "2.0", - metadata: { - name: "test2" - }, - framework: { - libraries: [ - { - name: "lib3" - }, - { - name: "lib4", - optional: true - } - ] - }, - dependencies: [ - { - id: "test3", - specVersion: "2.0", - metadata: { - name: "test3" - }, - framework: { - libraries: [ - { - name: "lib5" - }, - { - name: "lib7", - development: true - } - ] - }, - dependencies: [ - { - metadata: { - name: "lib5" - }, - dependencies: [] - } - ] - }, - { - metadata: { - name: "lib3" - }, - dependencies: [ - { - metadata: { - name: "lib4" - }, - dependencies: [ - { - metadata: { - name: "lib6" - }, - dependencies: [] - } - ] - } - ] - }, - { - metadata: { - name: "lib4" - }, - dependencies: [ - { - metadata: { - name: "lib6" - }, - dependencies: [] - } - ] - } - ] - }, - { - id: "@foo/library", - specVersion: "1.1", - metadata: { - name: "foo.library" - }, - framework: { - libraries: [ - { - name: "should.also.be.ignored" - } - ] - }, - dependencies: [] - }, - { - metadata: { - name: "lib1" - }, - dependencies: [] - }, - { - metadata: { - name: "lib2" - }, - dependencies: [] - }, - { - metadata: { - name: "lib6" - }, - dependencies: [] - }, - ] - }); -}); - -test.serial("utils.mergeTrees: Missing framework library", (t) => { - const projectTree = { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [] - }; - const frameworkTree = { - metadata: { - name: "test1" - }, - _transparentProject: true, - dependencies: [ - { - metadata: { - name: "lib2" - }, - dependencies: [] - } - ] - }; - const error = t.throws(() => { - ui5Framework.mergeTrees(projectTree, frameworkTree); - }); - t.is(error.message, `Missing framework library lib1 required by project test1`); -}); - -test.serial("utils.mergeTrees: Do not abort merge if project has already been processed", (t) => { - const projectTree = { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [ - { - id: "test2", - specVersion: "2.0", - metadata: { - name: "test2" - }, - dependencies: [ - { - id: "test4", - specVersion: "2.0", - metadata: { - name: "test4" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [{ - id: "test2", - deduped: true, - dependencies: [] - }] - } - ] - }, - { - id: "test3", - specVersion: "2.0", - metadata: { - name: "test3" - }, - dependencies: [ - { - id: "test4", - specVersion: "2.0", - metadata: { - name: "test4" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [] - }, - { - id: "test5", - specVersion: "2.0", - metadata: { - name: "test5" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [] - } - ] - } - ] - }; - const frameworkTree = { - metadata: { - name: "test1" - }, - _transparentProject: true, - dependencies: [ - { - metadata: { - name: "lib1" - }, - dependencies: [] - } - ] - }; - const mergedProjectTree = ui5Framework.mergeTrees(projectTree, frameworkTree); - t.deepEqual(mergedProjectTree, { - id: "test1", - specVersion: "2.0", - metadata: { - name: "test1" - }, - _isRoot: true, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [ - { - id: "test2", - specVersion: "2.0", - metadata: { - name: "test2" - }, - dependencies: [ - { - id: "test4", - specVersion: "2.0", - metadata: { - name: "test4" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [ - { - id: "test2", - deduped: true, - dependencies: [] - }, { - metadata: { - name: "lib1" - }, - dependencies: [] - } - ] - } - ] - }, - { - id: "test3", - specVersion: "2.0", - metadata: { - name: "test3" - }, - dependencies: [ - { - id: "test4", - specVersion: "2.0", - metadata: { - name: "test4" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [] - }, - { - id: "test5", - specVersion: "2.0", - metadata: { - name: "test5" - }, - framework: { - libraries: [ - { - name: "lib1" - } - ] - }, - dependencies: [ - { - metadata: { - name: "lib1" - }, - dependencies: [] - } - ] - } - ] - }, - { - metadata: { - name: "lib1" - }, - dependencies: [] - } - ] - }); -}); - -// TODO test: utils.getAllNodesOfTree - -// TODO test: ProjectProcessor From feea1711149dfa79088d96669dd054e640578e53 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 6 May 2022 10:17:46 +0200 Subject: [PATCH 51/99] Enhance archive integration tests --- lib/buildHelpers/createArchiveMetadata.js | 3 - .../application.a/.ui5/archive-metadata.json | 4 +- .../library.e/.ui5/archive-metadata.json | 43 +++++++++++ test/fixtures/archives/library.e/package.json | 11 +++ .../library.e/resources/library/e/.library | 11 +++ .../library.e/resources/library/e/some.js | 4 + .../test-resources/library/e/Test.html | 0 test/lib/buildHelpers/archive.integration.js | 75 +++++++++++++------ .../lib/buildHelpers/createArchiveMetadata.js | 4 +- 9 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/archives/library.e/.ui5/archive-metadata.json create mode 100644 test/fixtures/archives/library.e/package.json create mode 100644 test/fixtures/archives/library.e/resources/library/e/.library create mode 100644 test/fixtures/archives/library.e/resources/library/e/some.js create mode 100644 test/fixtures/archives/library.e/test-resources/library/e/Test.html diff --git a/lib/buildHelpers/createArchiveMetadata.js b/lib/buildHelpers/createArchiveMetadata.js index 3b657089d..82c113b3a 100644 --- a/lib/buildHelpers/createArchiveMetadata.js +++ b/lib/buildHelpers/createArchiveMetadata.js @@ -14,9 +14,6 @@ module.exports = async function(project, buildConfig) { break; case "library": case "theme-library": - pathMapping.src = `resources/${project.getNamespace()}`; - pathMapping.test = `test-resources/${project.getNamespace()}`; - break; case "legacy-library": pathMapping.src = `resources`; pathMapping.test = `test-resources`; diff --git a/test/fixtures/archives/application.a/.ui5/archive-metadata.json b/test/fixtures/archives/application.a/.ui5/archive-metadata.json index a644c0c6f..1a575cafe 100644 --- a/test/fixtures/archives/application.a/.ui5/archive-metadata.json +++ b/test/fixtures/archives/application.a/.ui5/archive-metadata.json @@ -19,7 +19,7 @@ "includedTasks": [], "excludedTasks": [] }, - "id": "id1", + "id": "application.a", "version": "0.2.0", "namespace": "id1", "tags": { @@ -33,7 +33,7 @@ } }, "resources": { - "configuration": { + "configuration": { "paths": { "webapp": "resources/id1" } diff --git a/test/fixtures/archives/library.e/.ui5/archive-metadata.json b/test/fixtures/archives/library.e/.ui5/archive-metadata.json new file mode 100644 index 000000000..00793ce4d --- /dev/null +++ b/test/fixtures/archives/library.e/.ui5/archive-metadata.json @@ -0,0 +1,43 @@ +{ + "specVersion": "2.3", + "type": "library", + "metadata": { + "name": "library.e" + }, + "customConfiguration": { + "_archive": { + "archiveSpecVersion": "0.1", + "timestamp": "2022-05-06T09:54:29.051Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "library.e", + "version": "1.0.0", + "namespace": "library/e", + "tags": { + "/resources/library/e/some.js": { + "ui5:HasDebugVariant": true + }, + "/resources/library/e/some-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } + }, + "resources": { + "configuration": { + "paths": { + "src": "resources", + "test": "test-resources", + } + } + } +} diff --git a/test/fixtures/archives/library.e/package.json b/test/fixtures/archives/library.e/package.json new file mode 100644 index 000000000..9ce874ff5 --- /dev/null +++ b/test/fixtures/archives/library.e/package.json @@ -0,0 +1,11 @@ +{ + "name": "library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/archives/library.e/resources/library/e/.library b/test/fixtures/archives/library.e/resources/library/e/.library new file mode 100644 index 000000000..c1f37d772 --- /dev/null +++ b/test/fixtures/archives/library.e/resources/library/e/.library @@ -0,0 +1,11 @@ + + + + library.e + SAP SE + Some fancy copyright + ${version} + + Library E + + diff --git a/test/fixtures/archives/library.e/resources/library/e/some.js b/test/fixtures/archives/library.e/resources/library/e/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/archives/library.e/resources/library/e/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/test/fixtures/archives/library.e/test-resources/library/e/Test.html b/test/fixtures/archives/library.e/test-resources/library/e/Test.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/lib/buildHelpers/archive.integration.js b/test/lib/buildHelpers/archive.integration.js index f162fc803..dfdd515fe 100644 --- a/test/lib/buildHelpers/archive.integration.js +++ b/test/lib/buildHelpers/archive.integration.js @@ -1,15 +1,12 @@ const test = require("ava"); -const sinon = require("sinon"); -const mock = require("mock-require"); const path = require("path"); -const logger = require("@ui5/logger"); const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMetadata"); const Module = require("../../../lib/graph/Module"); const Specification = require("../../../lib/specifications/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const archiveApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "archives", "application.a"); -const basicProjectInput = { +const applicationAConfig = { id: "application.a.id", version: "1.0.0", modulePath: applicationAPath, @@ -20,6 +17,19 @@ const basicProjectInput = { metadata: {name: "application.a"} } }; +const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); +const archiveLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "archives", "library.e"); +const libraryEConfig = { + id: "library.e.id", + version: "1.0.0", + modulePath: libraryEPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: {name: "library.e"} + } +}; const buildConfig = { selfContained: false, @@ -28,28 +38,16 @@ const buildConfig = { excludedTasks: [] }; -test.beforeEach((t) => { - t.context.log = { - warn: sinon.stub() - }; - sinon.stub(logger, "getLogger").callThrough() - .withArgs("buildHelpers:composeProjectList").returns(t.context.log); - t.context.composeProjectList = mock.reRequire("../../../lib/buildHelpers/composeProjectList"); -}); - -test.afterEach.always((t) => { - sinon.restore(); - mock.stopAll(); -}); +// Note: The actual archive-metadata.json files in the fixtures are never used in these tests -test("Create archive from project and compare to fixture", async (t) => { - const project = await Specification.create(basicProjectInput); +test("Create archive from application project archive", async (t) => { + const project = await Specification.create(applicationAConfig); project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); const metadata = await createArchiveMetadata(project, buildConfig); const m = new Module({ id: "archive-application.a.id", - version: "1.0.0", + version: "2.0.0", modulePath: archiveApplicationAPath, configuration: metadata }); @@ -57,8 +55,41 @@ test("Create archive from project and compare to fixture", async (t) => { const {project: archiveProject} = await m.getSpecifications(); t.truthy(archiveProject, "Module was able to create project from archive metadata"); t.is(archiveProject.getName(), project.getName(), "Archive project has correct name"); - t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct name"); - t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct name"); + t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); t.is(archiveProject.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, "Archive project has correct tag"); + t.is(archiveProject.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const resources = await archiveProject.getReader().byGlob("**/test.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/id1/test.js", + "Resource has expected path"); +}); + +test("Create archive from library project archive", async (t) => { + const project = await Specification.create(libraryEConfig); + project.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); + + const metadata = await createArchiveMetadata(project, buildConfig); + const m = new Module({ + id: "archive-library.e.id", + version: "2.0.0", + modulePath: archiveLibraryEPath, + configuration: metadata + }); + + const {project: archiveProject} = await m.getSpecifications(); + t.truthy(archiveProject, "Module was able to create project from archive metadata"); + t.is(archiveProject.getName(), project.getName(), "Archive project has correct name"); + t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); + t.is(archiveProject.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); + t.is(archiveProject.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const resources = await archiveProject.getReader().byGlob("**/some.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/library/e/some.js", + "Resource has expected path"); }); diff --git a/test/lib/buildHelpers/createArchiveMetadata.js b/test/lib/buildHelpers/createArchiveMetadata.js index 6efdcab24..d297da4d7 100644 --- a/test/lib/buildHelpers/createArchiveMetadata.js +++ b/test/lib/buildHelpers/createArchiveMetadata.js @@ -118,8 +118,8 @@ test("Create library archive from project", async (t) => { resources: { configuration: { paths: { - src: "resources/library/d", - test: "test-resources/library/d", + src: "resources", + test: "test-resources", }, }, } From 9c84e637a7511805377c4932f0d636fe1f10e111 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 13 May 2022 21:48:11 +0200 Subject: [PATCH 52/99] Take over tests for modules moved from ui5-builder --- lib/buildHelpers/BuildContext.js | 12 - lib/buildHelpers/ProjectBuildContext.js | 10 +- lib/builder.js | 6 + test/lib/buildHelpers/BuildContext.js | 104 +++++++ test/lib/buildHelpers/ProjectBuildContext.js | 219 +++++++++++++++ test/lib/buildHelpers/composeTaskList.js | 258 ++++++++++++++++++ ...s => createArchiveMetadata.integration.js} | 0 .../graph/helpers/ui5Framework.integration.js | 2 +- 8 files changed, 596 insertions(+), 15 deletions(-) create mode 100644 test/lib/buildHelpers/BuildContext.js create mode 100644 test/lib/buildHelpers/ProjectBuildContext.js create mode 100644 test/lib/buildHelpers/composeTaskList.js rename test/lib/buildHelpers/{archive.integration.js => createArchiveMetadata.integration.js} (100%) diff --git a/lib/buildHelpers/BuildContext.js b/lib/buildHelpers/BuildContext.js index 795626c60..396bc1ca0 100644 --- a/lib/buildHelpers/BuildContext.js +++ b/lib/buildHelpers/BuildContext.js @@ -1,12 +1,5 @@ const ProjectBuildContext = require("./ProjectBuildContext"); -// Note: When adding standard tags, always update the public documentation in TaskUtil -// (Type "module:@ui5/builder.tasks.TaskUtil~StandardBuildTags") -const GLOBAL_TAGS = Object.freeze({ - IsDebugVariant: "ui5:IsDebugVariant", - HasDebugVariant: "ui5:HasDebugVariant", -}); - /** * Context of a build process * @@ -47,11 +40,6 @@ class BuildContext { })); } - getResourceTagCollection() { - return this._resourceTagCollection; - } - - /** * Retrieve a single project from the dependency graph * diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js index 1bb02ab95..74fc9c231 100644 --- a/lib/buildHelpers/ProjectBuildContext.js +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -17,8 +17,14 @@ const STANDARD_TAGS = Object.freeze({ */ class ProjectBuildContext { constructor({buildContext, log, project}) { - if (!buildContext || !log || !project) { - throw new Error(`One or more mandatory parameters are missing`); + if (!buildContext) { + throw new Error(`Missing parameter 'buildContext'`); + } + if (!log) { + throw new Error(`Missing parameter 'log'`); + } + if (!project) { + throw new Error(`Missing parameter 'project'`); } this._buildContext = buildContext; this._project = project; diff --git a/lib/builder.js b/lib/builder.js index f197421c0..bad2950f9 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -101,6 +101,12 @@ module.exports = async function({ selfContained = false, cssVariables = false, jsdoc = false, archive = false, includedTasks = [], excludedTasks = [], }) { + if (!graph) { + throw new Error(`Missing parameter 'graph'`); + } + if (!destPath) { + throw new Error(`Missing parameter 'destPath'`); + } if (graph.isSealed()) { throw new Error( `Can not build project graph with root node ${this._rootProjectName}: Graph has already been sealed`); diff --git a/test/lib/buildHelpers/BuildContext.js b/test/lib/buildHelpers/BuildContext.js new file mode 100644 index 000000000..e3b04525d --- /dev/null +++ b/test/lib/buildHelpers/BuildContext.js @@ -0,0 +1,104 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +const BuildContext = require("../../../lib/buildHelpers/BuildContext"); + +test("Missing parameters", (t) => { + const error = t.throws(() => { + new BuildContext({}); + }); + + t.is(error.message, `Missing parameter 'graph'`, "Threw with expected error message"); +}); + +test("getRootProject", (t) => { + const buildContext = new BuildContext({ + graph: { + getRoot: () => "pony" + } + }); + + t.is(buildContext.getRootProject(), "pony", "Returned correct value"); +}); +test("getProject", (t) => { + const getProjectStub = sinon.stub().returns("pony"); + const buildContext = new BuildContext({ + graph: { + getProject: getProjectStub + } + }); + + t.is(buildContext.getProject("pony project"), "pony", "Returned correct value"); + t.is(getProjectStub.getCall(0).args[0], "pony project", "getProject got called with correct argument"); +}); + +test("getBuildOption", (t) => { + const buildContext = new BuildContext({ + graph: "graph", + options: { + a: true, + b: "Pony", + c: 235, + d: { + d1: "Bee" + } + } + }); + + t.is(buildContext.getOption("a"), true, "Returned 'boolean' value is correct"); + t.is(buildContext.getOption("b"), "Pony", "Returned 'String' value is correct"); + t.is(buildContext.getOption("c"), 235, "Returned 'Number' value is correct"); + t.deepEqual(buildContext.getOption("d"), {d1: "Bee"}, "Returned 'object' value is correct"); +}); + +test.serial("createProjectContext", (t) => { + class DummyProjectContext { + constructor({buildContext, project, log}) { + t.is(buildContext, testBuildContext, "Correct buildContext parameter"); + t.is(project, "project", "Correct project parameter"); + t.is(log, "log", "Correct log parameter"); + } + } + mock("../../../lib/buildHelpers/ProjectBuildContext", DummyProjectContext); + + const BuildContext = mock.reRequire("../../../lib/buildHelpers/BuildContext"); + const testBuildContext = new BuildContext({ + graph: "graph" + }); + + const projectContext = testBuildContext.createProjectContext({ + project: "project", + log: "log" + }); + + t.true(projectContext instanceof DummyProjectContext, + "Project context is an instance of DummyProjectContext"); + t.is(testBuildContext._projectBuildContexts[0], projectContext, + "BuildContext stored correct ProjectBuildContext"); +}); + +test("executeCleanupTasks", async (t) => { + const buildContext = new BuildContext({ + graph: "graph" + }); + + const executeCleanupTasks = sinon.stub().resolves(); + + buildContext._projectBuildContexts.push({ + executeCleanupTasks + }); + buildContext._projectBuildContexts.push({ + executeCleanupTasks + }); + + await buildContext.executeCleanupTasks(); + + t.is(executeCleanupTasks.callCount, 2, + "Project context executeCleanupTasks got called twice"); +}); diff --git a/test/lib/buildHelpers/ProjectBuildContext.js b/test/lib/buildHelpers/ProjectBuildContext.js new file mode 100644 index 000000000..f1f8ce542 --- /dev/null +++ b/test/lib/buildHelpers/ProjectBuildContext.js @@ -0,0 +1,219 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; + +test.beforeEach((t) => { + t.context.resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["me:MyTag"] + }); +}); +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +const ProjectBuildContext = require("../../../lib/buildHelpers/ProjectBuildContext"); + +test("Missing parameters", (t) => { + t.throws(() => { + new ProjectBuildContext({ + project: "project", + log: "log", + }); + }, { + message: `Missing parameter 'buildContext'` + }, "Correct error message"); + + t.throws(() => { + new ProjectBuildContext({ + buildContext: "buildContext", + log: "log", + }); + }, { + message: `Missing parameter 'project'` + }, "Correct error message"); + + t.throws(() => { + new ProjectBuildContext({ + buildContext: "buildContext", + project: "project", + }); + }, { + message: `Missing parameter 'log'` + }, "Correct error message"); +}); + +test("isRootProject: true", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getRootProject: () => "root project" + }, + project: "root project", + log: "log" + }); + + t.true(projectBuildContext.isRootProject(), "Correctly identified root project"); +}); + +test("isRootProject: false", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getRootProject: () => "root project" + }, + project: "not the root project", + log: "log" + }); + + t.false(projectBuildContext.isRootProject(), "Correctly identified non-root project"); +}); + +test("getBuildOption", (t) => { + const getOptionStub = sinon.stub().returns("pony"); + const projectBuildContext = new ProjectBuildContext({ + buildContext: { + getOption: getOptionStub + }, + project: "project", + log: "log" + }); + + t.is(projectBuildContext.getOption("option"), "pony", "Returned value is correct"); + t.is(getOptionStub.getCall(0).args[0], "option", "getOption called with correct argument"); +}); + +test("registerCleanupTask", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: "project", + log: "log" + }); + projectBuildContext.registerCleanupTask("my task 1"); + projectBuildContext.registerCleanupTask("my task 2"); + + t.is(projectBuildContext._queues.cleanup[0], "my task 1", "Cleanup task registered"); + t.is(projectBuildContext._queues.cleanup[1], "my task 2", "Cleanup task registered"); +}); + +test("executeCleanupTasks", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: "project", + log: "log" + }); + const task1 = sinon.stub().resolves(); + const task2 = sinon.stub().resolves(); + projectBuildContext.registerCleanupTask(task1); + projectBuildContext.registerCleanupTask(task2); + + projectBuildContext.executeCleanupTasks(); + + t.is(task1.callCount, 1, "Cleanup task 1 got called"); + t.is(task2.callCount, 1, "my task 2", "Cleanup task 2 got called"); +}); + +test("STANDARD_TAGS constant", (t) => { + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: "project", + log: "log" + }); + + t.deepEqual(projectBuildContext.STANDARD_TAGS, { + OmitFromBuildResult: "ui5:OmitFromBuildResult", + IsBundle: "ui5:IsBundle" + }, "Exposes correct STANDARD_TAGS constant"); +}); + +test.serial("getResourceTagCollection", (t) => { + const projectAcceptsTagStub = sinon.stub().returns(false); + projectAcceptsTagStub.withArgs("project-tag").returns(true); + const projectContextAcceptsTagStub = sinon.stub().returns(false); + projectContextAcceptsTagStub.withArgs("project-context-tag").returns(true); + + class DummyResourceTagCollection { + constructor({allowedTags, allowedNamespaces}) { + t.deepEqual(allowedTags, [ + "ui5:OmitFromBuildResult", + "ui5:IsBundle" + ], + "Correct allowedTags parameter supplied"); + + t.deepEqual(allowedNamespaces, [ + "build" + ], + "Correct allowedNamespaces parameter supplied"); + } + acceptsTag(tag) { + // Redirect to stub + return projectContextAcceptsTagStub(tag); + } + } + mock("@ui5/fs", { + ResourceTagCollection: DummyResourceTagCollection + }); + + const ProjectBuildContext = mock.reRequire("../../../lib/buildHelpers/ProjectBuildContext"); + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: "project", + log: "log" + }); + + const fakeProjectCollection = { + acceptsTag: projectAcceptsTagStub + }; + const fakeResource = { + getProject: () => { + return { + getResourceTagCollection: () => fakeProjectCollection + }; + }, + getPath: () => "/resource/path", + hasProject: () => true + }; + const collection1 = projectBuildContext.getResourceTagCollection(fakeResource, "project-tag"); + t.is(collection1, fakeProjectCollection, "Returned tag collection of resource project"); + + const collection2 = projectBuildContext.getResourceTagCollection(fakeResource, "project-context-tag"); + t.true(collection2 instanceof DummyResourceTagCollection, + "Returned tag collection of project build context"); + + t.throws(() => { + projectBuildContext.getResourceTagCollection(fakeResource, "not-accepted-tag"); + }, { + message: `Could not find collection for resource /resource/path and tag not-accepted-tag` + }); +}); + +test("getResourceTagCollection: Assigns project to resource if necessary", (t) => { + const fakeProject = { + getName: () => "project" + }; + const projectBuildContext = new ProjectBuildContext({ + buildContext: {}, + project: fakeProject, + log: { + verbose: () => {} + } + }); + + const setProjectStub = sinon.stub(); + const fakeResource = { + getProject: () => { + return { + getResourceTagCollection: () => { + return { + acceptsTag: () => false + }; + } + }; + }, + getPath: () => "/resource/path", + hasProject: () => false, + setProject: setProjectStub + }; + projectBuildContext.getResourceTagCollection(fakeResource, "build:MyTag"); + t.is(setProjectStub.callCount, 1, "setProject got called once"); + t.is(setProjectStub.getCall(0).args[0], fakeProject, "setProject got called with correct argument"); +}); diff --git a/test/lib/buildHelpers/composeTaskList.js b/test/lib/buildHelpers/composeTaskList.js new file mode 100644 index 000000000..da18856ad --- /dev/null +++ b/test/lib/buildHelpers/composeTaskList.js @@ -0,0 +1,258 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); + +test.beforeEach((t) => { + t.context.log = { + warn: sinon.stub() + }; + sinon.stub(logger, "getLogger").withArgs("buildHelpers:composeTaskList").returns(t.context.log); + + t.context.composeTaskList = mock.reRequire("../../../lib/buildHelpers/composeTaskList"); +}); + +test.afterEach.always(() => { + sinon.restore(); + mock.stopAll(); +}); + +const allTasks = [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateVersionInfo", + "generateManifestBundle", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateThemeDesignerResources", + "generateStandaloneAppBundle", + "generateBundle", + "generateLibraryPreload", + "generateCachebusterInfo", +]; + + +[ + [ + "composeTaskList: archive=false / selfContained=false / jsdoc=false", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: archive=true / selfContained=false / jsdoc=false", { + archive: true, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: archive=false / selfContained=true / jsdoc=false", { + archive: false, + selfContained: true, + jsdoc: false, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateStandaloneAppBundle", + "generateBundle" + ] + ], + [ + "composeTaskList: archive=false / selfContained=false / jsdoc=true", { + archive: false, + selfContained: false, + jsdoc: true, + includedTasks: [], + excludedTasks: [] + }, [ + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "buildThemes", + "generateBundle", + ] + ], + [ + "composeTaskList: includedTasks / excludedTasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["generateResourcesJson", "replaceVersion"], + excludedTasks: ["replaceCopyright", "generateApiIndex"] + }, [ + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: includedTasks=*", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["*"], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "executeJsdocSdkTransformation", + "generateApiIndex", + "generateJsdoc", + "minify", + "buildThemes", + "transformBootstrapHtml", + "generateLibraryManifest", + "generateVersionInfo", + "generateManifestBundle", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateResourcesJson", + "generateThemeDesignerResources", + "generateStandaloneAppBundle", + "generateBundle", + "generateLibraryPreload", + "generateCachebusterInfo", + ] + ], + [ + "composeTaskList: excludedTasks=*", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: ["*"] + }, [] + ], + [ + "composeTaskList: includedTasks with unknown tasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: ["foo", "bar"], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ], (t) => { + const {log} = t.context; + t.is(log.warn.callCount, 2); + t.deepEqual(log.warn.getCall(0).args, [ + "Unable to include task 'foo': Task is unknown" + ]); + t.deepEqual(log.warn.getCall(1).args, [ + "Unable to include task 'bar': Task is unknown" + ]); + } + ], + [ + "composeTaskList: excludedTasks with unknown tasks", { + archive: false, + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: ["foo", "bar"], + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ], (t) => { + const {log} = t.context; + t.is(log.warn.callCount, 2); + t.deepEqual(log.warn.getCall(0).args, [ + "Unable to exclude task 'foo': Task is unknown" + ]); + t.deepEqual(log.warn.getCall(1).args, [ + "Unable to exclude task 'bar': Task is unknown" + ]); + } + ], +].forEach(([testTitle, args, expectedTaskList, assertCb]) => { + test.serial(testTitle, (t) => { + const {composeTaskList, log} = t.context; + const taskList = composeTaskList(allTasks, args); + t.deepEqual(taskList, expectedTaskList); + if (assertCb) { + assertCb(t); + } else { + // When no cb is defined, no logs are expected + t.is(log.warn.callCount, 0); + } + }); +}); diff --git a/test/lib/buildHelpers/archive.integration.js b/test/lib/buildHelpers/createArchiveMetadata.integration.js similarity index 100% rename from test/lib/buildHelpers/archive.integration.js rename to test/lib/buildHelpers/createArchiveMetadata.integration.js diff --git a/test/lib/graph/helpers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js index a311f665a..98b2152ff 100644 --- a/test/lib/graph/helpers/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -371,7 +371,7 @@ function defineErrorTest(testName, { expectedErrorMessage }) { test.serial(testName, async (t) => { - const {ui5Framework, Installer, logInfoSpy} = t.context; + const {ui5Framework, Installer} = t.context; const dependencyTree = { id: "test-id", From 26fe28faa9b5925288b8a2e95e71262b73090686 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 18 May 2022 18:33:13 +0200 Subject: [PATCH 53/99] Specification: Allow spec versions 2.4, 2.5 and 2.6 --- lib/specifications/Specification.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index b1150e681..53fe03eab 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -64,7 +64,8 @@ class Specification { } if (config.specVersion !== "2.0" && config.specVersion !== "2.1" && config.specVersion !== "2.2" && - config.specVersion !== "2.3") { + config.specVersion !== "2.3" && config.specVersion !== "2.4" && + config.specVersion !== "2.5" && config.specVersion !== "2.6") { throw new Error( `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + From 6d2881cefd4b206124bee449a3d192c6c258a1a2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 18 May 2022 18:43:59 +0200 Subject: [PATCH 54/99] Apply changes from https://github.com/SAP/ui5-builder/pull/741 --- lib/buildDefinitions/ApplicationBuilder.js | 11 ++++++++--- lib/buildDefinitions/LibraryBuilder.js | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index eecfafad2..8d9323a2b 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -40,6 +40,10 @@ class ApplicationBuilder extends AbstractBuilder { this.addTask("generateFlexChangesBundle"); this.addTask("generateManifestBundle"); + const bundles = project.getBundles(); + const existingBundleDefinitionNames = + bundles.map(({bundleDefinition}) => bundleDefinition.name).filter(Boolean) || []; + const componentPreloadPaths = project.getComponentPreloadPaths(); const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); const componentPreloadExcludes = project.getComponentPreloadNamespaces(); @@ -48,7 +52,8 @@ class ApplicationBuilder extends AbstractBuilder { options: { paths: componentPreloadPaths, namespaces: componentPreloadNamespaces, - excludes: componentPreloadExcludes + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames } }); } else { @@ -56,7 +61,8 @@ class ApplicationBuilder extends AbstractBuilder { this.addTask("generateComponentPreload", { options: { namespaces: [project.getNamespace()], - excludes: componentPreloadExcludes + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames } }); } @@ -65,7 +71,6 @@ class ApplicationBuilder extends AbstractBuilder { this.addTask("transformBootstrapHtml"); - const bundles = project.getBundles(); if (bundles.length) { this.addTask("generateBundle", {requiresDependencies: true}, async ({workspace, dependencies, taskUtil, options}) => { diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js index a4b755816..43b5985ba 100644 --- a/lib/buildDefinitions/LibraryBuilder.js +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -80,6 +80,10 @@ class LibraryBuilder extends AbstractBuilder { this.addTask("generateLibraryManifest"); this.addTask("generateManifestBundle"); + + const bundles = project.getBundles(); + const existingBundleDefinitionNames = + bundles.map(({bundleDefinition}) => bundleDefinition.name).filter(Boolean) || []; const componentPreloadPaths = project.getComponentPreloadPaths(); const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); const componentPreloadExcludes = project.getComponentPreloadNamespaces(); @@ -88,18 +92,19 @@ class LibraryBuilder extends AbstractBuilder { options: { paths: componentPreloadPaths, namespaces: componentPreloadNamespaces, - excludes: componentPreloadExcludes + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames } }); } this.addTask("generateLibraryPreload", { options: { - excludes: project.getLibraryPreloadExcludes() + excludes: project.getLibraryPreloadExcludes(), + skipBundles: existingBundleDefinitionNames } }); - const bundles = project.getBundles(); if (bundles.length) { this.addTask("generateBundle", {requiresDependencies: true}, async ({workspace, dependencies, taskUtil, options}) => { From 3d2893af7ac100be687ddf3dd3217a72f6c95e2a Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 18 May 2022 18:50:26 +0200 Subject: [PATCH 55/99] Apply changes from https://github.com/SAP/ui5-builder/pull/745 As well as https://github.com/SAP/ui5-builder/pull/748 --- lib/buildDefinitions/AbstractBuilder.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 74b29f9db..1359a8d47 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -234,7 +234,7 @@ class AbstractBuilder { * @param {module:@ui5/fs.ReaderCollection} buildParams.dependencies Dependencies reader collection * @returns {Promise} Returns promise chain with tasks */ - build(buildConfig, buildParams) { + async build(buildConfig, buildParams) { const tasksToRun = composeTaskList(Object.keys(this.tasks), buildConfig); const allTasks = this.taskExecutionOrder.filter((taskName) => { // There might be a numeric suffix in case a custom task is configured multiple times. @@ -251,15 +251,13 @@ class AbstractBuilder { this.taskLog.addWork(allTasks.length); - return allTasks.reduce((taskChain, taskName) => { + for (const taskName of allTasks) { const taskFunction = this.tasks[taskName].task; if (typeof taskFunction === "function") { - taskChain = taskChain.then(this.wrapTask(taskName, taskFunction, buildParams)); + await this.executeTask(taskName, taskFunction, buildParams); } - - return taskChain; - }, Promise.resolve()); + } } requiresDependencies(buildConfig) { @@ -292,13 +290,16 @@ class AbstractBuilder { * @param {string} taskName Name of the task * @param {Function} taskFunction Function which executed the task * @param {object} taskParams Base parameters for all tasks - * @returns {Function} Wrapped task function + * @returns {Promise} Resolves when task has finished */ - wrapTask(taskName, taskFunction, taskParams) { - return () => { - this.taskLog.startWork(`Running task ${taskName}...`); - return taskFunction(taskParams).then(() => this.taskLog.completeWork(1)); - }; + async executeTask(taskName, taskFunction, taskParams) { + this.taskLog.startWork(`Running task ${taskName}...`); + this._taskStart = performance.now(); + await taskFunction(taskParams); + this.taskLog.completeWork(1); + if (process.env.UI5_LOG_TASK_PERF) { + this.taskLog.info(`Task succeeded in ${Math.round((performance.now() - this._taskStart))} ms`); + } } /** From 6a432425f0795fcef282d270bbb2fac486b650cc Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 10:30:27 +0200 Subject: [PATCH 56/99] ProjectGraph: Update JSDoc --- lib/graph/ProjectGraph.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index 69793de60..dbe55d3dc 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -28,6 +28,8 @@ class ProjectGraph { } /** + * Get the root project of the graph + * * @public * @returns {module:@ui5/project.specifications.Project} Root project */ @@ -40,6 +42,8 @@ class ProjectGraph { } /** + * Add a project to the graph + * * @public * @param {module:@ui5/project.specifications.Project} project Project which should be added to the graph * @param {boolean} [ignoreDuplicates=false] Whether an error should be thrown when a duplicate project is added @@ -79,15 +83,18 @@ class ProjectGraph { } /** + * Get all projects as a nested array containing pairs of project name and -instance. + * * @public - * @returns {object} + * @returns {Array>} */ getAllProjects() { return Object.entries(this._projects); } - /** + * Add an extension to the graph + * * @public * @param {module:@ui5/project.specification.Extension} extension Extension which should be available in the graph */ @@ -118,6 +125,12 @@ class ProjectGraph { return this._extensions[extensionName]; } + /** + * Get all extensions as a nested array containing pairs of extension name and -instance. + * + * @public + * @returns {Array>} + */ getAllExtensions() { return Object.entries(this._extensions); } @@ -198,6 +211,8 @@ class ProjectGraph { } /** + * Get all direct dependencies of a project as an array of project names + * * @public * @param {string} projectName Name of the project to retrieve the dependencies of * @returns {module:@ui5/project.specifications.Project[]} Project instances of the given project's dependencies From 2e980a8eea769e9902143fb38f4b9861779503f0 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 11:11:41 +0200 Subject: [PATCH 57/99] package.json: Bump @ui5/fs alpha version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40c15d31c..994eb40b0 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ }, "dependencies": { "@ui5/builder": "^3.0.0-alpha.6", - "@ui5/fs": "3.0.0-alpha.2", + "@ui5/fs": "3.0.0-alpha.3", "@ui5/logger": "^3.0.1-alpha.1", "@ui5/server": "^3.0.0-alpha.1", "ajv": "^6.12.6", From cb2fa20a1ea092c950ab60378b39c810fb13bb05 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 11:24:37 +0200 Subject: [PATCH 58/99] Specifications: Update JSDoc --- lib/specifications/Project.js | 18 ++++++++---- lib/specifications/types/ThemeLibrary.js | 36 +++++++++--------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 0f1c07714..7d1ed8e7a 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -78,14 +78,21 @@ class Project extends Specification { /* === Resource Access === */ /** - * TODO + * Get a [ReaderCollection]{@link module:@ui5/fs.ReaderCollection} for accessing all resources of the + * project in the specified "style": + * + *
      + *
    • buildtime: Resource paths are always prefixed with /resources/ + * or /test-resources/ followed by the project's namespace
    • + *
    • runtime: Access resources the same way the UI5 runtime would do
    • + *
    • flat: No prefix, no namespace
    • + *
    * * @public * @param {object} [options] * @param {string} [options.style=buildtime] Path style to access resources. Can be "buildtime", "runtime" or "flat" - * TODO: describe styles - * This parameter might be ignored by some specifications - * @returns {module:@ui5/fs.ReaderCollection} Reader collection + * This parameter might be ignored by some project types + * @returns {module:@ui5/fs.ReaderCollection} Reader collection allowing access to all resources of the project */ getReader(options) { throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`); @@ -104,7 +111,8 @@ class Project extends Specification { } /** - * TODO + * Get a [DuplexCollection]{@link module:@ui5/fs.DuplexCollection} for accessing and modifying a + * project's resources. This is always of style buildtime. * * @public * @returns {module:@ui5/fs.DuplexCollection} DuplexCollection diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index ae84fc3e9..b0f2b635d 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -21,25 +21,13 @@ class ThemeLibrary extends Project { /* === Resource Access === */ /** - * Get a resource reader for the sources of the project (excluding any test resources) - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - getSourceReader() { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/", - name: `Source reader for theme-library project ${this.getName()}`, - project: this - }); - } - - /** - * Get a resource reader for accessing the project resources the same way the UI5 runtime would do + * Get a [ReaderCollection]{@link module:@ui5/fs.ReaderCollection} for accessing all resources of the + * project. + * This is always of style buildtime, wich for theme libraries is identical to style + * runtime. * * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection + * @returns {module:@ui5/fs.ReaderCollection} Reader collection allowing access to all resources of the project */ getReader() { let reader = resourceFactory.createReader({ @@ -69,11 +57,15 @@ class ThemeLibrary extends Project { } /** - * Get a resource reader/writer for accessing and modifying a project's resources - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance - */ + * Get a [DuplexCollection]{@link module:@ui5/fs.DuplexCollection} for accessing and modifying a + * project's resources. + * + * This is always of style buildtime, wich for theme libraries is identical to style + * runtime. + * + * @public + * @returns {module:@ui5/fs.DuplexCollection} DuplexCollection + */ getWorkspace() { const reader = this.getReader(); From 03f8b275fed54ce979d3be25db87f8e38b751a4c Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 14:10:07 +0200 Subject: [PATCH 59/99] [INTERNAL] Project: Add missing #getCustomMiddleware API --- lib/specifications/Project.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 7d1ed8e7a..25d25fcd3 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -64,6 +64,10 @@ class Project extends Specification { return this._config.builder?.customTasks || []; } + getCustomMiddleware() { + return this._config.builder?.customMiddleware || []; + } + getServerSettings() { return this._config.server?.settings; } From 098d4eb57588cf69cfd55a89aefa3223f229d6b5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 14:10:42 +0200 Subject: [PATCH 60/99] [INTERNAL] Fix several uses of legacy project.metadata.* attributes --- lib/buildDefinitions/AbstractBuilder.js | 12 ++++++------ lib/builder.js | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 1359a8d47..3fa8df2df 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -83,16 +83,16 @@ class AbstractBuilder { 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} ` + + throw new Error(`Missing name for custom task definition of project ${project.getName()} ` + `at index ${i}`); } if (taskDef.beforeTask && taskDef.afterTask) { - throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + + throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` + `defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`); } if (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) { // Iff there are tasks configured, beforeTask or afterTask must be given - throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + + throw new Error(`Custom task definition ${taskDef.name} of project ${project.getName()} ` + `defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`); } @@ -158,7 +158,7 @@ class AbstractBuilder { 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}`); + `to be scheduled for project ${project.getName()}`); } if (taskDef.afterTask) { // Insert after index of referenced task @@ -185,10 +185,10 @@ class AbstractBuilder { */ addTask(taskName, {requiresDependencies = false, options = {}} = {}, taskFunction) { if (this.tasks[taskName]) { - throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`); + throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.getName()}`); } if (this.taskExecutionOrder.includes(taskName)) { - throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` + + throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.getName()}. ` + `It has already been scheduled for execution.`); } diff --git a/lib/builder.js b/lib/builder.js index bad2950f9..83dbd1726 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -75,8 +75,7 @@ function getElapsedTime(startTime) { * * @public * @param {object} parameters Parameters - * @param {module:@ui5/project.graph.ProjectGraph} parameters.graph Project graph as generated by the - * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @param {module:@ui5/project.graph.ProjectGraph} parameters.graph Project graph * @param {string} parameters.destPath Target path * @param {boolean} [parameters.cleanDest=false] Decides whether project should clean the target path before build * @param {Array.} [parameters.includedDependencies=[]] From 5cdf7256a05705dab6a029394da4ee0646f7869b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 20 May 2022 14:11:48 +0200 Subject: [PATCH 61/99] [INTERNAL] AbstractBuilder: Fix specVersion selection for task interface Use specVersion of the custom task, not of the project using it --- lib/buildDefinitions/AbstractBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 3fa8df2df..644696f65 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -139,7 +139,7 @@ class AbstractBuilder { params.dependencies = dependencies; } - const taskUtilInterface = taskUtil.getInterface(project.getSpecVersion()); + const taskUtilInterface = taskUtil.getInterface(task.getSpecVersion()); // Interface is undefined if specVersion does not support taskUtil if (taskUtilInterface) { params.taskUtil = taskUtilInterface; From 5a12078667238bffdaa9a4541f4fc6710752251a Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 23 May 2022 11:12:43 +0200 Subject: [PATCH 62/99] [INTERNAL] Extensions: Add getters for modules --- lib/buildDefinitions/AbstractBuilder.js | 2 +- .../types/extensions/ServerMiddleware.js | 10 ++++++++++ lib/specifications/types/extensions/Task.js | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 644696f65..c11ec1841 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -144,7 +144,7 @@ class AbstractBuilder { if (taskUtilInterface) { params.taskUtil = taskUtilInterface; } - return task(params); + return task.getTask()(params); }; this.tasks[newTaskName] = { diff --git a/lib/specifications/types/extensions/ServerMiddleware.js b/lib/specifications/types/extensions/ServerMiddleware.js index 1134d342d..f6a6b6d67 100644 --- a/lib/specifications/types/extensions/ServerMiddleware.js +++ b/lib/specifications/types/extensions/ServerMiddleware.js @@ -1,3 +1,4 @@ +const path = require("path"); const Extension = require("../../Extension"); class ServerMiddleware extends Extension { @@ -5,6 +6,15 @@ class ServerMiddleware extends Extension { super(parameters); } + /* === Attributes === */ + /** + * @public + */ + getMiddleware() { + const middlewarePath = path.join(this.getPath(), this._config.middleware.path); + return require(middlewarePath); + } + /* === Internals === */ /** * @private diff --git a/lib/specifications/types/extensions/Task.js b/lib/specifications/types/extensions/Task.js index bc1f09007..275e8dbc5 100644 --- a/lib/specifications/types/extensions/Task.js +++ b/lib/specifications/types/extensions/Task.js @@ -1,3 +1,4 @@ +const path = require("path"); const Extension = require("../../Extension"); class Task extends Extension { @@ -5,6 +6,15 @@ class Task extends Extension { super(parameters); } + /* === Attributes === */ + /** + * @public + */ + getTask() { + const taskPath = path.join(this.getPath(), this._config.task.path); + return require(taskPath); + } + /* === Internals === */ /** * @private From cd70fce7e3f269ba149e8ee2bc0768621520779d Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 23 May 2022 18:08:49 +0200 Subject: [PATCH 63/99] [INTERNAL] Transfer formatter tests from ui5-builder I --- lib/specifications/ComponentProject.js | 5 +- lib/specifications/Specification.js | 3 +- test/fixtures/application.h/pom.xml | 41 +++ .../webapp-project.artifactId/manifest.json | 13 + .../webapp-properties.appId/manifest.json | 13 + .../manifest.json | 13 + .../application.h/webapp/Component.js | 8 + .../application.h/webapp/manifest.json | 13 + .../webapp/sectionsA/section1.js | 3 + .../webapp/sectionsA/section2.js | 3 + .../webapp/sectionsA/section3.js | 3 + .../webapp/sectionsB/section1.js | 3 + .../webapp/sectionsB/section2.js | 3 + .../webapp/sectionsB/section3.js | 3 + test/lib/specifications/ComponentProject.js | 135 ++++++++ test/lib/specifications/Project.js | 20 -- test/lib/specifications/Specification.js | 75 +++++ test/lib/specifications/types/Application.js | 294 ++++++++++++++++++ test/lib/specifications/types/Library.js | 51 ++- 19 files changed, 667 insertions(+), 35 deletions(-) create mode 100644 test/fixtures/application.h/pom.xml create mode 100644 test/fixtures/application.h/webapp-project.artifactId/manifest.json create mode 100644 test/fixtures/application.h/webapp-properties.appId/manifest.json create mode 100644 test/fixtures/application.h/webapp-properties.componentName/manifest.json create mode 100644 test/fixtures/application.h/webapp/Component.js create mode 100644 test/fixtures/application.h/webapp/manifest.json create mode 100644 test/fixtures/application.h/webapp/sectionsA/section1.js create mode 100644 test/fixtures/application.h/webapp/sectionsA/section2.js create mode 100644 test/fixtures/application.h/webapp/sectionsA/section3.js create mode 100644 test/fixtures/application.h/webapp/sectionsB/section1.js create mode 100644 test/fixtures/application.h/webapp/sectionsB/section2.js create mode 100644 test/fixtures/application.h/webapp/sectionsB/section3.js create mode 100644 test/lib/specifications/ComponentProject.js create mode 100644 test/lib/specifications/Specification.js diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index dbcbae986..6ce879936 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -273,7 +273,7 @@ class ComponentProject extends Project { if (parts) { this._log.verbose( `"${value} contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); - const pom = await this.getPom(); + const pom = await this._getPom(); let mvnValue; if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { mvnValue = pom.project.properties[parts[1]]; @@ -286,7 +286,7 @@ class ComponentProject extends Project { } if (!mvnValue) { throw new Error(`"${value}" couldn't be resolved from maven property ` + - `"${parts[1]}" of pom.xml of project ${this._project.metadata.name}`); + `"${parts[1]}" of pom.xml of project ${this.getName()}`); } return mvnValue; } else { @@ -303,6 +303,7 @@ class ComponentProject extends Project { if (this._pPom) { return this._pPom; } + return this._pPom = this.getRootReader().byPath("/pom.xml") .then(async (resource) => { if (!resource) { diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 53fe03eab..b88264eaf 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -163,7 +163,8 @@ class Specification { * @param {string} dirPath Path of directory, relative to the project root */ async _dirExists(dirPath) { - if (await this.getRootReader().byPath(dirPath, {nodir: false})) { + const resource = await this.getRootReader().byPath(dirPath, {nodir: false}); + if (resource && resource.getStatInfo().isDirectory()) { return true; } return false; diff --git a/test/fixtures/application.h/pom.xml b/test/fixtures/application.h/pom.xml new file mode 100644 index 000000000..478ebc85c --- /dev/null +++ b/test/fixtures/application.h/pom.xml @@ -0,0 +1,41 @@ + + + + + + + 4.0.0 + + + + + com.sap.test + application.h + 1.0.0 + war + + + + + application.h + Simple SAPUI5 based application + + + + + + + application.h + + + + + diff --git a/test/fixtures/application.h/webapp-project.artifactId/manifest.json b/test/fixtures/application.h/webapp-project.artifactId/manifest.json new file mode 100644 index 000000000..7de6072ce --- /dev/null +++ b/test/fixtures/application.h/webapp-project.artifactId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${project.artifactId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/test/fixtures/application.h/webapp-properties.appId/manifest.json b/test/fixtures/application.h/webapp-properties.appId/manifest.json new file mode 100644 index 000000000..e1515df70 --- /dev/null +++ b/test/fixtures/application.h/webapp-properties.appId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${appId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/test/fixtures/application.h/webapp-properties.componentName/manifest.json b/test/fixtures/application.h/webapp-properties.componentName/manifest.json new file mode 100644 index 000000000..7d63e359c --- /dev/null +++ b/test/fixtures/application.h/webapp-properties.componentName/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${componentName}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/test/fixtures/application.h/webapp/Component.js b/test/fixtures/application.h/webapp/Component.js new file mode 100644 index 000000000..cb9bd4068 --- /dev/null +++ b/test/fixtures/application.h/webapp/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/test/fixtures/application.h/webapp/manifest.json b/test/fixtures/application.h/webapp/manifest.json new file mode 100644 index 000000000..32b7e4a84 --- /dev/null +++ b/test/fixtures/application.h/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "application.h", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/test/fixtures/application.h/webapp/sectionsA/section1.js b/test/fixtures/application.h/webapp/sectionsA/section1.js new file mode 100644 index 000000000..ac4a81296 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsA/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/test/fixtures/application.h/webapp/sectionsA/section2.js b/test/fixtures/application.h/webapp/sectionsA/section2.js new file mode 100644 index 000000000..e009c8286 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsA/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/test/fixtures/application.h/webapp/sectionsA/section3.js b/test/fixtures/application.h/webapp/sectionsA/section3.js new file mode 100644 index 000000000..5fd9349d4 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsA/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/test/fixtures/application.h/webapp/sectionsB/section1.js b/test/fixtures/application.h/webapp/sectionsB/section1.js new file mode 100644 index 000000000..ac4a81296 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsB/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/test/fixtures/application.h/webapp/sectionsB/section2.js b/test/fixtures/application.h/webapp/sectionsB/section2.js new file mode 100644 index 000000000..e009c8286 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsB/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/test/fixtures/application.h/webapp/sectionsB/section3.js b/test/fixtures/application.h/webapp/sectionsB/section3.js new file mode 100644 index 000000000..5fd9349d4 --- /dev/null +++ b/test/fixtures/application.h/webapp/sectionsB/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/test/lib/specifications/ComponentProject.js b/test/lib/specifications/ComponentProject.js new file mode 100644 index 000000000..9b867f59d --- /dev/null +++ b/test/lib/specifications/ComponentProject.js @@ -0,0 +1,135 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); +const Specification = require("../../../lib/specifications/Specification"); + +function clone(o) { + return JSON.parse(JSON.stringify(o)); +} + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("hasMavenPlaceholder: has maven placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + const res = project._hasMavenPlaceholder("${mvn-pony}"); + t.true(res, "String has maven placeholder"); +}); + +test("hasMavenPlaceholder: has no maven placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + + const res = project._hasMavenPlaceholder("$mvn-pony}"); + t.false(res, "String has no maven placeholder"); +}); + +test("_resolveMavenPlaceholder: resolves maven placeholder from first POM level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({ + project: { + properties: { + "mvn-pony": "unicorn" + } + } + }); + + const res = await project._resolveMavenPlaceholder("${mvn-pony}"); + t.deepEqual(res, "unicorn", "Resolved placeholder correctly"); +}); + +test("_resolveMavenPlaceholder: resolves maven placeholder from deeper POM level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({ + "mvn-pony": { + some: { + id: "unicorn" + } + } + }); + + const res = await project._resolveMavenPlaceholder("${mvn-pony.some.id}"); + t.deepEqual(res, "unicorn", "Resolved placeholder correctly"); +}); + +test("_resolveMavenPlaceholder: can't resolve from POM", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getPom").resolves({}); + + const err = await t.throwsAsync(project._resolveMavenPlaceholder("${mvn-pony}")); + t.deepEqual(err.message, + `"\${mvn-pony}" couldn't be resolved from maven property "mvn-pony" ` + + `of pom.xml of project application.a`, + "Rejected with correct error message"); +}); + +test("_resolveMavenPlaceholder: provided value is no placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + + const err = await t.throwsAsync(project._resolveMavenPlaceholder("My ${mvn-pony}")); + t.deepEqual(err.message, + `"My \${mvn-pony}" is not a maven placeholder`, + "Rejected with correct error message"); +}); + +test("_getPom: reads correctly", async (t) => { + const projectInput = clone(basicProjectInput); + // Application H contains a pom.xml + const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h"); + projectInput.modulePath = applicationHPath; + projectInput.configuration.metadata.name = "application.h"; + const project = await Specification.create(projectInput); + + const res = await project._getPom(); + t.deepEqual(res.project.modelVersion, "4.0.0", "pom.xml content has been read"); +}); + +test.serial("_getPom: fs read error", async (t) => { + const project = await Specification.create(basicProjectInput); + project.getRootReader = () => { + return { + byPath: async () => { + throw new Error("EPON: Pony Error"); + } + }; + }; + const error = await t.throwsAsync(project._getPom()); + t.deepEqual(error.message, + "Failed to read pom.xml for project application.a: " + + "EPON: Pony Error", + "Rejected with correct error message"); +}); + +test.serial("_getPom: result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => `no unicorn` + }); + + project.getRootReader = () => { + return { + byPath: byPathStub + }; + }; + + let res = await project._getPom(); + t.deepEqual(res, {pony: "no unicorn"}, "Correct result on first call"); + res = await project._getPom(); + t.deepEqual(res, {pony: "no unicorn"}, "Correct result on second call"); + + t.deepEqual(byPathStub.callCount, 1, "getRootReader().byPath got called exactly once (and then cached)"); +}); diff --git a/test/lib/specifications/Project.js b/test/lib/specifications/Project.js index 6baf2755d..a8917234a 100644 --- a/test/lib/specifications/Project.js +++ b/test/lib/specifications/Project.js @@ -19,19 +19,6 @@ const basicProjectInput = { } }; -test("Instantiate a basic project", async (t) => { - const project = await Specification.create(basicProjectInput); - t.is(project.getName(), "application.a", "Returned correct name"); - t.is(project.getVersion(), "1.0.0", "Returned correct version"); - t.is(project.getPath(), applicationAPath, "Returned correct project path"); -}); - -test("Configurations", async (t) => { - const project = await Specification.create(basicProjectInput); - t.is(project.getKind(), "project", "Returned correct kind configuration"); - t.is(project.getType(), "application", "Returned correct type configuration"); -}); - test("Invalid configuration", async (t) => { const customProjectInput = clone(basicProjectInput); customProjectInput.configuration.resources = { @@ -45,10 +32,3 @@ test("Invalid configuration", async (t) => { Configuration resources/configuration/propertiesFileSourceEncoding must be equal to one of the allowed values Allowed values: UTF-8, ISO-8859-1`, "Threw with validation error"); }); - -test("Access project root resources via reader", async (t) => { - const project = await Specification.create(basicProjectInput); - const rootReader = await project.getRootReader(); - const packageJsonResource = await rootReader.byPath("/package.json"); - t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); -}); diff --git a/test/lib/specifications/Specification.js b/test/lib/specifications/Specification.js new file mode 100644 index 000000000..f6c2ca65c --- /dev/null +++ b/test/lib/specifications/Specification.js @@ -0,0 +1,75 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); + +const Specification = require("../../../lib/specifications/Specification"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const basicProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +test("Instantiate a basic project", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getName(), "application.a", "Returned correct name"); + t.is(project.getVersion(), "1.0.0", "Returned correct version"); + t.is(project.getPath(), applicationAPath, "Returned correct project path"); +}); + +test("Configurations", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getKind(), "project", "Returned correct kind configuration"); + t.is(project.getType(), "application", "Returned correct type configuration"); +}); + +test("Access project root resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const rootReader = await project.getRootReader(); + const packageJsonResource = await rootReader.byPath("/package.json"); + t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource"); +}); + +test("_dirExists: Directory exists", async (t) => { + const project = await Specification.create(basicProjectInput); + const bExists = await project._dirExists("/webapp"); + t.true(bExists, "directory exists"); +}); + +test("_dirExists: Missing leading slash", async (t) => { + const project = await Specification.create(basicProjectInput); + const bExists = await project._dirExists("webapp"); + t.false(bExists, "directory is not found"); +}); + +test("_dirExists: Trailing slash is ok", async (t) => { + const project = await Specification.create(basicProjectInput); + const bExists = await project._dirExists("/webapp/"); + t.true(bExists, "directory exists"); +}); + +test("_dirExists: Directory is a file", async (t) => { + const project = await Specification.create(basicProjectInput); + + const bExists = await project._dirExists("webapp/index.html"); + t.false(bExists, "directory is a file"); +}); + + +test("_dirExists: Directory does not exist", async (t) => { + const project = await Specification.create(basicProjectInput); + + const bExists = await project._dirExists("/w"); + t.false(bExists, "directory does not exist"); +}); diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index fadadbde2..156366224 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -1,5 +1,6 @@ const test = require("ava"); const path = require("path"); +const sinon = require("sinon"); const Specification = require("../../../../lib/specifications/Specification"); const Application = require("../../../../lib/specifications/types/Application"); @@ -20,6 +21,10 @@ const basicProjectInput = { } }; +test.afterEach.always((t) => { + sinon.restore(); +}); + test("Correct class", async (t) => { const project = await Specification.create(basicProjectInput); t.true(project instanceof Application, `Is an instance of the Application class`); @@ -58,3 +63,292 @@ test("Access project resources via reader: flat style", async (t) => { t.truthy(resource, "Found the requested resource"); t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); }); + +test("Access project resources via reader: runtime style", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader({style: "runtime"}); + const resource = await reader.byPath("/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); +}); + +test("Modify project resources via workspace and access via flat and runtime readers", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/id1/index.html"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("Application A", "Some Name"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const flatReader = await project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/index.html"); + t.truthy(flatReaderResource, "Found the requested resource byPath"); + t.is(flatReaderResource.getPath(), "/index.html", "Resource (byPath) has correct path"); + t.is(await flatReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const flatGlobResult = await flatReader.byGlob("**/index.html"); + t.is(flatGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(flatGlobResult[0].getPath(), "/index.html", "Resource (byGlob) has correct path"); + t.is(await flatGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); + + const runtimeReader = await project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/index.html"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath"); + t.is(runtimeReaderResource.getPath(), "/index.html", "Resource (byPath) has correct path"); + t.is(await runtimeReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/index.html"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(runtimeGlobResult[0].getPath(), "/index.html", "Resource (byGlob) has correct path"); + t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); +}); + +test("_getNamespaceFromManifestJson: No 'sap.app' configuration found", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.deepEqual(error.message, "No sap.app/id configuration found in manifest.json of project application.a", + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestJson: No application id in 'sap.app' configuration found", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {}}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.deepEqual(error.message, "No sap.app/id configuration found in manifest.json of project application.a"); +}); + +test("_getNamespaceFromManifestJson: set namespace to id", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {id: "my.id"}}); + + const namespace = await project._getNamespaceFromManifestJson(); + t.deepEqual(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespaceFromManifestAppDescVariant: No 'id' property found", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestAppDescVariant()); + t.deepEqual(error.message, `No "id" property found in manifest.appdescr_variant of project application.a`, + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestAppDescVariant: set namespace to id", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({id: "my.id"}); + + const namespace = await project._getNamespaceFromManifestAppDescVariant(); + t.deepEqual(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespace: Correct fallback to manifest.appdescr_variant if manifest.json is missing", async (t) => { + const project = await Specification.create(basicProjectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().resolves({id: "my.id"}); + + const namespace = await project._getNamespace(); + t.deepEqual(namespace, "my/id", "Returned correct namespace"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant failed", async (t) => { + const project = await Specification.create(basicProjectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant is not possible", async (t) => { + const project = await Specification.create(basicProjectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({message: "No such stable or directory: manifest.json", code: "ENOENT"}) + .onSecondCall().rejects({code: "ENOENT"}); // both files are missing + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, + "Could not find required manifest.json for project application.a: " + + "No such stable or directory: manifest.json", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: No fallback if manifest.json is present but failed to parse", async (t) => { + const project = await Specification.create(basicProjectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 1, "_getManifest called exactly once"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json only"); +}); + +test("_getManifest: reads correctly", async (t) => { + const project = await Specification.create(basicProjectInput); + + const content = await project._getManifest("/manifest.json"); + t.deepEqual(content._version, "1.1.0", "manifest.json content has been read"); +}); + +test("_getManifest: invalid JSON", async (t) => { + const project = await Specification.create(basicProjectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => "no json" + }); + + project._getSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error.message, + "Failed to read /some-manifest.json for project application.a: " + + "Unexpected token o in JSON at position 1", + "Rejected with correct error message"); + t.deepEqual(byPathStub.callCount, 1, "byPath got called once"); + t.deepEqual(byPathStub.getCall(0).args[0], "/some-manifest.json", "byPath got called with the correct argument"); +}); + +test.serial("_getManifest: File does not exist", async (t) => { + const project = await Specification.create(basicProjectInput); + + const error = await t.throwsAsync(project._getManifest("/does-not-exist.json")); + t.deepEqual(error.message, + "Failed to read /does-not-exist.json for project application.a: " + + "Could not find resource /does-not-exist.json in project application.a", + "Rejected with correct error message"); +}); + +test.serial("_getManifest: result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => `{"pony": "no unicorn"}` + }); + + project._getSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const content = await project._getManifest("/some-manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on first call"); + + const content2 = await project._getManifest("/some-other-manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "Correct result on second call"); + + t.deepEqual(byPathStub.callCount, 2, "byPath got called exactly twice (and then cached)"); +}); + +test.serial("_getManifest: Caches successes and failures", async (t) => { + const project = await Specification.create(basicProjectInput); + + const getStringStub = sinon.stub() + .onFirstCall().rejects(new Error("EPON: Pony Error")) + .onSecondCall().resolves(`{"pony": "no unicorn"}`); + const byPathStub = sinon.stub().resolves({ + getString: getStringStub + }); + + project._getSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error.message, + "Failed to read /some-manifest.json for project application.a: " + + "EPON: Pony Error", + "Rejected with correct error message"); + + const content = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on second call"); + + const error2 = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error2.message, + "Failed to read /some-manifest.json for project application.a: " + + "EPON: Pony Error", + "From cache: Rejected with correct error message"); + + const content2 = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "From cache: Correct result on first call"); + + t.deepEqual(byPathStub.callCount, 2, + "byPath got called exactly twice (and then cached)"); +}); + +const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h"); +const applicationH = { + id: "application.h.id", + version: "1.0.0", + modulePath: applicationHPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.h"}, + resources: { + configuration: { + paths: { + webapp: "webapp" + } + } + } + } +}; + +test("namespace: detect namespace from pom.xml via ${project.artifactId}", async (t) => { + const myProject = clone(applicationH); + myProject.configuration.resources.configuration.paths.webapp = "webapp-project.artifactId"; + const project = await Specification.create(myProject); + + t.deepEqual(project.getNamespace(), "application/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${componentName} from properties", async (t) => { + const myProject = clone(applicationH); + myProject.configuration.resources.configuration.paths.webapp = "webapp-properties.componentName"; + const project = await Specification.create(myProject); + + t.deepEqual(project.getNamespace(), "application/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${appId} from properties", async (t) => { + const myProject = clone(applicationH); + myProject.configuration.resources.configuration.paths.webapp = "webapp-properties.appId"; + + const error = await t.throwsAsync(Specification.create(myProject)); + t.deepEqual(error.message, "Failed to resolve namespace of project application.h: \"${appId}\"" + + " couldn't be resolved from maven property \"appId\" of pom.xml of project application.h"); +}); diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index f9fa05f3b..3e5561001 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -71,7 +71,7 @@ test("Access project resources via reader: flat style", async (t) => { t.is(resource.getPath(), "/.library", "Resource has correct path"); }); -test("Access project test-resources via reader: buildtime style, including test resources", async (t) => { +test("Access project test-resources via reader: buildtime style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader({style: "buildtime"}); const resource = await reader.byPath("/test-resources/library/d/Test.html"); @@ -79,23 +79,50 @@ test("Access project test-resources via reader: buildtime style, including test t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); }); -test("Modify project resources via workspace and access via flat reader", async (t) => { + +test("Access project test-resources via reader: runtime style", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader({style: "runtime"}); + const resource = await reader.byPath("/test-resources/library/d/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); +}); + +test("Modify project resources via workspace and access via flat and runtime reader", async (t) => { const project = await Specification.create(basicProjectInput); const workspace = await project.getWorkspace(); const workspaceResource = await workspace.byPath("/resources/library/d/.library"); + t.truthy(workspaceResource, "Found resource in workspace"); const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); workspaceResource.setString(newContent); await workspace.write(workspaceResource); - const reader = await project.getReader({style: "flat"}); - const readerResource = await reader.byPath("/.library"); - t.truthy(readerResource, "Found the requested resource byPath"); - t.is(readerResource.getPath(), "/.library", "Resource (byPath) has correct path"); - t.is(await readerResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); - - const globResult = await reader.byGlob("**/.library"); - t.is(globResult.length, 1, "Found the requested resource byGlob"); - t.is(globResult[0].getPath(), "/.library", "Resource (byGlob) has correct path"); - t.is(await globResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); + const flatReader = await project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/.library"); + t.truthy(flatReaderResource, "Found the requested resource byPath (flat)"); + t.is(flatReaderResource.getPath(), "/.library", "Resource (byPath) has correct path (flat)"); + t.is(await flatReaderResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content (flat)"); + + const flatGlobResult = await flatReader.byGlob("**/.library"); + t.is(flatGlobResult.length, 1, "Found the requested resource byGlob (flat)"); + t.is(flatGlobResult[0].getPath(), "/.library", "Resource (byGlob) has correct path (flat)"); + t.is(await flatGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content (flat)"); + + const runtimeReader = await project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/library/d/.library"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)"); + t.is(runtimeReaderResource.getPath(), "/resources/library/d/.library", + "Resource (byPath) has correct path (runtime)"); + t.is(await runtimeReaderResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content (runtime)"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/.library"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)"); + t.is(runtimeGlobResult[0].getPath(), "/resources/library/d/.library", + "Resource (byGlob) has correct path (runtime)"); + t.is(await runtimeGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content (runtime)"); }); From 24266426528ee1bed5ac8ba80ff054c8324fcfe7 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 24 May 2022 11:23:32 +0200 Subject: [PATCH 64/99] [INTERNAL] generateProjectGraph: Add option to ignore framework deps For example required by UI5 CLI add/remove/use commands which only require the configured root project and no framework dependencies --- lib/buildHelpers/composeProjectList.js | 2 +- lib/generateProjectGraph.js | 33 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/buildHelpers/composeProjectList.js b/lib/buildHelpers/composeProjectList.js index 06ae351f4..d05fe44a2 100644 --- a/lib/buildHelpers/composeProjectList.js +++ b/lib/buildHelpers/composeProjectList.js @@ -1,4 +1,4 @@ -const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); +const log = require("@ui5/logger").getLogger("buildHelpers:composeProjectList"); /** * Creates an object containing the flattened project dependency tree. Each dependency is defined as an object key while diff --git a/lib/generateProjectGraph.js b/lib/generateProjectGraph.js index 831228f5e..850eeba5e 100644 --- a/lib/generateProjectGraph.js +++ b/lib/generateProjectGraph.js @@ -33,9 +33,14 @@ const generateProjectGraph = { * @param {string} [options.rootConfigPath] * Configuration file to use for the root module instead the default ui5.yaml * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph * @returns {Promise} Promise resolving to a Project Graph instance */ - usingNodePackageDependencies: async function({cwd, rootConfiguration, rootConfigPath, versionOverride}) { + usingNodePackageDependencies: async function({ + cwd, rootConfiguration, rootConfigPath, + versionOverride, resolveFrameworkDependencies = true + }) { log.verbose(`Creating project graph using npm provider...`); const NpmProvider = require("./graph/providers/NodePackageDependencies"); @@ -47,7 +52,9 @@ const generateProjectGraph = { const projectGraph = await projectGraphBuilder(provider); - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + } return projectGraph; }, @@ -62,9 +69,14 @@ const generateProjectGraph = { * @param {object} [options.filePath=projectDependencies.yaml] Path to the dependency configuration file * @param {string} [options.cwd=process.cwd()] Directory to resolve relative paths to * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph * @returns {Promise} Promise resolving to a Project Graph instance */ - usingStaticFile: async function({cwd, filePath = "projectDependencies.yaml", versionOverride}) { + usingStaticFile: async function({ + cwd, filePath = "projectDependencies.yaml", + versionOverride, resolveFrameworkDependencies = true + }) { log.verbose(`Creating project graph using static file...`); const dependencyTree = await generateProjectGraph @@ -77,7 +89,9 @@ const generateProjectGraph = { const projectGraph = await projectGraphBuilder(provider); - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + } return projectGraph; }, @@ -91,9 +105,14 @@ const generateProjectGraph = { * @param {object} options * @param {module:@ui5/project.graph.providers.DependencyTree.TreeNode} options.dependencyTree * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project + * @param {string} [options.resolveFrameworkDependencies=true] + * Whether framework dependencies should be added to the graph * @returns {Promise} Promise resolving to a Project Graph instance */ - usingObject: async function({dependencyTree, versionOverride}) { + usingObject: async function({ + dependencyTree, + versionOverride, resolveFrameworkDependencies = true + }) { log.verbose(`Creating project graph using object...`); const DependencyTreeProvider = require("./graph/providers/DependencyTree"); @@ -103,7 +122,9 @@ const generateProjectGraph = { const projectGraph = await projectGraphBuilder(dependencyTreeProvider); - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + if (resolveFrameworkDependencies) { + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride}); + } return projectGraph; }, From 1f838b769178504db989aba42b9ba3a1c01af9db Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 24 May 2022 13:38:02 +0200 Subject: [PATCH 65/99] LibraryBuilder: Pass namespace to generateJsdoc task --- lib/buildDefinitions/LibraryBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js index 43b5985ba..24c08d056 100644 --- a/lib/buildDefinitions/LibraryBuilder.js +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -48,7 +48,7 @@ class LibraryBuilder extends AbstractBuilder { taskUtil, options: { projectName: options.projectName, - namespace: project.projectNamespace, + namespace: project.getNamespace(), version: project.getVersion(), pattern: patterns } From bc0c0b7e5aa5d5ef4781aac0e30a563d37ac0740 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 24 May 2022 14:48:02 +0200 Subject: [PATCH 66/99] builder: Fix handling of includedDependencies/excludedDependencies --- lib/builder.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/builder.js b/lib/builder.js index 83dbd1726..727681b23 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -117,9 +117,12 @@ module.exports = async function({ "Parameter 'complexDependencyIncludes' can't be used in conjunction " + "with parameters 'includedDependencies' or 'excludedDependencies"); } - const deps = await composeProjectList(graph, complexDependencyIncludes); - includedDependencies = deps.includedDependencies; - excludedDependencies = deps.excludedDependencies; + ({includedDependencies, excludedDependencies} = await composeProjectList(graph, complexDependencyIncludes)); + } else if (includedDependencies.length || excludedDependencies.length) { + ({includedDependencies, excludedDependencies} = await composeProjectList(graph, { + includeDependencyTree: includedDependencies, + excludeDependencyTree: excludedDependencies + })); } const startTime = process.hrtime(); @@ -160,18 +163,15 @@ module.exports = async function({ return true; } - // if everything is included, this overrules exclude lists - if (includedDependencies.includes("*")) return true; - let test = false; - - if (test && projectMatchesAny(excludedDependencies)) { - test = false; + if (projectMatchesAny(excludedDependencies)) { + return false; } - if (!test && projectMatchesAny(includedDependencies)) { - test = true; + + if (includedDependencies.includes("*") || projectMatchesAny(includedDependencies)) { + return true; } - return test; + return false; } // Count total number of projects to build From b77d4908e6686f1dc2697a1851bdde4ff55da62a Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 24 May 2022 14:49:37 +0200 Subject: [PATCH 67/99] ComponentProject: Ensure linking of resources that are not modified --- lib/specifications/ComponentProject.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index 6ce879936..ec9de3971 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -121,8 +121,11 @@ class ComponentProject extends Project { // Same as buildtime return this.getReader(); } - reader = this._getFlatSourceReader("/"); - break; + // TODO 3.0: Refactor this + return this.getReader().link({ + linkPath: `/`, + targetPath: `/resources/${this._namespace}/` + }); case "flat": reader = this._getFlatSourceReader("/"); break; From 40cc284eeebe2482ccadbaaf8a24afb1817dbbe5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 24 May 2022 22:12:35 +0200 Subject: [PATCH 68/99] [INTERNAL] projectGraphBuilder: Throw if node has top-level configuration attributes To help detect incorrect use of the new Project Graph APIs, an error is thrown in case a provided node has top-level configuration properties like 'specVersion' or 'metadata'. In the past these properties where expected as part of a tree node. Now they need to go into a dedicated 'configuration' attribute. --- lib/graph/projectGraphBuilder.js | 17 +++++++++++++++++ lib/specifications/Project.js | 1 - lib/specifications/types/Library.js | 3 ++- test/lib/generateProjectGraph.usingObject.js | 16 ---------------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index 1a23ddb29..45a0c6c35 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -23,6 +23,21 @@ function _handleExtensions(graph, shimCollection, extensions) { }); } +function validateNode(node) { + if (node.specVersion) { + throw new Error( + `Provided node with ID ${node.id} contains a top-level 'specVersion' property. ` + + `With UI5 Tooling 3.0, project configuration needs to be provided in a dedicated` + + `'configuration' object.`); + } + if (node.metadata) { + throw new Error( + `Provided node with ID ${node.id} contains a top-level 'metadata' property. ` + + `With UI5 Tooling 3.0, project configuration needs to be provided in a dedicated` + + `'configuration' object.`); + } +} + /** * Dependency graph node representing a module * @@ -77,6 +92,7 @@ module.exports = async function(nodeProvider) { const moduleCollection = {}; const rootNode = await nodeProvider.getRootNode(); + validateNode(rootNode); const rootModule = new Module({ id: rootNode.id, version: rootNode.version, @@ -131,6 +147,7 @@ module.exports = async function(nodeProvider) { let ui5Module = moduleCollection[node.id]; if (!ui5Module) { log.verbose(`Creating module ${node.id}...`); + validateNode(node); ui5Module = moduleCollection[node.id] = new Module({ id: node.id, version: node.version, diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 25d25fcd3..c3b2a44df 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -44,7 +44,6 @@ class Project extends Specification { * @private */ getFrameworkDependencies() { - // TODO: Clone or freeze object before exposing? return this._config.framework?.libraries || []; } diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index f7e2b6f66..7c2608e64 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -123,7 +123,8 @@ class Library extends ComponentProject { } catch (err) { // Catch error because copyright is optional // TODO: Make copyright mandatory? - this._log.verbose(`Failed to get copyright: ${err.message}`); + this._log.verbose( + `Failed to get copyright for project ${this.getName()}: ${err.message}`); } } diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index 8d204f2da..ff197dc5d 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -523,22 +523,12 @@ test("Inconsistent dependencies with same ID", async (t) => { const tree = { id: "application.a", version: "1.0.0", - specVersion: "2.3", path: applicationAPath, - type: "application", - metadata: { - name: "application.a" - }, dependencies: [ { id: "library.d", version: "1.0.0", - specVersion: "2.3", path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, resources: { configuration: { propertiesFileSourceEncoding: "UTF-8", @@ -552,7 +542,6 @@ test("Inconsistent dependencies with same ID", async (t) => { { id: "library.a", version: "1.0.0", - specVersion: "2.3", path: libraryBPath, // B, not A - inconsistency! configuration: { specVersion: "2.3", @@ -568,12 +557,7 @@ test("Inconsistent dependencies with same ID", async (t) => { { id: "library.a", version: "1.0.0", - specVersion: "2.3", path: libraryAPath, - type: "library", - metadata: { - name: "library.a", - }, dependencies: [] } ] From a736acf910b7a1a870066bc899ba34e8098ce4f4 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 25 May 2022 08:43:12 +0200 Subject: [PATCH 69/99] ApplicationBuilder: Fix componentPreload excludes config --- lib/buildDefinitions/ApplicationBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/buildDefinitions/ApplicationBuilder.js b/lib/buildDefinitions/ApplicationBuilder.js index 8d9323a2b..d01c6ed42 100644 --- a/lib/buildDefinitions/ApplicationBuilder.js +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -46,7 +46,7 @@ class ApplicationBuilder extends AbstractBuilder { const componentPreloadPaths = project.getComponentPreloadPaths(); const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); - const componentPreloadExcludes = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadExcludes(); if (componentPreloadPaths.length || componentPreloadNamespaces.length) { this.addTask("generateComponentPreload", { options: { From 717d4478b14197b52b48e22d69a72f4e638384e9 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 26 May 2022 16:23:15 +0200 Subject: [PATCH 70/99] [INTERNAL] Library: Add tests from ui5-builder --- lib/specifications/types/Library.js | 78 +- test/lib/specifications/types/Library.js | 973 ++++++++++++++++++++++- 2 files changed, 1009 insertions(+), 42 deletions(-) diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 7c2608e64..be9fb9391 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -118,18 +118,14 @@ class Library extends ComponentProject { this._namespace = await this._getNamespace(); if (!config.metadata.copyright) { - try { - config.metadata.copyright = await this._getCopyright(); - } catch (err) { - // Catch error because copyright is optional - // TODO: Make copyright mandatory? - this._log.verbose( - `Failed to get copyright for project ${this.getName()}: ${err.message}`); + const copyright = await this._getCopyrightFromDotLibrary(); + if (copyright) { + config.metadata.copyright = copyright; } } if (this.isFrameworkProject()) { - if (config.builder && config.builder.libraryPreload && config.builder.libraryPreload.excludes) { + if (config.builder?.libraryPreload?.excludes) { this._log.verbose( `Using preload excludes for framework library ${this.getName()} from project configuration`); } else { @@ -259,8 +255,9 @@ class Library extends ComponentProject { } else { try { const libraryJsPath = await this._getLibraryJsPath(); - namespace = posixPath.dirname(libraryJsPath); - if (!namespace || namespace === "/") { + namespacePath = posixPath.dirname(libraryJsPath); + namespace = namespacePath.replace("/", ""); // remove leading slash + if (namespace === "") { throw new Error(`Found library.js file in root directory. ` + `Expected it to be in namespace directory.`); } @@ -313,8 +310,8 @@ class Library extends ComponentProject { async _getNamespaceFromDotLibrary() { try { const {content: dotLibrary, filePath} = await this._getDotLibrary(); - if (dotLibrary && dotLibrary.library && dotLibrary.library.name) { - const namespace = dotLibrary.library.name._; + const namespace = dotLibrary?.library?.name?._; + if (namespace) { this._log.verbose( `Found namespace ${namespace} in .library file of project ${this.getName()} ` + `at ${filePath}`); @@ -338,31 +335,28 @@ class Library extends ComponentProject { /** * Determines library copyright from given project configuration with fallback to .library. * - * @returns {string} Copyright of the project - * @throws {Error} if copyright can not be determined + * @returns {string|null} Copyright of the project */ - async _getCopyright() { + async _getCopyrightFromDotLibrary() { // If no copyright replacement was provided by ui5.yaml, // check if the .library file has a valid copyright replacement - const {content: dotLibrary} = await this._getDotLibrary(); - if (dotLibrary && dotLibrary.library && dotLibrary.library.copyright) { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + if (dotLibrary?.library?.copyright?._) { this._log.verbose( - `Using copyright from .library for project ${this.getName()}...`); + `Using copyright from ${filePath} for project ${this.getName()}...`); return dotLibrary.library.copyright._; } else { - throw new Error(`No copyright configuration found in .library ` + + this._log.verbose( + `No copyright configuration found in ${filePath} ` + `of project ${this.getName()}`); + return null; } } async _getPreloadExcludesFromDotLibrary() { const {content: dotLibrary, filePath} = await this._getDotLibrary(); - if (dotLibrary && dotLibrary.library && dotLibrary.library.appData && - dotLibrary.library.appData.packaging && - dotLibrary.library.appData.packaging["all-in-one"] && - dotLibrary.library.appData.packaging["all-in-one"].exclude - ) { - let excludes = dotLibrary.library.appData.packaging["all-in-one"].exclude; + let excludes = dotLibrary?.library?.appData?.packaging?.["all-in-one"]?.exclude; + if (excludes) { if (!Array.isArray(excludes)) { excludes = [excludes]; } @@ -374,7 +368,6 @@ class Library extends ComponentProject { }); } else { this._log.verbose( - `No preload excludes found in .library of project ${this.getName()} ` + `at ${filePath}`); return null; @@ -385,7 +378,7 @@ class Library extends ComponentProject { * Reads the projects manifest.json * * @returns {Promise} resolves with an object containing the content (as JSON) and - * fsPath (as string) of the manifest.json file + * filePath (as string) of the manifest.json file */ async _getManifest() { if (this._pManifest) { @@ -408,7 +401,7 @@ class Library extends ComponentProject { }; } catch (err) { throw new Error( - `Failed to read manifest.json for project ${this.getName()}: ${err.message}`); + `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); } }); } @@ -417,7 +410,7 @@ class Library extends ComponentProject { * Reads the .library file * * @returns {Promise} resolves with an object containing the content (as JSON) and - * fsPath (as string) of the .library file + * filePath (as string) of the .library file */ async _getDotLibrary() { if (this._pDotLibrary) { @@ -434,17 +427,22 @@ class Library extends ComponentProject { } const resource = dotLibraryResources[0]; const content = await resource.getString(); - const xml2js = require("xml2js"); - const parser = new xml2js.Parser({ - explicitArray: false, - explicitCharkey: true - }); - const readXML = promisify(parser.parseString); - const contentJson = await readXML(content); - return { - content: contentJson, - filePath: resource.getPath() - }; + + try { + const xml2js = require("xml2js"); + const parser = new xml2js.Parser({ + explicitArray: false, + explicitCharkey: true + }); + const readXML = promisify(parser.parseString); + return { + content: await readXML(content), + filePath: resource.getPath() + }; + } catch (err) { + throw new Error( + `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`); + } }); } diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 3e5561001..6d2454ee7 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -1,7 +1,8 @@ const test = require("ava"); const path = require("path"); +const sinon = require("sinon"); +const mock = require("mock-require"); const Specification = require("../../../../lib/specifications/Specification"); -const Library = require("../../../../lib/specifications/types/Library"); function clone(obj) { return JSON.parse(JSON.stringify(obj)); @@ -30,7 +31,13 @@ const basicProjectInput = { } }; +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + test("Correct class", async (t) => { + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); const project = await Specification.create(basicProjectInput); t.true(project instanceof Library, `Is an instance of the Library class`); }); @@ -79,7 +86,6 @@ test("Access project test-resources via reader: buildtime style", async (t) => { t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path"); }); - test("Access project test-resources via reader: runtime style", async (t) => { const project = await Specification.create(basicProjectInput); const reader = await project.getReader({style: "runtime"}); @@ -126,3 +132,966 @@ test("Modify project resources via workspace and access via flat and runtime rea t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content (runtime)"); }); + +test("_parseConfiguration: Get copyright", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); +}); + +test("_parseConfiguration: Copyright already configured", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.metadata.copyright = "My copyright"; + const project = await Specification.create(projectInput); + + t.deepEqual(project.getCopyright(), "My copyright", "Copyright was not altered"); +}); + +test.serial("_parseConfiguration: Copyright retrieval fails", async (t) => { + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "_getCopyrightFromDotLibrary").resolves(null); + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getCopyright(), undefined, "Copyright was not altered"); +}); + +test.serial("_parseConfiguration: Preload excludes from .library", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + const loggerVerboseSpy = sinon.spy(loggerInstance, "verbose"); + + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]); + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test("_parseConfiguration: Preload excludes from project configuration (non-framework library)", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.builder = { + libraryPreload: { + excludes: ["test/exclude/**"] + } + }; + const project = await Specification.create(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); +}); + +test.serial("_parseConfiguration: Preload exclude fallback to .library (framework libraries only)", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + const loggerVerboseSpy = sinon.spy(loggerInstance, "verbose"); + + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]); + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test.serial("_parseConfiguration: No preload excludes from .library", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + const loggerVerboseSpy = sinon.spy(loggerInstance, "verbose"); + + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(null); + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), [], + "No library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "No preload excludes defined in project configuration of framework library library.d. " + + "Falling back to .library..." + ]); +}); + +test.serial("_parseConfiguration: Preload excludes from project configuration (framework library)", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + const loggerVerboseSpy = sinon.spy(loggerInstance, "verbose"); + + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "isFrameworkProject").returns(true); + const getPreloadExcludesFromDotLibraryStub = + sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves([]); + + const projectInput = clone(basicProjectInput); + projectInput.configuration.builder = { + libraryPreload: { + excludes: ["test/exclude/**"] + } + }; + const project = await Specification.create(projectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"], + "Correct library preload excludes have been set"); + + t.deepEqual(loggerVerboseSpy.getCall(10).args, [ + "Using preload excludes for framework library library.d from project configuration" + ]); + + t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called"); +}); + +test.serial("_parseConfiguration: No preload exclude fallback for non-framework libraries", async (t) => { + const Library = mock.reRequire("../../../../lib/specifications/types/Library"); + + sinon.stub(Library.prototype, "isFrameworkProject").returns(false); + const getPreloadExcludesFromDotLibraryStub = sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary") + .resolves(["test/exclude/**"]); + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getLibraryPreloadExcludes(), [], + "No library preload excludes have been set"); + t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called"); +}); + +test("_getManifest: Reads correctly", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const {content, filePath} = await project._getManifest(); + t.is(content.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getManifest: No manifest.json", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, + "Could not find manifest.json file for project library.d", + "Rejected with correct error message"); +}); + +test("_getManifest: Invalid JSON", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `no pony`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, + "Failed to read some path for project library.d: " + + "Unexpected token o in JSON at position 1", + "Rejected with correct error message"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getManifest: Propagates exception", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getManifest: Multiple manifest.json files", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }, { + getString: async () => `{"pony": "no shark"}`, + getPath: () => "some other path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getManifest()); + t.deepEqual(error.message, "Found multiple (2) manifest.json files for project library.d", + "Rejected with correct error message"); +}); + +test("_getManifest: Result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `{"pony": "no unicorn"}`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pManifest = null; // Clear cache from instantiation + const {content: content1, filePath: filePath1} = await project._getManifest(); + t.is(content1.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath1, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); + const {content: content2, filePath: filePath2} = await project._getManifest(); + + t.is(content2.pony, "no unicorn", "manifest.json content has been read"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: Reads correctly", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const {content, filePath} = await project._getDotLibrary(); + t.deepEqual(content, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: No .library file", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "Could not find .library file for project library.d", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Invalid XML", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `no pony`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "Failed to read some path for project library.d: " + + "Non-whitespace before first tag.\nLine: 0\nColumn: 1\nChar: n", + "Rejected with correct error message"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getDotLibrary: Propagates exception", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Multiple .library files", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }, { + getString: async () => `Hungry`, + getPath: () => "some other path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getDotLibrary()); + t.deepEqual(error.message, "Found multiple (2) .library files for project library.d", + "Rejected with correct error message"); +}); + +test("_getDotLibrary: Result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getString: async () => `Fancy`, + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pDotLibrary = null; // Clear cache from instantiation + const {content: content1, filePath: filePath1} = await project._getDotLibrary(); + t.deepEqual(content1, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath1, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); + const {content: content2, filePath: filePath2} = await project._getDotLibrary(); + + t.deepEqual(content2, {chicken: {_: "Fancy"}}, ".library content has been read"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments"); +}); + +test("_getLibraryJsPath: Reads correctly", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const filePath = await project._getLibraryJsPath(); + t.deepEqual(filePath, "some path", "Expected library.js path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); +}); + +test("_getLibraryJsPath: No library.js file", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.is(error.message, + "Could not find library.js file for project library.d", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Propagates exception", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().rejects(new Error("because shark")); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.is(error.message, + "because shark", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Multiple library.js files", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }, { + getPath: () => "some other path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const error = await t.throwsAsync(project._getLibraryJsPath()); + t.deepEqual(error.message, "Found multiple (2) library.js files for project library.d", + "Rejected with correct error message"); +}); + +test("_getLibraryJsPath: Result is cached", async (t) => { + const project = await Specification.create(basicProjectInput); + const byGlobStub = sinon.stub().resolves([{ + getPath: () => "some path" + }]); + + project._getSourceReader = () => { + return { + byGlob: byGlobStub + }; + }; + project._pLibraryJs = null; // Clear cache from instantiation + const filePath1 = await project._getLibraryJsPath(); + t.deepEqual(filePath1, "some path", "Expected library.js path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); + + const filePath2 = await project._getLibraryJsPath(); + t.deepEqual(filePath2, "some path", "Expected library.js path"); + t.is(filePath2, "some path", "Correct path"); + t.is(byGlobStub.callCount, 1, "byGlob got called once"); + t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments"); +}); + +test.serial("_getNamespace: namespace resolution fails", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + mock.reRequire("../../../../lib/specifications/types/Library"); + + const project = await Specification.create(basicProjectInput); + + sinon.stub(project, "_getNamespaceFromManifest").resolves({}); + sinon.stub(project, "_getNamespaceFromDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").rejects(new Error("pony error")); + const loggerVerboseSpy = sinon.spy(loggerInstance, "verbose"); + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, "Failed to detect namespace or namespace is empty for project library.d." + + " Check verbose log for details."); + + t.deepEqual(loggerVerboseSpy.callCount, 2, "2 calls to log.verbose should be done"); + const logVerboseCalls = loggerVerboseSpy.getCalls().map((call) => call.args[0]); + + t.true(logVerboseCalls.includes( + "Failed to resolve namespace of project library.d from manifest.json or .library file. " + + "Falling back to library.js file path..."), + "should contain message for missing manifest.json"); + + t.true(logVerboseCalls.includes( + "Namespace resolution from library.js file path failed for project library.d: pony error"), + "should contain message for missing library.js"); +}); + +test("_getNamespace: from manifest.json with .library on same level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/mani-pony/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: "dot-pony"} + }, + filePath: "/mani-pony/.library" + }); + const res = await project._getNamespace(); + t.deepEqual(res, "mani-pony", "Returned correct namespace"); +}); + +test("_getNamespace: from manifest.json with .library on same level but different directory", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/mani-pony/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/different-pony/.library" + }); + + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + `Failed to detect namespace for project library.d: Found a manifest.json on the same directory level ` + + `but in a different directory than the .library file. They should be in the same directory.\n` + + ` manifest.json path: /mani-pony/manifest.json\n` + + ` is different to\n` + + ` .library path: /different-pony/.library`, + "Rejected with correct error message"); +}); + +test("_getNamespace: from manifest.json with not matching file path", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/different/namespace/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: "dot-pony"} + }, + filePath: "/different/namespace/.library" + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, `Detected namespace "mani-pony" does not match detected directory structure ` + + `"different/namespace" for project library.d`, "Rejected with correct error message"); +}); + +test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + mock.reRequire("../../../../lib/specifications/types/Library"); + + const project = await Specification.create(basicProjectInput); + const manifestPath = "/different/namespace/manifest.json"; + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + } + }, + filePath: manifestPath + }); + sinon.stub(project, "_getDotLibrary").resolves({}); + + const loggerSpy = sinon.spy(loggerInstance, "verbose"); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + `Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.`, + "Rejected with correct error message"); + t.is(loggerSpy.callCount, 4, "calls to verbose"); + + + t.is(loggerSpy.getCall(0).args[0], + `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`, + "correct verbose message"); +}); + +test("_getNamespace: from .library", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/dot-pony/.library" + }); + const res = await project._getNamespace(); + t.deepEqual(res, "dot-pony", "Returned correct namespace"); +}); + +test("_getNamespace: from .library with ignored manifest.json on lower level", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/namespace/somedir/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/dot-pony/.library" + }); + const res = await project._getNamespace(); + t.deepEqual(res, "dot-pony", "Returned correct namespace"); +}); + +test("_getNamespace: manifest.json on higher level than .library", async (t) => { + const manifestFsPath = "/namespace/manifest.json"; + const dotLibraryFsPath = "/namespace/morenamespace/.library"; + + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: manifestFsPath + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: dotLibraryFsPath + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + `Failed to detect namespace for project library.d: ` + + `Found a manifest.json on a higher directory level than the .library file. ` + + `It should be on the same or a lower level. ` + + `Note that a manifest.json on a lower level will be ignored.\n` + + ` manifest.json path: ${manifestFsPath}\n` + + ` is higher than\n` + + ` .library path: ${dotLibraryFsPath}`, + "Rejected with correct error message"); +}); + +test("_getNamespace: from .library with maven placeholder", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "${mvn-pony}"}} + }, + filePath: "/mvn-unicorn/.library" + }); + const resolveMavenPlaceholderStub = + sinon.stub(project, "_resolveMavenPlaceholder").resolves("mvn-unicorn"); + const res = await project._getNamespace(); + + t.deepEqual(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", + "resolveMavenPlaceholder called with correct argument"); + t.deepEqual(res, "mvn-unicorn", "Returned correct namespace"); +}); + +test("_getNamespace: from .library with not matching file path", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "mvn-pony"}} + }, + filePath: "/different/namespace/.library" + }); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, `Detected namespace "mvn-pony" does not match detected directory structure ` + + `"different/namespace" for project library.d`, + "Rejected with correct error message"); +}); + +test("_getNamespace: from library.js", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({}); + sinon.stub(project, "_getDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").resolves("/my/namespace/library.js"); + const res = await project._getNamespace(); + t.deepEqual(res, "my/namespace", "Returned correct namespace"); +}); + +test.serial("_getNamespace: from project root level library.js", async (t) => { + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("specifications:types:Library"); + + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + const loggerSpy = sinon.spy(loggerInstance, "verbose"); + + mock.reRequire("../../../../lib/specifications/types/Library"); + + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({}); + sinon.stub(project, "_getDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").resolves("/library.js"); + const err = await t.throwsAsync(project._getNamespace()); + + t.deepEqual(err.message, + "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.", + "Rejected with correct error message"); + + const logCalls = loggerSpy.getCalls().map((call) => call.args[0]); + t.true(logCalls.includes( + "Namespace resolution from library.js file path failed for project library.d: " + + "Found library.js file in root directory. " + + "Expected it to be in namespace directory."), + "should contain message for root level library.js"); +}); + +test("_getNamespace: neither manifest nor .library or library.js path contain it", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({}); + sinon.stub(project, "_getDotLibrary").resolves({}); + sinon.stub(project, "_getLibraryJsPath").rejects(new Error("Not found bla")); + const err = await t.throwsAsync(project._getNamespace()); + t.deepEqual(err.message, + "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.", + "Rejected with correct error message"); +}); + +test("_getNamespace: maven placeholder resolution fails", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "${mvn-pony}" + } + }, + filePath: "/not/used" + }); + sinon.stub(project, "_getDotLibrary").resolves({}); + const resolveMavenPlaceholderStub = + sinon.stub(project, "_resolveMavenPlaceholder") + .rejects(new Error("because squirrel")); + const err = await t.throwsAsync(project._getNamespace()); + t.deepEqual(err.message, + "Failed to resolve namespace maven placeholder of project library.d: because squirrel", + "Rejected with correct error message"); + t.deepEqual(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", + "resolveMavenPlaceholder called with correct argument"); +}); + +test("_getCopyrightFromDotLibrary", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + copyright: { + _: "copyleft" + } + } + } + }); + const copyright = await project._getCopyrightFromDotLibrary(); + t.is(copyright, "copyleft", "Returned correct copyright"); +}); + +test("_getCopyrightFromDotLibrary: No copyright in .library file", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const copyright = await project._getCopyrightFromDotLibrary(); + t.is(copyright, null, "No copyright returned"); +}); + +test("_getCopyrightFromDotLibrary: Propagates exception", async (t) => { + const project = await Specification.create(basicProjectInput); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const err = await t.throwsAsync(project._getCopyrightFromDotLibrary()); + t.is(err.message, "because shark", + "Threw with excepted error message"); +}); + +test("_getPreloadExcludesFromDotLibrary: Single exclude", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + appData: { + packaging: { + "all-in-one": { + exclude: { + $: { + name: "test/exclude/**" + } + } + } + } + } + } + } + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.deepEqual(excludes, [ + "test/exclude/**", + ], "_getPreloadExcludesFromDotLibrary should return array with excludes"); +}); + +test("_getPreloadExcludesFromDotLibrary: Multiple excludes", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + appData: { + packaging: { + "all-in-one": { + exclude: [ + { + $: { + name: "test/exclude1/**" + } + }, + { + $: { + name: "test/exclude2/**" + } + }, + { + $: { + name: "test/exclude3/**" + } + } + ] + } + } + } + } + } + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.deepEqual(excludes, [ + "test/exclude1/**", + "test/exclude2/**", + "test/exclude3/**" + ], "_getPreloadExcludesFromDotLibrary should return array with excludes"); +}); + +test("_getPreloadExcludesFromDotLibrary: No excludes in .library file", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const excludes = await project._getPreloadExcludesFromDotLibrary(); + t.is(excludes, null, "No excludes returned"); +}); + +test("_getPreloadExcludesFromDotLibrary: Propagates exception", async (t) => { + const project = await Specification.create(basicProjectInput); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const err = await t.throwsAsync(project._getPreloadExcludesFromDotLibrary()); + t.is(err.message, "because shark", + "Threw with excepted error message"); +}); + +test("_getNamespaceFromDotLibrary", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: { + name: { + _: "library namespace" + } + } + }, + filePath: "some path" + }); + const {namespace, filePath} = await project._getNamespaceFromDotLibrary(); + t.is(namespace, "library namespace", + "Returned correct namespace"); + t.is(filePath, "some path", + "Returned correct file path"); +}); + +test("_getNamespaceFromDotLibrary: No library name in .library file", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {} + }, + filePath: "some path" + }); + const res = await project._getNamespaceFromDotLibrary(); + t.deepEqual(res, {}, "Empty object returned"); +}); + +test("_getNamespaceFromDotLibrary: Does not propagate exception", async (t) => { + const project = await Specification.create(basicProjectInput); + + sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark")); + const res = await project._getNamespaceFromDotLibrary(); + t.deepEqual(res, {}, "Empty object returned"); +}); From ecda5c1604529575d3139667fd680a87836441ed Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 27 May 2022 09:37:49 +0200 Subject: [PATCH 71/99] [INTERNAL] Library: Add missing test for getNamespaceFromManifest --- lib/specifications/types/Library.js | 2 +- test/lib/specifications/types/Library.js | 36 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index be9fb9391..dba64ad75 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -295,7 +295,7 @@ class Library extends ComponentProject { filePath }; } else { - this._log.verbose( + throw new Error( `No sap.app/id configuration found in manifest.json of project ${this.getName()} ` + `at ${filePath}`); } diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 6d2454ee7..40b02d373 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -761,6 +761,7 @@ test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => t.is(loggerSpy.getCall(0).args[0], + `Namespace resolution from manifest.json failed for project library.d: ` + `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`, "correct verbose message"); }); @@ -1057,6 +1058,41 @@ test("_getPreloadExcludesFromDotLibrary: Propagates exception", async (t) => { "Threw with excepted error message"); }); +test("_getNamespaceFromManifest", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "library namespace" + } + }, + filePath: "some path" + }); + const {namespace, filePath} = await project._getNamespaceFromManifest(); + t.is(namespace, "library namespace", "Returned correct namespace"); + t.is(filePath, "some path", "Returned correct file path"); +}); + +test("_getNamespaceFromManifest: No ID in manifest.json file", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": {} + }, + filePath: "some path" + }); + const res = await project._getNamespaceFromManifest(); + t.deepEqual(res, {}, "Empty object returned"); +}); + +test("_getNamespaceFromManifest: Does not propagate exception", async (t) => { + const project = await Specification.create(basicProjectInput); + + sinon.stub(project, "_getManifest").rejects(new Error("because shark")); + const res = await project._getNamespaceFromManifest(); + t.deepEqual(res, {}, "Empty object returned"); +}); + test("_getNamespaceFromDotLibrary", async (t) => { const project = await Specification.create(basicProjectInput); sinon.stub(project, "_getDotLibrary").resolves({ From f2f607ff6eb27dcbc8f74668d3a423b12932f985 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 27 May 2022 10:39:13 +0200 Subject: [PATCH 72/99] [INTERNAL] Module: Add tests from ui5-builder --- lib/specifications/types/Module.js | 5 +- test/fixtures/module.a/dev/devTools.js | 1 + test/fixtures/module.a/dist/index.js | 1 + test/fixtures/module.a/ui5.yaml | 5 + test/lib/specifications/types/Library.js | 42 +++++++ test/lib/specifications/types/Module.js | 142 +++++++++++++++++++++++ 6 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/module.a/dev/devTools.js create mode 100644 test/fixtures/module.a/dist/index.js create mode 100644 test/fixtures/module.a/ui5.yaml create mode 100644 test/lib/specifications/types/Module.js diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 5aa6910b0..0cc2e5b60 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -71,13 +71,12 @@ class Module extends Project { async _configureAndValidatePaths(config) { await super._configureAndValidatePaths(config); - this._log.verbose(`Path mapping for library project ${this.getName()}:`); this._log.verbose(` Physical root path: ${this.getPath()}`); this._log.verbose(` Mapped to:`); this._log.verbose(` /resources/ => ${this._srcPath}`); - if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources?.configuration?.paths) { this._paths = await Promise.all(Object.entries(config.resources.configuration.paths) .map(async ([virBasePath, relFsPath]) => { this._log.verbose(` ${virBasePath} => ${relFsPath}`); @@ -86,7 +85,7 @@ class Module extends Project { `Unable to find directory '${relFsPath}' in module project ${this.getName()}`); } return { - name: `'${relFsPath}'' reader for moduleproject ${this.getName()}`, + name: `'${relFsPath}'' reader for module project ${this.getName()}`, virBasePath, fsBasePath: fsPath.join(this.getPath(), relFsPath), project: this diff --git a/test/fixtures/module.a/dev/devTools.js b/test/fixtures/module.a/dev/devTools.js new file mode 100644 index 000000000..e035bfaea --- /dev/null +++ b/test/fixtures/module.a/dev/devTools.js @@ -0,0 +1 @@ +console.log("dev dev dev"); diff --git a/test/fixtures/module.a/dist/index.js b/test/fixtures/module.a/dist/index.js new file mode 100644 index 000000000..019c0f4bc --- /dev/null +++ b/test/fixtures/module.a/dist/index.js @@ -0,0 +1 @@ +console.log("Hello World!"); diff --git a/test/fixtures/module.a/ui5.yaml b/test/fixtures/module.a/ui5.yaml new file mode 100644 index 000000000..af957cf1e --- /dev/null +++ b/test/fixtures/module.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.6" +type: module +metadata: + name: module.a diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 40b02d373..259cc9d3b 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -133,6 +133,48 @@ test("Modify project resources via workspace and access via flat and runtime rea "Found resource (byGlob) has expected (changed) content (runtime)"); }); +test("_configureAndValidatePaths: Default paths", async (t) => { + const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); + const projectInput = { + id: "library.e.id", + version: "1.0.0", + modulePath: libraryEPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "library", + metadata: { + name: "library.e", + } + } + }; + + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct default path for src"); + t.is(project._testPath, "test", "Correct default path for test"); + t.true(project._testPathExists, "Test path detected as existing"); +}); + +test("_configureAndValidatePaths: Test directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources.configuration.paths.test = "does/not/exist"; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "main/src", "Correct path for src"); + t.is(project._testPath, "does/not/exist", "Correct path for test"); + t.false(project._testPathExists, "Test path detected as non-existent"); +}); + + +test("_configureAndValidatePaths: Source directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources.configuration.paths.src = "does/not/exist"; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find directory 'does/not/exist' in library project library.d"); +}); + test("_parseConfiguration: Get copyright", async (t) => { const project = await Specification.create(basicProjectInput); diff --git a/test/lib/specifications/types/Module.js b/test/lib/specifications/types/Module.js new file mode 100644 index 000000000..952dd5a0e --- /dev/null +++ b/test/lib/specifications/types/Module.js @@ -0,0 +1,142 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const Specification = require("../../../../lib/specifications/Specification"); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const moduleA = path.join(__dirname, "..", "..", "..", "fixtures", "module.a"); +const basicProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: moduleA, + configuration: { + specVersion: "2.3", + kind: "project", + type: "module", + metadata: { + name: "module.a", + copyright: "Some fancy copyright" // allowed but ignored + }, + resources: { + configuration: { + paths: { + "/": "dist", + "/dev/": "dev" + } + } + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test("Correct class", async (t) => { + const Module = mock.reRequire("../../../../lib/specifications/types/Module"); + const project = await Specification.create(basicProjectInput); + t.true(project instanceof Module, `Is an instance of the Module class`); +}); + +test("Access project resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource1 = await reader.byPath("/dev/devTools.js"); + t.truthy(resource1, "Found the requested resource"); + t.is(resource1.getPath(), "/dev/devTools.js", "Resource has correct path"); + + const resource2 = await reader.byPath("/index.js"); + t.truthy(resource2, "Found the requested resource"); + t.is(resource2.getPath(), "/index.js", "Resource has correct path"); +}); + +test("Modify project resources via workspace and access via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + const workspaceResource = await workspace.byPath("/dev/devTools.js"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace(/dev/g, "duck duck"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = await project.getReader(); + const readerResource = await reader.byPath("/dev/devTools.js"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/dev/devTools.js", "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const gGlobResult = await reader.byGlob("**/devTools.js"); + t.is(gGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(gGlobResult[0].getPath(), "/dev/devTools.js", "Resource (byGlob) has correct path"); + t.is(await gGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("Modify project resources via workspace and access via reader for other path mapping", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + const workspaceResource = await workspace.byPath("/index.js"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("world", "duck"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = await project.getReader(); + const readerResource = await reader.byPath("/index.js"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/index.js", "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const gGlobResult = await reader.byGlob("**/index.js"); + t.is(gGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(gGlobResult[0].getPath(), "/index.js", "Resource (byGlob) has correct path"); + t.is(await gGlobResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("_configureAndValidatePaths: Default path mapping", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = {}; + const project = await Specification.create(projectInput); + + t.is(project._paths.length, 1, "One default path mapping"); + t.is(project._paths[0].virBasePath, "/", "Default path mapping for /"); + t.is(project._paths[0].fsBasePath, projectInput.modulePath, "Correct fs path"); +}); + +test("_configureAndValidatePaths: Configured path mapping", async (t) => { + const projectInput = clone(basicProjectInput); + const project = await Specification.create(projectInput); + + t.is(project._paths.length, 2, "Two path mappings"); + t.is(project._paths[0].virBasePath, "/", "Correct virtual base path for /"); + t.is(project._paths[0].fsBasePath, projectInput.modulePath + "/dist", "Correct fs path"); + t.is(project._paths[1].virBasePath, "/dev/", "Correct virtual base path for /dev/"); + t.is(project._paths[1].fsBasePath, projectInput.modulePath + "/dev", "Correct fs path"); +}); + +test("_configureAndValidatePaths: Default directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = {}; + projectInput.modulePath = "does/not/exist"; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find root directory of module project module.a"); +}); + +test("_configureAndValidatePaths: Directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources.configuration.paths.doesNotExist = "does/not/exist"; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find directory 'does/not/exist' in module project module.a"); +}); From 46968e4bb9190cd58c7c7724067fa12ed3abe8aa Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 27 May 2022 11:04:16 +0200 Subject: [PATCH 73/99] [INTERNAL] Application: Add missing tests for _configureAndValidatePaths --- lib/specifications/types/Application.js | 6 ++ test/lib/specifications/ComponentProject.js | 18 ++++++ test/lib/specifications/types/Application.js | 64 +++++++++++++++++--- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 17e608d88..38123c0e8 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -15,6 +15,12 @@ class Application extends ComponentProject { /* === Attributes === */ + + /** + * Get the cachebuster signature type configuration of the project + * + * @returns {string} time or hash + */ getCachebusterSignatureType() { return this._config.builder && this._config.builder.cachebuster && this._config.builder.cachebuster.signatureType || "time"; diff --git a/test/lib/specifications/ComponentProject.js b/test/lib/specifications/ComponentProject.js index 9b867f59d..ab2d4f426 100644 --- a/test/lib/specifications/ComponentProject.js +++ b/test/lib/specifications/ComponentProject.js @@ -24,6 +24,24 @@ test.afterEach.always((t) => { sinon.restore(); }); +test("getPropertiesFileSourceEncoding: Default", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + +test("getPropertiesFileSourceEncoding: Configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.resources = { + configuration: { + propertiesFileSourceEncoding: "ISO-8859-1" + } + }; + const project = await Specification.create(customProjectInput); + t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", + "Returned correct default propertiesFileSourceEncoding configuration"); +}); + test("hasMavenPlaceholder: has maven placeholder", async (t) => { const project = await Specification.create(basicProjectInput); const res = project._hasMavenPlaceholder("${mvn-pony}"); diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index 156366224..05b4d9c68 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -30,22 +30,22 @@ test("Correct class", async (t) => { t.true(project instanceof Application, `Is an instance of the Application class`); }); -test("getPropertiesFileSourceEncoding: Default", async (t) => { +test("getCachebusterSignatureType: Default", async (t) => { const project = await Specification.create(basicProjectInput); - t.is(project.getPropertiesFileSourceEncoding(), "UTF-8", - "Returned correct default propertiesFileSourceEncoding configuration"); + t.is(project.getCachebusterSignatureType(), "time", + "Returned correct default cachebuster signature type configuration"); }); -test("getPropertiesFileSourceEncoding: Configuration", async (t) => { +test("getCachebusterSignatureType: Configuration", async (t) => { const customProjectInput = clone(basicProjectInput); - customProjectInput.configuration.resources = { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1" + customProjectInput.configuration.builder = { + cachebuster: { + signatureType: "hash" } }; const project = await Specification.create(customProjectInput); - t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1", - "Returned correct default propertiesFileSourceEncoding configuration"); + t.is(project.getCachebusterSignatureType(), "hash", + "Returned correct default cachebuster signature type configuration"); }); test("Access project resources via reader: buildtime style", async (t) => { @@ -105,6 +105,52 @@ test("Modify project resources via workspace and access via flat and runtime rea t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); }); +test("_configureAndValidatePaths: Default paths", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.is(project._webappPath, "webapp", "Correct default path"); +}); + +test("_configureAndValidatePaths: Custom webapp directory", async (t) => { + const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h"); + const projectInput = { + id: "application.h.id", + version: "1.0.0", + modulePath: applicationHPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.h"}, + resources: { + configuration: { + paths: { + webapp: "webapp-properties.componentName" + } + } + } + } + }; + + const project = await Specification.create(projectInput); + + t.is(project._webappPath, "webapp-properties.componentName", "Correct path for src"); +}); + +test("_configureAndValidatePaths: Webapp directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = { + configuration: { + paths: { + webapp: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find directory 'does/not/exist' in application project application.a"); +}); + test("_getNamespaceFromManifestJson: No 'sap.app' configuration found", async (t) => { const project = await Specification.create(basicProjectInput); sinon.stub(project, "_getManifest").resolves({}); From 6bae28267c448666c0302be8d7fcafe0a6e78fa2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 1 Jun 2022 17:44:21 +0200 Subject: [PATCH 74/99] [INTERNAL] ComponentProject: Allow writing resources outside of namespace --- lib/specifications/ComponentProject.js | 129 ++++++++++-------- lib/specifications/types/Application.js | 4 +- lib/specifications/types/Library.js | 68 +++++---- lib/specifications/types/ThemeLibrary.js | 1 + test/fixtures/library.h/src/.library | 11 ++ test/fixtures/library.h/src/manifest.json | 26 ++++ test/fixtures/library.h/src/some.js | 4 + test/fixtures/library.h/ui5.yaml | 5 + .../theme/library/e/themes/my_theme/.theme | 9 ++ .../theme/library/e/themes/my_theme/.theming | 27 ++++ .../e/themes/my_theme/library.source.less | 9 ++ .../test/theme/library/e/Test.html | 0 test/fixtures/theme.library.e/ui5.yaml | 9 ++ test/lib/specifications/types/Application.js | 46 ++++++- test/lib/specifications/types/Library.js | 106 +++++++++++--- test/lib/specifications/types/ThemeLibrary.js | 122 +++++++++++++++++ 16 files changed, 465 insertions(+), 111 deletions(-) create mode 100644 test/fixtures/library.h/src/.library create mode 100644 test/fixtures/library.h/src/manifest.json create mode 100644 test/fixtures/library.h/src/some.js create mode 100644 test/fixtures/library.h/ui5.yaml create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less create mode 100644 test/fixtures/theme.library.e/test/theme/library/e/Test.html create mode 100644 test/fixtures/theme.library.e/ui5.yaml create mode 100644 test/lib/specifications/types/ThemeLibrary.js diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index ec9de3971..2fad4065d 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -101,32 +101,27 @@ class ComponentProject extends Project { * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance */ getReader({style = "buildtime"} = {}) { - // TODO: Additional parameter 'includeWorkspace' to include reader to relevant Memory Adapter? // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? + + if (style === "runtime" && this._isRuntimeNamespaced) { + // If the project's runtime requires namespaces, paths are identical to "buildtime" style + style = "buildtime"; + } let reader; - let testReader; switch (style) { case "buildtime": - reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); - testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); - if (testReader) { - reader = resourceFactory.createReaderCollection({ - name: `Reader collection for project ${this.getName()}`, - readers: [reader, testReader] - }); - } + reader = this._getReader(); break; case "runtime": - if (this._isRuntimeNamespaced) { - // Same as buildtime - return this.getReader(); - } - // TODO 3.0: Refactor this - return this.getReader().link({ + // No test-resources for runtime resource access, + // unless runtime is namespaced + reader = this.getReader().link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); + break; case "flat": + // No test-resources for flat resource access reader = this._getFlatSourceReader("/"); break; default: @@ -138,16 +133,7 @@ class ComponentProject extends Project { } /** - * Get a resource reader for the sources of the project (not including any test resources) - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getFullSourceReader() { - throw new Error(`_getFullSourceReader must be implemented by subclass ${this.constructor.name}`); - } - - /** - * TODO + * Get a resource reader for the resources of the project * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ @@ -156,15 +142,7 @@ class ComponentProject extends Project { } /** - * Get a resource reader for the test-sources of the project - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getFullTestReader() { - throw new Error(`_getFullTestReader must be implemented by subclass ${this.constructor.name}`); - } - /** - * TODO + * Get a resource reader for the test resources of the project * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ @@ -180,63 +158,98 @@ class ComponentProject extends Project { */ getWorkspace() { // Workspace is always of style "buildtime" - const reader = this.getReader({ - style: "buildtime" - }); - - const writer = this._getWriter(); return resourceFactory.createWorkspace({ - reader, - writer + name: `Workspace for project ${this.getName()}`, + reader: this._getReader(), + writer: this._getWriter().collection }); } _getWriter() { - if (!this._writer) { + if (!this._writers) { // writer is always of style "buildtime" - this._writer = resourceFactory.createAdapter({ + const namespaceWriter = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + + const generalWriter = resourceFactory.createAdapter({ virBasePath: "/", project: this }); + + const collection = resourceFactory.createWriterCollection({ + name: `Writers for project ${this.getName()}`, + writerMapping: { + [`/resources/${this._namespace}/`]: namespaceWriter, + [`/test-resources/${this._namespace}/`]: namespaceWriter, + [`/`]: generalWriter + } + }); + + this._writers = { + namespaceWriter, + generalWriter, + collection + }; + } + return this._writers; + } + + _getReader() { + let reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); + const testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); } - return this._writer; + return reader; } - _addWriter(reader, style = "buildtime") { - let writer = this._getWriter(); + _addWriter(reader, style) { + const {namespaceWriter, generalWriter} = this._getWriter(); + + if (style === "runtime" && this._isRuntimeNamespaced) { + // If the project's runtime requires namespaces, "runtime" paths are identical to "buildtime" paths + style = "buildtime"; + } + const readers = []; switch (style) { case "buildtime": { // Writer already uses buildtime style + readers.push(namespaceWriter); + readers.push(generalWriter); break; } case "runtime": { - if (this._isRuntimeNamespaced) { - // Same as buildtime - return this._addWriter(reader); - } - - // Rewrite paths from "runtime" to "buildtime" - writer = writer.link({ + // Runtime is not namespaced: link namespace to / + readers.push(namespaceWriter.link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` - }); + })); + // Add general writer as is + readers.push(generalWriter); break; } case "flat": { // Rewrite paths from "flat" to "buildtime" - writer = writer.link({ + readers.push(namespaceWriter.link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` - }); + })); + // General writer resources can't be flattened, so they are not available break; } default: throw new Error(`Unknown path mapping style ${style}`); } + readers.push(reader); return resourceFactory.createReaderCollectionPrioritized({ name: `Reader/Writer collection for project ${this.getName()}`, - readers: [writer, reader] + readers }); } diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 38123c0e8..d11509704 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -46,7 +46,7 @@ class Application extends ComponentProject { return null; // Applications do not have a dedicated test directory } - _getSourceReader() { + _getRawSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath: "/", @@ -190,7 +190,7 @@ class Application extends ComponentProject { if (this._pManifests[filePath]) { return this._pManifests[filePath]; } - return this._pManifests[filePath] = this._getSourceReader().byPath(filePath) + return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath) .then(async (resource) => { if (!resource) { throw new Error( diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index dba64ad75..53c72d013 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -15,6 +15,8 @@ class Library extends ComponentProject { this._srcPath = "src"; this._testPath = "test"; this._testPathExists = false; + this._isSourceNamespaced = true; + this._propertiesFilesSourceEncoding = "UTF-8"; } @@ -29,27 +31,14 @@ class Library extends ComponentProject { } /* === Resource Access === */ - - /** - * - * Get a resource reader for the sources of the project (excluding any test resources) - * In the future the path structure can be flat or namespaced depending on the project - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getSourceReader() { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/", - name: `Source reader for library project ${this.getName()}`, - project: this - }); - } - _getFlatSourceReader(virBasePath = "/") { // TODO: Throw for libraries with additional namespaces like sap.ui.core? + let fsBasePath = fsPath.join(this.getPath(), this._srcPath); + if (this._isSourceNamespaced) { + fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + } return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath, ...this._namespace.split("/")), + fsBasePath, virBasePath, name: `Source reader for library project ${this.getName()}`, project: this @@ -60,8 +49,12 @@ class Library extends ComponentProject { if (!this._testPathExists) { return null; } + let fsBasePath = fsPath.join(this.getPath(), this._testPath); + if (this._isSourceNamespaced) { + fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + } const testReader = resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._testPath, ...this._namespace.split("/")), + fsBasePath, virBasePath, name: `Runtime test-resources reader for library project ${this.getName()}`, project: this @@ -69,6 +62,22 @@ class Library extends ComponentProject { return testReader; } + /** + * + * Get a resource reader for the sources of the project (excluding any test resources) + * In the future the path structure can be flat or namespaced depending on the project + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getRawSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/", + name: `Source reader for library project ${this.getName()}`, + project: this + }); + } + /* === Internals === */ /** * @private @@ -246,11 +255,16 @@ class Library extends ComponentProject { } namespace = libraryNs.replace(/\./g, "/"); - namespacePath = namespacePath.replace("/", ""); // remove leading slash - if (namespacePath !== namespace) { - throw new Error( - `Detected namespace "${namespace}" does not match detected directory ` + - `structure "${namespacePath}" for project ${this.getName()}`); + if (namespacePath === "/") { + this._log.verbose(`Detected flat library source structure for project ${this.getName()}`); + this._isSourceNamespaced = false; + } else { + namespacePath = namespacePath.replace("/", ""); // remove leading slash + if (namespacePath !== namespace) { + throw new Error( + `Detected namespace "${namespace}" does not match detected directory ` + + `structure "${namespacePath}" for project ${this.getName()}`); + } } } else { try { @@ -384,7 +398,7 @@ class Library extends ComponentProject { if (this._pManifest) { return this._pManifest; } - return this._pManifest = this._getSourceReader().byGlob("**/manifest.json") + return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json") .then(async (manifestResources) => { if (!manifestResources.length) { throw new Error(`Could not find manifest.json file for project ${this.getName()}`); @@ -416,7 +430,7 @@ class Library extends ComponentProject { if (this._pDotLibrary) { return this._pDotLibrary; } - return this._pDotLibrary = this._getSourceReader().byGlob("**/.library") + return this._pDotLibrary = this._getRawSourceReader().byGlob("**/.library") .then(async (dotLibraryResources) => { if (!dotLibraryResources.length) { throw new Error(`Could not find .library file for project ${this.getName()}`); @@ -456,7 +470,7 @@ class Library extends ComponentProject { if (this._pLibraryJs) { return this._pLibraryJs; } - return this._pLibraryJs = this._getSourceReader().byGlob("**/library.js") + return this._pLibraryJs = this._getRawSourceReader().byGlob("**/library.js") .then(async (libraryJsResources) => { if (!libraryJsResources.length) { throw new Error(`Could not find library.js file for project ${this.getName()}`); diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index b0f2b635d..54154ec1d 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -8,6 +8,7 @@ class ThemeLibrary extends Project { this._srcPath = "src"; this._testPath = "test"; + this._testPathExists = false; this._writer = null; } diff --git a/test/fixtures/library.h/src/.library b/test/fixtures/library.h/src/.library new file mode 100644 index 000000000..8de6bd2eb --- /dev/null +++ b/test/fixtures/library.h/src/.library @@ -0,0 +1,11 @@ + + + + library.h + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/test/fixtures/library.h/src/manifest.json b/test/fixtures/library.h/src/manifest.json new file mode 100644 index 000000000..2279cb6ce --- /dev/null +++ b/test/fixtures/library.h/src/manifest.json @@ -0,0 +1,26 @@ +{ + "_version": "1.21.0", + "sap.app": { + "id": "library.h", + "type": "library", + "embeds": [], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "Library H", + "description": "Library H" + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.0", + "libs": {} + }, + "library": { + "i18n": false + } + } +} diff --git a/test/fixtures/library.h/src/some.js b/test/fixtures/library.h/src/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/library.h/src/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/test/fixtures/library.h/ui5.yaml b/test/fixtures/library.h/ui5.yaml new file mode 100644 index 000000000..cbea83db5 --- /dev/null +++ b/test/fixtures/library.h/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.6" +type: library +metadata: + name: library.h diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme new file mode 100644 index 000000000..4c62f2611 --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme @@ -0,0 +1,9 @@ + + + + my_theme + me + ${copyright} + ${version} + + \ No newline at end of file diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming new file mode 100644 index 000000000..83b6c785a --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming @@ -0,0 +1,27 @@ +{ + "sEntity": "Theme", + "sId": "sap_belize", + "oExtends": "base", + "sVendor": "SAP", + "aBundled": ["sap_belize_plus"], + "mCssScopes": { + "library": { + "sBaseFile": "library", + "sEmbeddingMethod": "APPEND", + "aScopes": [ + { + "sLabel": "Contrast", + "sSelector": "sapContrast", + "sEmbeddedFile": "sap_belize_plus.library", + "sEmbeddedCompareFile": "library", + "sThemeIdSuffix": "Contrast", + "sThemability": "PUBLIC", + "aThemabilityFilter": [ + "Color" + ], + "rExcludeSelector": "\\.sapContrastPlus\\W" + } + ] + } + } +} diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less new file mode 100644 index 000000000..d3286002b --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less @@ -0,0 +1,9 @@ +/*! + * ${copyright} + */ + +@mycolor: blue; + +.sapUiBody { + background-color: @mycolor; +} diff --git a/test/fixtures/theme.library.e/test/theme/library/e/Test.html b/test/fixtures/theme.library.e/test/theme/library/e/Test.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/theme.library.e/ui5.yaml b/test/fixtures/theme.library.e/ui5.yaml new file mode 100644 index 000000000..cf89c2432 --- /dev/null +++ b/test/fixtures/theme.library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "1.1" +type: theme-library +metadata: + name: theme.library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index 05b4d9c68..769e9d495 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -1,6 +1,7 @@ const test = require("ava"); const path = require("path"); const sinon = require("sinon"); +const {createResource} = require("@ui5/fs").resourceFactory; const Specification = require("../../../../lib/specifications/Specification"); const Application = require("../../../../lib/specifications/types/Application"); @@ -105,6 +106,45 @@ test("Modify project resources via workspace and access via flat and runtime rea t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); }); + +test("Read and write resources outside of app namespace", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + + await workspace.write(createResource({ + path: "/resources/my-custom-bundle.js" + })); + + const buildtimeReader = await project.getReader({style: "buildtime"}); + const buildtimeReaderResource = await buildtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(buildtimeReaderResource, "Found the requested resource byPath (buildtime)"); + t.is(buildtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (buildtime)"); + + const buildtimeGlobResult = await buildtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(buildtimeGlobResult.length, 1, "Found the requested resource byGlob (buildtime)"); + t.is(buildtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (buildtime)"); + + const flatReader = await project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/resources/my-custom-bundle.js"); + t.falsy(flatReaderResource, "Resource outside of app namespace can't be read using flat reader"); + + const flatGlobResult = await flatReader.byGlob("**/my-custom-bundle.js"); + t.is(flatGlobResult.length, 0, "Resource outside of app namespace can't be found using flat reader"); + + const runtimeReader = await project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)"); + t.is(runtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (runtime)"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)"); + t.is(runtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (runtime)"); +}); + test("_configureAndValidatePaths: Default paths", async (t) => { const project = await Specification.create(basicProjectInput); @@ -267,7 +307,7 @@ test("_getManifest: invalid JSON", async (t) => { getString: async () => "no json" }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; @@ -299,7 +339,7 @@ test.serial("_getManifest: result is cached", async (t) => { getString: async () => `{"pony": "no unicorn"}` }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; @@ -324,7 +364,7 @@ test.serial("_getManifest: Caches successes and failures", async (t) => { getString: getStringStub }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 259cc9d3b..2fb78dfa5 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -31,6 +31,21 @@ const basicProjectInput = { } }; +const libraryHPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.h"); +const flatProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryHPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "library", + metadata: { + name: "library.h", + } + } +}; + test.afterEach.always((t) => { sinon.restore(); mock.stopAll(); @@ -133,6 +148,14 @@ test("Modify project resources via workspace and access via flat and runtime rea "Found resource (byGlob) has expected (changed) content (runtime)"); }); +test("Access flat project resources via reader: buildtime style", async (t) => { + const project = await Specification.create(flatProjectInput); + const reader = await project.getReader({style: "buildtime"}); + const resource = await reader.byPath("/resources/library/h/some.js"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/library/h/some.js", "Resource has correct path"); +}); + test("_configureAndValidatePaths: Default paths", async (t) => { const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); const projectInput = { @@ -166,7 +189,6 @@ test("_configureAndValidatePaths: Test directory does not exist", async (t) => { t.false(project._testPathExists, "Test path detected as non-existent"); }); - test("_configureAndValidatePaths: Source directory does not exist", async (t) => { const projectInput = clone(basicProjectInput); projectInput.configuration.resources.configuration.paths.src = "does/not/exist"; @@ -340,7 +362,7 @@ test("_getManifest: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -357,7 +379,7 @@ test("_getManifest: No manifest.json", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -376,7 +398,7 @@ test("_getManifest: Invalid JSON", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -395,7 +417,7 @@ test("_getManifest: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -417,7 +439,7 @@ test("_getManifest: Multiple manifest.json files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -435,7 +457,7 @@ test("_getManifest: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -461,7 +483,7 @@ test("_getDotLibrary: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -478,7 +500,7 @@ test("_getDotLibrary: No .library file", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -497,7 +519,7 @@ test("_getDotLibrary: Invalid XML", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -516,7 +538,7 @@ test("_getDotLibrary: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -538,7 +560,7 @@ test("_getDotLibrary: Multiple .library files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -556,7 +578,7 @@ test("_getDotLibrary: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -581,7 +603,7 @@ test("_getLibraryJsPath: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -597,7 +619,7 @@ test("_getLibraryJsPath: No library.js file", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -613,7 +635,7 @@ test("_getLibraryJsPath: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -633,7 +655,7 @@ test("_getLibraryJsPath: Multiple library.js files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -650,7 +672,7 @@ test("_getLibraryJsPath: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -714,12 +736,48 @@ test("_getNamespace: from manifest.json with .library on same level", async (t) }); sinon.stub(project, "_getDotLibrary").resolves({ content: { - library: {name: "dot-pony"} + library: {name: {_: "dot-pony"}} }, filePath: "/mani-pony/.library" }); const res = await project._getNamespace(); - t.deepEqual(res, "mani-pony", "Returned correct namespace"); + t.is(res, "mani-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from manifest.json for flat project", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "mani-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); +}); + +test("_getNamespace: from .library for flat project", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "dot-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); }); test("_getNamespace: from manifest.json with .library on same level but different directory", async (t) => { @@ -762,7 +820,7 @@ test("_getNamespace: from manifest.json with not matching file path", async (t) }); sinon.stub(project, "_getDotLibrary").resolves({ content: { - library: {name: "dot-pony"} + library: {name: {_: "dot-pony"}} }, filePath: "/different/namespace/.library" }); @@ -806,6 +864,7 @@ test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => `Namespace resolution from manifest.json failed for project library.d: ` + `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`, "correct verbose message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library", async (t) => { @@ -819,6 +878,7 @@ test("_getNamespace: from .library", async (t) => { }); const res = await project._getNamespace(); t.deepEqual(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library with ignored manifest.json on lower level", async (t) => { @@ -839,6 +899,7 @@ test("_getNamespace: from .library with ignored manifest.json on lower level", a }); const res = await project._getNamespace(); t.deepEqual(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: manifest.json on higher level than .library", async (t) => { @@ -889,6 +950,7 @@ test("_getNamespace: from .library with maven placeholder", async (t) => { t.deepEqual(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", "resolveMavenPlaceholder called with correct argument"); t.deepEqual(res, "mvn-unicorn", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library with not matching file path", async (t) => { @@ -905,6 +967,7 @@ test("_getNamespace: from .library with not matching file path", async (t) => { t.deepEqual(err.message, `Detected namespace "mvn-pony" does not match detected directory structure ` + `"different/namespace" for project library.d`, "Rejected with correct error message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from library.js", async (t) => { @@ -914,6 +977,7 @@ test("_getNamespace: from library.js", async (t) => { sinon.stub(project, "_getLibraryJsPath").resolves("/my/namespace/library.js"); const res = await project._getNamespace(); t.deepEqual(res, "my/namespace", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test.serial("_getNamespace: from project root level library.js", async (t) => { diff --git a/test/lib/specifications/types/ThemeLibrary.js b/test/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 000000000..e2f053f02 --- /dev/null +++ b/test/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,122 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const Specification = require("../../../../lib/specifications/Specification"); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const themeLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "theme.library.e"); +const basicProjectInput = { + id: "theme.library.e.id", + version: "1.0.0", + modulePath: themeLibraryEPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "theme-library", + metadata: { + name: "theme.library.e", + copyright: "Some fancy copyright" + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test("Correct class", async (t) => { + const ThemeLibrary = mock.reRequire("../../../../lib/specifications/types/ThemeLibrary"); + const project = await Specification.create(basicProjectInput); + t.true(project instanceof ThemeLibrary, `Is an instance of the ThemeLibrary class`); +}); + +test("getCopyright", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); +}); + +test("Access project resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/resources/theme/library/e/themes/my_theme/.theme"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/theme/library/e/themes/my_theme/.theme", "Resource has correct path"); +}); + +test("Access project test-resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/test-resources/theme/library/e/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/theme/library/e/Test.html", "Resource has correct path"); +}); + +test("Modify project resources via workspace and access via flat and runtime reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = await project.getReader(); + const readerResource = await reader.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const globResult = await reader.byGlob("**/library.source.less"); + t.is(globResult.length, 1, "Found the requested resource byGlob"); + t.is(globResult[0].getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byGlob) has correct path"); + t.is(await globResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("_configureAndValidatePaths: Default paths", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.is(project._srcPath, "src", "Correct default path for src"); + t.is(project._testPath, "test", "Correct default path for test"); + t.true(project._testPathExists, "Test path detected as existing"); +}); + +test("_configureAndValidatePaths: Test directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = { + configuration: { + paths: { + test: "does/not/exist" + } + } + }; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct path for src"); + t.is(project._testPath, "does/not/exist", "Correct path for test"); + t.false(project._testPathExists, "Test path detected as non-existent"); +}); + +test("_configureAndValidatePaths: Source directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = { + configuration: { + paths: { + src: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find directory 'does/not/exist' in theme-library project theme.library.e"); +}); From bae6c21cbe27e35f91e2e8f06ee160b52f94d8ae Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 7 Jun 2022 14:43:40 +0200 Subject: [PATCH 75/99] [INTERNAL] ProjectGraph: getAll* to return flat array --- lib/graph/ProjectGraph.js | 4 ++-- test/lib/graph/ProjectGraph.js | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js index dbe55d3dc..2613e4235 100644 --- a/lib/graph/ProjectGraph.js +++ b/lib/graph/ProjectGraph.js @@ -89,7 +89,7 @@ class ProjectGraph { * @returns {Array>} */ getAllProjects() { - return Object.entries(this._projects); + return Object.values(this._projects); } /** @@ -132,7 +132,7 @@ class ProjectGraph { * @returns {Array>} */ getAllExtensions() { - return Object.entries(this._extensions); + return Object.values(this._extensions); } /** diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js index 2f4610bad..ba88b1543 100644 --- a/test/lib/graph/ProjectGraph.js +++ b/test/lib/graph/ProjectGraph.js @@ -208,11 +208,9 @@ test("getAllProjects", async (t) => { graph.addProject(project2); const res = graph.getAllProjects(); - t.deepEqual(res, [[ - "application.a", project1 - ], [ - "application.b", project2 - ]], "Should return all projects in a nested array"); + t.deepEqual(res, [ + project1, project2 + ], "Should return all projects in a flat array"); }); test("add-/getExtension", async (t) => { @@ -281,11 +279,9 @@ test("getAllExtensions", async (t) => { const extension2 = await createExtension("extension.b"); graph.addExtension(extension2); const res = graph.getAllExtensions(); - t.deepEqual(res, [[ - "extension.a", extension1 - ], [ - "extension.b", extension2 - ]], "Should return all extensions in a nested array"); + t.deepEqual(res, [ + extension1, extension2 + ], "Should return all extensions in a flat array"); }); test("declareDependency / getDependencies", async (t) => { From 6433eaab32bc72ed5ca98504e8c503ec7a6950f0 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 7 Jun 2022 14:44:04 +0200 Subject: [PATCH 76/99] NodePackageDependencies: Fix dependency resolution --- lib/graph/providers/NodePackageDependencies.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/graph/providers/NodePackageDependencies.js b/lib/graph/providers/NodePackageDependencies.js index 8bbdc86df..35652671b 100644 --- a/lib/graph/providers/NodePackageDependencies.js +++ b/lib/graph/providers/NodePackageDependencies.js @@ -104,11 +104,15 @@ class NodePackageDependencies { async _getDependencies(modulePath, packageJson, rootModule = false) { const dependencies = []; if (packageJson.dependencies) { - const packageJsonDependencies = Object.keys(packageJson.dependencies); - if (rootModule && packageJson.devDependencies) { - packageJsonDependencies.push(...Object.keys(packageJson.devDependencies)); - } - packageJsonDependencies.forEach((depName) => { + Object.keys(packageJson.dependencies).forEach((depName) => { + dependencies.push({ + name: depName, + optional: false + }); + }); + } + if (rootModule && packageJson.devDependencies) { + Object.keys(packageJson.devDependencies).forEach((depName) => { dependencies.push({ name: depName, optional: false From 0ac1569adab0a90cb86a0a18c8ec69392ed7d3cc Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 7 Jun 2022 14:47:59 +0200 Subject: [PATCH 77/99] [INTERNAL] builder: Adapt to ProjectGraph#getAllProjects flat array change --- lib/builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/builder.js b/lib/builder.js index 727681b23..c37298d7b 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -175,7 +175,7 @@ module.exports = async function({ } // Count total number of projects to build - const requestedProjects = graph.getAllProjects().map((arr) => arr[0]).filter(function(projectName) { + const requestedProjects = graph.getAllProjects().map((p) => p.getName()).filter(function(projectName) { return projectFilter(projectName); }); From 84abeb6fc875a2349ea4cc018ee6c8b9abb353fb Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 15:34:03 +0200 Subject: [PATCH 78/99] [INTERNAL] Projects: Pass builder resource excludes to adapters --- lib/buildDefinitions/AbstractBuilder.js | 1 - lib/specifications/ComponentProject.js | 1 + lib/specifications/Project.js | 2 +- lib/specifications/types/Application.js | 3 ++- lib/specifications/types/LegacyLibrary.js | 9 ++++++--- lib/specifications/types/Library.js | 6 ++++-- lib/specifications/types/Module.js | 6 ++++-- lib/specifications/types/ThemeLibrary.js | 6 ++++-- 8 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index c11ec1841..a339b296a 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -40,7 +40,6 @@ class AbstractBuilder { this.tasks = {}; this.taskExecutionOrder = []; - this.addStandardTasks({ project, taskUtil, diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index 2fad4065d..dc7bd7284 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -113,6 +113,7 @@ class ComponentProject extends Project { reader = this._getReader(); break; case "runtime": + // Use buildtime reader and link it to / // No test-resources for runtime resource access, // unless runtime is namespaced reader = this.getReader().link({ diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index c3b2a44df..1e65039d5 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -55,7 +55,7 @@ class Project extends Specification { return this._config.customConfiguration; } - getBuilderResourceExcludes() { + getBuilderResourcesExcludes() { return this._config.builder?.resources?.excludes || []; } diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index d11509704..9964f20aa 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -38,7 +38,8 @@ class Application extends ComponentProject { fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath, name: `Source reader for application project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); } diff --git a/lib/specifications/types/LegacyLibrary.js b/lib/specifications/types/LegacyLibrary.js index 7f34e84d9..c0b0da375 100644 --- a/lib/specifications/types/LegacyLibrary.js +++ b/lib/specifications/types/LegacyLibrary.js @@ -26,7 +26,8 @@ class LegacyLibrary extends Library { fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/", name: `Source reader for library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); } @@ -35,7 +36,8 @@ class LegacyLibrary extends Library { fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: this._stripNamespace(virBasePath), name: `Source reader for library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); } @@ -47,7 +49,8 @@ class LegacyLibrary extends Library { fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: this._stripNamespace(virBasePath), name: `Runtime test-resources reader for library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); return testReader; } diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 53c72d013..34dad41e9 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -41,7 +41,8 @@ class Library extends ComponentProject { fsBasePath, virBasePath, name: `Source reader for library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); } @@ -57,7 +58,8 @@ class Library extends ComponentProject { fsBasePath, virBasePath, name: `Runtime test-resources reader for library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); return testReader; } diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 0cc2e5b60..6b6ba787e 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -88,7 +88,8 @@ class Module extends Project { name: `'${relFsPath}'' reader for module project ${this.getName()}`, virBasePath, fsBasePath: fsPath.join(this.getPath(), relFsPath), - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }; })); } else { @@ -101,7 +102,8 @@ class Module extends Project { name: `Root reader for module project ${this.getName()}`, virBasePath: "/", fsBasePath: this.getPath(), - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }]; } } diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index 54154ec1d..cd817947b 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -35,14 +35,16 @@ class ThemeLibrary extends Project { fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath: "/resources/", name: `Runtime resources reader for theme-library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); if (this._testPathExists) { const testReader = resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath: "/test-resources/", name: `Runtime test-resources reader for theme-library project ${this.getName()}`, - project: this + project: this, + excludes: this.getBuilderResourcesExcludes() }); reader = resourceFactory.createReaderCollection({ name: `Reader collection for theme-library project ${this.getName()}`, From 0eecee55cd632095ac0944c75df32a36ced3d080 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 16:09:52 +0200 Subject: [PATCH 79/99] Rename archive metadata to build description Restructure metadata file --- lib/buildHelpers/composeTaskList.js | 3 +- ...eMetadata.js => createBuildDescription.js} | 44 +++++++++---------- lib/builder.js | 22 ++++++---- lib/graph/Module.js | 27 +++++++----- lib/specifications/Project.js | 16 +------ lib/specifications/Specification.js | 14 +++++- lib/specifications/types/Application.js | 9 ++-- lib/specifications/types/Library.js | 9 ++-- lib/specifications/types/ThemeLibrary.js | 8 ---- 9 files changed, 77 insertions(+), 75 deletions(-) rename lib/buildHelpers/{createArchiveMetadata.js => createBuildDescription.js} (54%) diff --git a/lib/buildHelpers/composeTaskList.js b/lib/buildHelpers/composeTaskList.js index 09328d774..ee4763564 100644 --- a/lib/buildHelpers/composeTaskList.js +++ b/lib/buildHelpers/composeTaskList.js @@ -13,12 +13,11 @@ const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); * @param {boolean} parameters.selfContained * True if a the build should be self-contained or false for prelead build bundles * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed - * @param {boolean} parameters.archive True if an archive build should be executed * @param {Array} parameters.includedTasks Task list to be included from build * @param {Array} parameters.excludedTasks Task list to be excluded from build * @returns {Array} Return a task list for the builder */ -module.exports = function composeTaskList(allTasks, {selfContained, jsdoc, archive, includedTasks, excludedTasks}) { +module.exports = function composeTaskList(allTasks, {selfContained, jsdoc, includedTasks, excludedTasks}) { let selectedTasks = allTasks.reduce((list, key) => { list[key] = true; return list; diff --git a/lib/buildHelpers/createArchiveMetadata.js b/lib/buildHelpers/createBuildDescription.js similarity index 54% rename from lib/buildHelpers/createArchiveMetadata.js rename to lib/buildHelpers/createBuildDescription.js index 82c113b3a..59196a4d3 100644 --- a/lib/buildHelpers/createArchiveMetadata.js +++ b/lib/buildHelpers/createBuildDescription.js @@ -25,30 +25,30 @@ module.exports = async function(project, buildConfig) { } const metadata = { - specVersion: project.getSpecVersion(), - type, - metadata: { - name: projectName, - }, - customConfiguration: { // TODO 3.0: Make "_archive" a top-level property - _archive: { - archiveSpecVersion: "0.1", - timestamp: new Date().toISOString(), - versions: { - builderVersion: getVersion("@ui5/builder"), - projectVersion: getVersion("@ui5/project"), - fsVersion: getVersion("@ui5/fs"), - }, - buildConfig, - version: project.getVersion(), - namespace: project.getNamespace(), - tags: project.getResourceTagCollection().getAllTags() + project: { + specVersion: project.getSpecVersion(), + type, + metadata: { + name: projectName, + }, + resources: { + configuration: { + paths: pathMapping + } } }, - resources: { - configuration: { - paths: pathMapping - } + buildDescription: { + descriptionSpecVersion: "0.1", + timestamp: new Date().toISOString(), + versions: { + builderVersion: getVersion("@ui5/builder"), + projectVersion: getVersion("@ui5/project"), + fsVersion: getVersion("@ui5/fs"), + }, + buildConfig, + version: project.getVersion(), + namespace: project.getNamespace(), + tags: project.getResourceTagCollection().getAllTags() } }; diff --git a/lib/builder.js b/lib/builder.js index c37298d7b..369bd9766 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -87,7 +87,7 @@ function getElapsedTime(startTime) { * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build - * @param {boolean} [parameters.archive=false] Whether to create an archive build + * @param {boolean} [parameters.createBuildDescription=false] Whether to create a build metadata file * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. @@ -97,7 +97,7 @@ module.exports = async function({ graph, destPath, cleanDest = false, includedDependencies = [], excludedDependencies = [], complexDependencyIncludes, - selfContained = false, cssVariables = false, jsdoc = false, archive = false, + selfContained = false, cssVariables = false, jsdoc = false, createBuildDescription = false, includedTasks = [], excludedTasks = [], }) { if (!graph) { @@ -188,6 +188,10 @@ module.exports = async function({ const buildLogger = log.createTaskLogger("🛠 "); await graph.traverseBreadthFirst(async function({project, getDependencies}) { + if (project.hasBuildDescription()) { + log.verbose(`Found a build description for project ${projectName}. Skipping build.`); + return; + } const projectName = project.getName(); const projectContext = buildContext.createProjectContext({ project, @@ -262,18 +266,18 @@ module.exports = async function({ } const resources = await project.getReader({ - // Always use buildtime (=namespace) style when writing an archive - style: archive ? "buildtime" : "runtime" + // Always use buildtime (=namespace) style when writing an createBuildDescription + style: createBuildDescription ? "buildtime" : "runtime" }).byGlob("/**/*"); log.verbose(`Writing out files...`); - if (archive) { - // Create and write archive metadata file - const createArchiveMetadata = require("./buildHelpers/createArchiveMetadata"); - const metadata = await createArchiveMetadata(project, buildConfig); + if (createBuildDescription) { + // Create and write createBuildDescription metadata file + const createBuildDescription = require("./buildHelpers/createBuildDescription"); + const metadata = await createBuildDescription(project, buildConfig); await fsTarget.write(resourceFactory.createResource({ - path: `/.ui5/archive-metadata.json`, + path: `/.ui5/build-description.json`, string: JSON.stringify(metadata, null, "\t") // TODO 3.0: minify? })); } diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 5ab942cbf..1314edf86 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -147,11 +147,16 @@ class Module { }); const specs = await Promise.all(configs.map(async (configuration) => { + const cacheMetadata = configuration._buildDescription; + if (configuration._buildDescription) { + delete configuration._buildDescription; + } const spec = await Specification.create({ id: this.getId(), version: this.getVersion(), modulePath: this.getPath(), - configuration + configuration, + cacheMetadata }); log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); @@ -187,7 +192,7 @@ class Module { configurations = await this._getSuppliedConfigurations(); if (!configurations || !configurations.length) { - configurations = await this._getArchiveConfigurations(); + configurations = await this._getBuildDescriptionConfigurations(); } if (!configurations || !configurations.length) { configurations = await this._getYamlConfigurations(); @@ -348,22 +353,24 @@ class Module { return configs; } - async _getArchiveConfigurations() { - const config = await this._readArchiveMetadata(); + async _getBuildDescriptionConfigurations() { + const cacheConfig = await this._readBuildDescription(); - if (!config) { - log.verbose(`Could not find any archive metadata files in module ${this.getId()}`); + if (!cacheConfig) { + log.verbose(`Could not find any build description in module ${this.getId()}`); return []; } + const config = cacheConfig.project; + config._buildDescription = cacheConfig.buildDescription; return [this._normalizeAndApplyShims(config)]; } - async _readArchiveMetadata() { + async _readBuildDescription() { const reader = await this.getReader(); - const archiveMetadataResource = await reader.byPath("/.ui5/archive-metadata.json"); - if (archiveMetadataResource) { - return JSON.parse(await archiveMetadataResource.getString()); + const buildDescriptionResource = await reader.byPath("/.ui5/build-description.json"); + if (buildDescriptionResource) { + return JSON.parse(await buildDescriptionResource.getString()); } } diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 1e65039d5..ef6f53e58 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -75,10 +75,6 @@ class Project extends Specification { return this._config.builder?.settings; } - getArchiveMetadata() { - return this._config.customConfiguration?._archive; - } - /* === Resource Access === */ /** * Get a [ReaderCollection]{@link module:@ui5/fs.ReaderCollection} for accessing all resources of the @@ -107,7 +103,7 @@ class Project extends Specification { this._resourceTagCollection = new ResourceTagCollection({ allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], allowedNamespaces: ["project"], - tags: this.getArchiveMetadata()?.tags + tags: this.getBuildDescription()?.tags }); } return this._resourceTagCollection; @@ -125,19 +121,11 @@ class Project extends Specification { } /* === Internals === */ - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } - async _validate() { await super._validate(); if (this.getKind() !== "project") { throw new Error( - `Configuration missmatch: Supplied configuration must be of kind 'project' but ` + + `Configuration mismatch: Supplied configuration must be of kind 'project' but ` + `is of kind '${this.getKind()}'`); } } diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index b88264eaf..f98f9ee60 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -14,8 +14,9 @@ class Specification { * @param {string} parameters.version Version * @param {string} parameters.modulePath File System path to access resources * @param {object} parameters.configuration Configuration object + * @param {object} parameters.buildDescription Build metadata object */ - async init({id, version, modulePath, configuration}) { + async init({id, version, modulePath, configuration, buildDescription}) { if (!id) { throw new Error(`Could not create specification: Missing or empty parameter 'id'`); } @@ -77,9 +78,10 @@ class Specification { this._kind = config.kind; this._type = config.type; this._specVersion = config.specVersion; + this._buildDescription = buildDescription; await this._configureAndValidatePaths(config); - await this._parseConfiguration(config); + await this._parseConfiguration(config, buildDescription); this._config = config; return this; } @@ -129,6 +131,14 @@ class Specification { return this._modulePath; } + hasBuildDescription() { + return !!this._buildDescription; + } + + getBuildDescription() { + return this._buildDescription || {}; + } + /* === Resource Access === */ /** * Get a resource reader for the root directory of the project diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 9964f20aa..783a574f6 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -82,12 +82,13 @@ class Application extends ComponentProject { /** * @private * @param {object} config Configuration object + * @param {object} buildDescription Cache metadata object */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); - if (config.customConfiguration?._archive) { - this._namespace = config.customConfiguration._archive.namespace; + if (buildDescription) { + this._namespace = buildDescription.namespace; return; } this._namespace = await this._getNamespace(); diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index 34dad41e9..a7846c2c9 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -117,12 +117,13 @@ class Library extends ComponentProject { /** * @private * @param {object} config Configuration object + * @param {object} buildDescription Cache metadata object */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); - if (config.customConfiguration?._archive) { - this._namespace = config.customConfiguration._archive.namespace; + if (buildDescription) { + this._namespace = buildDescription.namespace; return; } diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index cd817947b..9d66e7be0 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -123,14 +123,6 @@ class ThemeLibrary extends Project { this._testPathExists = true; } } - - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } } module.exports = ThemeLibrary; From 96e5ae691a87a7dc79cdbf29952839a1f3a7e0d2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 16:18:43 +0200 Subject: [PATCH 80/99] Module: Fix build descripton parameter naming --- lib/buildHelpers/createBuildDescription.js | 2 +- lib/graph/Module.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/buildHelpers/createBuildDescription.js b/lib/buildHelpers/createBuildDescription.js index 59196a4d3..dcbc86b66 100644 --- a/lib/buildHelpers/createBuildDescription.js +++ b/lib/buildHelpers/createBuildDescription.js @@ -38,7 +38,7 @@ module.exports = async function(project, buildConfig) { } }, buildDescription: { - descriptionSpecVersion: "0.1", + descriptionVersion: "0.1", timestamp: new Date().toISOString(), versions: { builderVersion: getVersion("@ui5/builder"), diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 1314edf86..a6d49c20f 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -147,7 +147,7 @@ class Module { }); const specs = await Promise.all(configs.map(async (configuration) => { - const cacheMetadata = configuration._buildDescription; + const buildDescription = configuration._buildDescription; if (configuration._buildDescription) { delete configuration._buildDescription; } @@ -156,7 +156,7 @@ class Module { version: this.getVersion(), modulePath: this.getPath(), configuration, - cacheMetadata + buildDescription }); log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); From 16a6d760bd774dad10e7eef7a04b4e87262e2948 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 16:30:33 +0200 Subject: [PATCH 81/99] builder: Fix handling for projects with build description --- lib/builder.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/builder.js b/lib/builder.js index 369bd9766..3bc49f411 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -184,14 +184,10 @@ module.exports = async function({ // Copy list of requested projects. We might need to build more projects than requested to // in order to satisfy tasks requiring dependencies to be built but we will still only write the // resources of the requested projects to the build result - const projectsToBuild = [...requestedProjects]; + let projectsToBuild = [...requestedProjects]; const buildLogger = log.createTaskLogger("🛠 "); await graph.traverseBreadthFirst(async function({project, getDependencies}) { - if (project.hasBuildDescription()) { - log.verbose(`Found a build description for project ${projectName}. Skipping build.`); - return; - } const projectName = project.getName(); const projectContext = buildContext.createProjectContext({ project, @@ -214,6 +210,11 @@ module.exports = async function({ if (projectsToBuild.includes(projectName) && builder.requiresDependencies(buildConfig)) { getDependencies().forEach((dep) => { const depName = dep.getName(); + if (project.hasBuildDescription() && !dep.hasBuildDescription()) { + throw new Error( + `Project ${depName} must provide a build description since it is a dependency of ` + + `project ${projectName} which already provides a build description`); + } log.info(`Project ${projectName} requires dependency ${depName} to be built`); if (!projectsToBuild.includes(depName)) { projectsToBuild.push(depName); @@ -222,6 +223,14 @@ module.exports = async function({ } }); + projectsToBuild = projectsToBuild.filter((projectName) => { + if (graph.getProject(projectName).hasBuildDescription()) { + log.verbose(`Found a build description for project ${projectName}. Skipping build.`); + return false; + } + return true; + }); + buildLogger.addWork(projectsToBuild.length); log.info(`Building projects: `); log.info(` > ${projectsToBuild.join("\n > ")}`); From 769d75985aa24a34d9a8b721071624183176d63f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 16:49:54 +0200 Subject: [PATCH 82/99] Adapt tests for refactored build description --- lib/graph/Module.js | 15 ++- .../application.a/.ui5/archive-metadata.json | 42 ------ .../library.e/.ui5/archive-metadata.json | 43 ------ .../application.a/.ui5/build-description.json | 42 ++++++ .../application.a/package.json | 0 .../application.a/resources/id1/index.html | 0 .../application.a/resources/id1/manifest.json | 0 .../application.a/resources/id1/test-dbg.js | 0 .../application.a/resources/id1/test.js | 0 .../library.e/.ui5/archive-metadata.json | 43 ++++++ .../library.e/package.json | 0 .../library.e/resources/library/e/.library | 0 .../library.e/resources/library/e/some.js | 0 .../test-resources/library/e/Test.html | 0 .../createArchiveMetadata.integration.js | 95 ------------- .../lib/buildHelpers/createArchiveMetadata.js | 127 ------------------ .../createBuildDescription.integration.js | 95 +++++++++++++ .../buildHelpers/createBuildDescription.js | 127 ++++++++++++++++++ test/lib/graph/Module.js | 7 +- 19 files changed, 321 insertions(+), 315 deletions(-) delete mode 100644 test/fixtures/archives/application.a/.ui5/archive-metadata.json delete mode 100644 test/fixtures/archives/library.e/.ui5/archive-metadata.json create mode 100644 test/fixtures/build-descriptions/application.a/.ui5/build-description.json rename test/fixtures/{archives => build-descriptions}/application.a/package.json (100%) rename test/fixtures/{archives => build-descriptions}/application.a/resources/id1/index.html (100%) rename test/fixtures/{archives => build-descriptions}/application.a/resources/id1/manifest.json (100%) rename test/fixtures/{archives => build-descriptions}/application.a/resources/id1/test-dbg.js (100%) rename test/fixtures/{archives => build-descriptions}/application.a/resources/id1/test.js (100%) create mode 100644 test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json rename test/fixtures/{archives => build-descriptions}/library.e/package.json (100%) rename test/fixtures/{archives => build-descriptions}/library.e/resources/library/e/.library (100%) rename test/fixtures/{archives => build-descriptions}/library.e/resources/library/e/some.js (100%) rename test/fixtures/{archives => build-descriptions}/library.e/test-resources/library/e/Test.html (100%) delete mode 100644 test/lib/buildHelpers/createArchiveMetadata.integration.js delete mode 100644 test/lib/buildHelpers/createArchiveMetadata.js create mode 100644 test/lib/buildHelpers/createBuildDescription.integration.js create mode 100644 test/lib/buildHelpers/createBuildDescription.js diff --git a/lib/graph/Module.js b/lib/graph/Module.js index a6d49c20f..c2ec5e5d8 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -234,7 +234,12 @@ class Module { async _getSuppliedConfigurations() { if (this._suppliedConfigs.length) { log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); - return await Promise.all(this._suppliedConfigs.map(async (config) => { + return await Promise.all(this._suppliedConfigs.map(async (suppliedConfig) => { + let config = suppliedConfig; + if (suppliedConfig.buildDescription) { + config = suppliedConfig.project; + config._buildDescription = suppliedConfig.buildDescription; + } return this._normalizeAndApplyShims(config); })); } @@ -354,15 +359,15 @@ class Module { } async _getBuildDescriptionConfigurations() { - const cacheConfig = await this._readBuildDescription(); + const buildDescriptionMetadata = await this._readBuildDescription(); - if (!cacheConfig) { + if (!buildDescriptionMetadata) { log.verbose(`Could not find any build description in module ${this.getId()}`); return []; } - const config = cacheConfig.project; - config._buildDescription = cacheConfig.buildDescription; + const config = buildDescriptionMetadata.project; + config._buildDescription = buildDescriptionMetadata.buildDescription; return [this._normalizeAndApplyShims(config)]; } diff --git a/test/fixtures/archives/application.a/.ui5/archive-metadata.json b/test/fixtures/archives/application.a/.ui5/archive-metadata.json deleted file mode 100644 index 1a575cafe..000000000 --- a/test/fixtures/archives/application.a/.ui5/archive-metadata.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "specVersion": "2.3", - "type": "application", - "metadata": { - "name": "application.a" - }, - "customConfiguration": { - "_archive": { - "archiveSpecVersion": "0.1", - "timestamp": "2022-05-04T12:45:30.024Z", - "versions": { - "builderVersion": "3.0.0", - "projectVersion": "3.0.0", - "fsVersion": "3.0.0" - }, - "buildConfig": { - "selfContained": false, - "jsdoc": false, - "includedTasks": [], - "excludedTasks": [] - }, - "id": "application.a", - "version": "0.2.0", - "namespace": "id1", - "tags": { - "/resources/id1/test.js": { - "ui5:HasDebugVariant": true - }, - "/resources/id1/test-dbg.js": { - "ui5:IsDebugVariant": true - } - } - } - }, - "resources": { - "configuration": { - "paths": { - "webapp": "resources/id1" - } - } - } -} diff --git a/test/fixtures/archives/library.e/.ui5/archive-metadata.json b/test/fixtures/archives/library.e/.ui5/archive-metadata.json deleted file mode 100644 index 00793ce4d..000000000 --- a/test/fixtures/archives/library.e/.ui5/archive-metadata.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "specVersion": "2.3", - "type": "library", - "metadata": { - "name": "library.e" - }, - "customConfiguration": { - "_archive": { - "archiveSpecVersion": "0.1", - "timestamp": "2022-05-06T09:54:29.051Z", - "versions": { - "builderVersion": "3.0.0", - "projectVersion": "3.0.0", - "fsVersion": "3.0.0" - }, - "buildConfig": { - "selfContained": false, - "jsdoc": false, - "includedTasks": [], - "excludedTasks": [] - }, - "id": "library.e", - "version": "1.0.0", - "namespace": "library/e", - "tags": { - "/resources/library/e/some.js": { - "ui5:HasDebugVariant": true - }, - "/resources/library/e/some-dbg.js": { - "ui5:IsDebugVariant": true - } - } - } - }, - "resources": { - "configuration": { - "paths": { - "src": "resources", - "test": "test-resources", - } - } - } -} diff --git a/test/fixtures/build-descriptions/application.a/.ui5/build-description.json b/test/fixtures/build-descriptions/application.a/.ui5/build-description.json new file mode 100644 index 000000000..9afd75511 --- /dev/null +++ b/test/fixtures/build-descriptions/application.a/.ui5/build-description.json @@ -0,0 +1,42 @@ +{ + "project": { + "specVersion": "2.3", + "type": "application", + "metadata": { + "name": "application.a" + }, + "resources": { + "configuration": { + "paths": { + "webapp": "resources/id1" + } + } + } + }, + "buildDescription": { + "descriptionVersion": "0.1", + "timestamp": "2022-05-04T12:45:30.024Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "application.a", + "version": "0.2.0", + "namespace": "id1", + "tags": { + "/resources/id1/test.js": { + "ui5:HasDebugVariant": true + }, + "/resources/id1/test-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } +} diff --git a/test/fixtures/archives/application.a/package.json b/test/fixtures/build-descriptions/application.a/package.json similarity index 100% rename from test/fixtures/archives/application.a/package.json rename to test/fixtures/build-descriptions/application.a/package.json diff --git a/test/fixtures/archives/application.a/resources/id1/index.html b/test/fixtures/build-descriptions/application.a/resources/id1/index.html similarity index 100% rename from test/fixtures/archives/application.a/resources/id1/index.html rename to test/fixtures/build-descriptions/application.a/resources/id1/index.html diff --git a/test/fixtures/archives/application.a/resources/id1/manifest.json b/test/fixtures/build-descriptions/application.a/resources/id1/manifest.json similarity index 100% rename from test/fixtures/archives/application.a/resources/id1/manifest.json rename to test/fixtures/build-descriptions/application.a/resources/id1/manifest.json diff --git a/test/fixtures/archives/application.a/resources/id1/test-dbg.js b/test/fixtures/build-descriptions/application.a/resources/id1/test-dbg.js similarity index 100% rename from test/fixtures/archives/application.a/resources/id1/test-dbg.js rename to test/fixtures/build-descriptions/application.a/resources/id1/test-dbg.js diff --git a/test/fixtures/archives/application.a/resources/id1/test.js b/test/fixtures/build-descriptions/application.a/resources/id1/test.js similarity index 100% rename from test/fixtures/archives/application.a/resources/id1/test.js rename to test/fixtures/build-descriptions/application.a/resources/id1/test.js diff --git a/test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json b/test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json new file mode 100644 index 000000000..b2c7c1a7b --- /dev/null +++ b/test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json @@ -0,0 +1,43 @@ +{ + "project": { + "specVersion": "2.3", + "type": "library", + "metadata": { + "name": "library.e" + }, + "resources": { + "configuration": { + "paths": { + "src": "resources", + "test": "test-resources", + } + } + } + }, + "buildDescription": { + "descriptionVersion": "0.1", + "timestamp": "2022-05-06T09:54:29.051Z", + "versions": { + "builderVersion": "3.0.0", + "projectVersion": "3.0.0", + "fsVersion": "3.0.0" + }, + "buildConfig": { + "selfContained": false, + "jsdoc": false, + "includedTasks": [], + "excludedTasks": [] + }, + "id": "library.e", + "version": "1.0.0", + "namespace": "library/e", + "tags": { + "/resources/library/e/some.js": { + "ui5:HasDebugVariant": true + }, + "/resources/library/e/some-dbg.js": { + "ui5:IsDebugVariant": true + } + } + } +} diff --git a/test/fixtures/archives/library.e/package.json b/test/fixtures/build-descriptions/library.e/package.json similarity index 100% rename from test/fixtures/archives/library.e/package.json rename to test/fixtures/build-descriptions/library.e/package.json diff --git a/test/fixtures/archives/library.e/resources/library/e/.library b/test/fixtures/build-descriptions/library.e/resources/library/e/.library similarity index 100% rename from test/fixtures/archives/library.e/resources/library/e/.library rename to test/fixtures/build-descriptions/library.e/resources/library/e/.library diff --git a/test/fixtures/archives/library.e/resources/library/e/some.js b/test/fixtures/build-descriptions/library.e/resources/library/e/some.js similarity index 100% rename from test/fixtures/archives/library.e/resources/library/e/some.js rename to test/fixtures/build-descriptions/library.e/resources/library/e/some.js diff --git a/test/fixtures/archives/library.e/test-resources/library/e/Test.html b/test/fixtures/build-descriptions/library.e/test-resources/library/e/Test.html similarity index 100% rename from test/fixtures/archives/library.e/test-resources/library/e/Test.html rename to test/fixtures/build-descriptions/library.e/test-resources/library/e/Test.html diff --git a/test/lib/buildHelpers/createArchiveMetadata.integration.js b/test/lib/buildHelpers/createArchiveMetadata.integration.js deleted file mode 100644 index dfdd515fe..000000000 --- a/test/lib/buildHelpers/createArchiveMetadata.integration.js +++ /dev/null @@ -1,95 +0,0 @@ -const test = require("ava"); -const path = require("path"); -const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMetadata"); -const Module = require("../../../lib/graph/Module"); -const Specification = require("../../../lib/specifications/Specification"); - -const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const archiveApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "archives", "application.a"); -const applicationAConfig = { - id: "application.a.id", - version: "1.0.0", - modulePath: applicationAPath, - configuration: { - specVersion: "2.3", - kind: "project", - type: "application", - metadata: {name: "application.a"} - } -}; -const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); -const archiveLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "archives", "library.e"); -const libraryEConfig = { - id: "library.e.id", - version: "1.0.0", - modulePath: libraryEPath, - configuration: { - specVersion: "2.3", - kind: "project", - type: "library", - metadata: {name: "library.e"} - } -}; - -const buildConfig = { - selfContained: false, - jsdoc: false, - includedTasks: [], - excludedTasks: [] -}; - -// Note: The actual archive-metadata.json files in the fixtures are never used in these tests - -test("Create archive from application project archive", async (t) => { - const project = await Specification.create(applicationAConfig); - project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); - - const metadata = await createArchiveMetadata(project, buildConfig); - const m = new Module({ - id: "archive-application.a.id", - version: "2.0.0", - modulePath: archiveApplicationAPath, - configuration: metadata - }); - - const {project: archiveProject} = await m.getSpecifications(); - t.truthy(archiveProject, "Module was able to create project from archive metadata"); - t.is(archiveProject.getName(), project.getName(), "Archive project has correct name"); - t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); - t.is(archiveProject.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, - "Archive project has correct tag"); - t.is(archiveProject.getVersion(), "2.0.0", "Archive project has version from archive module"); - - const resources = await archiveProject.getReader().byGlob("**/test.js"); - t.is(resources.length, 1, - "Found requested resource in archive project"); - t.is(resources[0].getPath(), "/resources/id1/test.js", - "Resource has expected path"); -}); - -test("Create archive from library project archive", async (t) => { - const project = await Specification.create(libraryEConfig); - project.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); - - const metadata = await createArchiveMetadata(project, buildConfig); - const m = new Module({ - id: "archive-library.e.id", - version: "2.0.0", - modulePath: archiveLibraryEPath, - configuration: metadata - }); - - const {project: archiveProject} = await m.getSpecifications(); - t.truthy(archiveProject, "Module was able to create project from archive metadata"); - t.is(archiveProject.getName(), project.getName(), "Archive project has correct name"); - t.is(archiveProject.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); - t.is(archiveProject.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, - "Archive project has correct tag"); - t.is(archiveProject.getVersion(), "2.0.0", "Archive project has version from archive module"); - - const resources = await archiveProject.getReader().byGlob("**/some.js"); - t.is(resources.length, 1, - "Found requested resource in archive project"); - t.is(resources[0].getPath(), "/resources/library/e/some.js", - "Resource has expected path"); -}); diff --git a/test/lib/buildHelpers/createArchiveMetadata.js b/test/lib/buildHelpers/createArchiveMetadata.js deleted file mode 100644 index d297da4d7..000000000 --- a/test/lib/buildHelpers/createArchiveMetadata.js +++ /dev/null @@ -1,127 +0,0 @@ -const test = require("ava"); -const path = require("path"); -const createArchiveMetadata = require("../../../lib/buildHelpers/createArchiveMetadata"); -const Specification = require("../../../lib/specifications/Specification"); - -const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const applicationProjectInput = { - id: "application.a.id", - version: "1.0.0", - modulePath: applicationAPath, - configuration: { - specVersion: "2.3", - kind: "project", - type: "application", - metadata: {name: "application.a"} - } -}; - -const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); -const libraryProjectInput = { - id: "library.d.id", - version: "1.0.0", - modulePath: libraryDPath, - configuration: { - specVersion: "2.3", - kind: "project", - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - paths: { - src: "main/src", - test: "main/test" - } - } - }, - } -}; - -test("Create application archive from project", async (t) => { - const project = await Specification.create(applicationProjectInput); - project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); - - const metadata = await createArchiveMetadata(project, "buildConfig"); - t.truthy(new Date(metadata.customConfiguration._archive.timestamp), "Timestamp is valid"); - metadata.customConfiguration._archive.timestamp = ""; - - t.deepEqual(metadata, { - specVersion: "2.3", - type: "application", - metadata: { - name: "application.a", - }, - customConfiguration: { - _archive: { - archiveSpecVersion: "0.1", - buildConfig: "buildConfig", - namespace: "id1", - timestamp: "", - version: "1.0.0", - versions: { - builderVersion: require("@ui5/builder/package.json").version, - fsVersion: require("@ui5/fs/package.json").version, - projectVersion: require("@ui5/project/package.json").version, - }, - tags: { - "/resources/id1/foo.js": { - "ui5:HasDebugVariant": true, - }, - } - }, - }, - resources: { - configuration: { - paths: { - webapp: "resources/id1", - }, - }, - } - }, "Returned correct metadata"); -}); - -test("Create library archive from project", async (t) => { - const project = await Specification.create(libraryProjectInput); - project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); - - const metadata = await createArchiveMetadata(project, "buildConfig"); - t.truthy(new Date(metadata.customConfiguration._archive.timestamp), "Timestamp is valid"); - metadata.customConfiguration._archive.timestamp = ""; - - t.deepEqual(metadata, { - specVersion: "2.3", - type: "library", - metadata: { - name: "library.d", - }, - customConfiguration: { - _archive: { - archiveSpecVersion: "0.1", - buildConfig: "buildConfig", - namespace: "library/d", - timestamp: "", - version: "1.0.0", - versions: { - builderVersion: require("@ui5/builder/package.json").version, - fsVersion: require("@ui5/fs/package.json").version, - projectVersion: require("@ui5/project/package.json").version, - }, - tags: { - "/resources/library/d/foo.js": { - "ui5:HasDebugVariant": true, - }, - } - }, - }, - resources: { - configuration: { - paths: { - src: "resources", - test: "test-resources", - }, - }, - } - }, "Returned correct metadata"); -}); diff --git a/test/lib/buildHelpers/createBuildDescription.integration.js b/test/lib/buildHelpers/createBuildDescription.integration.js new file mode 100644 index 000000000..d327c22a2 --- /dev/null +++ b/test/lib/buildHelpers/createBuildDescription.integration.js @@ -0,0 +1,95 @@ +const test = require("ava"); +const path = require("path"); +const createBuildDescription = require("../../../lib/buildHelpers/createBuildDescription"); +const Module = require("../../../lib/graph/Module"); +const Specification = require("../../../lib/specifications/Specification"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const buildDescrApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "application.a"); +const applicationAConfig = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; +const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); +const buildDescrLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "library.e"); +const libraryEConfig = { + id: "library.e.id", + version: "1.0.0", + modulePath: libraryEPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: {name: "library.e"} + } +}; + +const buildConfig = { + selfContained: false, + jsdoc: false, + includedTasks: [], + excludedTasks: [] +}; + +// Note: The actual build-description.json files in the fixtures are never used in these tests + +test("Create project from application project providing a build description", async (t) => { + const inputProject = await Specification.create(applicationAConfig); + inputProject.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createBuildDescription(inputProject, buildConfig); + const m = new Module({ + id: "build-descr-application.a.id", + version: "2.0.0", + modulePath: buildDescrApplicationAPath, + configuration: metadata + }); + + const {project} = await m.getSpecifications(); + t.truthy(project, "Module was able to create project from build description metadata"); + t.is(project.getName(), project.getName(), "Archive project has correct name"); + t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); + t.is(project.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); + t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const resources = await project.getReader().byGlob("**/test.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/id1/test.js", + "Resource has expected path"); +}); + +test("Create project from library project providing a build description", async (t) => { + const inputProject = await Specification.create(libraryEConfig); + inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); + + const metadata = await createBuildDescription(inputProject, buildConfig); + const m = new Module({ + id: "build-descr-library.e.id", + version: "2.0.0", + modulePath: buildDescrLibraryEPath, + configuration: metadata + }); + + const {project} = await m.getSpecifications(); + t.truthy(project, "Module was able to create project from build description metadata"); + t.is(project.getName(), project.getName(), "Archive project has correct name"); + t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); + t.is(project.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, + "Archive project has correct tag"); + t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module"); + + const resources = await project.getReader().byGlob("**/some.js"); + t.is(resources.length, 1, + "Found requested resource in archive project"); + t.is(resources[0].getPath(), "/resources/library/e/some.js", + "Resource has expected path"); +}); diff --git a/test/lib/buildHelpers/createBuildDescription.js b/test/lib/buildHelpers/createBuildDescription.js new file mode 100644 index 000000000..f682a79d8 --- /dev/null +++ b/test/lib/buildHelpers/createBuildDescription.js @@ -0,0 +1,127 @@ +const test = require("ava"); +const path = require("path"); +const createBuildDescription = require("../../../lib/buildHelpers/createBuildDescription"); +const Specification = require("../../../lib/specifications/Specification"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const applicationProjectInput = { + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); +const libraryProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryDPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "main/src", + test: "main/test" + } + } + }, + } +}; + +test("Create application archive from project", async (t) => { + const project = await Specification.create(applicationProjectInput); + project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createBuildDescription(project, "buildConfig"); + t.truthy(new Date(metadata.buildDescription.timestamp), "Timestamp is valid"); + metadata.buildDescription.timestamp = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + resources: { + configuration: { + paths: { + webapp: "resources/id1", + }, + }, + } + }, + buildDescription: { + descriptionVersion: "0.1", + buildConfig: "buildConfig", + namespace: "id1", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: require("@ui5/builder/package.json").version, + fsVersion: require("@ui5/fs/package.json").version, + projectVersion: require("@ui5/project/package.json").version, + }, + tags: { + "/resources/id1/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + } + }, "Returned correct metadata"); +}); + +test("Create library archive from project", async (t) => { + const project = await Specification.create(libraryProjectInput); + project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); + + const metadata = await createBuildDescription(project, "buildConfig"); + t.truthy(new Date(metadata.buildDescription.timestamp), "Timestamp is valid"); + metadata.buildDescription.timestamp = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "resources", + test: "test-resources", + }, + }, + } + }, + buildDescription: { + descriptionVersion: "0.1", + buildConfig: "buildConfig", + namespace: "library/d", + timestamp: "", + version: "1.0.0", + versions: { + builderVersion: require("@ui5/builder/package.json").version, + fsVersion: require("@ui5/fs/package.json").version, + projectVersion: require("@ui5/project/package.json").version, + }, + tags: { + "/resources/library/d/foo.js": { + "ui5:HasDebugVariant": true, + }, + } + } + }, "Returned correct metadata"); +}); diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index d8f974902..84f116b4a 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -4,7 +4,8 @@ const path = require("path"); const Module = require("../../../lib/graph/Module"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const archiveApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "archives", "application.a"); +const buildDescriptionApplicationAPath = + path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "application.a"); const basicModuleInput = { id: "application.a.id", @@ -14,7 +15,7 @@ const basicModuleInput = { const archiveProjectInput = { id: "application.a.id", version: "1.0.0", - modulePath: archiveApplicationAPath + modulePath: buildDescriptionApplicationAPath }; // test.beforeEach((t) => { @@ -45,7 +46,7 @@ test("Get specifications from module", async (t) => { t.is(extensions.length, 0, "Should return no extensions"); }); -test.only("Get specifications from archive project", async (t) => { +test.only("Get specifications from project with build description", async (t) => { const ui5Module = new Module(archiveProjectInput); const {project, extensions} = await ui5Module.getSpecifications(); t.is(project.getName(), "application.a", "Should return correct project"); From 29100bd339c49b086ab124b673b8de4f947990c5 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 17:03:05 +0200 Subject: [PATCH 83/99] Module: Fix resources excludes configuration --- lib/specifications/types/Module.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 6b6ba787e..5e82075a7 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -89,7 +89,7 @@ class Module extends Project { virBasePath, fsBasePath: fsPath.join(this.getPath(), relFsPath), project: this, - excludes: this.getBuilderResourcesExcludes() + excludes: config.builder?.resources?.excludes }; })); } else { @@ -103,7 +103,7 @@ class Module extends Project { virBasePath: "/", fsBasePath: this.getPath(), project: this, - excludes: this.getBuilderResourcesExcludes() + excludes: config.builder?.resources?.excludes }]; } } From 0ed62d055c41721d66b11924da942b7ed00befe0 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 17:30:18 +0200 Subject: [PATCH 84/99] [INTERNAL] ui5Framework: Throw in case required dependencies of framework lib are missing from the graph --- lib/graph/helpers/ui5Framework.js | 6 +++++ test/lib/graph/helpers/ui5Framework.js | 35 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js index 86374243e..a834fded5 100644 --- a/lib/graph/helpers/ui5Framework.js +++ b/lib/graph/helpers/ui5Framework.js @@ -138,6 +138,12 @@ module.exports = { const rootProject = projectGraph.getRoot(); if (rootProject.isFrameworkProject()) { + rootProject.getFrameworkDependencies().forEach((dep) => { + if (utils.shouldIncludeDependency(dep) && !projectGraph.getProject(dep.name)) { + throw new Error( + `Missing framework dependency ${dep.name} for project ${rootProject.getName()}`); + } + }); // Ignoring UI5 Framework libraries in dependencies return projectGraph; } diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js index 6497bb39e..a45a00a7b 100644 --- a/test/lib/graph/helpers/ui5Framework.js +++ b/test/lib/graph/helpers/ui5Framework.js @@ -238,7 +238,8 @@ test.serial("generateDependencyTree should skip framework project with version a version: "1.2.3", libraries: [ { - name: "lib1" + name: "lib1", + optional: true } ] } @@ -252,6 +253,38 @@ test.serial("generateDependencyTree should skip framework project with version a t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); }); +test.serial("generateDependencyTree should throw for framework project with dependency missing in graph", async (t) => { + const {ui5Framework} = t.context; + const dependencyTree = { + id: "@sapui5/project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.2.3", + libraries: [ + { + name: "lib1" + } + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph)); + t.is(err.message, `Missing framework dependency lib1 for project application.a`, + "Threw with expected error message"); +}); + test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { From ec31cc492c6868c88deaab6fd5d6dc36c10f68ac Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 8 Jun 2022 17:51:01 +0200 Subject: [PATCH 85/99] [INTERNAL] Cleanup specifications: Move project specifics into project class --- lib/specifications/Project.js | 58 ++++++++++++++++--- lib/specifications/Specification.js | 41 +------------ lib/specifications/types/Module.js | 8 --- .../types/extensions/ProjectShim.js | 13 ----- .../types/extensions/ServerMiddleware.js | 13 ----- lib/specifications/types/extensions/Task.js | 13 ----- 6 files changed, 53 insertions(+), 93 deletions(-) diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index ef6f53e58..c44671a2e 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -18,6 +18,45 @@ class Project extends Specification { this._resourceTagCollection = null; } + /** + * @param {object} parameters Specification parameters + * @param {string} parameters.id Unique ID + * @param {string} parameters.version Version + * @param {string} parameters.modulePath File System path to access resources + * @param {object} parameters.configuration Configuration object + * @param {object} [parameters.buildDescription] Build metadata object + */ + async init(parameters) { + await super.init(parameters); + + const config = this._config; + if (!this.__id.startsWith("@openui5/") && !this.__id.startsWith("@sapui5/")) { + if (config.specVersion === "0.1" || config.specVersion === "1.0" || + config.specVersion === "1.1") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined for ` + + `${config.kind} ${config.metadata.name}. The new Specification API can only be ` + + `used with specification versions >= 2.0. For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + if (config.specVersion !== "2.0" && + config.specVersion !== "2.1" && config.specVersion !== "2.2" && + config.specVersion !== "2.3" && config.specVersion !== "2.4" && + config.specVersion !== "2.5" && config.specVersion !== "2.6") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + + `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + } + + this._buildDescription = parameters.buildDescription; + + await this._configureAndValidatePaths(config); + await this._parseConfiguration(config, parameters.buildDescription); + + return this; + } + /* === Attributes === */ /** * @public @@ -121,14 +160,17 @@ class Project extends Specification { } /* === Internals === */ - async _validate() { - await super._validate(); - if (this.getKind() !== "project") { - throw new Error( - `Configuration mismatch: Supplied configuration must be of kind 'project' but ` + - `is of kind '${this.getKind()}'`); - } - } + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) {} + + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) {} } module.exports = Project; diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index f98f9ee60..cd9d48133 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -14,9 +14,8 @@ class Specification { * @param {string} parameters.version Version * @param {string} parameters.modulePath File System path to access resources * @param {object} parameters.configuration Configuration object - * @param {object} parameters.buildDescription Build metadata object */ - async init({id, version, modulePath, configuration, buildDescription}) { + async init({id, version, modulePath, configuration}) { if (!id) { throw new Error(`Could not create specification: Missing or empty parameter 'id'`); } @@ -51,38 +50,16 @@ class Specification { // Check whether the given configuration matches the class by guessing the type name from the class name if (config.type.replace("-", "") !== this.constructor.name.toLowerCase()) { throw new Error( - `Configuration missmatch: Supplied configuration of type '${config.type}' does not match with ` + + `Configuration mismatch: Supplied configuration of type '${config.type}' does not match with ` + `specification class ${this.constructor.name}`); } - if (!id.startsWith("@openui5/") && !id.startsWith("@sapui5/")) { - if (config.specVersion === "0.1" || config.specVersion === "1.0" || - config.specVersion === "1.1") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined for ` + - `${config.kind} ${config.metadata.name}. The new Specification API can only be ` + - `used with specification versions >= 2.0. For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - if (config.specVersion !== "2.0" && - config.specVersion !== "2.1" && config.specVersion !== "2.2" && - config.specVersion !== "2.3" && config.specVersion !== "2.4" && - config.specVersion !== "2.5" && config.specVersion !== "2.6") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + - `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - } - this._name = config.metadata.name; this._kind = config.kind; this._type = config.type; this._specVersion = config.specVersion; - this._buildDescription = buildDescription; - - await this._configureAndValidatePaths(config); - await this._parseConfiguration(config, buildDescription); this._config = config; + return this; } @@ -155,18 +132,6 @@ class Specification { } /* === Internals === */ - /** - * @private - * @param {object} config Configuration object - */ - async _configureAndValidatePaths(config) {} - - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) {} - /* === Helper === */ /** * @private diff --git a/lib/specifications/types/Module.js b/lib/specifications/types/Module.js index 5e82075a7..752d9f6fa 100644 --- a/lib/specifications/types/Module.js +++ b/lib/specifications/types/Module.js @@ -107,14 +107,6 @@ class Module extends Project { }]; } } - - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } } module.exports = Module; diff --git a/lib/specifications/types/extensions/ProjectShim.js b/lib/specifications/types/extensions/ProjectShim.js index e9706a827..d337240f2 100644 --- a/lib/specifications/types/extensions/ProjectShim.js +++ b/lib/specifications/types/extensions/ProjectShim.js @@ -27,19 +27,6 @@ class ProjectShim extends Extension { getCollectionShims() { return this._config.shims.collections; } - - /* === Internals === */ - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } - - async _validate() { - await super._validate(); - } } module.exports = ProjectShim; diff --git a/lib/specifications/types/extensions/ServerMiddleware.js b/lib/specifications/types/extensions/ServerMiddleware.js index f6a6b6d67..fcdacc339 100644 --- a/lib/specifications/types/extensions/ServerMiddleware.js +++ b/lib/specifications/types/extensions/ServerMiddleware.js @@ -14,19 +14,6 @@ class ServerMiddleware extends Extension { const middlewarePath = path.join(this.getPath(), this._config.middleware.path); return require(middlewarePath); } - - /* === Internals === */ - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } - - async _validate() { - await super._validate(); - } } module.exports = ServerMiddleware; diff --git a/lib/specifications/types/extensions/Task.js b/lib/specifications/types/extensions/Task.js index 275e8dbc5..11b7d6d0d 100644 --- a/lib/specifications/types/extensions/Task.js +++ b/lib/specifications/types/extensions/Task.js @@ -14,19 +14,6 @@ class Task extends Extension { const taskPath = path.join(this.getPath(), this._config.task.path); return require(taskPath); } - - /* === Internals === */ - /** - * @private - * @param {object} config Configuration object - */ - async _parseConfiguration(config) { - await super._parseConfiguration(config); - } - - async _validate() { - await super._validate(); - } } module.exports = Task; From 3fc936b6481e57963ef4f94816a7dc236d7ae738 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 9 Jun 2022 16:04:12 +0200 Subject: [PATCH 86/99] [INTERNAL] Add basic migration of legacy spec versions --- lib/specifications/Project.js | 24 ++---------------- lib/specifications/Specification.js | 23 +++++++++++++++++ .../graph/helpers/ui5Framework.integration.js | 25 +++++++++++++++---- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index c44671a2e..282db9ad5 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -29,30 +29,10 @@ class Project extends Specification { async init(parameters) { await super.init(parameters); - const config = this._config; - if (!this.__id.startsWith("@openui5/") && !this.__id.startsWith("@sapui5/")) { - if (config.specVersion === "0.1" || config.specVersion === "1.0" || - config.specVersion === "1.1") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined for ` + - `${config.kind} ${config.metadata.name}. The new Specification API can only be ` + - `used with specification versions >= 2.0. For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - if (config.specVersion !== "2.0" && - config.specVersion !== "2.1" && config.specVersion !== "2.2" && - config.specVersion !== "2.3" && config.specVersion !== "2.4" && - config.specVersion !== "2.5" && config.specVersion !== "2.6") { - throw new Error( - `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + - `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); - } - } - this._buildDescription = parameters.buildDescription; - await this._configureAndValidatePaths(config); - await this._parseConfiguration(config, parameters.buildDescription); + await this._configureAndValidatePaths(this._config); + await this._parseConfiguration(this._config, this._buildDescription); return this; } diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index cd9d48133..0163e17db 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -38,6 +38,24 @@ class Specification { const config = JSON.parse(JSON.stringify(configuration)); + if (config.specVersion === "0.1" || config.specVersion === "1.0" || + config.specVersion === "1.1") { + this._log.verbose(`Detected legacy specification version ${config.specVersion}, defined for ` + + `${config.kind} ${config.metadata.name}. ` + + `Attempting to migrate the project to latest specVersion...`); + this._migrateLegacyProject(config); + } + + if (config.specVersion !== "2.0" && + config.specVersion !== "2.1" && config.specVersion !== "2.2" && + config.specVersion !== "2.3" && config.specVersion !== "2.4" && + config.specVersion !== "2.5" && config.specVersion !== "2.6") { + throw new Error( + `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + + `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } + // Deep clone config to prevent changes by reference const {validate} = require("../validation/validator"); await validate({ @@ -145,6 +163,11 @@ class Specification { return false; } + _migrateLegacyProject(config) { + // TODO 3.0: Implement proper migration + config.specVersion = "2.6"; + } + static async create(params) { if (!params.configuration) { throw new Error( diff --git a/test/lib/graph/helpers/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js index 98b2152ff..67215b202 100644 --- a/test/lib/graph/helpers/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -215,7 +215,10 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib1" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): @@ -225,7 +228,10 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib2" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): @@ -235,7 +241,10 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib3" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): @@ -245,7 +254,10 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib4" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): @@ -255,7 +267,10 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib8" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; default: throw new Error( From d2a61f3964e33ce59e2aa34407f6cc0de4c7490c Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 10 Jun 2022 10:41:31 +0200 Subject: [PATCH 87/99] Rename build description to build manifest --- ...dDescription.js => createBuildManifest.js} | 4 +- lib/builder.js | 20 +++++----- lib/graph/Module.js | 39 +++++++++++-------- lib/specifications/Project.js | 16 ++++++-- lib/specifications/Specification.js | 8 ---- .../application.a/.ui5/build-manifest.json} | 4 +- .../application.a/package.json | 0 .../application.a/resources/id1/index.html | 0 .../application.a/resources/id1/manifest.json | 0 .../application.a/resources/id1/test-dbg.js | 0 .../application.a/resources/id1/test.js | 0 .../library.e/.ui5/build-manifest.json} | 6 +-- .../library.e/package.json | 0 .../library.e/resources/library/e/.library | 0 .../library.e/resources/library/e/some.js | 0 .../test-resources/library/e/Test.html | 0 ....js => createBuildManifest.integration.js} | 20 +++++----- ...dDescription.js => createBuildManifest.js} | 26 ++++++------- test/lib/graph/Module.js | 23 +++++++++-- 19 files changed, 93 insertions(+), 73 deletions(-) rename lib/buildHelpers/{createBuildDescription.js => createBuildManifest.js} (96%) rename test/fixtures/{build-descriptions/application.a/.ui5/build-description.json => build-manifest/application.a/.ui5/build-manifest.json} (92%) rename test/fixtures/{build-descriptions => build-manifest}/application.a/package.json (100%) rename test/fixtures/{build-descriptions => build-manifest}/application.a/resources/id1/index.html (100%) rename test/fixtures/{build-descriptions => build-manifest}/application.a/resources/id1/manifest.json (100%) rename test/fixtures/{build-descriptions => build-manifest}/application.a/resources/id1/test-dbg.js (100%) rename test/fixtures/{build-descriptions => build-manifest}/application.a/resources/id1/test.js (100%) rename test/fixtures/{build-descriptions/library.e/.ui5/archive-metadata.json => build-manifest/library.e/.ui5/build-manifest.json} (89%) rename test/fixtures/{build-descriptions => build-manifest}/library.e/package.json (100%) rename test/fixtures/{build-descriptions => build-manifest}/library.e/resources/library/e/.library (100%) rename test/fixtures/{build-descriptions => build-manifest}/library.e/resources/library/e/some.js (100%) rename test/fixtures/{build-descriptions => build-manifest}/library.e/test-resources/library/e/Test.html (100%) rename test/lib/buildHelpers/{createBuildDescription.integration.js => createBuildManifest.integration.js} (84%) rename test/lib/buildHelpers/{createBuildDescription.js => createBuildManifest.js} (78%) diff --git a/lib/buildHelpers/createBuildDescription.js b/lib/buildHelpers/createBuildManifest.js similarity index 96% rename from lib/buildHelpers/createBuildDescription.js rename to lib/buildHelpers/createBuildManifest.js index dcbc86b66..78f67deda 100644 --- a/lib/buildHelpers/createBuildDescription.js +++ b/lib/buildHelpers/createBuildManifest.js @@ -37,8 +37,8 @@ module.exports = async function(project, buildConfig) { } } }, - buildDescription: { - descriptionVersion: "0.1", + buildManifest: { + manifestVersion: "0.1", timestamp: new Date().toISOString(), versions: { builderVersion: getVersion("@ui5/builder"), diff --git a/lib/builder.js b/lib/builder.js index 3bc49f411..c9c14d815 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -87,7 +87,7 @@ function getElapsedTime(startTime) { * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build - * @param {boolean} [parameters.createBuildDescription=false] Whether to create a build metadata file + * @param {boolean} [parameters.createBuildManifest=false] Whether to create a build metadata file * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. @@ -97,7 +97,7 @@ module.exports = async function({ graph, destPath, cleanDest = false, includedDependencies = [], excludedDependencies = [], complexDependencyIncludes, - selfContained = false, cssVariables = false, jsdoc = false, createBuildDescription = false, + selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, includedTasks = [], excludedTasks = [], }) { if (!graph) { @@ -275,19 +275,19 @@ module.exports = async function({ } const resources = await project.getReader({ - // Always use buildtime (=namespace) style when writing an createBuildDescription - style: createBuildDescription ? "buildtime" : "runtime" + // Always use buildtime (=namespace) style when writing with a build manifest + style: createBuildManifest ? "buildtime" : "runtime" }).byGlob("/**/*"); log.verbose(`Writing out files...`); - if (createBuildDescription) { - // Create and write createBuildDescription metadata file - const createBuildDescription = require("./buildHelpers/createBuildDescription"); - const metadata = await createBuildDescription(project, buildConfig); + if (createBuildManifest) { + // Create and write a build manifest metadata file + const createBuildManifest = require("./buildHelpers/createBuildManifest"); + const metadata = await createBuildManifest(project, buildConfig); await fsTarget.write(resourceFactory.createResource({ - path: `/.ui5/build-description.json`, - string: JSON.stringify(metadata, null, "\t") // TODO 3.0: minify? + path: `/.ui5/build-manifest.json`, + string: JSON.stringify(metadata, null, "\t") })); } diff --git a/lib/graph/Module.js b/lib/graph/Module.js index c2ec5e5d8..3ff5e5469 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -147,16 +147,16 @@ class Module { }); const specs = await Promise.all(configs.map(async (configuration) => { - const buildDescription = configuration._buildDescription; - if (configuration._buildDescription) { - delete configuration._buildDescription; + const buildManifest = configuration._buildManifest; + if (configuration._buildManifest) { + delete configuration._buildManifest; } const spec = await Specification.create({ id: this.getId(), version: this.getVersion(), modulePath: this.getPath(), configuration, - buildDescription + buildManifest }); log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); @@ -192,7 +192,7 @@ class Module { configurations = await this._getSuppliedConfigurations(); if (!configurations || !configurations.length) { - configurations = await this._getBuildDescriptionConfigurations(); + configurations = await this._getBuildManifestConfigurations(); } if (!configurations || !configurations.length) { configurations = await this._getYamlConfigurations(); @@ -236,9 +236,12 @@ class Module { log.verbose(`Configuration for module ${this.getId()} has been supplied directly`); return await Promise.all(this._suppliedConfigs.map(async (suppliedConfig) => { let config = suppliedConfig; - if (suppliedConfig.buildDescription) { + + // If we got supplied with a build manifest object, we need to move the build manifest metadata + // into the project and only return the project + if (suppliedConfig.buildManifest) { config = suppliedConfig.project; - config._buildDescription = suppliedConfig.buildDescription; + config._buildManifest = suppliedConfig.buildManifest; } return this._normalizeAndApplyShims(config); })); @@ -358,24 +361,26 @@ class Module { return configs; } - async _getBuildDescriptionConfigurations() { - const buildDescriptionMetadata = await this._readBuildDescription(); + async _getBuildManifestConfigurations() { + const buildManifestMetadata = await this._readBuildManifest(); - if (!buildDescriptionMetadata) { - log.verbose(`Could not find any build description in module ${this.getId()}`); + if (!buildManifestMetadata) { + log.verbose(`Could not find any build manifest in module ${this.getId()}`); return []; } - const config = buildDescriptionMetadata.project; - config._buildDescription = buildDescriptionMetadata.buildDescription; + // This function is expected to return the configuration of a project, so we add the buildManifest metadata + // to a temporary attribute of the project configuration and retrieve it later for Specification creation + const config = buildManifestMetadata.project; + config._buildManifest = buildManifestMetadata.buildManifest; return [this._normalizeAndApplyShims(config)]; } - async _readBuildDescription() { + async _readBuildManifest() { const reader = await this.getReader(); - const buildDescriptionResource = await reader.byPath("/.ui5/build-description.json"); - if (buildDescriptionResource) { - return JSON.parse(await buildDescriptionResource.getString()); + const buildManifestResource = await reader.byPath("/.ui5/build-manifest.json"); + if (buildManifestResource) { + return JSON.parse(await buildManifestResource.getString()); } } diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js index 282db9ad5..74fab9c12 100644 --- a/lib/specifications/Project.js +++ b/lib/specifications/Project.js @@ -24,15 +24,15 @@ class Project extends Specification { * @param {string} parameters.version Version * @param {string} parameters.modulePath File System path to access resources * @param {object} parameters.configuration Configuration object - * @param {object} [parameters.buildDescription] Build metadata object + * @param {object} [parameters.buildManifest] Build metadata object */ async init(parameters) { await super.init(parameters); - this._buildDescription = parameters.buildDescription; + this._buildManifest = parameters.buildManifest; await this._configureAndValidatePaths(this._config); - await this._parseConfiguration(this._config, this._buildDescription); + await this._parseConfiguration(this._config, this._buildManifest); return this; } @@ -94,6 +94,14 @@ class Project extends Specification { return this._config.builder?.settings; } + hasBuildManifest() { + return !!this._buildManifest; + } + + getBuildManifest() { + return this._buildManifest || {}; + } + /* === Resource Access === */ /** * Get a [ReaderCollection]{@link module:@ui5/fs.ReaderCollection} for accessing all resources of the @@ -122,7 +130,7 @@ class Project extends Specification { this._resourceTagCollection = new ResourceTagCollection({ allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], allowedNamespaces: ["project"], - tags: this.getBuildDescription()?.tags + tags: this.getBuildManifest()?.tags }); } return this._resourceTagCollection; diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 0163e17db..4d5454ec1 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -126,14 +126,6 @@ class Specification { return this._modulePath; } - hasBuildDescription() { - return !!this._buildDescription; - } - - getBuildDescription() { - return this._buildDescription || {}; - } - /* === Resource Access === */ /** * Get a resource reader for the root directory of the project diff --git a/test/fixtures/build-descriptions/application.a/.ui5/build-description.json b/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json similarity index 92% rename from test/fixtures/build-descriptions/application.a/.ui5/build-description.json rename to test/fixtures/build-manifest/application.a/.ui5/build-manifest.json index 9afd75511..03ff08f24 100644 --- a/test/fixtures/build-descriptions/application.a/.ui5/build-description.json +++ b/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json @@ -13,8 +13,8 @@ } } }, - "buildDescription": { - "descriptionVersion": "0.1", + "buildManifest": { + "manifestVersion": "0.1", "timestamp": "2022-05-04T12:45:30.024Z", "versions": { "builderVersion": "3.0.0", diff --git a/test/fixtures/build-descriptions/application.a/package.json b/test/fixtures/build-manifest/application.a/package.json similarity index 100% rename from test/fixtures/build-descriptions/application.a/package.json rename to test/fixtures/build-manifest/application.a/package.json diff --git a/test/fixtures/build-descriptions/application.a/resources/id1/index.html b/test/fixtures/build-manifest/application.a/resources/id1/index.html similarity index 100% rename from test/fixtures/build-descriptions/application.a/resources/id1/index.html rename to test/fixtures/build-manifest/application.a/resources/id1/index.html diff --git a/test/fixtures/build-descriptions/application.a/resources/id1/manifest.json b/test/fixtures/build-manifest/application.a/resources/id1/manifest.json similarity index 100% rename from test/fixtures/build-descriptions/application.a/resources/id1/manifest.json rename to test/fixtures/build-manifest/application.a/resources/id1/manifest.json diff --git a/test/fixtures/build-descriptions/application.a/resources/id1/test-dbg.js b/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js similarity index 100% rename from test/fixtures/build-descriptions/application.a/resources/id1/test-dbg.js rename to test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js diff --git a/test/fixtures/build-descriptions/application.a/resources/id1/test.js b/test/fixtures/build-manifest/application.a/resources/id1/test.js similarity index 100% rename from test/fixtures/build-descriptions/application.a/resources/id1/test.js rename to test/fixtures/build-manifest/application.a/resources/id1/test.js diff --git a/test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json b/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json similarity index 89% rename from test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json rename to test/fixtures/build-manifest/library.e/.ui5/build-manifest.json index b2c7c1a7b..5205a51a8 100644 --- a/test/fixtures/build-descriptions/library.e/.ui5/archive-metadata.json +++ b/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json @@ -9,13 +9,13 @@ "configuration": { "paths": { "src": "resources", - "test": "test-resources", + "test": "test-resources" } } } }, - "buildDescription": { - "descriptionVersion": "0.1", + "buildManifest": { + "manifestVersion": "0.1", "timestamp": "2022-05-06T09:54:29.051Z", "versions": { "builderVersion": "3.0.0", diff --git a/test/fixtures/build-descriptions/library.e/package.json b/test/fixtures/build-manifest/library.e/package.json similarity index 100% rename from test/fixtures/build-descriptions/library.e/package.json rename to test/fixtures/build-manifest/library.e/package.json diff --git a/test/fixtures/build-descriptions/library.e/resources/library/e/.library b/test/fixtures/build-manifest/library.e/resources/library/e/.library similarity index 100% rename from test/fixtures/build-descriptions/library.e/resources/library/e/.library rename to test/fixtures/build-manifest/library.e/resources/library/e/.library diff --git a/test/fixtures/build-descriptions/library.e/resources/library/e/some.js b/test/fixtures/build-manifest/library.e/resources/library/e/some.js similarity index 100% rename from test/fixtures/build-descriptions/library.e/resources/library/e/some.js rename to test/fixtures/build-manifest/library.e/resources/library/e/some.js diff --git a/test/fixtures/build-descriptions/library.e/test-resources/library/e/Test.html b/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html similarity index 100% rename from test/fixtures/build-descriptions/library.e/test-resources/library/e/Test.html rename to test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html diff --git a/test/lib/buildHelpers/createBuildDescription.integration.js b/test/lib/buildHelpers/createBuildManifest.integration.js similarity index 84% rename from test/lib/buildHelpers/createBuildDescription.integration.js rename to test/lib/buildHelpers/createBuildManifest.integration.js index d327c22a2..d5befab82 100644 --- a/test/lib/buildHelpers/createBuildDescription.integration.js +++ b/test/lib/buildHelpers/createBuildManifest.integration.js @@ -1,11 +1,11 @@ const test = require("ava"); const path = require("path"); -const createBuildDescription = require("../../../lib/buildHelpers/createBuildDescription"); +const createBuildManifest = require("../../../lib/buildHelpers/createBuildManifest"); const Module = require("../../../lib/graph/Module"); const Specification = require("../../../lib/specifications/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); -const buildDescrApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "application.a"); +const buildDescrApplicationAPath = path.join(__dirname, "..", "..", "fixtures", "build-manifest", "application.a"); const applicationAConfig = { id: "application.a.id", version: "1.0.0", @@ -18,7 +18,7 @@ const applicationAConfig = { } }; const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); -const buildDescrLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "library.e"); +const buildDescrLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "build-manifest", "library.e"); const libraryEConfig = { id: "library.e.id", version: "1.0.0", @@ -38,13 +38,13 @@ const buildConfig = { excludedTasks: [] }; -// Note: The actual build-description.json files in the fixtures are never used in these tests +// Note: The actual build-manifest.json files in the fixtures are never used in these tests -test("Create project from application project providing a build description", async (t) => { +test("Create project from application project providing a build manifest", async (t) => { const inputProject = await Specification.create(applicationAConfig); inputProject.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); - const metadata = await createBuildDescription(inputProject, buildConfig); + const metadata = await createBuildManifest(inputProject, buildConfig); const m = new Module({ id: "build-descr-application.a.id", version: "2.0.0", @@ -53,7 +53,7 @@ test("Create project from application project providing a build description", as }); const {project} = await m.getSpecifications(); - t.truthy(project, "Module was able to create project from build description metadata"); + t.truthy(project, "Module was able to create project from build manifest metadata"); t.is(project.getName(), project.getName(), "Archive project has correct name"); t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); t.is(project.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true, @@ -67,11 +67,11 @@ test("Create project from application project providing a build description", as "Resource has expected path"); }); -test("Create project from library project providing a build description", async (t) => { +test("Create project from library project providing a build manifest", async (t) => { const inputProject = await Specification.create(libraryEConfig); inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); - const metadata = await createBuildDescription(inputProject, buildConfig); + const metadata = await createBuildManifest(inputProject, buildConfig); const m = new Module({ id: "build-descr-library.e.id", version: "2.0.0", @@ -80,7 +80,7 @@ test("Create project from library project providing a build description", async }); const {project} = await m.getSpecifications(); - t.truthy(project, "Module was able to create project from build description metadata"); + t.truthy(project, "Module was able to create project from build manifest metadata"); t.is(project.getName(), project.getName(), "Archive project has correct name"); t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace"); t.is(project.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true, diff --git a/test/lib/buildHelpers/createBuildDescription.js b/test/lib/buildHelpers/createBuildManifest.js similarity index 78% rename from test/lib/buildHelpers/createBuildDescription.js rename to test/lib/buildHelpers/createBuildManifest.js index f682a79d8..c143a788e 100644 --- a/test/lib/buildHelpers/createBuildDescription.js +++ b/test/lib/buildHelpers/createBuildManifest.js @@ -1,6 +1,6 @@ const test = require("ava"); const path = require("path"); -const createBuildDescription = require("../../../lib/buildHelpers/createBuildDescription"); +const createBuildManifest = require("../../../lib/buildHelpers/createBuildManifest"); const Specification = require("../../../lib/specifications/Specification"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); @@ -39,13 +39,13 @@ const libraryProjectInput = { } }; -test("Create application archive from project", async (t) => { +test("Create application from project with build manifest", async (t) => { const project = await Specification.create(applicationProjectInput); project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant"); - const metadata = await createBuildDescription(project, "buildConfig"); - t.truthy(new Date(metadata.buildDescription.timestamp), "Timestamp is valid"); - metadata.buildDescription.timestamp = ""; + const metadata = await createBuildManifest(project, "buildConfig"); + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; t.deepEqual(metadata, { project: { @@ -62,8 +62,8 @@ test("Create application archive from project", async (t) => { }, } }, - buildDescription: { - descriptionVersion: "0.1", + buildManifest: { + manifestVersion: "0.1", buildConfig: "buildConfig", namespace: "id1", timestamp: "", @@ -82,13 +82,13 @@ test("Create application archive from project", async (t) => { }, "Returned correct metadata"); }); -test("Create library archive from project", async (t) => { +test("Create library from project with build manifest", async (t) => { const project = await Specification.create(libraryProjectInput); project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant"); - const metadata = await createBuildDescription(project, "buildConfig"); - t.truthy(new Date(metadata.buildDescription.timestamp), "Timestamp is valid"); - metadata.buildDescription.timestamp = ""; + const metadata = await createBuildManifest(project, "buildConfig"); + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; t.deepEqual(metadata, { project: { @@ -106,8 +106,8 @@ test("Create library archive from project", async (t) => { }, } }, - buildDescription: { - descriptionVersion: "0.1", + buildManifest: { + manifestVersion: "0.1", buildConfig: "buildConfig", namespace: "library/d", timestamp: "", diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index 84f116b4a..6bd2dc7b1 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -5,19 +5,27 @@ const Module = require("../../../lib/graph/Module"); const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); const buildDescriptionApplicationAPath = - path.join(__dirname, "..", "..", "fixtures", "build-descriptions", "application.a"); + path.join(__dirname, "..", "..", "fixtures", "build-manifest", "application.a"); +const buildDescriptionLibraryAPath = + path.join(__dirname, "..", "..", "fixtures", "build-manifest", "library.e"); const basicModuleInput = { id: "application.a.id", version: "1.0.0", modulePath: applicationAPath }; -const archiveProjectInput = { +const archiveAppProjectInput = { id: "application.a.id", version: "1.0.0", modulePath: buildDescriptionApplicationAPath }; +const archiveLibProjectInput = { + id: "library.e.id", + version: "1.0.0", + modulePath: buildDescriptionLibraryAPath +}; + // test.beforeEach((t) => { // }); @@ -46,9 +54,16 @@ test("Get specifications from module", async (t) => { t.is(extensions.length, 0, "Should return no extensions"); }); -test.only("Get specifications from project with build description", async (t) => { - const ui5Module = new Module(archiveProjectInput); +test.only("Get specifications from application project with build manifest", async (t) => { + const ui5Module = new Module(archiveAppProjectInput); const {project, extensions} = await ui5Module.getSpecifications(); t.is(project.getName(), "application.a", "Should return correct project"); t.is(extensions.length, 0, "Should return no extensions"); }); + +test.only("Get specifications from library project with build manifest", async (t) => { + const ui5Module = new Module(archiveLibProjectInput); + const {project, extensions} = await ui5Module.getSpecifications(); + t.is(project.getName(), "library.e", "Should return correct project"); + t.is(extensions.length, 0, "Should return no extensions"); +}); From b4f0594ca5bd0a62874fac9b79800d8d8c917735 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Fri, 10 Jun 2022 12:29:14 +0200 Subject: [PATCH 88/99] generateProjectGraph: Allow rootConfiguration/rootConfigPath in all variants --- lib/generateProjectGraph.js | 18 ++++++++++++++++-- lib/graph/Module.js | 15 ++++++++++++--- lib/graph/projectGraphBuilder.js | 2 +- lib/graph/providers/DependencyTree.js | 16 +++++++++++++--- test/lib/generateProjectGraph.usingObject.js | 2 +- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/generateProjectGraph.js b/lib/generateProjectGraph.js index 850eeba5e..c00f75b34 100644 --- a/lib/generateProjectGraph.js +++ b/lib/generateProjectGraph.js @@ -68,6 +68,10 @@ const generateProjectGraph = { * @param {object} options * @param {object} [options.filePath=projectDependencies.yaml] Path to the dependency configuration file * @param {string} [options.cwd=process.cwd()] Directory to resolve relative paths to + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph @@ -75,6 +79,7 @@ const generateProjectGraph = { */ usingStaticFile: async function({ cwd, filePath = "projectDependencies.yaml", + rootConfiguration, rootConfigPath, versionOverride, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using static file...`); @@ -84,7 +89,9 @@ const generateProjectGraph = { const DependencyTreeProvider = require("./graph/providers/DependencyTree"); const provider = new DependencyTreeProvider({ - dependencyTree + dependencyTree, + rootConfiguration, + rootConfigPath }); const projectGraph = await projectGraphBuilder(provider); @@ -104,6 +111,10 @@ const generateProjectGraph = { * @public * @param {object} options * @param {module:@ui5/project.graph.providers.DependencyTree.TreeNode} options.dependencyTree + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph @@ -111,13 +122,16 @@ const generateProjectGraph = { */ usingObject: async function({ dependencyTree, + rootConfiguration, rootConfigPath, versionOverride, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using object...`); const DependencyTreeProvider = require("./graph/providers/DependencyTree"); const dependencyTreeProvider = new DependencyTreeProvider({ - dependencyTree + dependencyTree, + rootConfiguration, + rootConfigPath }); const projectGraph = await projectGraphBuilder(dependencyTreeProvider); diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 3ff5e5469..9d8f83168 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -34,11 +34,12 @@ class Module { * Either a path relative to `modulePath` which will be resolved by @ui5/fs (default), * or an absolute File System path to the configuration file. * @param {object|object[]} [parameters.configuration] - * Configuration object or array of objects to use. If supplied, no ui5.yaml will be read + * Configuration object or array of objects to use. If supplied, no configuration files + * will be read and the `configPath` option must not be provided. * @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection] * Collection of shims that might be relevant for this module */ - constructor({id, version, modulePath, configPath = DEFAULT_CONFIG_PATH, configuration = [], shimCollection}) { + constructor({id, version, modulePath, configPath, configuration = [], shimCollection}) { if (!id) { throw new Error(`Could not create Module: Missing or empty parameter 'id'`); } @@ -48,11 +49,19 @@ class Module { if (!modulePath) { throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`); } + if ( + ((Array.isArray(configuration) && configuration.length > 0) || typeof configuration === "object") && + configPath + ) { + throw new Error( + `Could not create Module: 'configPath' must not be provided in combination with 'configuration'` + ); + } this._id = id; this._version = version; this._modulePath = modulePath; - this._configPath = configPath; + this._configPath = configPath || DEFAULT_CONFIG_PATH; this._dependencies = {}; if (!Array.isArray(configuration)) { diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js index 45a0c6c35..f32e0f9b7 100644 --- a/lib/graph/projectGraphBuilder.js +++ b/lib/graph/projectGraphBuilder.js @@ -103,7 +103,7 @@ module.exports = async function(nodeProvider) { const {project: rootProject, extensions: rootExtensions} = await rootModule.getSpecifications(); if (!rootProject) { throw new Error( - `Failed to crate a UI5 project from module ${rootNode.id} at ${rootNode.path}. ` + + `Failed to create a UI5 project from module ${rootNode.id} at ${rootNode.path}. ` + `Make sure the path is correct and a project configuration is present or supplied.`); } diff --git a/lib/graph/providers/DependencyTree.js b/lib/graph/providers/DependencyTree.js index abd7106a1..b9a470486 100644 --- a/lib/graph/providers/DependencyTree.js +++ b/lib/graph/providers/DependencyTree.js @@ -18,14 +18,24 @@ class DependencyTree { * * @public * @alias module:@ui5/project.graph.providers.DependencyTree - * @param {object} parameters - * @param {TreeNode} parameters.dependencyTree Dependency tree as returned by a translator + * @param {object} options + * @param {TreeNode} options.dependencyTree Dependency tree as returned by a translator + * @param {object} [options.rootConfiguration] + * Configuration object to use for the root module instead of reading from a configuration file + * @param {string} [options.rootConfigPath] + * Configuration file to use for the root module instead the default ui5.yaml */ - constructor({dependencyTree}) { + constructor({dependencyTree, rootConfiguration, rootConfigPath}) { if (!dependencyTree) { throw new Error(`Failed to instantiate DependencyTree provider: Missing parameter 'dependencyTree'`); } this._tree = dependencyTree; + if (rootConfiguration) { + this._tree.configuration = rootConfiguration; + } + if (rootConfigPath) { + this._tree.configPath = rootConfigPath; + } } async getRootNode() { diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index ff197dc5d..40e2c6ab1 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -310,7 +310,7 @@ test("Missing configuration file for root project", async (t) => { await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), { message: - "Failed to crate a UI5 project from module application.a.id at non-existent. " + + "Failed to create a UI5 project from module application.a.id at non-existent. " + "Make sure the path is correct and a project configuration is present or supplied." }, "Rejected with error"); From 663099d5717860c30d5878aa491522a32d1d7a7f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 10 Jun 2022 13:54:43 +0200 Subject: [PATCH 89/99] Builder: Fix renaming of build manifest --- lib/builder.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/builder.js b/lib/builder.js index c9c14d815..c04c8af45 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -210,10 +210,10 @@ module.exports = async function({ if (projectsToBuild.includes(projectName) && builder.requiresDependencies(buildConfig)) { getDependencies().forEach((dep) => { const depName = dep.getName(); - if (project.hasBuildDescription() && !dep.hasBuildDescription()) { + if (project.hasBuildManifest() && !dep.hasBuildManifest()) { throw new Error( - `Project ${depName} must provide a build description since it is a dependency of ` + - `project ${projectName} which already provides a build description`); + `Project ${depName} must provide a build manifest since it is a dependency of ` + + `project ${projectName} which already provides a build manifest`); } log.info(`Project ${projectName} requires dependency ${depName} to be built`); if (!projectsToBuild.includes(depName)) { @@ -224,8 +224,8 @@ module.exports = async function({ }); projectsToBuild = projectsToBuild.filter((projectName) => { - if (graph.getProject(projectName).hasBuildDescription()) { - log.verbose(`Found a build description for project ${projectName}. Skipping build.`); + if (graph.getProject(projectName).hasBuildManifest()) { + log.verbose(`Found a build manifest for project ${projectName}. Skipping build.`); return false; } return true; From 0e8e56be2c3ff2f9e29e4334da4aa3ba7f20da05 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Fri, 10 Jun 2022 14:07:57 +0200 Subject: [PATCH 90/99] graph.Module: Fix parameters handling --- lib/graph/Module.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 9d8f83168..94d4f9dc6 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -50,8 +50,10 @@ class Module { throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`); } if ( - ((Array.isArray(configuration) && configuration.length > 0) || typeof configuration === "object") && - configPath + ( + (Array.isArray(configuration) && configuration.length > 0) || + (!Array.isArray(configuration) && typeof configuration === "object") + ) && configPath ) { throw new Error( `Could not create Module: 'configPath' must not be provided in combination with 'configuration'` From 0317cd01d0afe540ccfdce73a2d3cd8d491763af Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 10 Jun 2022 15:47:15 +0200 Subject: [PATCH 91/99] Remove type legacy-library --- lib/buildDefinitions/getInstance.js | 1 - lib/buildHelpers/createBuildManifest.js | 1 - lib/graph/Module.js | 5 -- lib/specifications/ComponentProject.js | 26 +++++---- lib/specifications/Specification.js | 3 - lib/specifications/types/Application.js | 7 +-- lib/specifications/types/LegacyLibrary.js | 69 ----------------------- lib/specifications/types/Library.js | 24 ++++---- lib/validation/validator.js | 4 -- 9 files changed, 33 insertions(+), 107 deletions(-) delete mode 100644 lib/specifications/types/LegacyLibrary.js diff --git a/lib/buildDefinitions/getInstance.js b/lib/buildDefinitions/getInstance.js index a55ade037..2cd6eb730 100644 --- a/lib/buildDefinitions/getInstance.js +++ b/lib/buildDefinitions/getInstance.js @@ -18,7 +18,6 @@ module.exports = function(parameters) { case "application": return createInstance("ApplicationBuilder", parameters); case "library": - case "legacy-library": return createInstance("LibraryBuilder", parameters); case "module": return createInstance("ModuleBuilder", parameters); diff --git a/lib/buildHelpers/createBuildManifest.js b/lib/buildHelpers/createBuildManifest.js index 78f67deda..7f46915dd 100644 --- a/lib/buildHelpers/createBuildManifest.js +++ b/lib/buildHelpers/createBuildManifest.js @@ -14,7 +14,6 @@ module.exports = async function(project, buildConfig) { break; case "library": case "theme-library": - case "legacy-library": pathMapping.src = `resources`; pathMapping.test = `test-resources`; break; diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 94d4f9dc6..875f7ff6e 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -11,7 +11,6 @@ const log = require("@ui5/logger").getLogger("graph:Module"); const DEFAULT_CONFIG_PATH = "ui5.yaml"; const SAP_THEMES_NS_EXEMPTIONS = ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]; -const SAP_LEGACY_LIBRARIES = ["sap.ui.core"]; function clone(obj) { return JSON.parse(JSON.stringify(obj)); @@ -150,10 +149,6 @@ class Module { if (SAP_THEMES_NS_EXEMPTIONS.includes(libraryName)) { configuration.type = "theme-library"; } - // Old legacy-libraries where configured as type "library" - if (SAP_LEGACY_LIBRARIES.includes(libraryName)) { - configuration.type = "legacy-library"; - } } }); diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index dc7bd7284..b609d5f9a 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -104,7 +104,8 @@ class ComponentProject extends Project { // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? if (style === "runtime" && this._isRuntimeNamespaced) { - // If the project's runtime requires namespaces, paths are identical to "buildtime" style + // If the project's runtime paths contains its namespace too, + // "runtime" style paths are identical to "buildtime" style paths style = "buildtime"; } let reader; @@ -116,14 +117,19 @@ class ComponentProject extends Project { // Use buildtime reader and link it to / // No test-resources for runtime resource access, // unless runtime is namespaced - reader = this.getReader().link({ + reader = this._getReader().link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); break; case "flat": - // No test-resources for flat resource access - reader = this._getFlatSourceReader("/"); + // Use buildtime reader and link it to / + // No test-resources for runtime resource access, + // unless runtime is namespaced + reader = this._getReader("/").link({ + linkPath: `/`, + targetPath: `/resources/${this._namespace}/` + }); break; default: throw new Error(`Unknown path mapping style ${style}`); @@ -138,8 +144,8 @@ class ComponentProject extends Project { * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - _getFlatSourceReader() { - throw new Error(`_getFlatSourceReader must be implemented by subclass ${this.constructor.name}`); + _getSourceReader() { + throw new Error(`_getSourceReader must be implemented by subclass ${this.constructor.name}`); } /** @@ -147,8 +153,8 @@ class ComponentProject extends Project { * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - _getFlatTestReader() { - throw new Error(`_getFlatTestReader must be implemented by subclass ${this.constructor.name}`); + _getTestReader() { + throw new Error(`_getTestReader must be implemented by subclass ${this.constructor.name}`); } /** @@ -198,8 +204,8 @@ class ComponentProject extends Project { } _getReader() { - let reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); - const testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); + let reader = this._getSourceReader(); + const testReader = this._getTestReader(); if (testReader) { reader = resourceFactory.createReaderCollection({ name: `Reader collection for project ${this.getName()}`, diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 4d5454ec1..2e2487319 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -177,9 +177,6 @@ class Specification { case "library": { return createAndInitializeSpec("Library", params); } - case "legacy-library": { - return createAndInitializeSpec("LegacyLibrary", params); - } case "theme-library": { return createAndInitializeSpec("ThemeLibrary", params); } diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 783a574f6..c3a421f80 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -30,20 +30,19 @@ class Application extends ComponentProject { /** * Get a resource reader for the sources of the project (excluding any test resources) * - * @param {string} virBasePath * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ - _getFlatSourceReader(virBasePath = "/") { + _getSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), - virBasePath, + virBasePath: `/resources/${this._namespace}/`, name: `Source reader for application project ${this.getName()}`, project: this, excludes: this.getBuilderResourcesExcludes() }); } - _getFlatTestReader() { + _getTestReader() { return null; // Applications do not have a dedicated test directory } diff --git a/lib/specifications/types/LegacyLibrary.js b/lib/specifications/types/LegacyLibrary.js deleted file mode 100644 index c0b0da375..000000000 --- a/lib/specifications/types/LegacyLibrary.js +++ /dev/null @@ -1,69 +0,0 @@ -const fsPath = require("path"); -const resourceFactory = require("@ui5/fs").resourceFactory; -const Library = require("./Library"); - -/** - * Legacy UI5 library with resources outside its namespace - */ -class LegacyLibrary extends Library { - constructor(parameters) { - super(parameters); - } - - /* === Attributes === */ - - /* === Resource Access === */ - /* - * - * Get a resource reader for the sources of the project (excluding any test resources) - * In the future the path structure can be flat or namespaced depending on the project - * - * @public - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getSourceReader() { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/", - name: `Source reader for library project ${this.getName()}`, - project: this, - excludes: this.getBuilderResourcesExcludes() - }); - } - - _getFlatSourceReader(virBasePath = "/") { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: this._stripNamespace(virBasePath), - name: `Source reader for library project ${this.getName()}`, - project: this, - excludes: this.getBuilderResourcesExcludes() - }); - } - - _getFlatTestReader(virBasePath = "/") { - if (!this._testPathExists) { - return null; - } - const testReader = resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._testPath), - virBasePath: this._stripNamespace(virBasePath), - name: `Runtime test-resources reader for library project ${this.getName()}`, - project: this, - excludes: this.getBuilderResourcesExcludes() - }); - return testReader; - } - - /** - * Legacy libraries have resources outside their namespace or multiple namespaces - * Therefore it is necessary to remove the namespace form any virtual base paths - * - * @param {string} string Virtual base path to remove an eventual namespace from - */ - _stripNamespace(string) { - return string.replace(this._namespace + "/", ""); - } -} - -module.exports = LegacyLibrary; diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index a7846c2c9..8e3144754 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -31,14 +31,16 @@ class Library extends ComponentProject { } /* === Resource Access === */ - _getFlatSourceReader(virBasePath = "/") { + _getSourceReader() { // TODO: Throw for libraries with additional namespaces like sap.ui.core? - let fsBasePath = fsPath.join(this.getPath(), this._srcPath); - if (this._isSourceNamespaced) { - fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + let virBasePath = "/resources/"; + if (!this._isSourceNamespaced) { + // In case the namespace is not represented in the source directory + // structure, add it to the virtual base path + virBasePath += `${this._namespace}/`; } return resourceFactory.createReader({ - fsBasePath, + fsBasePath: fsPath.join(this.getPath(), this._srcPath), virBasePath, name: `Source reader for library project ${this.getName()}`, project: this, @@ -46,16 +48,18 @@ class Library extends ComponentProject { }); } - _getFlatTestReader(virBasePath = "/") { + _getTestReader() { if (!this._testPathExists) { return null; } - let fsBasePath = fsPath.join(this.getPath(), this._testPath); - if (this._isSourceNamespaced) { - fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + let virBasePath = "/test-resources/"; + if (!this._isSourceNamespaced) { + // In case the namespace is not represented in the source directory + // structure, add it to the virtual base path + virBasePath += `${this._namespace}/`; } const testReader = resourceFactory.createReader({ - fsBasePath, + fsBasePath: fsPath.join(this.getPath(), this._testPath), virBasePath, name: `Runtime test-resources reader for library project ${this.getName()}`, project: this, diff --git a/lib/validation/validator.js b/lib/validation/validator.js index b97a725e6..bdee5de46 100644 --- a/lib/validation/validator.js +++ b/lib/validation/validator.js @@ -37,10 +37,6 @@ class Validator { async validate({config, project, yaml}) { const fnValidate = await this._compileSchema(); - if (config && config.type === "legacy-library") { - // TODO: Introduce legacy-library schema - return; - } const valid = fnValidate(config); if (!valid) { const ValidationError = require("./ValidationError"); From 073418c3432f612bc666ec9101624ae851c6d5a7 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 10 Jun 2022 20:38:32 +0200 Subject: [PATCH 92/99] Stop passing legacy 'namespace' parameter to tasks --- lib/buildDefinitions/AbstractBuilder.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index a339b296a..9d731d9a2 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -195,7 +195,6 @@ class AbstractBuilder { options.projectName = this.project.getName(); // TODO: Deprecate "namespace" in favor of "projectNamespace" as already used for custom tasks? options.projectNamespace = this.project.getNamespace(); - options.namespace = this.project.getNamespace(); const params = { workspace, From 1064c9388cda0449b8f159ce30b2f72e54d07fb3 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Sat, 11 Jun 2022 11:22:47 +0200 Subject: [PATCH 93/99] Only create build manifest for root project and only for libraries --- lib/buildDefinitions/AbstractBuilder.js | 2 +- lib/builder.js | 29 +++++++++++++++---------- lib/specifications/ComponentProject.js | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js index 9d731d9a2..a65ee3e9c 100644 --- a/lib/buildDefinitions/AbstractBuilder.js +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -274,7 +274,7 @@ class AbstractBuilder { }); return allTasks.some((taskName) => { if (this.tasks[taskName].requiresDependencies) { - this.log.info(`Task ${taskName} for project ${this.project.getName()} requires dependencies`); + this.log.verbose(`Task ${taskName} for project ${this.project.getName()} requires dependencies`); return true; } return false; diff --git a/lib/builder.js b/lib/builder.js index c04c8af45..c89888b9a 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -87,7 +87,8 @@ function getElapsedTime(startTime) { * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.cssVariables=false] Flag to activate CSS variables generation * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build - * @param {boolean} [parameters.createBuildManifest=false] Whether to create a build metadata file + * @param {boolean} [parameters.createBuildManifest=false] Whether to create a build manifest file for the root project. + * This is currently only supported for projects of type 'library' and 'theme-library' * @param {Array.} [parameters.includedTasks=[]] List of tasks to be included * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. @@ -127,6 +128,12 @@ module.exports = async function({ const startTime = process.hrtime(); const rootProjectName = graph.getRoot().getName(); + + if (createBuildManifest && !["library", "theme-library"].includes(graph.getRoot().getType())) { + throw new Error( + `Build manifest creation is currently not supported for projects of type ${graph.getRoot().getType()}`); + } + log.info(`Building project ${rootProjectName}`); if (includedDependencies.length) { log.info(` Including dependencies:`); @@ -148,7 +155,7 @@ module.exports = async function({ const buildContext = new BuildContext({ graph, options: { - cssVariables: cssVariables + cssVariables } }); const cleanupSigHooks = registerCleanupSigHooks(buildContext); @@ -193,7 +200,7 @@ module.exports = async function({ project, log: buildLogger }); - log.info(`Preparing project ${projectName}...`); + log.verbose(`Preparing project ${projectName}...`); const taskUtil = projectContext.getTaskUtil(); const builder = getBuildDefinitionInstance({ @@ -213,10 +220,11 @@ module.exports = async function({ if (project.hasBuildManifest() && !dep.hasBuildManifest()) { throw new Error( `Project ${depName} must provide a build manifest since it is a dependency of ` + - `project ${projectName} which already provides a build manifest`); + `project ${projectName} which already provides a build manifest and therefore ` + + `cannot be re-built`); } - log.info(`Project ${projectName} requires dependency ${depName} to be built`); if (!projectsToBuild.includes(depName)) { + log.info(`Project ${projectName} requires dependency ${depName} to be built`); projectsToBuild.push(depName); } }); @@ -274,14 +282,14 @@ module.exports = async function({ return; } + log.verbose(`Writing out files...`); + const taskUtil = projectContext.getTaskUtil(); const resources = await project.getReader({ - // Always use buildtime (=namespace) style when writing with a build manifest - style: createBuildManifest ? "buildtime" : "runtime" + // Force buildtime (=namespaced) style when writing with a build manifest + style: taskUtil.isRootProject() && createBuildManifest ? "buildtime" : "runtime" }).byGlob("/**/*"); - log.verbose(`Writing out files...`); - - if (createBuildManifest) { + if (taskUtil.isRootProject() && createBuildManifest) { // Create and write a build manifest metadata file const createBuildManifest = require("./buildHelpers/createBuildManifest"); const metadata = await createBuildManifest(project, buildConfig); @@ -291,7 +299,6 @@ module.exports = async function({ })); } - const taskUtil = projectContext.getTaskUtil(); await Promise.all(resources.map((resource) => { if (taskUtil.getTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult)) { log.verbose(`Skipping write of resource tagged as "OmitFromBuildResult": ` + diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index b609d5f9a..2f77f7ba0 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -126,7 +126,7 @@ class ComponentProject extends Project { // Use buildtime reader and link it to / // No test-resources for runtime resource access, // unless runtime is namespaced - reader = this._getReader("/").link({ + reader = this._getReader().link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); From 50d41378892642135211e71d4a7265a378d9637e Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 13 Jun 2022 10:36:08 +0200 Subject: [PATCH 94/99] Remove projectContext.STANDARD_TAGS They are exposed in TaskUtil already --- lib/buildHelpers/ProjectBuildContext.js | 11 +---------- test/lib/buildHelpers/ProjectBuildContext.js | 13 ------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js index 74fc9c231..47bed053e 100644 --- a/lib/buildHelpers/ProjectBuildContext.js +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -1,13 +1,6 @@ const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; const TaskUtil = require("@ui5/builder").tasks.TaskUtil; -// Note: When adding standard tags, always update the public documentation in TaskUtil -// (Type "module:@ui5/builder.tasks.TaskUtil~StandardBuildTags") -const STANDARD_TAGS = Object.freeze({ - OmitFromBuildResult: "ui5:OmitFromBuildResult", - IsBundle: "ui5:IsBundle", -}); - /** * Build context of a single project. Always part of an overall * [Build Context]{@link module:@ui5/builder.builder.BuildContext} @@ -33,10 +26,8 @@ class ProjectBuildContext { cleanup: [] }; - this.STANDARD_TAGS = STANDARD_TAGS; - this._resourceTagCollection = new ResourceTagCollection({ - allowedTags: Object.values(this.STANDARD_TAGS), + allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"], allowedNamespaces: ["build"] }); } diff --git a/test/lib/buildHelpers/ProjectBuildContext.js b/test/lib/buildHelpers/ProjectBuildContext.js index f1f8ce542..c1e726699 100644 --- a/test/lib/buildHelpers/ProjectBuildContext.js +++ b/test/lib/buildHelpers/ProjectBuildContext.js @@ -112,19 +112,6 @@ test("executeCleanupTasks", (t) => { t.is(task2.callCount, 1, "my task 2", "Cleanup task 2 got called"); }); -test("STANDARD_TAGS constant", (t) => { - const projectBuildContext = new ProjectBuildContext({ - buildContext: {}, - project: "project", - log: "log" - }); - - t.deepEqual(projectBuildContext.STANDARD_TAGS, { - OmitFromBuildResult: "ui5:OmitFromBuildResult", - IsBundle: "ui5:IsBundle" - }, "Exposes correct STANDARD_TAGS constant"); -}); - test.serial("getResourceTagCollection", (t) => { const projectAcceptsTagStub = sinon.stub().returns(false); projectAcceptsTagStub.withArgs("project-tag").returns(true); From 9503f2112a6c1195283d276f17a00ae3f712fecc Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Mon, 13 Jun 2022 10:37:27 +0200 Subject: [PATCH 95/99] Add missing tests --- .../application.a/ui5-test-configPath.yaml | 7 ++ test/lib/generateProjectGraph.usingObject.js | 104 ++++++++++++------ .../generateProjectGraph.usingStaticFile.js | 29 +++++ test/lib/graph/Module.js | 103 ++++++++++++++++- 4 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 test/fixtures/application.a/ui5-test-configPath.yaml diff --git a/test/fixtures/application.a/ui5-test-configPath.yaml b/test/fixtures/application.a/ui5-test-configPath.yaml new file mode 100644 index 000000000..a50b3c48b --- /dev/null +++ b/test/fixtures/application.a/ui5-test-configPath.yaml @@ -0,0 +1,7 @@ +--- +specVersion: "2.3" +type: application +metadata: + name: application.a +customConfiguration: + configPathTest: true \ No newline at end of file diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index 40e2c6ab1..a0485c144 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -40,14 +40,14 @@ test.afterEach.always((t) => { test("Application A", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); + const projectGraph = await projectGraphFromTree({dependencyTree: getApplicationATree()}); const rootProject = projectGraph.getRoot(); t.is(rootProject.getName(), "application.a", "Returned correct root project"); }); test("Application A: Traverse project graph breadth first", async (t) => { const {projectGraphFromTree} = t.context; - const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); + const projectGraph = await projectGraphFromTree({dependencyTree: getApplicationATree()}); const callbackStub = t.context.sinon.stub().resolves(); await projectGraph.traverseBreadthFirst(callbackStub); @@ -107,7 +107,7 @@ test("Application Cycle B: Traverse project graph breadth first with cycles", as test("Application A: Traverse project graph depth first", async (t) => { const {projectGraphFromTree, sinon} = t.context; - const projectGraph = await projectGraphFromTree({dependencyTree: applicationATree}); + const projectGraph = await projectGraphFromTree({dependencyTree: getApplicationATree()}); const callbackStub = sinon.stub().resolves(); await projectGraph.traverseDepthFirst(callbackStub); @@ -641,38 +641,40 @@ test("Project with nested invalid dependencies", async (t) => { /* ========================= */ /* ======= Test data ======= */ -const applicationATree = { - id: "application.a.id", - version: "1.0.0", - path: applicationAPath, - dependencies: [ - { - id: "library.d.id", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "library.d"), - dependencies: [ - { - id: "library.a.id", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.a"), - dependencies: [] - }, - { - id: "library.b.id", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.b"), - dependencies: [] - }, - { - id: "library.c.id", - version: "1.0.0", - path: path.join(applicationAPath, "node_modules", "collection", "library.c"), - dependencies: [] - } - ] - } - ] -}; +function getApplicationATree() { + return { + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [ + { + id: "library.d.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "library.d"), + dependencies: [ + { + id: "library.a.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.a"), + dependencies: [] + }, + { + id: "library.b.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.b"), + dependencies: [] + }, + { + id: "library.c.id", + version: "1.0.0", + path: path.join(applicationAPath, "node_modules", "collection", "library.c"), + dependencies: [] + } + ] + } + ] + }; +} const applicationCycleATreeIncDeduped = { @@ -1618,3 +1620,35 @@ test("Project with middleware extension dependency", async (t) => { ]); t.truthy(graph.getExtension("middleware.a"), "Extension should be added to the graph"); }); + +test("rootConfiguration", async (t) => { + const {projectGraphFromTree} = t.context; + const projectGraph = await projectGraphFromTree({ + dependencyTree: getApplicationATree(), + rootConfiguration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + rootConfigurationTest: true + } + } + }); + + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigurationTest: true + }); +}); + +test("rootConfig", async (t) => { + const {projectGraphFromTree} = t.context; + const projectGraph = await projectGraphFromTree({ + dependencyTree: getApplicationATree(), + rootConfigPath: "ui5-test-rootConfigPath.yaml" + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigPathTest: true + }); +}); diff --git a/test/lib/generateProjectGraph.usingStaticFile.js b/test/lib/generateProjectGraph.usingStaticFile.js index d61474c8e..61c8229a8 100644 --- a/test/lib/generateProjectGraph.usingStaticFile.js +++ b/test/lib/generateProjectGraph.usingStaticFile.js @@ -41,3 +41,32 @@ test("Throws error if file not found", async (t) => { `ENOENT: no such file or directory, open '${notExistingPath}/projectDependencies.yaml'`, "Correct error message"); }); + +test("rootConfiguration", async (t) => { + const projectGraph = await projectGraphFromStaticFile({ + cwd: applicationHPath, + rootConfiguration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + rootConfigurationTest: true + } + } + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigurationTest: true + }); +}); + +test("rootConfig", async (t) => { + const projectGraph = await projectGraphFromStaticFile({ + cwd: applicationHPath, + rootConfigPath: "ui5-test-configPath.yaml" + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + configPathTest: true + }); +}); diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index 6bd2dc7b1..fd362bf03 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -54,16 +54,115 @@ test("Get specifications from module", async (t) => { t.is(extensions.length, 0, "Should return no extensions"); }); -test.only("Get specifications from application project with build manifest", async (t) => { +test("Get specifications from application project with build manifest", async (t) => { const ui5Module = new Module(archiveAppProjectInput); const {project, extensions} = await ui5Module.getSpecifications(); t.is(project.getName(), "application.a", "Should return correct project"); t.is(extensions.length, 0, "Should return no extensions"); }); -test.only("Get specifications from library project with build manifest", async (t) => { +test("Get specifications from library project with build manifest", async (t) => { const ui5Module = new Module(archiveLibProjectInput); const {project, extensions} = await ui5Module.getSpecifications(); t.is(project.getName(), "library.e", "Should return correct project"); t.is(extensions.length, 0, "Should return no extensions"); }); + +test("configuration (object)", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + configurationTest: true + } + } + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true + }); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("configuration (array)", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: [{ + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a" + }, + customConfiguration: { + configurationTest: true + } + }, { + specVersion: "2.6", + kind: "extension", + type: "project-shim", + metadata: { + name: "my-project-shim" + }, + shims: {} + }] + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configurationTest: true + }); + t.is(extensions.length, 1, "Should return one extension"); +}); + +test("configPath", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-configPath.yaml" + }); + const {project, extensions} = await ui5Module.getSpecifications(); + t.deepEqual(project.getCustomConfiguration(), { + configPathTest: true + }); + t.is(extensions.length, 0, "Should return no extensions"); +}); + +test("configuration + configPath must not be provided", async (t) => { + // 'configuration' as object + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "test-ui5.yaml", + configuration: { + test: "configuration" + } + }); + }, { + message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'" + }); + // 'configuration' as array + t.throws(() => { + new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "test-ui5.yaml", + configuration: [{ + test: "configuration" + }] + }); + }, { + message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'" + }); +}); From ba1927ef653771589166887c018318e61e310d27 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 13 Jun 2022 10:46:05 +0200 Subject: [PATCH 96/99] Update log messages --- lib/builder.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/builder.js b/lib/builder.js index c89888b9a..74d7c802e 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -136,11 +136,11 @@ module.exports = async function({ log.info(`Building project ${rootProjectName}`); if (includedDependencies.length) { - log.info(` Including dependencies:`); + log.info(` Requested dependencies:`); log.info(` + ${includedDependencies.join("\n + ")}`); } if (excludedDependencies.length) { - log.info(` Excluding dependencies:`); + log.info(` Excluded dependencies:`); log.info(` - ${excludedDependencies.join("\n + ")}`); } log.info(` Target directory: ${destPath}`); @@ -240,7 +240,7 @@ module.exports = async function({ }); buildLogger.addWork(projectsToBuild.length); - log.info(`Building projects: `); + log.info(`Projects required to build: `); log.info(` > ${projectsToBuild.join("\n > ")}`); if (cleanDest) { From 6dfbb45cc87c09d89a35aeee46103f596609a8b1 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Mon, 13 Jun 2022 11:04:07 +0200 Subject: [PATCH 97/99] Fix generateProjectGraph.usingObject test --- test/lib/generateProjectGraph.usingObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/generateProjectGraph.usingObject.js b/test/lib/generateProjectGraph.usingObject.js index a0485c144..7bad266d8 100644 --- a/test/lib/generateProjectGraph.usingObject.js +++ b/test/lib/generateProjectGraph.usingObject.js @@ -1646,7 +1646,7 @@ test("rootConfig", async (t) => { const {projectGraphFromTree} = t.context; const projectGraph = await projectGraphFromTree({ dependencyTree: getApplicationATree(), - rootConfigPath: "ui5-test-rootConfigPath.yaml" + rootConfigPath: "ui5-test-configPath.yaml" }); t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { rootConfigPathTest: true From 236585b712dbda71ce0faedd133b2901063f7994 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Mon, 13 Jun 2022 11:04:42 +0200 Subject: [PATCH 98/99] Implement migration of legacy projects --- lib/specifications/Specification.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 2e2487319..06039b78c 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -156,8 +156,15 @@ class Specification { } _migrateLegacyProject(config) { - // TODO 3.0: Implement proper migration config.specVersion = "2.6"; + + // propertiesFileSourceEncoding default has been changed to UTF-8 with specVersion 2.0 + // Adding back the old default if no configuration is provided. + if (!config.resources?.configuration?.propertiesFileSourceEncoding) { + config.resources = config.resources || {}; + config.resources.configuration = config.resources.configuration || {}; + config.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; + } } static async create(params) { From 737aa243b11a7d996ebdf5021370b01903712cf6 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 13 Jun 2022 11:20:43 +0200 Subject: [PATCH 99/99] Add error handling for failed spec version migration --- lib/specifications/Specification.js | 43 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js index 06039b78c..8983fbc1b 100644 --- a/lib/specifications/Specification.js +++ b/lib/specifications/Specification.js @@ -36,17 +36,36 @@ class Specification { // The ID property as supplied by the translators is only here for debugging and potential tracing purposes this.__id = id; + // Deep clone config to prevent changes by reference const config = JSON.parse(JSON.stringify(configuration)); + const {validate} = require("../validation/validator"); if (config.specVersion === "0.1" || config.specVersion === "1.0" || config.specVersion === "1.1") { + const originalSpecVersion = config.specVersion; this._log.verbose(`Detected legacy specification version ${config.specVersion}, defined for ` + `${config.kind} ${config.metadata.name}. ` + - `Attempting to migrate the project to latest specVersion...`); + `Attempting to migrate the project to a supported specification version...`); this._migrateLegacyProject(config); - } - - if (config.specVersion !== "2.0" && + try { + await validate({ + config, + project: { + id + } + }); + } catch (err) { + this._log.verbose( + `Validation error after migration of ${config.kind} ${config.metadata.name}:`); + this._log.verbose(err.message); + throw new Error( + `${config.kind} ${config.metadata.name} defines unsupported specification version ` + + `${originalSpecVersion}. Please manually upgrade to 2.0 or higher. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions - ` + + `An attempted migration to a supported specification version failed, ` + + `likely due to unrecognized configuration. Check verbose log for details.`); + } + } else if (config.specVersion !== "2.0" && config.specVersion !== "2.1" && config.specVersion !== "2.2" && config.specVersion !== "2.3" && config.specVersion !== "2.4" && config.specVersion !== "2.5" && config.specVersion !== "2.6") { @@ -54,17 +73,15 @@ class Specification { `Unsupported specification version ${config.specVersion} defined in ${config.kind} ` + `${config.metadata.name}. Your UI5 CLI installation might be outdated. ` + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`); + } else { + await validate({ + config, + project: { + id + } + }); } - // Deep clone config to prevent changes by reference - const {validate} = require("../validation/validator"); - await validate({ - config, - project: { - id - } - }); - // Check whether the given configuration matches the class by guessing the type name from the class name if (config.type.replace("-", "") !== this.constructor.name.toLowerCase()) { throw new Error(