diff --git a/index.js b/index.js index e21fbfee6..53a9134b1 100644 --- a/index.js +++ b/index.js @@ -4,13 +4,13 @@ */ module.exports = { /** - * @type {import('./lib/normalizer')} + * @type {import('./lib/builder')} */ - normalizer: "./lib/normalizer", + builder: "./lib/builder", /** - * @type {import('./lib/projectPreprocessor')} + * @type {import('./lib/generateProjectGraph')} */ - projectPreprocessor: "./lib/projectPreprocessor", + generateProjectGraph: "./lib/generateProjectGraph", /** * @public * @alias module:@ui5/project.ui5Framework @@ -42,20 +42,20 @@ module.exports = { ValidationError: "./lib/validation/ValidationError" }, /** - * @private - * @alias module:@ui5/project.translators + * @public + * @alias module:@ui5/project.graph * @namespace */ - translators: { + graph: { /** - * @type {import('./lib/translators/npm')} + * @type {typeof import('./lib/graph/ProjectGraph')} */ - npm: "./lib/translators/npm", + ProjectGraph: "./lib/graph/ProjectGraph", /** - * @type {import('./lib/translators/static')} + * @type {typeof import('./lib/graph/projectGraphBuilder')} */ - static: "./lib/translators/static" - } + projectGraphBuilder: "./lib/graph/projectGraphBuilder", + }, }; function exportModules(exportRoot, modulePaths) { diff --git a/lib/buildDefinitions/AbstractBuilder.js b/lib/buildDefinitions/AbstractBuilder.js new file mode 100644 index 000000000..a65ee3e9c --- /dev/null +++ b/lib/buildDefinitions/AbstractBuilder.js @@ -0,0 +1,327 @@ +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 + */ + constructor({graph, project, parentLogger, taskUtil}) { + if (new.target === AbstractBuilder) { + throw new TypeError("Class 'AbstractBuilder' is abstract"); + } + + this.project = project; + this.graph = graph; + this.taskUtil = taskUtil; + + this.log = parentLogger.createSubLogger(project.getType() + " " + project.getName(), 0.2); + this.taskLog = this.log.createTaskLogger("🔨"); + + this.tasks = {}; + this.taskExecutionOrder = []; + + this.addStandardTasks({ + project, + taskUtil, + getTask + }); + this.addCustomTasks({ + graph, + project, + taskUtil + }); + } + + /** + * Adds all standard tasks to execute + * + * @abstract + * @protected + * @param {object} parameters + * @param {object} parameters.taskUtil + * @param {object} parameters.project + */ + addStandardTasks({project, taskUtil}) { + throw new Error("Function 'addStandardTasks' is not implemented"); + } + + /** + * Adds custom tasks to execute + * + * @private + * @param {object} parameters + * @param {object} parameters.graph + * @param {object} parameters.project + * @param {object} parameters.taskUtil + */ + addCustomTasks({graph, project, taskUtil}) { + 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.getName()} ` + + `at index ${i}`); + } + if (taskDef.beforeTask && taskDef.afterTask) { + 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.getName()} ` + + `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); + // 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 + {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, + options: { + projectName: project.getName(), + projectNamespace: project.getNamespace(), + configuration: taskDef.configuration + } + }; + + if (requiresDependencies) { + params.dependencies = dependencies; + } + + const taskUtilInterface = taskUtil.getInterface(task.getSpecVersion()); + // Interface is undefined if specVersion does not support taskUtil + if (taskUtilInterface) { + params.taskUtil = taskUtilInterface; + } + return task.getTask()(params); + }; + + 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 + 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.getName()}`); + } + 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 {object} [parameters] + * @param {boolean} [parameters.requiresDependencies] + * @param {object} [parameters.options] + * @param {Function} [taskFunction] + */ + addTask(taskName, {requiresDependencies = false, options = {}} = {}, taskFunction) { + if (this.tasks[taskName]) { + 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.getName()}. ` + + `It has already been scheduled for execution.`); + } + + 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(); + + const params = { + workspace, + taskUtil: this.taskUtil, + options + }; + + 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} buildConfig + * @param {boolean} buildConfig.selfContained + * True if a the build should be self-contained or false for prelead build bundles + * @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 + */ + 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. + // 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); + }); + + this.taskLog.addWork(allTasks.length); + + for (const taskName of allTasks) { + const taskFunction = this.tasks[taskName].task; + + if (typeof taskFunction === "function") { + await this.executeTask(taskName, taskFunction, buildParams); + } + } + } + + 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.verbose(`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 {Promise} Resolves when task has finished + */ + 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`); + } + } + + /** + * 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..d01c6ed42 --- /dev/null +++ b/lib/buildDefinitions/ApplicationBuilder.js @@ -0,0 +1,113 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class ApplicationBuilder extends AbstractBuilder { + addStandardTasks({project, taskUtil, getTask}) { + this.addTask("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + 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 + 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", { + options: { + pattern: minificationPattern + } + }); + + 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.getComponentPreloadExcludes(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + this.addTask("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } else { + // Default component preload for application namespace + this.addTask("generateComponentPreload", { + options: { + namespaces: [project.getNamespace()], + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } + + this.addTask("generateStandaloneAppBundle", {requiresDependencies: true}); + + this.addTask("transformBootstrapHtml"); + + if (bundles.length) { + 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()); + }); + } + + this.addTask("generateVersionInfo", { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }); + + this.addTask("generateCachebusterInfo", { + options: { + signatureType: project.getCachebusterSignatureType(), + } + }); + + this.addTask("generateApiIndex", {requiresDependencies: true}); + this.addTask("generateResourcesJson", {requiresDependencies: true}); + } +} + +module.exports = ApplicationBuilder; diff --git a/lib/buildDefinitions/LibraryBuilder.js b/lib/buildDefinitions/LibraryBuilder.js new file mode 100644 index 000000000..24c08d056 --- /dev/null +++ b/lib/buildDefinitions/LibraryBuilder.js @@ -0,0 +1,152 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class LibraryBuilder extends AbstractBuilder { + addStandardTasks({project, taskUtil, getTask}) { + this.addTask("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,library,css,less,theme,html}" + } + }); + + this.addTask("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json,library,css,less,theme,html}" + } + }); + + this.addTask("replaceBuildtime", { + options: { + pattern: "/resources/sap/ui/Global.js" + } + }); + + this.addTask("generateJsdoc", {requiresDependencies: true}, + async ({workspace, dependencies, taskUtil, options}) => { + 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, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + namespace: project.getNamespace(), + version: project.getVersion(), + pattern: patterns + } + }); + }); + + 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"]; + if (["2.6"].includes(project.getSpecVersion())) { + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + this.enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + } + + this.addTask("minify", { + options: { + pattern: minificationPattern + } + }); + + 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(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + this.addTask("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } + + this.addTask("generateLibraryPreload", { + options: { + excludes: project.getLibraryPreloadExcludes(), + skipBundles: existingBundleDefinitionNames + } + }); + + if (bundles.length) { + 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()); + }); + } + + 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", { + requiresDependencies: true, + options: { + version: project.getVersion() + } + }); + + this.addTask("generateResourcesJson", { + requiresDependencies: true + }); + } +} + +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..1225bbd19 --- /dev/null +++ b/lib/buildDefinitions/ThemeLibraryBuilder.js @@ -0,0 +1,40 @@ +const AbstractBuilder = require("./AbstractBuilder"); + +class ThemeLibraryBuilder extends AbstractBuilder { + addStandardTasks({project, taskUtil, getTask}) { + this.addTask("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/resources/**/*.{less,theme}" + } + }); + + this.addTask("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/resources/**/*.{less,theme}" + } + }); + + 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", { + requiresDependencies: true, + options: { + version: project.getVersion() + } + }); + + this.addTask("generateResourcesJson", {requiresDependencies: true}); + } +} + +module.exports = ThemeLibraryBuilder; diff --git a/lib/buildDefinitions/getInstance.js b/lib/buildDefinitions/getInstance.js new file mode 100644 index 000000000..2cd6eb730 --- /dev/null +++ b/lib/buildDefinitions/getInstance.js @@ -0,0 +1,27 @@ + +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": + 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..396bc1ca0 --- /dev/null +++ b/lib/buildHelpers/BuildContext.js @@ -0,0 +1,55 @@ +const ProjectBuildContext = require("./ProjectBuildContext"); + +/** + * 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._options = options; + } + + getRootProject() { + return this._graph.getRoot(); + } + + getOption(key) { + return this._options[key]; + } + + createProjectContext({project, log}) { + const projectBuildContext = new ProjectBuildContext({ + buildContext: this, + project, + log + }); + this._projectBuildContexts.push(projectBuildContext); + return projectBuildContext; + } + + async executeCleanupTasks() { + await Promise.all(this._projectBuildContexts.map((ctx) => { + return ctx.executeCleanupTasks(); + })); + } + + /** + * 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); + } +} + +module.exports = BuildContext; diff --git a/lib/buildHelpers/ProjectBuildContext.js b/lib/buildHelpers/ProjectBuildContext.js new file mode 100644 index 000000000..47bed053e --- /dev/null +++ b/lib/buildHelpers/ProjectBuildContext.js @@ -0,0 +1,96 @@ +const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; +const TaskUtil = require("@ui5/builder").tasks.TaskUtil; + +/** + * 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, log, project}) { + 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; + this._log = log; + this._queues = { + cleanup: [] + }; + + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:OmitFromBuildResult", "ui5:IsBundle"], + allowedNamespaces: ["build"] + }); + } + + 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(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() { + if (!this._taskUtil) { + this._taskUtil = new TaskUtil({ + projectBuildContext: this + }); + } + + return this._taskUtil; + } + + /** + * 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; + } +} + +module.exports = ProjectBuildContext; diff --git a/lib/buildHelpers/composeProjectList.js b/lib/buildHelpers/composeProjectList.js new file mode 100644 index 000000000..d05fe44a2 --- /dev/null +++ b/lib/buildHelpers/composeProjectList.js @@ -0,0 +1,193 @@ +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 + * 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} 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 + * "*" 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 ( + !includeAllDependencies && + !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/buildHelpers/composeTaskList.js b/lib/buildHelpers/composeTaskList.js new file mode 100644 index 000000000..ee4763564 --- /dev/null +++ b/lib/buildHelpers/composeTaskList.js @@ -0,0 +1,107 @@ +const log = require("@ui5/logger").getLogger("buildHelpers:composeTaskList"); + +/** + * 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 (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.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, {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; + selectedTasks.generateVersionInfo = 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; + } + + 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; + } + + // 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/buildHelpers/createBuildManifest.js b/lib/buildHelpers/createBuildManifest.js new file mode 100644 index 000000000..7f46915dd --- /dev/null +++ b/lib/buildHelpers/createBuildManifest.js @@ -0,0 +1,55 @@ +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(); + + const pathMapping = {}; + switch (type) { + case "application": + pathMapping.webapp = `resources/${project.getNamespace()}`; + break; + case "library": + case "theme-library": + pathMapping.src = `resources`; + pathMapping.test = `test-resources`; + break; + default: + throw new Error( + `Unable to create archive metadata for project ${project.getName()}: ` + + `Project type ${type} is currently not supported`); + } + + const metadata = { + project: { + specVersion: project.getSpecVersion(), + type, + metadata: { + name: projectName, + }, + resources: { + configuration: { + paths: pathMapping + } + } + }, + buildManifest: { + manifestVersion: "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() + } + }; + + return metadata; +}; diff --git a/lib/builder.js b/lib/builder.js new file mode 100644 index 000000000..74d7c802e --- /dev/null +++ b/lib/builder.js @@ -0,0 +1,319 @@ +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 composeProjectList = require("./buildHelpers/composeProjectList"); +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 + * @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=[]] + * 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 names of projects to exclude from the build result. + * @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 {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. + * @returns {Promise} Promise resolving to undefined once build has finished + */ +module.exports = async function({ + graph, destPath, cleanDest = false, + includedDependencies = [], excludedDependencies = [], + complexDependencyIncludes, + selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = 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`); + } + + if (complexDependencyIncludes) { + if (includedDependencies.length || excludedDependencies.length) { + throw new Error( + "Parameter 'complexDependencyIncludes' can't be used in conjunction " + + "with parameters 'includedDependencies' or '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(); + 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(` Requested dependencies:`); + log.info(` + ${includedDependencies.join("\n + ")}`); + } + if (excludedDependencies.length) { + log.info(` Excluded dependencies:`); + log.info(` - ${excludedDependencies.join("\n + ")}`); + } + log.info(` Target directory: ${destPath}`); + + const buildConfig = {selfContained, jsdoc, includedTasks, excludedTasks}; + + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: destPath, + virBasePath: "/" + }); + + const buildContext = new BuildContext({ + graph, + options: { + cssVariables + } + }); + const cleanupSigHooks = registerCleanupSigHooks(buildContext); + function projectFilter(projectName) { + function projectMatchesAny(deps) { + return deps.some((dep) => dep instanceof RegExp ? + dep.test(projectName) : dep === projectName); + } + + if (projectName === rootProjectName) { + // Always include the root project + return true; + } + + if (projectMatchesAny(excludedDependencies)) { + return false; + } + + if (includedDependencies.includes("*") || projectMatchesAny(includedDependencies)) { + return true; + } + + return false; + } + + // Count total number of projects to build + const requestedProjects = graph.getAllProjects().map((p) => p.getName()).filter(function(projectName) { + return projectFilter(projectName); + }); + + 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 + let 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.verbose(`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(); + 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 and therefore ` + + `cannot be re-built`); + } + if (!projectsToBuild.includes(depName)) { + log.info(`Project ${projectName} requires dependency ${depName} to be built`); + projectsToBuild.push(depName); + } + }); + } + }); + + projectsToBuild = projectsToBuild.filter((projectName) => { + if (graph.getProject(projectName).hasBuildManifest()) { + log.verbose(`Found a build manifest for project ${projectName}. Skipping build.`); + return false; + } + return true; + }); + + buildLogger.addWork(projectsToBuild.length); + log.info(`Projects required to build: `); + log.info(` > ${projectsToBuild.join("\n > ")}`); + + if (cleanDest) { + await rimraf(destPath); + } + + await graph.traverseDepthFirst(async function({project, getDependencies}) { + const projectName = project.getName(); + if (!projectsToBuild.includes(projectName)) { + return; + } + 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()); + }); + + const dependencies = resourceFactory.createReaderCollection({ + name: `Dependency reader collection for project ${project.getName()}`, + readers + }); + + await builder.build(buildConfig, { + workspace: project.getWorkspace(), + dependencies, + }); + log.verbose("Finished building project %s", project.getName()); + buildLogger.completeWork(1); + + if (!requestedProjects.includes(projectName)) { + // This project shall not be part of the build result + return; + } + + log.verbose(`Writing out files...`); + const taskUtil = projectContext.getTaskUtil(); + const resources = await project.getReader({ + // Force buildtime (=namespaced) style when writing with a build manifest + style: taskUtil.isRootProject() && createBuildManifest ? "buildtime" : "runtime" + }).byGlob("/**/*"); + + if (taskUtil.isRootProject() && 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-manifest.json`, + string: JSON.stringify(metadata, null, "\t") + })); + } + + await Promise.all(resources.map((resource) => { + 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 + } + 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/generateProjectGraph.js b/lib/generateProjectGraph.js new file mode 100644 index 000000000..c00f75b34 --- /dev/null +++ b/lib/generateProjectGraph.js @@ -0,0 +1,171 @@ +const path = require("path"); +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 + * + * @public + * @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 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=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] + * 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, resolveFrameworkDependencies = true + }) { + log.verbose(`Creating project graph using npm provider...`); + const NpmProvider = require("./graph/providers/NodePackageDependencies"); + + const provider = new NpmProvider({ + cwd: cwd ? path.resolve(cwd) : process.cwd(), + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(provider); + + if (resolveFrameworkDependencies) { + 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 {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 + * @returns {Promise} Promise resolving to a Project Graph instance + */ + usingStaticFile: async function({ + cwd, filePath = "projectDependencies.yaml", + rootConfiguration, rootConfigPath, + versionOverride, resolveFrameworkDependencies = true + }) { + log.verbose(`Creating project graph using static file...`); + + const dependencyTree = await generateProjectGraph + ._readDependencyConfigFile(cwd ? path.resolve(cwd) : process.cwd(), filePath); + + const DependencyTreeProvider = require("./graph/providers/DependencyTree"); + const provider = new DependencyTreeProvider({ + dependencyTree, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(provider); + + if (resolveFrameworkDependencies) { + 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 {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 + * @returns {Promise} Promise resolving to a Project Graph instance + */ + 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, + rootConfiguration, + rootConfigPath + }); + + const projectGraph = await projectGraphBuilder(dependencyTreeProvider); + + if (resolveFrameworkDependencies) { + 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; + } +}; + +module.exports = generateProjectGraph; diff --git a/lib/graph/Module.js b/lib/graph/Module.js new file mode 100644 index 000000000..875f7ff6e --- /dev/null +++ b/lib/graph/Module.js @@ -0,0 +1,446 @@ +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 Specification = require("../specifications/Specification"); +const {validate} = require("../validation/validator"); + +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"]; + +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 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 configuration file. + * @param {object|object[]} [parameters.configuration] + * 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, configuration = [], shimCollection}) { + if (!id) { + throw new Error(`Could not create Module: Missing or empty parameter 'id'`); + } + if (!version) { + throw new Error(`Could not create Module: Missing or empty parameter 'version'`); + } + if (!modulePath) { + throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`); + } + if ( + ( + (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'` + ); + } + + this._id = id; + this._version = version; + this._modulePath = modulePath; + this._configPath = configPath || DEFAULT_CONFIG_PATH; + this._dependencies = {}; + + if (!Array.isArray(configuration)) { + configuration = [configuration]; + } + this._suppliedConfigs = configuration; + + 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.getProjectConfigurationShims(this.getId()); + if (shims && shims.length) { + this._projectConfigShims = clone(shims); + } + } + } + + getId() { + return this._id; + } + + getVersion() { + return this._version; + } + + getPath() { + 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; + } + + return this._pGetSpecifications = this._getSpecifications(); + } + + async _getSpecifications() { + // Retrieve all configurations available for this module + let configs = await this._getConfigurations(); + + // 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 + 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"; + }); + } + + // Patch configs + configs.forEach((configuration) => { + if (configuration.kind === "project" && configuration.type === "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"; + } + } + }); + + const specs = await Promise.all(configs.map(async (configuration) => { + 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, + buildManifest + }); + + log.verbose(`Module ${this.getId()} contains ${spec.getKind()} ${spec.getName()}`); + return spec; + })); + + 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: projects[0], + extensions + }; + } + + /** + * Configuration + */ + async _getConfigurations() { + let configurations; + + configurations = await this._getSuppliedConfigurations(); + + if (!configurations || !configurations.length) { + configurations = await this._getBuildManifestConfigurations(); + } + if (!configurations || !configurations.length) { + configurations = await this._getYamlConfigurations(); + } + if (!configurations || !configurations.length) { + configurations = await this._getShimConfigurations(); + } + return configurations || []; + } + + _normalizeAndApplyShims(config) { + this._normalizeConfig(config); + + if (config.kind !== "project") { // TODO 3.0: Shouldn't this be '==='? + this._applyProjectShims(config); + } + return config; + } + + async _createConfigurationFromShim() { + const config = this._applyProjectShims(); + if (config) { + this._normalizeConfig(config); + return config; + } + } + + _applyProjectShims(config = {}) { + if (!this._projectConfigShims) { + return; + } + this._projectConfigShims.forEach(({name, shim}) => { + log.verbose(`Applying project shim ${name} for module ${this.getId()}...`); + Object.assign(config, shim); + }); + return config; + } + + 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 (suppliedConfig) => { + let config = suppliedConfig; + + // 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._buildManifest = suppliedConfig.buildManifest; + } + return this._normalizeAndApplyShims(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) { + log.verbose(`Could not find a configuration file for module ${this.getId()}`); + return []; + } + + return await Promise.all(configs.map((config) => { + return this._normalizeAndApplyShims(config); + })); + } + + async _readConfigFile() { + 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) { + // 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 { + // Handle relative file paths with the @ui5/fs (virtual) file system + 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 !== DEFAULT_CONFIG_PATH) { + 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.getString(); + } + + 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) { + // 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 + 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) { + // Throw any validation errors + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + + return configs; + } + + async _getBuildManifestConfigurations() { + const buildManifestMetadata = await this._readBuildManifest(); + + if (!buildManifestMetadata) { + log.verbose(`Could not find any build manifest in module ${this.getId()}`); + return []; + } + + // 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 _readBuildManifest() { + const reader = await this.getReader(); + const buildManifestResource = await reader.byPath("/.ui5/build-manifest.json"); + if (buildManifestResource) { + return JSON.parse(await buildManifestResource.getString()); + } + } + + _normalizeConfig(config) { + if (!config.kind) { + config.kind = "project"; // default + } + return config; + } + + _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 + */ + async getReader() { + return resourceFactory.createReader({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Reader for module ${this.getId()}` + }); + } +} + +module.exports = Module; diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js new file mode 100644 index 000000000..2613e4235 --- /dev/null +++ b/lib/graph/ProjectGraph.js @@ -0,0 +1,507 @@ +const log = require("@ui5/logger").getLogger("graph:ProjectGraph"); +/** + * A rooted, directed graph representing a UI5 project, its dependencies and available extensions + * + * @public + * @memberof module:@ui5/project.graph + */ +class ProjectGraph { + /** + * @public + * @param {object} parameters Parameters + * @param {string} parameters.rootProjectName Root project name + */ + constructor({rootProjectName}) { + if (!rootProjectName) { + throw new Error(`Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'`); + } + this._rootProjectName = rootProjectName; + + 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 + } + + /** + * Get the root project of the graph + * + * @public + * @returns {module:@ui5/project.specifications.Project} Root project + */ + getRoot() { + const rootProject = this._projects[this._rootProjectName]; + if (!rootProject) { + throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`); + } + return rootProject; + } + + /** + * 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 + */ + addProject(project, ignoreDuplicates) { + this._checkSealed(); + const projectName = project.getName(); + if (this._projects[projectName]) { + if (ignoreDuplicates) { + return; + } + throw new Error( + `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 + // 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`); + } + log.verbose(`Adding project: ${projectName}`); + this._projects[projectName] = project; + this._adjList[projectName] = []; + this._optAdjList[projectName] = []; + } + + /** + * 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} + * project instance or undefined if the project is unknown to the graph + */ + getProject(projectName) { + return this._projects[projectName]; + } + + /** + * Get all projects as a nested array containing pairs of project name and -instance. + * + * @public + * @returns {Array>} + */ + getAllProjects() { + return Object.values(this._projects); + } + + /** + * Add an extension to the graph + * + * @public + * @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( + `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]; + } + + /** + * Get all extensions as a nested array containing pairs of extension name and -instance. + * + * @public + * @returns {Array>} + */ + getAllExtensions() { + return Object.values(this._extensions); + } + + /** + * 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) { + this._checkSealed(); + 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}: ` + + 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( + `Unable to find dependency project with name ${toProjectName} in project graph`); + } + if (fromProjectName === toProjectName) { + throw new Error( + `A project can't depend on itself`); + } + if (map[fromProjectName].includes(toProjectName)) { + log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); + } else { + map[fromProjectName].push(toProjectName); + } + } + + /** + * 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 + */ + getDependencies(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 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 optional + */ + isOptionalDependency(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; + } + + /** + * Transforms any optional dependencies declared in the graph to non-optional dependency, if the target + * can already be reached from the root project. + * + * @public + */ + 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; + 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)) { + // 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)) { + 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); + } + } + } + } + } + + /** + * 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 + */ + + + // 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. + * In case a cycle is detected, an error is thrown + * + * @public + * @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(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`); + } + + const queue = [{ + projectNames: [startName], + 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) => { + this._checkCycle(predecessors, projectName); + + if (visited[projectName]) { + return visited[projectName]; + } + + return visited[projectName] = (async () => { + const newPredecessors = [...predecessors, projectName]; + const dependencies = this.getDependencies(projectName); + + queue.push({ + projectNames: dependencies, + predecessors: newPredecessors + }); + + await callback({ + project: this.getProject(projectName), + getDependencies: () => { + return dependencies.map(($) => this.getProject($)); + } + }); + })(); + })); + } + } + + /** + * 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 {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(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`); + } + return this._traverseDepthFirst(startName, {}, [], callback); + } + + async _traverseDepthFirst(projectName, visited, predecessors, callback) { + this._checkCycle(predecessors, projectName); + + if (visited[projectName]) { + return visited[projectName]; + } + return visited[projectName] = (async () => { + const newPredecessors = [...predecessors, projectName]; + const dependencies = this.getDependencies(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($)); + } + }); + })(); + } + + _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 + * + * @public + * @param {module:@ui5/project.graph.ProjectGraph} projectGraph Project Graph to merge into this one + */ + 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); + } catch (err) { + throw new Error( + `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; + } + + /** + * 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. + * + * @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`); + } + } + + // TODO: introduce function to check for dangling nodes/consistency in general? +} + +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/graph/ShimCollection.js b/lib/graph/ShimCollection.js new file mode 100644 index 000000000..fa78c5be6 --- /dev/null +++ b/lib/graph/ShimCollection.js @@ -0,0 +1,53 @@ +const log = require("@ui5/logger").getLogger("graph: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._projectConfigShims = {}; + this._dependencyShims = {}; + this._collectionShims = {}; + } + + addProjectShim(shimExtension) { + const name = shimExtension.getName(); + log.verbose(`Adding new shim ${name}...`); + + 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); + } + } + + getProjectConfigurationShims(moduleId) { + return this._projectConfigShims[moduleId]; + } + + getAllDependencyShims() { + return this._dependencyShims; + } + + getCollectionShims(moduleId) { + return this._collectionShims[moduleId]; + } +} + +module.exports = ShimCollection; diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js new file mode 100644 index 000000000..a834fded5 --- /dev/null +++ b/lib/graph/helpers/ui5Framework.js @@ -0,0 +1,233 @@ +const Module = require("../Module"); +const ProjectGraph = require("../ProjectGraph"); +const log = require("@ui5/logger").getLogger("graph:helpers: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 = { + 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 Specification API is >= 2.0 anyways + 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 + return; + } + + frameworkDependencies.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 Specification API is >= 2.0 anyways + 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 + return; + } + + frameworkDependencies.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 resolving with the given graph instance to allow method chaining + */ + enrichProjectGraph: async function(projectGraph, options = {}) { + 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; + } + + 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; + } + + 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 (!frameworkVersion) { + 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 ` + + `${frameworkVersion} with version ${version}` + ); + } else { + version = frameworkVersion; + } + + 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 projectGraph; + } + + 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: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` + }); + 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/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js new file mode 100644 index 000000000..f32e0f9b7 --- /dev/null +++ b/lib/graph/projectGraphBuilder.js @@ -0,0 +1,322 @@ +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.addProjectShim(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'`); + } + }); +} + +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 + * + * @public + * @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 {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 + */ + +/** + * 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 + * @param {NodeProvider} nodeProvider + * @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(); + validateNode(rootNode); + 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 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.`); + } + + 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}...`); + validateNode(node); + 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 in a deterministic order + for (let i = 0; i < res.length; 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); + + // 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) { + // 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 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 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, + }); + } + } + } + + // Apply 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()); + } + } + } + await projectGraph.resolveOptionalDependencies(); + + return projectGraph; +}; diff --git a/lib/graph/providers/DependencyTree.js b/lib/graph/providers/DependencyTree.js new file mode 100644 index 000000000..b9a470486 --- /dev/null +++ b/lib/graph/providers/DependencyTree.js @@ -0,0 +1,53 @@ +/** + * 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.providers.DependencyTree + * @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, 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() { + return this._tree; + } + + async getDependencies(node) { + if (node.deduped || !node.dependencies) { + return []; + } + return node.dependencies; + } +} + +module.exports = DependencyTree; diff --git a/lib/graph/providers/NodePackageDependencies.js b/lib/graph/providers/NodePackageDependencies.js new file mode 100644 index 000000000..35652671b --- /dev/null +++ b/lib/graph/providers/NodePackageDependencies.js @@ -0,0 +1,152 @@ +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:NodePackageDependencies"); + +// Packages to consider: +// * https://github.com/npm/read-package-json-fast +// * https://github.com/npm/name-from-folder ? + + +class NodePackageDependencies { + /** + * Generates a project graph from npm modules + * + * @public + * @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 + */ + 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}'...`); + 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; + } catch (err) { + throw new Error( + `Unable to locate module ${moduleName} via resolve logic: ${err.message}`); + } + } + + 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) { + 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 + }); + }); + } + 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 = NodePackageDependencies; diff --git a/lib/normalizer.js b/lib/normalizer.js deleted file mode 100644 index 00b38a2fa..000000000 --- a/lib/normalizer.js +++ /dev/null @@ -1,87 +0,0 @@ -const log = require("@ui5/logger").getLogger("normalizer:normalizer"); -const projectPreprocessor = require("./projectPreprocessor"); - - -/** - * 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 = {}) { - 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 c4f080f61..000000000 --- a/lib/projectPreprocessor.js +++ /dev/null @@ -1,662 +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" - ]; - 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/ComponentProject.js b/lib/specifications/ComponentProject.js new file mode 100644 index 000000000..2f77f7ba0 --- /dev/null +++ b/lib/specifications/ComponentProject.js @@ -0,0 +1,351 @@ +const {promisify} = require("util"); +const Project = require("./Project"); +const resourceFactory = require("@ui5/fs").resourceFactory; + +/* +* Subclass for projects potentially containing Components +*/ + +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; + this._isRuntimeNamespaced = true; + } + + /* === Attributes === */ + /** + * @public + */ + getNamespace() { + return this._namespace; + } + + /** + * @private + */ + getCopyright() { + return this._config.metadata.copyright; + } + + /** + * @private + */ + getComponentPreloadPaths() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.paths || []; + } + + /** + * @private + */ + getComponentPreloadNamespaces() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.namespaces || []; + } + + /** + * @private + */ + getComponentPreloadExcludes() { + return this._config.builder && this._config.builder.componentPreload && + this._config.builder.componentPreload.excludes || []; + } + + /** + * @private + */ + getJsdocExcludes() { + return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || []; + } + + /** + * @private + */ + getMinificationExcludes() { + return this._config.builder && this._config.builder.minification && + this._config.builder.minification.excludes || []; + } + + /** + * @private + */ + getBundles() { + return this._config.builder && this._config.builder.bundles || []; + } + + /** + * @private + */ + getPropertiesFileSourceEncoding() { + return this._config.resources && this._config.resources.configuration && + this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; + } + + /* === 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 + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ + getReader({style = "buildtime"} = {}) { + // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? + + if (style === "runtime" && this._isRuntimeNamespaced) { + // If the project's runtime paths contains its namespace too, + // "runtime" style paths are identical to "buildtime" style paths + style = "buildtime"; + } + let reader; + switch (style) { + case "buildtime": + 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({ + linkPath: `/`, + targetPath: `/resources/${this._namespace}/` + }); + break; + case "flat": + // 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}`); + } + + reader = this._addWriter(reader, style); + return reader; + } + + /** + * Get a resource reader for the resources of the project + * + * @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 the test resources of the project + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getTestReader() { + throw new Error(`_getTestReader must be implemented by subclass ${this.constructor.name}`); + } + + /** + * Get a resource reader/writer for accessing and modifying a project's resources + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance + */ + getWorkspace() { + // Workspace is always of style "buildtime" + return resourceFactory.createWorkspace({ + name: `Workspace for project ${this.getName()}`, + reader: this._getReader(), + writer: this._getWriter().collection + }); + } + + _getWriter() { + if (!this._writers) { + // writer is always of style "buildtime" + 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._getSourceReader(); + const testReader = this._getTestReader(); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); + } + return reader; + } + + _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": { + // 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" + 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 + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _parseConfiguration(config) { + await super._parseConfiguration(config); + } + + 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.getName()}`); + } + 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 new file mode 100644 index 000000000..4df68c060 --- /dev/null +++ b/lib/specifications/Extension.js @@ -0,0 +1,16 @@ +const Specification = require("./Specification"); + +class Extension extends Specification { + constructor(parameters) { + super(parameters); + if (new.target === Extension) { + throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); + } + } + + /* + * TODO + */ +} + +module.exports = Extension; diff --git a/lib/specifications/Project.js b/lib/specifications/Project.js new file mode 100644 index 000000000..74fab9c12 --- /dev/null +++ b/lib/specifications/Project.js @@ -0,0 +1,164 @@ +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); + if (new.target === Project) { + throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses"); + } + + 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.buildManifest] Build metadata object + */ + async init(parameters) { + await super.init(parameters); + + this._buildManifest = parameters.buildManifest; + + await this._configureAndValidatePaths(this._config); + await this._parseConfiguration(this._config, this._buildManifest); + + return this; + } + + /* === Attributes === */ + /** + * @public + */ + getNamespace() { + // Default namespace for general Projects: + // Their resources should be structured with globally unique paths, hence their namespace is undefined + return null; + } + + /** + * @private + */ + getFrameworkName() { + return this._config.framework?.name; + } + /** + * @private + */ + getFrameworkVersion() { + return this._config.framework?.version; + } + /** + * @private + */ + getFrameworkDependencies() { + return this._config.framework?.libraries || []; + } + + isFrameworkProject() { + return this.__id.startsWith("@openui5/") || this.__id.startsWith("@sapui5/"); + } + + getCustomConfiguration() { + return this._config.customConfiguration; + } + + getBuilderResourcesExcludes() { + return this._config.builder?.resources?.excludes || []; + } + + getCustomTasks() { + return this._config.builder?.customTasks || []; + } + + getCustomMiddleware() { + return this._config.builder?.customMiddleware || []; + } + + getServerSettings() { + return this._config.server?.settings; + } + + getBuilderSettings() { + 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 + * 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" + * 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}`); + } + + getResourceTagCollection() { + if (!this._resourceTagCollection) { + const ResourceTagCollection = require("@ui5/fs").ResourceTagCollection; + this._resourceTagCollection = new ResourceTagCollection({ + allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"], + allowedNamespaces: ["project"], + tags: this.getBuildManifest()?.tags + }); + } + return this._resourceTagCollection; + } + + /** + * 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 + */ + getWorkspace() { + throw new Error(`getWorkspace must be implemented by subclass ${this.constructor.name}`); + } + + /* === Internals === */ + /** + * @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 new file mode 100644 index 000000000..8983fbc1b --- /dev/null +++ b/lib/specifications/Specification.js @@ -0,0 +1,231 @@ +const resourceFactory = require("@ui5/fs").resourceFactory; + +class Specification { + constructor() { + 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}`); + } + + /** + * @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 + */ + async init({id, version, modulePath, configuration}) { + 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'`); + } + + this._version = version; + this._modulePath = modulePath; + + // 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; + + // 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 a supported specification version...`); + this._migrateLegacyProject(config); + 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") { + 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`); + } else { + 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( + `Configuration mismatch: Supplied configuration of type '${config.type}' does not match with ` + + `specification class ${this.constructor.name}`); + } + + this._name = config.metadata.name; + this._kind = config.kind; + this._type = config.type; + this._specVersion = config.specVersion; + this._config = config; + + return this; + } + + /* === Attributes === */ + /** + * @public + */ + getName() { + return this._name; + } + + /** + * @public + */ + getKind() { + return this._kind; + } + + /** + * @public + */ + getType() { + return this._type; + } + + /** + * @public + */ + getSpecVersion() { + return this._specVersion; + } + + /** + * @public + */ + getVersion() { + return this._version; + } + + /** + * Might not be POSIX + * + * @private + */ + getPath() { + return this._modulePath; + } + + /* === 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({ + fsBasePath: this.getPath(), + virBasePath: "/", + name: `Root reader for ${this.getType()} ${this.getKind()} ${this.getName()}` + }); + } + + /* === Internals === */ + /* === Helper === */ + /** + * @private + * @param {string} dirPath Path of directory, relative to the project root + */ + async _dirExists(dirPath) { + const resource = await this.getRootReader().byPath(dirPath, {nodir: false}); + if (resource && resource.getStatInfo().isDirectory()) { + return true; + } + return false; + } + + _migrateLegacyProject(config) { + 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) { + 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("extensions/Task", params); + } + case "server-middleware": { + return createAndInitializeSpec("extensions/ServerMiddleware", params); + } + case "project-shim": { + return createAndInitializeSpec("extensions/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 = Specification; diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js new file mode 100644 index 000000000..c3a421f80 --- /dev/null +++ b/lib/specifications/types/Application.js @@ -0,0 +1,209 @@ +const fsPath = require("path"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const ComponentProject = require("../ComponentProject"); + +class Application extends ComponentProject { + constructor(parameters) { + super(parameters); + + this._pManifests = {}; + + this._webappPath = "webapp"; + + this._isRuntimeNamespaced = false; + } + + + /* === 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"; + } + + /* === Resource Access === */ + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._webappPath), + virBasePath: `/resources/${this._namespace}/`, + name: `Source reader for application project ${this.getName()}`, + project: this, + excludes: this.getBuilderResourcesExcludes() + }); + } + + _getTestReader() { + return null; // Applications do not have a dedicated test directory + } + + _getRawSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._webappPath), + virBasePath: "/", + name: `Source reader for application project ${this.getName()}`, + project: this + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && + config.resources.configuration.paths && config.resources.configuration.paths.webapp) { + this._webappPath = config.resources.configuration.paths.webapp; + } + + 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}`); + + 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 + * @param {object} buildDescription Cache metadata object + */ + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); + + if (buildDescription) { + this._namespace = buildDescription.namespace; + return; + } + this._namespace = await this._getNamespace(); + } + + /** + * 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._getRawSourceReader().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/lib/specifications/types/Library.js b/lib/specifications/types/Library.js new file mode 100644 index 000000000..8e3144754 --- /dev/null +++ b/lib/specifications/types/Library.js @@ -0,0 +1,495 @@ +const fsPath = require("path"); +const posixPath = require("path").posix; +const {promisify} = require("util"); +const resourceFactory = require("@ui5/fs").resourceFactory; +const ComponentProject = require("../ComponentProject"); + +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._isSourceNamespaced = true; + + this._propertiesFilesSourceEncoding = "UTF-8"; + } + + /* === Attributes === */ + /** + * + * @private + */ + getLibraryPreloadExcludes() { + return this._config.builder && this._config.builder.libraryPreload && + this._config.builder.libraryPreload.excludes || []; + } + + /* === Resource Access === */ + _getSourceReader() { + // TODO: Throw for libraries with additional namespaces like sap.ui.core? + 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: fsPath.join(this.getPath(), this._srcPath), + virBasePath, + name: `Source reader for library project ${this.getName()}`, + project: this, + excludes: this.getBuilderResourcesExcludes() + }); + } + + _getTestReader() { + if (!this._testPathExists) { + return null; + } + 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: fsPath.join(this.getPath(), this._testPath), + virBasePath, + name: `Runtime test-resources reader for library project ${this.getName()}`, + project: this, + excludes: this.getBuilderResourcesExcludes() + }); + 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 + * @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 + * @param {object} buildDescription Cache metadata object + */ + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); + + if (buildDescription) { + this._namespace = buildDescription.namespace; + return; + } + + this._namespace = await this._getNamespace(); + + if (!config.metadata.copyright) { + const copyright = await this._getCopyrightFromDotLibrary(); + if (copyright) { + config.metadata.copyright = copyright; + } + } + + if (this.isFrameworkProject()) { + if (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, "/"); + 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 { + const libraryJsPath = await this._getLibraryJsPath(); + 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.`); + } + 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 { + throw new Error( + `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(); + const namespace = dotLibrary?.library?.name?._; + if (namespace) { + 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 {}; + } + + /** + * Determines library copyright from given project configuration with fallback to .library. + * + * @returns {string|null} Copyright of the project + */ + async _getCopyrightFromDotLibrary() { + // If no copyright replacement was provided by ui5.yaml, + // check if the .library file has a valid copyright replacement + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + if (dotLibrary?.library?.copyright?._) { + this._log.verbose( + `Using copyright from ${filePath} for project ${this.getName()}...`); + return dotLibrary.library.copyright._; + } else { + this._log.verbose( + `No copyright configuration found in ${filePath} ` + + `of project ${this.getName()}`); + return null; + } + } + + async _getPreloadExcludesFromDotLibrary() { + const {content: dotLibrary, filePath} = await this._getDotLibrary(); + let excludes = dotLibrary?.library?.appData?.packaging?.["all-in-one"]?.exclude; + if (excludes) { + 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 + * filePath (as string) of the manifest.json file + */ + async _getManifest() { + if (this._pManifest) { + return this._pManifest; + } + 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()}`); + } + 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 ${resource.getPath()} for project ${this.getName()}: ${err.message}`); + } + }); + } + + /** + * Reads the .library file + * + * @returns {Promise} resolves with an object containing the content (as JSON) and + * filePath (as string) of the .library file + */ + async _getDotLibrary() { + if (this._pDotLibrary) { + return this._pDotLibrary; + } + 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()}`); + } + 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(); + + 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}`); + } + }); + } + + /** + * 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._getRawSourceReader().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..752d9f6fa --- /dev/null +++ b/lib/specifications/types/Module.js @@ -0,0 +1,112 @@ +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; + this._writer = null; + } + + /* === Attributes === */ + /** + * @public + */ + + /* === Resource Access === */ + /** + * Get a resource reader for accessing the project resources + * + * @public + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + getReader() { + const readers = this._paths.map((readerArgs) => resourceFactory.createReader(readerArgs)); + if (readers.length === 1) { + return readers[0]; + } + 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/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() { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + } + + return this._writer; + } + + /* === 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?.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 module project ${this.getName()}`, + virBasePath, + fsBasePath: fsPath.join(this.getPath(), relFsPath), + project: this, + excludes: config.builder?.resources?.excludes + }; + })); + } 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(), + project: this, + excludes: config.builder?.resources?.excludes + }]; + } + } +} + +module.exports = Module; diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 000000000..9d66e7be0 --- /dev/null +++ b/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,128 @@ +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"; + this._testPathExists = false; + this._writer = null; + } + + /* === Attributes === */ + /** + * @private + */ + getCopyright() { + return this._config.metadata.copyright; + } + + /* === Resource Access === */ + /** + * 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 allowing access to all resources of the project + */ + 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, + 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, + excludes: this.getBuilderResourcesExcludes() + }); + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for theme-library project ${this.getName()}`, + readers: [reader, testReader] + }); + } + const writer = this._getWriter(); + + return resourceFactory.createReaderCollectionPrioritized({ + name: `Reader/Writer collection for project ${this.getName()}`, + readers: [writer, reader] + }); + } + + /** + * 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(); + + const writer = this._getWriter(); + return resourceFactory.createWorkspace({ + reader, + writer + }); + } + + _getWriter() { + if (!this._writer) { + this._writer = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + } + + return this._writer; + } + + /* === 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; + } + } +} + +module.exports = ThemeLibrary; diff --git a/lib/specifications/types/extensions/ProjectShim.js b/lib/specifications/types/extensions/ProjectShim.js new file mode 100644 index 000000000..d337240f2 --- /dev/null +++ b/lib/specifications/types/extensions/ProjectShim.js @@ -0,0 +1,32 @@ +const Extension = require("../../Extension"); + +class ProjectShim extends Extension { + constructor(parameters) { + super(parameters); + } + + + /* === Attributes === */ + /** + * @public + */ + getDependencyShims() { + return this._config.shims.dependencies; + } + + /** + * @public + */ + getConfigurationShims() { + return this._config.shims.configurations; + } + + /** + * @public + */ + getCollectionShims() { + return this._config.shims.collections; + } +} + +module.exports = ProjectShim; diff --git a/lib/specifications/types/extensions/ServerMiddleware.js b/lib/specifications/types/extensions/ServerMiddleware.js new file mode 100644 index 000000000..fcdacc339 --- /dev/null +++ b/lib/specifications/types/extensions/ServerMiddleware.js @@ -0,0 +1,19 @@ +const path = require("path"); +const Extension = require("../../Extension"); + +class ServerMiddleware extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Attributes === */ + /** + * @public + */ + getMiddleware() { + const middlewarePath = path.join(this.getPath(), this._config.middleware.path); + return require(middlewarePath); + } +} + +module.exports = ServerMiddleware; diff --git a/lib/specifications/types/extensions/Task.js b/lib/specifications/types/extensions/Task.js new file mode 100644 index 000000000..11b7d6d0d --- /dev/null +++ b/lib/specifications/types/extensions/Task.js @@ -0,0 +1,19 @@ +const path = require("path"); +const Extension = require("../../Extension"); + +class Task extends Extension { + constructor(parameters) { + super(parameters); + } + + /* === Attributes === */ + /** + * @public + */ + getTask() { + const taskPath = path.join(this.getPath(), this._config.task.path); + return require(taskPath); + } +} + +module.exports = Task; 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/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/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 -}; diff --git a/package.json b/package.json index 5987c9644..994eb40b0 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.3", "@ui5/logger": "^3.0.1-alpha.1", "@ui5/server": "^3.0.0-alpha.1", "ajv": "^6.12.6", 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/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.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-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/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/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.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/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/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.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.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/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..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 @@ -1,10 +1,5 @@ --- -specVersion: "0.1" +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.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.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/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.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/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.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.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/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..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 @@ -1,3 +1,5 @@ --- -name: library.d +specVersion: "2.3" type: library +metadata: + name: library.d 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.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.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/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.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/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/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/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.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/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/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/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/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/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/fixtures/build-manifest/application.a/.ui5/build-manifest.json b/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json new file mode 100644 index 000000000..03ff08f24 --- /dev/null +++ b/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json @@ -0,0 +1,42 @@ +{ + "project": { + "specVersion": "2.3", + "type": "application", + "metadata": { + "name": "application.a" + }, + "resources": { + "configuration": { + "paths": { + "webapp": "resources/id1" + } + } + } + }, + "buildManifest": { + "manifestVersion": "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/build-manifest/application.a/package.json b/test/fixtures/build-manifest/application.a/package.json new file mode 100644 index 000000000..b5401c1e6 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/application.a/resources/id1/index.html b/test/fixtures/build-manifest/application.a/resources/id1/index.html new file mode 100644 index 000000000..77b0207cc --- /dev/null +++ b/test/fixtures/build-manifest/application.a/resources/id1/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + \ No newline at end of file diff --git a/test/fixtures/build-manifest/application.a/resources/id1/manifest.json b/test/fixtures/build-manifest/application.a/resources/id1/manifest.json new file mode 100644 index 000000000..781945df9 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/application.a/resources/id1/test-dbg.js b/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js new file mode 100644 index 000000000..a3df410c3 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/application.a/resources/id1/test.js b/test/fixtures/build-manifest/application.a/resources/id1/test.js new file mode 100644 index 000000000..a3df410c3 --- /dev/null +++ b/test/fixtures/build-manifest/application.a/resources/id1/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json b/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json new file mode 100644 index 000000000..5205a51a8 --- /dev/null +++ b/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json @@ -0,0 +1,43 @@ +{ + "project": { + "specVersion": "2.3", + "type": "library", + "metadata": { + "name": "library.e" + }, + "resources": { + "configuration": { + "paths": { + "src": "resources", + "test": "test-resources" + } + } + } + }, + "buildManifest": { + "manifestVersion": "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/build-manifest/library.e/package.json b/test/fixtures/build-manifest/library.e/package.json new file mode 100644 index 000000000..9ce874ff5 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/library.e/resources/library/e/.library b/test/fixtures/build-manifest/library.e/resources/library/e/.library new file mode 100644 index 000000000..c1f37d772 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/library.e/resources/library/e/some.js b/test/fixtures/build-manifest/library.e/resources/library/e/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/build-manifest/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/build-manifest/library.e/test-resources/library/e/Test.html b/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html new file mode 100644 index 000000000..e69de29bb 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/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/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.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/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.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/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.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/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.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/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/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/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/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/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/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/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/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/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/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.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/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.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/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.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.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/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/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/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/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/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/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/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..c1e726699 --- /dev/null +++ b/test/lib/buildHelpers/ProjectBuildContext.js @@ -0,0 +1,206 @@ +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.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/composeProjectList.js b/test/lib/buildHelpers/composeProjectList.js new file mode 100644 index 000000000..e96040ec0 --- /dev/null +++ b/test/lib/buildHelpers/composeProjectList.js @@ -0,0 +1,308 @@ +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: 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, + 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/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/createBuildManifest.integration.js b/test/lib/buildHelpers/createBuildManifest.integration.js new file mode 100644 index 000000000..d5befab82 --- /dev/null +++ b/test/lib/buildHelpers/createBuildManifest.integration.js @@ -0,0 +1,95 @@ +const test = require("ava"); +const path = require("path"); +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-manifest", "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-manifest", "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-manifest.json files in the fixtures are never used in these tests + +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 createBuildManifest(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 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, + "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 manifest", async (t) => { + const inputProject = await Specification.create(libraryEConfig); + inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant"); + + const metadata = await createBuildManifest(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 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, + "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/createBuildManifest.js b/test/lib/buildHelpers/createBuildManifest.js new file mode 100644 index 000000000..c143a788e --- /dev/null +++ b/test/lib/buildHelpers/createBuildManifest.js @@ -0,0 +1,127 @@ +const test = require("ava"); +const path = require("path"); +const createBuildManifest = require("../../../lib/buildHelpers/createBuildManifest"); +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 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 createBuildManifest(project, "buildConfig"); + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "application", + metadata: { + name: "application.a", + }, + resources: { + configuration: { + paths: { + webapp: "resources/id1", + }, + }, + } + }, + buildManifest: { + manifestVersion: "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 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 createBuildManifest(project, "buildConfig"); + t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid"); + metadata.buildManifest.timestamp = ""; + + t.deepEqual(metadata, { + project: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.d", + }, + resources: { + configuration: { + paths: { + src: "resources", + test: "test-resources", + }, + }, + } + }, + buildManifest: { + manifestVersion: "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/extensions.js b/test/lib/extensions.js deleted file mode 100644 index 9b309d06d..000000000 --- a/test/lib/extensions.js +++ /dev/null @@ -1,953 +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) => { - // "project-type" extension handling not yet implemented => test currently checks for error - 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) => { - // "project-type" extension handling not yet implemented => test currently checks for error - 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) => { - // "project-type" extension handling not yet implemented => test currently checks for error - 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) => { - // "project-type" extension handling not yet implemented => test currently checks for error - 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 new file mode 100644 index 000000000..7bad266d8 --- /dev/null +++ b/test/lib/generateProjectGraph.usingObject.js @@ -0,0 +1,1654 @@ +const test = require("ava"); +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"); +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"); + +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: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 +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Application A", async (t) => { + const {projectGraphFromTree} = t.context; + 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: getApplicationATree()}); + 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()); + + 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 {projectGraphFromTree, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); + const callbackStub = 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 {projectGraphFromTree, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); + 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 + // 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 {projectGraphFromTree, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: getApplicationATree()}); + const callbackStub = 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()); + + 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 {projectGraphFromTree, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleATreeIncDeduped}); + const callbackStub = 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 {projectGraphFromTree, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: applicationCycleBTreeIncDeduped}); + const callbackStub = 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"); +}); + + +/* ================================================================================================= */ +/* ======= The following tests have been derived from the existing projectPreprocessor tests ======= */ + +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, sinon} = t.context; + const projectGraph = await projectGraphFromTree({dependencyTree: tree}); + const callbackStub = 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("Project with inline configuration", 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 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 {projectGraphFromTree} = t.context; + 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: "application", + metadata: { + name: "yz" + } + }] + }; + + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), + { + message: + `Found 2 configurations of kind 'project' for module application.a.id. ` + + `There must be only one project per module.` + }, + "Rejected with error"); +}); + +test("Project with configPath", async (t) => { + const tree = { + 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 testBasicGraphCreationDfs(t, tree, [ + "application.b" + ]); +}); + +test("Project with ui5.yaml at default location", async (t) => { + const tree = { + id: "application.a.id", + version: "1.0.0", + path: applicationAPath, + dependencies: [] + }; + + await testBasicGraphCreationDfs(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 testBasicGraphCreationDfs(t, tree, [ + "application.c" + ]); +}); + +test("Missing configuration file for root project", async (t) => { + const {projectGraphFromTree} = t.context; + const tree = { + id: "application.a.id", + version: "1.0.0", + path: "non-existent", + dependencies: [] + }; + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), + { + message: + "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"); +}); + +test("Missing id for root project", async (t) => { + const {projectGraphFromTree} = t.context; + const tree = { + path: path.join(__dirname, "fixtures/application.a"), + dependencies: [] + }; + await t.throwsAsync(projectGraphFromTree({dependencyTree: tree}), + {message: "Could not create Module: Missing or empty parameter 'id'"}, "Rejected with error"); +}); + +test("No type configured for root project", async (t) => { + const {projectGraphFromTree} = t.context; + const tree = { + id: "application.a.id", + 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({dependencyTree: tree})); + + t.is(error.message, `Unable to create Specification instance: Unknown specification type 'undefined'`); +}); + +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({dependencyTree: 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({dependencyTree: 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", + path: applicationAPath, + dependencies: [ + { + id: "library.d", + version: "1.0.0", + path: libraryDPath, + resources: { + configuration: { + propertiesFileSourceEncoding: "UTF-8", + paths: { + src: "main/src", + test: "main/test" + } + } + }, + dependencies: [ + { + id: "library.a", + version: "1.0.0", + path: libraryBPath, // B, not A - inconsistency! + configuration: { + specVersion: "2.3", + type: "library", + metadata: { + name: "library.XY", + } + }, + dependencies: [] + } + ] + }, + { + id: "library.a", + version: "1.0.0", + path: libraryAPath, + 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 ======= */ + +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 = { + 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 + } + ] + } + ] + } + ] +}; + + +/* === 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" + } + } +}; + +/* ======================================================================================= */ +/* ======= 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 {projectGraphFromTree} = t.context; + 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 validationError = await t.throwsAsync(projectGraphFromTree({dependencyTree: 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"); +}); + +// TODO: Fixme +test.skip("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.getFrameworkName(), { + name: "OpenUI5" + }, "Configuration from collection project should be taken over into shimmed project"); +}); + +test("Project with unknown extension dependency inline configuration", async (t) => { + const {projectGraphFromTree} = t.context; + 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 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"); +}); + +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"); +}); + +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-configPath.yaml" + }); + t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), { + rootConfigPathTest: true + }); +}); diff --git a/test/lib/generateProjectGraph.usingStaticFile.js b/test/lib/generateProjectGraph.usingStaticFile.js new file mode 100644 index 000000000..61c8229a8 --- /dev/null +++ b/test/lib/generateProjectGraph.usingStaticFile.js @@ -0,0 +1,72 @@ +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"); +}); + +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 new file mode 100644 index 000000000..fd362bf03 --- /dev/null +++ b/test/lib/graph/Module.js @@ -0,0 +1,168 @@ +const test = require("ava"); +const sinon = require("sinon"); +const path = require("path"); +const Module = require("../../../lib/graph/Module"); + +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); +const buildDescriptionApplicationAPath = + 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 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) => { +// }); + +test.afterEach.always(() => { + sinon.restore(); +}); + +test("Instantiate a basic module", async (t) => { + const ui5Module = new Module(basicModuleInput); + 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) => { + 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 specifications from module", async (t) => { + const ui5Module = new Module(basicModuleInput); + 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("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("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'" + }); +}); diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js new file mode 100644 index 000000000..ba88b1543 --- /dev/null +++ b/test/lib/graph/ProjectGraph.js @@ -0,0 +1,1187 @@ +const path = require("path"); +const test = require("ava"); +const sinonGlobal = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); +const Specification = require("../../../lib/specifications/Specification"); +const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a"); + +async function createProject(name) { + return await Specification.create({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name} + } + }); +} + +async function createExtension(name) { + return await Specification.create({ + id: "extension.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "extension", + type: "task", + task: {}, + metadata: {name} + } + }); +} + +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" + }); + }, "Should not throw"); +}); + +test("Instantiate a basic project with missing parameter rootProjectName", async (t) => { + const {ProjectGraph} = t.context; + const error = t.throws(() => { + new ProjectGraph({}); + }); + 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 = await 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 project 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 = await 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 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await createProject("application.a"); + const error = t.throws(() => { + graph.addProject(project2); + }); + t.is(error.message, + "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"); + 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 = await createProject("application.a"); + graph.addProject(project1); + + const project2 = await 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 = await 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("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, [ + project1, project2 + ], "Should return all projects in a flat array"); +}); + +test("add-/getExtension", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension = await 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 = await createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = await 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 = await 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("getAllExtensions", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + const extension1 = await createExtension("extension.a"); + graph.addExtension(extension1); + + const extension2 = await createExtension("extension.b"); + graph.addExtension(extension2); + const res = graph.getAllExtensions(); + t.deepEqual(res, [ + extension1, extension2 + ], "Should return all extensions in a flat array"); +}); + +test("declareDependency / getDependencies", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await createProject("library.b")); + + graph.declareDependency("library.a", "library.b"); + 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.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"); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "Should declare dependency as non-optional"); + + t.is(graph.isOptionalDependency("library.b", "library.a"), false, + "Should declare dependency as non-optional"); +}); + +test("declareDependency: Unknown source", async (t) => { + const {ProjectGraph} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await 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 depending project with name library.a in project 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(await 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 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(await createProject("library.a")); + graph.addProject(await 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("declareDependency: Already declared", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await 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("declareDependency: Already declared as optional", async (t) => { + const {ProjectGraph, log} = t.context; + const graph = new ProjectGraph({ + rootProjectName: "my root project" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await 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.isOptionalDependency("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(await createProject("library.a")); + graph.addProject(await 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.isOptionalDependency("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(await createProject("library.a")); + graph.addProject(await 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.isOptionalDependency("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(await 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.getDependencies("library.x"); + }); + t.is(error.message, + "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(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.a", "library.d"); + graph.declareDependency("library.d", "library.b"); + graph.declareDependency("library.d", "library.c"); + + await graph.resolveOptionalDependencies(); + + t.is(graph.isOptionalDependency("library.a", "library.b"), false, + "library.a should have no optional dependency to library.b anymore"); + t.is(graph.isOptionalDependency("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(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.a", "library.d"); + + 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.d", + "library.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; + const graph = new ProjectGraph({ + rootProjectName: "library.a" + }); + graph.addProject(await createProject("library.a")); + graph.addProject(await 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(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"); + + 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(await createProject("library.a")); + graph.addProject(await 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(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.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 project 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(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"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseBreadthFirst("library.b", callbackStub); + + 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(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"); + + 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(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"); + + await traverseBreadthFirst(t, graph1, [ + "library.a", + "library.b", + "library.c", + "library.d" + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + 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"); + 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(await createProject("library.a")); + graph.addProject(await 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(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"); + + 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(await createProject("library.a")); + graph.addProject(await 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(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.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 project 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(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"); + + const callbackStub = t.context.sinon.stub().resolves(); + await graph.traverseDepthFirst("library.b", callbackStub); + + 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(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"); + + 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(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"); + + await traverseDepthFirst(t, graph1, [ + "library.b", + "library.c", + "library.d", + "library.a", + ]); + + const graph2 = new ProjectGraph({ + rootProjectName: "library.a" + }); + 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"); + graph2.declareDependency("library.a", "library.b"); + + await traverseDepthFirst(t, graph2, [ + "library.d", + "library.c", + "library.b", + "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(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 = await createExtension("extension.a"); + graph1.addExtension(extensionA); + + 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 = await 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: 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({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addProject(await createProject("library.a")); + graph2.addProject(await createProject("library.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + `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"); +}); + +test("join: Unexpected extension intersection", async (t) => { + const {ProjectGraph} = t.context; + const graph1 = new ProjectGraph({ + rootProjectName: "😹" + }); + const graph2 = new ProjectGraph({ + rootProjectName: "😼" + }); + graph1.addExtension(await createExtension("extension.a")); + graph2.addExtension(await createExtension("extension.a")); + + + const error = t.throws(() => { + graph1.join(graph2); + }); + t.is(error.message, + `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/isSealed", 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.b", "library.c"); + graph.declareOptionalDependency("library.c", "library.a"); + + graph.addExtension(await 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"; + + const libX = await createProject("library.x"); + t.throws(() => { + graph.addProject(libX); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.declareDependency("library.c", "library.b"); + }, { + message: expectedSealMsg + }); + t.throws(() => { + graph.declareOptionalDependency("library.b", "library.a"); + }, { + message: expectedSealMsg + }); + const extB = await createExtension("extension.b"); + t.throws(() => { + graph.addExtension(extB); + }, { + 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 should not be added"); + + const extension = graph.getExtension("extension.b"); + t.is(extension, undefined, "extension.b should not be added"); +}); diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/graph/helpers/ui5Framework.integration.js similarity index 56% rename from test/lib/translators/ui5Framework.integration.js rename to test/lib/graph/helpers/ui5Framework.integration.js index ad6842195..67215b202 100644 --- a/test/lib/translators/ui5Framework.integration.js +++ b/test/lib/graph/helpers/ui5Framework.integration.js @@ -9,10 +9,11 @@ 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; +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/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 @@ -45,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/translators/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) => { @@ -103,176 +111,102 @@ function defineTest(testName, { } }; - 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 {ui5Framework, Installer, logInfoSpy} = t.context; const testDependency = { id: "test-dependency-id", version: "4.5.6", path: path.join(fakeBaseDir, "project-test-dependency"), - dependencies: [] + 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 = { + const dependencyTree = { 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 ] - }, - { - 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" - } - ] - } - }]; + 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 [{ @@ -281,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"): @@ -291,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"): @@ -301,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"): @@ -311,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"): @@ -321,19 +267,19 @@ function defineTest(testName, { metadata: { name: "sap.ui.lib8" }, - framework: {libraries: []} + framework: { + name: frameworkName, + libraries: [] + } }]; default: throw new Error( - "ProjectPreprocessor#readConfigFile stub called with unknown project: " + - (project && project.path) + "Module#_readConfigFile stub called with unknown project: " + + (this.getId()) ); } }); - // 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") { @@ -389,155 +335,46 @@ function defineTest(testName, { .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 provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); - const tree = await normalizer.generateProjectTree(); + await ui5Framework.enrichProjectGraph(projectGraph); - t.deepEqual(tree, expectedTree, "Returned tree should be correct"); - const frameworkLibAlreadyAddedInfoLogged = (t.context.logInfoSpy.getCalls() + 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 = (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", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "SAPUI5" }); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "SAPUI5", verbose: true }); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "OpenUI5" }); -defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { +defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", { frameworkName: "OpenUI5", verbose: true }); @@ -549,49 +386,34 @@ function defineErrorTest(testName, { expectedErrorMessage }) { test.serial(testName, async (t) => { - const translatorTree = { + const {ui5Framework, Installer} = t.context; + + const dependencyTree = { 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" + dependencies: [], + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" - }, - { - name: "sap.ui.lib4", - optional: true - } - ] + { + 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) => { @@ -700,13 +522,15 @@ function defineErrorTest(testName, { } } + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); + await ui5Framework.enrichProjectGraph(projectGraph); }, {message: expectedErrorMessage}); }); } -defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when metadata request fails", { +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: @@ -715,7 +539,7 @@ Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distrib 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", { +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: @@ -725,7 +549,7 @@ Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui `404 - @openui5/sap.ui.lib4` }); defineErrorTest( - "SAPUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { + "SAPUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { frameworkName: "SAPUI5", failMetadata: true, failExtract: true, @@ -737,14 +561,14 @@ Failed to resolve library sap.ui.lib4: Failed to extract package @sapui5/distrib }); -defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when metadata request fails", { +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 translator should throw a proper error when package extraction fails", { +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: @@ -754,7 +578,7 @@ Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui `404 - @openui5/sap.ui.lib4` }); defineErrorTest( - "OpenUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { + "OpenUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", { frameworkName: "OpenUI5", failMetadata: true, failExtract: true, @@ -763,155 +587,114 @@ Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.u 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 = { +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: [] - }; - const projectPreprocessorTree = Object.assign({}, translatorTree, { - specVersion: "1.1", - type: "application", - metadata: { - name: "test-project" + dependencies: [], + configuration: { + specVersion: "2.0", + 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(); + }; + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + await t.context.ui5Framework.enrichProjectGraph(projectGraph); - t.deepEqual(tree, expectedTree, "Returned tree should be correct"); - ui5FrameworkMock.verify(); + 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 translatorTree = { + const dependencyTree = { 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" + dependencies: [], + configuration: { + specVersion: "2.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 provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); const extractStub = sinon.stub(pacote, "extract"); const manifestStub = sinon.stub(pacote, "manifest"); - await normalizer.generateProjectTree(); + 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 translatorTree = { + const dependencyTree = { 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" + dependencies: [], + configuration: { + specVersion: "2.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); + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); - }, {message: `Unknown framework.name "UI5" for project test-id. Must be "OpenUI5" or "SAPUI5"`}); + 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 translatorTree = { + const dependencyTree = { 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"}, - ] + 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"}, + ] + } } - }); + }; - sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); - sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); sinon.stub(pacote, "extract").resolves(); - sinon.stub(Installer.prototype, "readJson") + sinon.stub(t.context.Installer.prototype, "readJson") .callThrough() .withArgs(path.join(fakeBaseDir, "homedir", ".ui5", "framework", "packages", @@ -937,7 +720,7 @@ test.serial( }); await t.throwsAsync(async () => { - await normalizer.generateProjectTree(); + 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"`}); diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js new file mode 100644 index 000000000..a45a00a7b --- /dev/null +++ b/test/lib/graph/helpers/ui5Framework.js @@ -0,0 +1,518 @@ +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", + optional: true + } + ] + } + } + }; + + 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 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 = { + 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 new file mode 100644 index 000000000..e1f59bfac --- /dev/null +++ b/test/lib/graph/providers/NodePackageDependencies.integration.js @@ -0,0 +1,219 @@ +const test = require("ava"); +const path = require("path"); +const sinonGlobal = require("sinon"); +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"); + +const projectGraphBuilder = require("../../../../lib/graph/projectGraphBuilder"); +const NodePackageDependenciesProvider = require("../../../../lib/graph/providers/NodePackageDependencies"); + +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 NodePackageDependenciesProvider({ + 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 NodePackageDependenciesProvider({ + 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 NodePackageDependenciesProvider({ + 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 NodePackageDependenciesProvider({ + 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 NodePackageDependenciesProvider({ + cwd: applicationDPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.e", + "application.d" + ]); +}); + +test("AppF: UI5-dependencies in package.json are ignored", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + 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 NodePackageDependenciesProvider({ + cwd: applicationGPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.d", + "application.g" + ]); + }); + +test("AppCycleA: cyclic dev deps", async (t) => { + const applicationCycleAPath = path.join(cycleDepsBasePath, "application.cycle.a"); + + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleAPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "library.cycle.a", + "library.cycle.b", + "component.cycle.a", + "application.cycle.a" + ]); +}); + +test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => { + const applicationCycleBPath = path.join(cycleDepsBasePath, "application.cycle.b"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleBPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.e", + "module.d", + "application.cycle.b" + ]); +}); + +test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => { + const applicationCycleCPath = path.join(cycleDepsBasePath, "application.cycle.c"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleCPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.f", + "module.g", + "application.cycle.c" + ]); + await testGraphCreationBfs(t, npmProvider, [ + "application.cycle.c", + "module.f", + "module.g", + ]); +}); + +test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => { + const applicationCycleDPath = path.join(cycleDepsBasePath, "application.cycle.d"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleDPath + }); + + 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("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => { + const applicationCycleEPath = path.join(cycleDepsBasePath, "application.cycle.e"); + const npmProvider = new NodePackageDependenciesProvider({ + cwd: applicationCycleEPath + }); + await testGraphCreationDfs(t, npmProvider, [ + "module.l", + "module.m", + "application.cycle.e" + ]); +}); + +test("Error: missing package.json", async (t) => { + const dir = path.parse(__dirname).root; + const npmProvider = new NodePackageDependenciesProvider({ + cwd: dir + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, `Failed to locate package.json for directory ${dir}`); +}); + +test("Error: missing dependency", async (t) => { + const npmProvider = new NodePackageDependenciesProvider({ + cwd: errApplicationAPath + }); + const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, [])); + t.is(error.message, + `Unable to locate module library.xx via resolve logic: Cannot find module 'library.xx/package.json' from ` + + `'${errApplicationAPath}'`); +}); 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 4b821ad2b..000000000 --- a/test/lib/projectPreprocessor.js +++ /dev/null @@ -1,2394 +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: "1.0", - 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: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "1.0", - 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: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - 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: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "1.0", - 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: "ISO-8859-1", - paths: { - webapp: "src" - } - }, - pathMappings: { - "/": "src", - } - }, - dependencies: [], - id: "application.c", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: applicationCPath - }, "Parsed correctly"); - }); -}); - -test("Missing configuration for root project", async (t) => { - const tree = { - id: "application.a", - path: "non-existent", - 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", (t) => { - const tree = { - id: "application.a", - version: "1.0.0", - specVersion: "0.1", - path: path.join(__dirname, "../fixtures/application.a"), - dependencies: [], - metadata: { - name: "application.a", - namespace: "id1" - } - }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - {message: "No type configured for root project application.a"}, - "Rejected with error"); -}); - -test("Missing dependencies", (t) => { - const tree = ({ - id: "application.a", - version: "1.0.0", - path: applicationAPath, - dependencies: [] - }); - 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: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp", - } - }, - dependencies: [], - id: "application.a", - kind: "project", - version: "1.0.0", - specVersion: "1.0", - 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: "0.1", - path: applicationAPath, - type: "application", - metadata: { - name: "application.a" - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "0.1", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - path: libraryBPath, // B, not A - inconsistency! - type: "library", - metadata: { - name: "library.XY", - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - 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: "0.1", - path: applicationAPath, - _level: 0, - _isRoot: true, - type: "application", - metadata: { - name: "application.a", - namespace: "id1" - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - webapp: "webapp" - } - }, - pathMappings: { - "/": "webapp" - } - }, - dependencies: [ - { - id: "library.d", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: libraryDPath, - _level: 1, - type: "library", - metadata: { - name: "library.d", - namespace: "library/d", - copyright: "Some fancy copyright", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - 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: "0.1", - path: libraryAPath, - _level: 1, - type: "library", - metadata: { - name: "library.a", - namespace: "library/a", - copyright: "Some fancy copyright ${currentYear}", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "src", - test: "test" - } - }, - pathMappings: { - "/resources/": "src", - "/test-resources/": "test" - } - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - kind: "project", - version: "1.0.0", - specVersion: "0.1", - path: libraryAPath, - _level: 1, - type: "library", - metadata: { - name: "library.a", - namespace: "library/a", - copyright: "Some fancy copyright ${currentYear}", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - 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 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: "1.0", - 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: "1.0", - type: "library", - metadata: {name: "library.b"} - } - ], - version: "1.0.0", - specVersion: "1.0", - 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": "1.0", - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}" - }, - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, { - "id": "library.b", - "path": libraryBPath, - "dependencies": [], - "version": "1.0.0", - "specVersion": "1.0", - "type": "library", - "metadata": { - "name": "library.b", - "namespace": "library/b", - "copyright": "Some fancy copyright ${currentYear}" - }, - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }], - "version": "1.0.0", - "specVersion": "1.0", - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "_level": 0, - "_isRoot": true, - "kind": "project", - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - } -}; - -/* === Tree A === */ -const treeAWithInlineConfigs = { - id: "application.a", - version: "1.0.0", - specVersion: "1.0", - path: applicationAPath, - type: "application", - metadata: { - name: "application.a", - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "0.1", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a", - }, - dependencies: [] - } - ] - }, - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - 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": "1.0", - "path": applicationAPath, - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "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": "0.1", - "path": libraryAPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - }, - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": libraryAPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "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": "1.0", - "path": applicationAPath, - "configPath": path.join(applicationAPath, "ui5.yaml"), - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.a", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "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": "ISO-8859-1", - "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": "0.1", - "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": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - }, - { - "id": "library.a", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "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": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] -}; - -/* === Tree B === */ -const treeBWithInlineConfigs = { - id: "application.b", - version: "1.0.0", - specVersion: "0.1", - path: applicationBPath, - type: "application", - metadata: { - name: "application.b" - }, - dependencies: [ - { - id: "library.b", - version: "1.0.0", - specVersion: "0.1", - path: libraryBPath, - type: "library", - metadata: { - name: "library.b", - }, - dependencies: [ - { - id: "library.d", - version: "1.0.0", - specVersion: "0.1", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a" - }, - dependencies: [] - } - ] - } - ] - }, - { - id: "library.d", - version: "1.0.0", - specVersion: "0.1", - path: libraryDPath, - type: "library", - metadata: { - name: "library.d", - }, - resources: { - configuration: { - propertiesFileSourceEncoding: "ISO-8859-1", - paths: { - src: "main/src", - test: "main/test" - } - } - }, - dependencies: [ - { - id: "library.a", - version: "1.0.0", - specVersion: "0.1", - path: libraryAPath, - type: "library", - metadata: { - name: "library.a" - }, - dependencies: [] - } - ] - } - ] -}; - -const expectedTreeBWithInlineConfigs = { - "id": "application.b", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": applicationBPath, - "_level": 0, - "_isRoot": true, - "type": "application", - "metadata": { - "name": "application.b", - "namespace": "id1" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "webapp": "webapp" - } - }, - "pathMappings": { - "/": "webapp" - } - }, - "dependencies": [ - { - "id": "library.b", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": libraryBPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.b", - "namespace": "library/b", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [ - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "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": "0.1", - "path": libraryAPath, - "_level": 2, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}", - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - } - ] - }, - { - "id": "library.d", - "kind": "project", - "version": "1.0.0", - "specVersion": "0.1", - "path": libraryDPath, - "_level": 1, - "type": "library", - "metadata": { - "name": "library.d", - "namespace": "library/d", - "copyright": "Some fancy copyright" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "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": "0.1", - "path": libraryAPath, - "_level": 2, - "type": "library", - "metadata": { - "name": "library.a", - "namespace": "library/a", - "copyright": "Some fancy copyright ${currentYear}" - }, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - }, - "dependencies": [] - } - ] - } - ] -}; - -const treeApplicationCycleA = { - id: "application.cycle.a", - version: "1.0.0", - specVersion: "0.1", - path: path.join(cycleDepsBasePath, "application.cycle.a"), - type: "application", - metadata: { - name: "application.cycle.a", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "0.1", - path: path.join(cycleDepsBasePath, "component.cycle.a"), - type: "library", - metadata: { - name: "component.cycle.a", - }, - dependencies: [ - { - id: "library.cycle.a", - version: "1.0.0", - specVersion: "0.1", - path: path.join(cycleDepsBasePath, "library.cycle.a"), - type: "library", - metadata: { - name: "library.cycle.a", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "0.1", - 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: "0.1", - path: path.join(cycleDepsBasePath, "library.cycle.b"), - type: "library", - metadata: { - name: "library.cycle.b", - }, - dependencies: [ - { - id: "component.cycle.a", - version: "1.0.0", - specVersion: "0.1", - 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: "0.1", - 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": "0.1", - "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": "0.1", - "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": "0.1", - "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": "0.1", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "type": "library", - "metadata": { - "name": "component.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 2, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, - { - "id": "library.cycle.b", - "version": "1.0.0", - "specVersion": "0.1", - "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": "0.1", - "path": path.join(cycleDepsBasePath, "component.cycle.a"), - "type": "library", - "metadata": { - "name": "component.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 2, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - }, - { - "id": "application.cycle.a", - "version": "1.0.0", - "specVersion": "0.1", - "path": path.join(cycleDepsBasePath, "application.cycle.a"), - "type": "application", - "metadata": { - "name": "application.cycle.a", - }, - "dependencies": [], - "deduped": true - } - ], - "kind": "project", - "_level": 1, - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "paths": { - "src": "src", - "test": "test" - } - }, - "pathMappings": { - "/resources/": "src", - "/test-resources/": "test" - } - } - } - ], - "_level": 0, - "_isRoot": true, - "kind": "project", - "resources": { - "configuration": { - "propertiesFileSourceEncoding": "ISO-8859-1", - "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: "0.1", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "0.1", "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: "1.0", - type: "application", - metadata: { - name: "xy" - } - }; - const res = await projectPreprocessor.processTree(tree); - t.deepEqual(res.specVersion, "1.0", "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/ComponentProject.js b/test/lib/specifications/ComponentProject.js new file mode 100644 index 000000000..ab2d4f426 --- /dev/null +++ b/test/lib/specifications/ComponentProject.js @@ -0,0 +1,153 @@ +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("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}"); + 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 new file mode 100644 index 000000000..a8917234a --- /dev/null +++ b/test/lib/specifications/Project.js @@ -0,0 +1,34 @@ +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", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.3", + kind: "project", + type: "application", + metadata: {name: "application.a"} + } +}; + +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"); +}); 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 new file mode 100644 index 000000000..769e9d495 --- /dev/null +++ b/test/lib/specifications/types/Application.js @@ -0,0 +1,440 @@ +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"); + +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.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`); +}); + +test("getCachebusterSignatureType: Default", async (t) => { + const project = await Specification.create(basicProjectInput); + t.is(project.getCachebusterSignatureType(), "time", + "Returned correct default cachebuster signature type configuration"); +}); + +test("getCachebusterSignatureType: Configuration", async (t) => { + const customProjectInput = clone(basicProjectInput); + customProjectInput.configuration.builder = { + cachebuster: { + signatureType: "hash" + } + }; + const project = await Specification.create(customProjectInput); + t.is(project.getCachebusterSignatureType(), "hash", + "Returned correct default cachebuster signature type configuration"); +}); + +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"); + 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", 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: 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("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); + + 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({}); + + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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 new file mode 100644 index 000000000..2fb78dfa5 --- /dev/null +++ b/test/lib/specifications/types/Library.js @@ -0,0 +1,1239 @@ +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 libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); +const basicProjectInput = { + 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" + } + } + }, + } +}; + +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(); +}); + +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`); +}); + +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", 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", 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 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"); + t.truthy(resource, "Found the requested resource"); + 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"}); + 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 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)"); +}); + +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 = { + 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); + + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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._getRawSourceReader = () => { + 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.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) => { + 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], + `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) => { + 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"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +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"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +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"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +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"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +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"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +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("_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({ + 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"); +}); 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"); +}); 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"); +}); 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.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