diff --git a/lib/graph/Module.js b/lib/graph/Module.js
index f098d7bcd..f495a3c28 100644
--- a/lib/graph/Module.js
+++ b/lib/graph/Module.js
@@ -100,7 +100,7 @@ class Module {
*
* @private
* @typedef {object} @ui5/project/graph/Module~SpecificationsResult
- * @property {@ui5/project/specifications/Project|undefined} Project found in the module (if one is found)
+ * @property {@ui5/project/specifications/Project|null} Project found in the module (if one is found)
* @property {@ui5/project/specifications/Extension[]} Array of extensions found in the module
*
*/
@@ -187,7 +187,7 @@ class Module {
}
return {
- project: projects[0],
+ project: projects[0] || null,
extensions
};
}
@@ -338,6 +338,8 @@ class Module {
}
// Validate found configurations with schema
+ // Validation is done again in the Specification class. But here we can reference the YAML file
+ // which adds helpful information like the line number
const validationResults = await Promise.all(
configs.map(async (config, documentIndex) => {
// Catch validation errors to ensure proper order of rejections within Promise.all
diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js
index 8ea683f4f..00d8973e3 100644
--- a/lib/graph/ProjectGraph.js
+++ b/lib/graph/ProjectGraph.js
@@ -2,7 +2,9 @@ import {getLogger} from "@ui5/logger";
const log = getLogger("graph:ProjectGraph");
/**
- * A rooted, directed graph representing a UI5 project, its dependencies and available extensions
+ * A rooted, directed graph representing a UI5 project, its dependencies and available extensions.
+ *
+ * While it allows defining cyclic dependencies, both traversal functions will throw an error if they encounter cycles.
*
* @public
* @class
@@ -20,9 +22,9 @@ class ProjectGraph {
}
this._rootProjectName = rootProjectName;
- this._projects = new Map(); // maps project name to instance
- this._adjList = new Map(); // maps project name to edges/dependencies
- this._optAdjList = new Map(); // maps project name to optional dependencies
+ this._projects = new Map(); // maps project name to instance (= nodes)
+ this._adjList = new Map(); // maps project name to dependencies (= edges)
+ this._optAdjList = new Map(); // maps project name to optional dependencies (= edges)
this._extensions = new Map(); // maps extension name to instance
@@ -175,10 +177,6 @@ class ProjectGraph {
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) {
@@ -199,10 +197,6 @@ class ProjectGraph {
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._hasUnresolvedOptionalDependencies = true;
@@ -267,6 +261,11 @@ class ProjectGraph {
*/
getTransitiveDependencies(projectName) {
const dependencies = new Set();
+ if (!this._projects.has(projectName)) {
+ throw new Error(
+ `Failed to get transitive dependencies for project ${projectName}: ` +
+ `Unable to find project in project graph`);
+ }
const processDependency = (depName) => {
const adjacencies = this._adjList.get(depName);
@@ -375,7 +374,6 @@ class ProjectGraph {
* graph traversal will wait and only continue once the promise has resolved.
*/
- // 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.
@@ -398,28 +396,28 @@ class ProjectGraph {
const queue = [{
projectNames: [startName],
- predecessors: []
+ ancestors: []
}];
const visited = Object.create(null);
while (queue.length) {
- const {projectNames, predecessors} = queue.shift(); // Get and remove first entry from queue
+ const {projectNames, ancestors} = queue.shift(); // Get and remove first entry from queue
await Promise.all(projectNames.map(async (projectName) => {
- this._checkCycle(predecessors, projectName);
+ this._checkCycle(ancestors, projectName);
if (visited[projectName]) {
return visited[projectName];
}
return visited[projectName] = (async () => {
- const newPredecessors = [...predecessors, projectName];
+ const newAncestors = [...ancestors, projectName];
const dependencies = this.getDependencies(projectName);
queue.push({
projectNames: dependencies,
- predecessors: newPredecessors
+ ancestors: newAncestors
});
await callback({
@@ -453,17 +451,17 @@ class ProjectGraph {
return this._traverseDepthFirst(startName, Object.create(null), [], callback);
}
- async _traverseDepthFirst(projectName, visited, predecessors, callback) {
- this._checkCycle(predecessors, projectName);
+ async _traverseDepthFirst(projectName, visited, ancestors, callback) {
+ this._checkCycle(ancestors, projectName);
if (visited[projectName]) {
return visited[projectName];
}
return visited[projectName] = (async () => {
- const newPredecessors = [...predecessors, projectName];
+ const newAncestors = [...ancestors, projectName];
const dependencies = this.getDependencies(projectName);
await Promise.all(dependencies.map((depName) => {
- return this._traverseDepthFirst(depName, visited, newPredecessors, callback);
+ return this._traverseDepthFirst(depName, visited, newAncestors, callback);
}));
await callback({
@@ -609,13 +607,13 @@ class ProjectGraph {
}
}
- _checkCycle(predecessors, projectName) {
- if (predecessors.includes(projectName)) {
- // We start to run in circles. That's neither expected nor something we can deal with
-
- // Mark first and last occurrence in chain with an asterisk
- predecessors[predecessors.indexOf(projectName)] = `${projectName}*`;
- throw new Error(`Detected cyclic dependency chain: ${predecessors.join(" -> ")} -> ${projectName}*`);
+ _checkCycle(ancestors, projectName) {
+ if (ancestors.includes(projectName)) {
+ // "Back-edge" detected. Neither BFS nor DFS searches should continue
+ // Mark first and last occurrence in chain with an asterisk and throw an error detailing the
+ // problematic dependency chain
+ ancestors[ancestors.indexOf(projectName)] = `*${projectName}*`;
+ throw new Error(`Detected cyclic dependency chain: ${ancestors.join(" -> ")} -> *${projectName}*`);
}
}
diff --git a/lib/graph/Workspace.js b/lib/graph/Workspace.js
new file mode 100644
index 000000000..f5b5a7756
--- /dev/null
+++ b/lib/graph/Workspace.js
@@ -0,0 +1,257 @@
+import fs from "graceful-fs";
+import {globby, isDynamicPattern} from "globby";
+import path from "node:path";
+import {promisify} from "node:util";
+import {getLogger} from "@ui5/logger";
+import Module from "./Module.js";
+import {validateWorkspace} from "../validation/validator.js";
+
+const readFile = promisify(fs.readFile);
+const log = getLogger("graph:Workspace");
+
+
+/**
+ * Dependency graph node representing a module
+ *
+ * @public
+ * @typedef {object} @ui5/project/graph/Workspace~Configuration
+ * @property {string} node.specVersion
+ * @property {object} node.metadata Version of the project
+ * @property {string} node.metadata.name Name of the workspace configuration
+ * @property {object} node.dependencyManagement
+ * @property {@ui5/project/graph/Workspace~DependencyManagementResolutions[]} node.dependencyManagement.resolutions
+ */
+
+/**
+ * A resolution entry for the dependency management section of the configuration
+ *
+ * @public
+ * @typedef {object} @ui5/project/graph/Workspace~DependencyManagementResolution
+ * @property {string} path Relative path to use for the workspace resolution process
+ */
+
+/**
+ * Workspace representation
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/graph/Workspace
+ */
+class Workspace {
+ #visitedNodePaths = new Set();
+ #configValidated = false;
+ #configuration;
+ #cwd;
+
+ /**
+ * @public
+ * @param {object} options
+ * @param {string} options.cwd Path to use for resolving all paths of the workspace configuration from.
+ * This should contain platform-specific path separators (i.e. must not be POSIX on non-POSIX systems)
+ * @param {@ui5/project/graph/Workspace~Configuration} options.configuration
+ * Workspace configuration
+ */
+ constructor({cwd, configuration}) {
+ if (!cwd) {
+ throw new Error(`Could not create Workspace: Missing or empty parameter 'cwd'`);
+ }
+ if (!configuration) {
+ throw new Error(`Could not create Workspace: Missing or empty parameter 'configuration'`);
+ }
+
+ this.#cwd = cwd;
+ this.#configuration = configuration;
+ }
+
+ /**
+ * Get the name of this workspace
+ *
+ * @public
+ * @returns {string} Name of this workspace configuration
+ */
+ getName() {
+ return this.#configuration.metadata.name;
+ }
+
+ /**
+ * For a given project name (e.g. the value of the metadata.name
property in a ui5.yaml),
+ * returns a [Module]{@ui5/project/graph/Module} instance or undefined
depending on whether the project
+ * has been found in the configured dependency-management resolution paths of this workspace
+ *
+ * @public
+ * @param {string} projectName Name of the project
+ * @returns {Promise<@ui5/project/graph/Module|undefined>}
+ * Module instance of undefined
if none is found
+ */
+ async getModuleByProjectName(projectName) {
+ const {projectNameMap} = await this._getResolvedModules();
+ return projectNameMap.get(projectName);
+ }
+
+ /**
+ * For a given node id (e.g. the value of the name property in a package.json),
+ * returns a [Module]{@eId Node ID of} instance or undefined
depending on whether the module
+ * has been found in the configured dependency-management resolution paths of this workspace
+ * and contains at least one project or extension
+ *
+ * @public
+ * @param {string} nodeId Node ID of the module
+ * @returns {Promise<@ui5/project/graph/Module|undefined>}
+ * Module instance of undefined
if none is found
+ */
+ async getModuleByNodeId(nodeId) {
+ const {moduleIdMap} = await this._getResolvedModules();
+ return moduleIdMap.get(nodeId);
+ }
+
+ _getResolvedModules() {
+ if (this._pResolvedModules) {
+ return this._pResolvedModules;
+ }
+
+ return this._pResolvedModules = this._resolveModules();
+ }
+
+ async _resolveModules() {
+ await this._validateConfig();
+ const resolutions = this.#configuration.dependencyManagement?.resolutions;
+ if (!resolutions?.length) {
+ return {
+ projectNameMap: new Map(),
+ moduleIdMap: new Map()
+ };
+ }
+
+ let resolvedModules = await Promise.all(resolutions.map(async (resolutionConfig) => {
+ if (!resolutionConfig.path) {
+ throw new Error(
+ `Missing property 'path' in dependency resolution configuration of workspace ${this.getName()}`);
+ }
+ return await this._getModulesFromPath(
+ this.#cwd, resolutionConfig.path);
+ }));
+
+ // Flatten array since package-workspaces might have resolved to multiple modules for a single resolution
+ resolvedModules = Array.prototype.concat.apply([], resolvedModules);
+
+ const projectNameMap = new Map();
+ const moduleIdMap = new Map();
+ await Promise.all(resolvedModules.map(async (module) => {
+ const {project, extensions} = await module.getSpecifications();
+ if (project || extensions.length) {
+ moduleIdMap.set(module.getId(), module);
+ } else {
+ log.warn(
+ `Failed to create a project or extensions from module ${module.getId()} at ${module.getPath()}`);
+ }
+ if (project) {
+ projectNameMap.set(project.getName(), module);
+ log.verbose(`Module ${module.getId()} contains project ${project.getName()}`);
+ }
+ if (extensions.length) {
+ const extensionNames = extensions.map((e) => e.getName()).join(", ");
+ log.verbose(`Module ${module.getId()} contains extensions: ${extensionNames}`);
+ }
+ }));
+ return {
+ projectNameMap,
+ moduleIdMap
+ };
+ }
+
+ async _getModulesFromPath(cwd, relPath) {
+ const nodePath = path.join(cwd, relPath);
+ if (this.#visitedNodePaths.has(nodePath)) {
+ log.verbose(`Module located at ${nodePath} has already been visited`);
+ return [];
+ }
+ this.#visitedNodePaths.add(nodePath);
+ let pkg;
+ try {
+ pkg = await this._readPackageJson(nodePath);
+ if (!pkg?.name || !pkg?.version) {
+ throw new Error(
+ `package.json must contain fields 'name' and 'version'`);
+ }
+ } catch (err) {
+ throw new Error(
+ `Failed to resolve workspace dependency resolution path ${relPath} to ${nodePath}: ${err.message}`);
+ }
+
+ // If the package.json defines an npm "workspaces", or an equivalent "ui5.workspaces" configuration,
+ // resolve the workspace and only use the resulting modules. The root package is ignored.
+ const packageWorkspaceConfig = pkg.ui5?.workspaces || pkg.workspaces;
+ if (packageWorkspaceConfig?.length) {
+ log.verbose(`Module ${pkg.name} provides a package.json workspaces configuration. ` +
+ `Ignoring the module and resolving workspaces instead...`);
+ const staticPatterns = [];
+ // Split provided patterns into dynamic and static patterns
+ // This is necessary, since fast-glob currently behaves different from
+ // "glob" (used by @npmcli/map-workspaces) in that it does not match the
+ // base directory in case it is equal to the pattern (https://github.com/mrmlnc/fast-glob/issues/47)
+ // For example a pattern "package-a" would not match a directory called
+ // "package-a" in the root directory of the project.
+ // We therefore detect the static pattern and resolve it directly
+ const dynamicPatterns = packageWorkspaceConfig.filter((pattern) => {
+ if (isDynamicPattern(pattern)) {
+ return true;
+ } else {
+ staticPatterns.push(pattern);
+ return false;
+ }
+ });
+
+ let searchPaths = [];
+ if (dynamicPatterns.length) {
+ searchPaths = await globby(dynamicPatterns, {
+ cwd: nodePath,
+ followSymbolicLinks: false,
+ onlyDirectories: true,
+ });
+ }
+ searchPaths.push(...staticPatterns);
+
+ const resolvedModules = new Map();
+ await Promise.all(searchPaths.map(async (pkgPath) => {
+ const modules = await this._getModulesFromPath(nodePath, pkgPath);
+ modules.forEach((module) => {
+ const id = module.getId();
+ if (!resolvedModules.get(id)) {
+ resolvedModules.set(id, module);
+ }
+ });
+ }));
+ return Array.from(resolvedModules.values());
+ } else {
+ return [new Module({
+ id: pkg.name,
+ version: pkg.version,
+ modulePath: nodePath
+ })];
+ }
+ }
+
+ /**
+ * Reads the package.json file and returns its content
+ *
+ * @private
+ * @param {string} modulePath Path to the module containing the package.json
+ * @returns {object} Package json content
+ */
+ async _readPackageJson(modulePath) {
+ const content = await readFile(path.join(modulePath, "package.json"), "utf8");
+ return JSON.parse(content);
+ }
+
+ async _validateConfig() {
+ if (this.#configValidated) {
+ return;
+ }
+ await validateWorkspace({
+ config: this.#configuration
+ });
+ this.#configValidated = true;
+ }
+}
+
+export default Workspace;
diff --git a/lib/graph/graph.js b/lib/graph/graph.js
index 4fc60c481..f0e10c613 100644
--- a/lib/graph/graph.js
+++ b/lib/graph/graph.js
@@ -1,6 +1,7 @@
import path from "node:path";
import projectGraphBuilder from "./projectGraphBuilder.js";
import ui5Framework from "./helpers/ui5Framework.js";
+import createWorkspace from "./helpers/createWorkspace.js";
import {getLogger} from "@ui5/logger";
const log = getLogger("generateProjectGraph");
@@ -46,27 +47,47 @@ function resolveProjectPaths(cwd, project) {
* @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
+ * @param {string} [options.workspaceName]
+ * Name of the workspace configuration that should be used if any is provided
+ * @param {string} [options.workspaceConfigPath=ui5-workspace.yaml]
+ * Workspace configuration file to use if no object has been provided
+ * @param {@ui5/project/graph/Workspace~Configuration} [options.workspaceConfiguration]
+ * Workspace configuration object to use instead of reading from a configuration file.
+ * Parameter workspaceName
can either be omitted or has to match with the given configuration name
* @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance
*/
export async function graphFromPackageDependencies({
cwd, rootConfiguration, rootConfigPath,
- versionOverride, resolveFrameworkDependencies = true
+ versionOverride, resolveFrameworkDependencies = true,
+ workspaceName, workspaceConfiguration, workspaceConfigPath = "ui5-workspace.yaml"
}) {
log.verbose(`Creating project graph using npm provider...`);
const {
default: NpmProvider
} = await import("./providers/NodePackageDependencies.js");
+ cwd = cwd ? path.resolve(cwd) : process.cwd();
+
+ let workspace;
+ if (workspaceName || workspaceConfiguration) {
+ workspace = await createWorkspace({
+ cwd,
+ name: workspaceName,
+ configObject: workspaceConfiguration,
+ configPath: workspaceConfigPath
+ });
+ }
+
const provider = new NpmProvider({
- cwd: cwd ? path.resolve(cwd) : process.cwd(),
+ cwd,
rootConfiguration,
rootConfigPath
});
- const projectGraph = await projectGraphBuilder(provider);
+ const projectGraph = await projectGraphBuilder(provider, workspace);
if (resolveFrameworkDependencies) {
- await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride});
+ await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, workspace});
}
return projectGraph;
diff --git a/lib/graph/helpers/createWorkspace.js b/lib/graph/helpers/createWorkspace.js
new file mode 100644
index 000000000..2283c698e
--- /dev/null
+++ b/lib/graph/helpers/createWorkspace.js
@@ -0,0 +1,146 @@
+import path from "node:path";
+import Workspace from "../Workspace.js";
+import {validateWorkspace} from "../../validation/validator.js";
+import {getLogger} from "@ui5/logger";
+const log = getLogger("generateProjectGraph");
+
+const DEFAULT_WORKSPACE_CONFIG_PATH = "ui5-workspace.yaml";
+const DEFAULT_WORKSPACE_NAME = "default";
+
+export default async function createWorkspace({
+ cwd, name, configObject, configPath
+}) {
+ if (!cwd || (!configObject && !configPath)) {
+ throw new Error(`createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'`);
+ }
+ if (configObject) {
+ if (!configObject?.metadata?.name) {
+ throw new Error(`Invalid workspace configuration: Missing or empty property 'metadata.name'`);
+ }
+ if (name && configObject.metadata.name !== name) {
+ throw new Error(
+ `The provided workspace name '${name}' does not match ` +
+ `the provided workspace configuration '${configObject.metadata.name}'`);
+ } else {
+ log.verbose(`Using provided workspace configuration ${configObject.metadata.name}...`);
+ return new Workspace({
+ cwd,
+ configuration: configObject
+ });
+ }
+ } else {
+ if (!name) {
+ throw new Error(`createWorkspace: Parameter 'configPath' implies parameter 'name', but it's empty`);
+ }
+ let filePath = configPath;
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.join(cwd, configPath);
+ }
+ try {
+ const workspaceConfigs =
+ await readWorkspaceConfigFile(filePath, );
+ const configuration = workspaceConfigs.find((config) => {
+ return config.metadata.name === name;
+ });
+
+ if (configuration) {
+ log.verbose(`Using workspace configuration "${name}" from ${configPath}...`);
+ return new Workspace({
+ cwd: path.dirname(filePath),
+ configuration
+ });
+ } else if (name === DEFAULT_WORKSPACE_NAME) {
+ // Requested workspace not found
+ // Do not throw if the requested name is the default
+ return null;
+ } else {
+ throw new Error(`Could not find a workspace named '${name}' in ${configPath}`);
+ }
+ } catch (err) {
+ if (name === DEFAULT_WORKSPACE_NAME && configPath === DEFAULT_WORKSPACE_CONFIG_PATH &&
+ err.cause?.code === "ENOENT") {
+ // Do not throw if the default workspace in the default file was requested but not found
+ log.verbose(`No workspace configuration file provided at ${filePath}`);
+ return null;
+ } else {
+ throw err;
+ }
+ }
+ }
+}
+
+async function readWorkspaceConfigFile(filePath, throwIfMissing) {
+ const {
+ default: fs
+ } = await import("graceful-fs");
+ const {promisify} = await import("node:util");
+ const readFile = promisify(fs.readFile);
+ const jsyaml = await import("js-yaml");
+
+ let fileContent;
+ try {
+ fileContent = await readFile(filePath, {encoding: "utf8"});
+ } catch (err) {
+ throw new Error(
+ `Failed to load workspace configuration from path ${filePath}: ${err.message}`, {
+ cause: err
+ });
+ }
+ let configs;
+ try {
+ configs = jsyaml.loadAll(fileContent, undefined, {
+ filename: filePath,
+ });
+ } catch (err) {
+ if (err.name === "YAMLException") {
+ throw new Error(`Failed to parse workspace configuration at ` +
+ `${filePath}\nError: ${err.message}`);
+ } else {
+ throw new Error(
+ `Failed to parse workspace configuration at ${filePath}: ${err.message}`);
+ }
+ }
+
+ if (!configs || !configs.length) {
+ // No configs found => exit here
+ log.verbose(`Found empty workspace configuration file at ${filePath}`);
+ return configs;
+ }
+
+ // Validate found configurations with schema
+ // Validation is done again in the Workspace class. But here we can reference the YAML file
+ // which adds helpful information like the line number
+ const validationResults = await Promise.all(
+ configs.map(async (config, documentIndex) => {
+ // Catch validation errors to ensure proper order of rejections within Promise.all
+ try {
+ await validateWorkspace({
+ config,
+ yaml: {
+ path: filePath,
+ source: fileContent,
+ 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;
+}
+
+// Export function for testing only
+/* istanbul ignore else */
+if (process.env.NODE_ENV === "test") {
+ createWorkspace._readWorkspaceConfigFile = readWorkspaceConfigFile;
+}
diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js
index a50af78f0..01f510364 100644
--- a/lib/graph/helpers/ui5Framework.js
+++ b/lib/graph/helpers/ui5Framework.js
@@ -4,33 +4,38 @@ import {getLogger} from "@ui5/logger";
const log = getLogger("graph:helpers:ui5Framework");
class ProjectProcessor {
- constructor({libraryMetadata}) {
+ constructor({libraryMetadata, graph, workspace}) {
this._libraryMetadata = libraryMetadata;
+ this._graph = graph;
+ this._workspace = workspace;
this._projectGraphPromises = Object.create(null);
}
- async addProjectToGraph(libName, projectGraph) {
+ async addProjectToGraph(libName, ancestors) {
+ if (ancestors) {
+ this._checkCycle(ancestors, libName);
+ }
if (this._projectGraphPromises[libName]) {
return this._projectGraphPromises[libName];
}
- return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, projectGraph);
+ return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, ancestors);
}
- async _addProjectToGraph(libName, projectGraph) {
+ async _addProjectToGraph(libName, ancestors = []) {
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];
+ const graph = this._graph;
- if (projectGraph.getProject(depMetadata.id)) {
+ if (graph.getProject(libName)) {
// Already added
return;
}
const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => {
- await this.addProjectToGraph(depName, projectGraph);
+ await this.addProjectToGraph(depName, [...ancestors, libName]);
return depName;
}));
@@ -38,7 +43,7 @@ class ProjectProcessor {
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);
+ await this.addProjectToGraph(depName, [...ancestors, libName]);
return depName;
}
}));
@@ -46,16 +51,63 @@ class ProjectProcessor {
dependencies.push(...resolvedOptionals.filter(($)=>$));
}
- const ui5Module = new Module({
- id: depMetadata.id,
- version: depMetadata.version,
- modulePath: depMetadata.path
- });
+ let projectIsFromWorkspace = false;
+ let ui5Module;
+ if (this._workspace) {
+ ui5Module = await this._workspace.getModuleByProjectName(libName);
+ if (ui5Module) {
+ log.info(`Resolved project ${libName} via ${this._workspace.getName()} workspace ` +
+ `to version ${ui5Module.getVersion()}`);
+ log.verbose(` Resolved module ${libName} to path ${ui5Module.getPath()}`);
+ log.verbose(` Requested version was: ${depMetadata.version}`);
+ projectIsFromWorkspace = true;
+ }
+ }
+
+ if (!ui5Module) {
+ ui5Module = new Module({
+ id: depMetadata.id,
+ version: depMetadata.version,
+ modulePath: depMetadata.path
+ });
+ }
const {project} = await ui5Module.getSpecifications();
- projectGraph.addProject(project);
+ graph.addProject(project);
dependencies.forEach((dependency) => {
- projectGraph.declareDependency(libName, dependency);
+ graph.declareDependency(libName, dependency);
});
+ if (projectIsFromWorkspace) {
+ // Add any dependencies that are only declared in the workspace resolved project
+ // Do not remove superfluous dependencies (might be added later though)
+ await Promise.all(project.getFrameworkDependencies().map(async ({name, optional, development}) => {
+ // Only proceed with dependencies which are not "optional" or "development",
+ // and not already listed in the dependencies of the original node
+ if (optional || development || dependencies.includes(name)) {
+ return;
+ }
+
+ if (!this._libraryMetadata[name]) {
+ throw new Error(
+ `Unable to find dependency ${name}, required by project ${project.getName()} ` +
+ `(resolved via ${this._workspace.getName()} workspace) in current set of libraries. ` +
+ `Try adding it temporarily to the root project's dependencies`);
+ }
+
+ await this.addProjectToGraph(name, [...ancestors, libName]);
+ graph.declareDependency(libName, name);
+ }));
+ }
+ }
+ _checkCycle(ancestors, projectName) {
+ if (ancestors.includes(projectName)) {
+ // "Back-edge" detected. This would cause a deadlock
+ // Mark first and last occurrence in chain with an asterisk and throw an error detailing the
+ // problematic dependency chain
+ ancestors[ancestors.indexOf(projectName)] = `*${projectName}*`;
+ throw new Error(
+ `ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: ` +
+ `${ancestors.join(" -> ")} -> *${projectName}*`);
+ }
}
}
@@ -158,6 +210,8 @@ export default {
* @param {object} [options]
* @param {string} [options.versionOverride] Framework version to use instead of the root projects framework
* version
+ * @param {@ui5/project/graph/Workspace} [options.workspace]
+ * Optional workspace instance to use for overriding node resolutions
* @returns {Promise<@ui5/project/graph/ProjectGraph>}
* Promise resolving with the given graph instance to allow method chaining
*/
@@ -245,15 +299,18 @@ export default {
`resolved in ${prettyHrtime(timeDiff)}`);
}
- const projectProcessor = new utils.ProjectProcessor({
- libraryMetadata
- });
-
const frameworkGraph = new ProjectGraph({
rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph`
});
+
+ const projectProcessor = new utils.ProjectProcessor({
+ libraryMetadata,
+ graph: frameworkGraph,
+ workspace: options.workspace
+ });
+
await Promise.all(referencedLibraries.map(async (libName) => {
- await projectProcessor.addProjectToGraph(libName, frameworkGraph);
+ await projectProcessor.addProjectToGraph(libName);
}));
log.verbose("Joining framework graph into project graph...");
diff --git a/lib/graph/projectGraphBuilder.js b/lib/graph/projectGraphBuilder.js
index 713e2fa43..0277ca1d1 100644
--- a/lib/graph/projectGraphBuilder.js
+++ b/lib/graph/projectGraphBuilder.js
@@ -83,7 +83,8 @@ function validateNode(node) {
* @public
* @function
* @name @ui5/project/graph/ProjectGraphBuilder~NodeProvider#getDependencies
- * @param {Node} The root node of the dependency graph
+ * @param {Node} node The root node of the dependency graph
+ * @param {@ui5/project/graph/Workspace} [workspace] workspace instance to use for overriding node resolution
* @returns {Node[]} Array of nodes which are direct dependencies of the given node
*/
@@ -95,9 +96,12 @@ function validateNode(node) {
* @function default
* @static
* @param {@ui5/project/graph/ProjectGraphBuilder~NodeProvider} nodeProvider
+ * Node provider instance to use for building the graph
+ * @param {@ui5/project/graph/Workspace} [workspace]
+ * Optional workspace instance to use for overriding project resolutions
* @returns {@ui5/project/graph/ProjectGraph} A new project graph instance
*/
-async function projectGraphBuilder(nodeProvider) {
+async function projectGraphBuilder(nodeProvider, workspace) {
const shimCollection = new ShimCollection();
const moduleCollection = Object.create(null);
@@ -141,7 +145,7 @@ async function projectGraphBuilder(nodeProvider) {
const queue = [];
- const rootDependencies = await nodeProvider.getDependencies(rootNode);
+ const rootDependencies = await nodeProvider.getDependencies(rootNode, workspace);
if (rootDependencies && rootDependencies.length) {
queue.push({
@@ -174,7 +178,6 @@ async function projectGraphBuilder(nodeProvider) {
}
const {project, extensions} = await ui5Module.getSpecifications();
-
return {
node,
project,
diff --git a/lib/graph/providers/NodePackageDependencies.js b/lib/graph/providers/NodePackageDependencies.js
index ad57d0804..ecf79c5ef 100644
--- a/lib/graph/providers/NodePackageDependencies.js
+++ b/lib/graph/providers/NodePackageDependencies.js
@@ -34,8 +34,6 @@ class NodePackageDependencies {
this._cwd = cwd;
this._rootConfiguration = rootConfiguration;
this._rootConfigPath = rootConfigPath;
-
- // this._nodes = {};
}
async getRootNode() {
@@ -49,9 +47,6 @@ class NodePackageDependencies {
`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,
@@ -62,19 +57,32 @@ class NodePackageDependencies {
};
}
- async getDependencies(node) {
+ async getDependencies(node, workspace) {
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);
+ const modulePath = await this._resolveModulePath(node.path, name, workspace);
return this._getNode(modulePath, optional);
}));
}
- async _resolveModulePath(baseDir, moduleName) {
+ async _resolveModulePath(baseDir, moduleName, workspace) {
log.verbose(`Resolving module path for '${moduleName}'...`);
+
+ if (workspace) {
+ // Check whether node can be resolved via the provided Workspace instance
+ // If yes, replace the node from NodeProvider with the one from Workspace
+ const workspaceNode = await workspace.getModuleByNodeId(moduleName);
+ if (workspaceNode) {
+ log.info(`Resolved module ${moduleName} via ${workspace.getName()} workspace ` +
+ `to version ${workspaceNode.getVersion()}`);
+ log.verbose(` Resolved module ${moduleName} to path ${workspaceNode.getPath()}`);
+ return workspaceNode.getPath();
+ }
+ }
+
try {
let packageJsonPath = await resolveModulePath(moduleName + "/package.json", {
basedir: baseDir,
diff --git a/package-lock.json b/package-lock.json
index fb51aec6b..406a0fab9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"ajv-errors": "^1.0.1",
"chalk": "^5.2.0",
"escape-string-regexp": "^5.0.0",
+ "globby": "^13.1.2",
"graceful-fs": "^4.2.10",
"js-yaml": "^4.1.0",
"libnpmconfig": "^1.2.1",
diff --git a/package.json b/package.json
index d1b28f06c..86b3b54ba 100644
--- a/package.json
+++ b/package.json
@@ -123,6 +123,7 @@
"ajv-errors": "^1.0.1",
"chalk": "^5.2.0",
"escape-string-regexp": "^5.0.0",
+ "globby": "^13.1.2",
"graceful-fs": "^4.2.10",
"js-yaml": "^4.1.0",
"libnpmconfig": "^1.2.1",
diff --git a/test/fixtures/application.a/ui5-test-corrupt.yaml b/test/fixtures/application.a/ui5-test-corrupt.yaml
new file mode 100644
index 000000000..ecce9d7e7
--- /dev/null
+++ b/test/fixtures/application.a/ui5-test-corrupt.yaml
@@ -0,0 +1 @@
+|-\nfoo\nbar
diff --git a/test/fixtures/application.a/ui5-test-empty.yaml b/test/fixtures/application.a/ui5-test-empty.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/collection.b/library.a/package.json b/test/fixtures/collection.b/library.a/package.json
new file mode 100644
index 000000000..aec498f72
--- /dev/null
+++ b/test/fixtures/collection.b/library.a/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "library.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/collection.b/library.a/src/library/a/.library b/test/fixtures/collection.b/library.a/src/library/a/.library
new file mode 100644
index 000000000..ef0ea1065
--- /dev/null
+++ b/test/fixtures/collection.b/library.a/src/library/a/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.a
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library A
+
+
+
+ library.d
+
+
+
+
diff --git a/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less b/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less
new file mode 100644
index 000000000..ff0f1d5e3
--- /dev/null
+++ b/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less
@@ -0,0 +1,6 @@
+@libraryAColor1: lightgoldenrodyellow;
+
+.library-a-foo {
+ color: @libraryAColor1;
+ padding: 1px 2px 3px 4px;
+}
diff --git a/test/fixtures/collection.b/library.a/test/library/a/Test.html b/test/fixtures/collection.b/library.a/test/library/a/Test.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/collection.b/library.a/ui5.yaml b/test/fixtures/collection.b/library.a/ui5.yaml
new file mode 100644
index 000000000..8d4784313
--- /dev/null
+++ b/test/fixtures/collection.b/library.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.a
diff --git a/test/fixtures/collection.b/library.b/package.json b/test/fixtures/collection.b/library.b/package.json
new file mode 100644
index 000000000..2a0243b16
--- /dev/null
+++ b/test/fixtures/collection.b/library.b/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "library.b",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/collection.b/library.b/src/library/b/.library b/test/fixtures/collection.b/library.b/src/library/b/.library
new file mode 100644
index 000000000..7128151f3
--- /dev/null
+++ b/test/fixtures/collection.b/library.b/src/library/b/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.b
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library B
+
+
+
+ library.d
+
+
+
+
diff --git a/test/fixtures/collection.b/library.b/test/library/b/Test.html b/test/fixtures/collection.b/library.b/test/library/b/Test.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/collection.b/library.b/ui5.yaml b/test/fixtures/collection.b/library.b/ui5.yaml
new file mode 100644
index 000000000..b2fe5be59
--- /dev/null
+++ b/test/fixtures/collection.b/library.b/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.b
diff --git a/test/fixtures/collection.b/library.c/package.json b/test/fixtures/collection.b/library.c/package.json
new file mode 100644
index 000000000..64ac75d6f
--- /dev/null
+++ b/test/fixtures/collection.b/library.c/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "library.c",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/collection.b/library.c/src/library/c/.library b/test/fixtures/collection.b/library.c/src/library/c/.library
new file mode 100644
index 000000000..4180ce2af
--- /dev/null
+++ b/test/fixtures/collection.b/library.c/src/library/c/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.c
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library C
+
+
+
+ library.d
+
+
+
+
diff --git a/test/fixtures/collection.b/library.c/test/LibraryC/Test.html b/test/fixtures/collection.b/library.c/test/LibraryC/Test.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/collection.b/library.c/ui5.yaml b/test/fixtures/collection.b/library.c/ui5.yaml
new file mode 100644
index 000000000..7c5e38a7f
--- /dev/null
+++ b/test/fixtures/collection.b/library.c/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.c
diff --git a/test/fixtures/collection.b/package.json b/test/fixtures/collection.b/package.json
new file mode 100644
index 000000000..25e37da04
--- /dev/null
+++ b/test/fixtures/collection.b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "collection",
+ "version": "1.0.0",
+ "description": "Simple Collection used for Workspace testing",
+ "dependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "ui5": {
+ "workspaces": [
+ "library.{a,b}",
+ "*c",
+ "sub-*"
+ ]
+ }
+}
diff --git a/test/fixtures/collection.b/sub-collection/package.json b/test/fixtures/collection.b/sub-collection/package.json
new file mode 100644
index 000000000..046177300
--- /dev/null
+++ b/test/fixtures/collection.b/sub-collection/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "sub-collection",
+ "version": "1.0.0",
+ "description": "Sub-Collection package",
+ "ui5": {
+ "workspaces": [
+ "../../library.d"
+ ]
+ }
+}
diff --git a/test/fixtures/collection.b/test.js b/test/fixtures/collection.b/test.js
new file mode 100644
index 000000000..d063db1e7
--- /dev/null
+++ b/test/fixtures/collection.b/test.js
@@ -0,0 +1,4 @@
+import {globby} from 'globby';
+
+const paths = await globby(["library.a"]);
+console.log("paths")
diff --git a/test/fixtures/collection/package.json b/test/fixtures/collection/package.json
index 81b948438..24849dbe4 100644
--- a/test/fixtures/collection/package.json
+++ b/test/fixtures/collection/package.json
@@ -8,11 +8,9 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
- "collection": {
- "modules": {
- "library.a": "./library.a",
- "library.b": "./library.b",
- "library.c": "./library.c"
- }
- }
+ "workspaces": [
+ "library.a",
+ "library.b",
+ "library.c"
+ ]
}
diff --git a/test/fixtures/collection/test.js b/test/fixtures/collection/test.js
new file mode 100644
index 000000000..d063db1e7
--- /dev/null
+++ b/test/fixtures/collection/test.js
@@ -0,0 +1,4 @@
+import {globby} from 'globby';
+
+const paths = await globby(["library.a"]);
+console.log("paths")
diff --git a/test/fixtures/extension.a/package.json b/test/fixtures/extension.a/package.json
index c1f690b16..c3f37004f 100644
--- a/test/fixtures/extension.a/package.json
+++ b/test/fixtures/extension.a/package.json
@@ -1,3 +1,4 @@
{
- "name": "extension.a"
+ "name": "extension.a",
+ "version": "1.0.0"
}
diff --git a/test/fixtures/extension.a/ui5.yaml b/test/fixtures/extension.a/ui5.yaml
new file mode 100644
index 000000000..1ae473d86
--- /dev/null
+++ b/test/fixtures/extension.a/ui5.yaml
@@ -0,0 +1,8 @@
+---
+specVersion: "2.0"
+kind: extension
+type: task
+metadata:
+ name: extension.a-ui5-yaml
+task:
+ path: lib/extensionModule.js
diff --git a/test/fixtures/library.d-adtl-deps/main/src/library/d/.library b/test/fixtures/library.d-adtl-deps/main/src/library/d/.library
new file mode 100644
index 000000000..53c2d14c9
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/main/src/library/d/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.d
+ SAP SE
+ Some fancy copyright
+ ${version}
+
+ Library D
+
+
diff --git a/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js b/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js
new file mode 100644
index 000000000..719155d1e
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
diff --git a/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html b/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json b/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json
new file mode 100644
index 000000000..d8f009d42
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library b/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library
new file mode 100644
index 000000000..c45172d48
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.f
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library F
+
+
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js b/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js
new file mode 100644
index 000000000..81e734360
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/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.d-adtl-deps/node_modules/library.f/ui5.yaml b/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml
new file mode 100644
index 000000000..52c17922b
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json b/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json
new file mode 100644
index 000000000..fff39011d
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.g",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "devDependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library b/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library
new file mode 100644
index 000000000..4d884278e
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.g
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library G
+
+
diff --git a/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js b/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js
new file mode 100644
index 000000000..81e734360
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/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.d-adtl-deps/node_modules/library.g/ui5.yaml b/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml
new file mode 100644
index 000000000..a20d2d499
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.g
diff --git a/test/fixtures/library.d-adtl-deps/package.json b/test/fixtures/library.d-adtl-deps/package.json
new file mode 100644
index 000000000..10cd27bde
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.d",
+ "version": "2.0.0",
+ "description": "Version of library.d that has additional dependencies defined. Used for testing UI5 Workspace resolutions",
+ "dependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/test/fixtures/library.d-adtl-deps/ui5.yaml b/test/fixtures/library.d-adtl-deps/ui5.yaml
new file mode 100644
index 000000000..9c8d48e11
--- /dev/null
+++ b/test/fixtures/library.d-adtl-deps/ui5.yaml
@@ -0,0 +1,9 @@
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d
+resources:
+ configuration:
+ paths:
+ src: main/src
+ test: main/test
diff --git a/test/fixtures/library.e/ui5-workspace.yaml b/test/fixtures/library.e/ui5-workspace.yaml
new file mode 100644
index 000000000..7e8fd36b8
--- /dev/null
+++ b/test/fixtures/library.e/ui5-workspace.yaml
@@ -0,0 +1,13 @@
+specVersion: workspace/1.0
+metadata:
+ name: config-a
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: config-b
+dependencyManagement:
+ resolutions:
+ - path: ../library.x
diff --git a/test/fixtures/library.h/corrupt-ui5-workspace.yaml b/test/fixtures/library.h/corrupt-ui5-workspace.yaml
new file mode 100644
index 000000000..ecce9d7e7
--- /dev/null
+++ b/test/fixtures/library.h/corrupt-ui5-workspace.yaml
@@ -0,0 +1 @@
+|-\nfoo\nbar
diff --git a/test/fixtures/library.h/custom-ui5-workspace.yaml b/test/fixtures/library.h/custom-ui5-workspace.yaml
new file mode 100644
index 000000000..177dea5f6
--- /dev/null
+++ b/test/fixtures/library.h/custom-ui5-workspace.yaml
@@ -0,0 +1,15 @@
+specVersion: workspace/1.0
+metadata:
+ name: library-d
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: all-libraries
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+ - path: ../library.e
+ - path: ../library.f
diff --git a/test/fixtures/library.h/empty-ui5-workspace.yaml b/test/fixtures/library.h/empty-ui5-workspace.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/library.h/invalid-ui5-workspace.yaml b/test/fixtures/library.h/invalid-ui5-workspace.yaml
new file mode 100644
index 000000000..ca4ad00f4
--- /dev/null
+++ b/test/fixtures/library.h/invalid-ui5-workspace.yaml
@@ -0,0 +1,6 @@
+specVersion: wörkspace/1.0
+metadata:
+ name: default
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
diff --git a/test/fixtures/library.h/ui5-workspace.yaml b/test/fixtures/library.h/ui5-workspace.yaml
new file mode 100644
index 000000000..ac0ea1c35
--- /dev/null
+++ b/test/fixtures/library.h/ui5-workspace.yaml
@@ -0,0 +1,15 @@
+specVersion: workspace/1.0
+metadata:
+ name: default
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: all-libraries
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+ - path: ../library.e
+ - path: ../library.f
diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js
index b7c7ec8ed..b5ca1afe1 100644
--- a/test/lib/graph/Module.js
+++ b/test/lib/graph/Module.js
@@ -6,14 +6,15 @@ import Module from "../../../lib/graph/Module.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
+const applicationAPath = path.join(fixturesPath, "application.a");
const buildDescriptionApplicationAPath =
- path.join(__dirname, "..", "..", "fixtures", "build-manifest", "application.a");
+ path.join(fixturesPath, "build-manifest", "application.a");
const buildDescriptionLibraryAPath =
- path.join(__dirname, "..", "..", "fixtures", "build-manifest", "library.e");
-const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h");
-const collectionPath = path.join(__dirname, "..", "..", "fixtures", "collection");
-const themeLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "theme.library.e");
+ path.join(fixturesPath, "build-manifest", "library.e");
+const applicationHPath = path.join(fixturesPath, "application.h");
+const collectionPath = path.join(fixturesPath, "collection");
+const themeLibraryEPath = path.join(fixturesPath, "theme.library.e");
const basicModuleInput = {
id: "application.a.id",
@@ -379,6 +380,49 @@ test("Invalid configuration in file", async (t) => {
t.truthy(err.yaml, "Error object contains yaml information");
});
+test("Corrupt configuration in file", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-corrupt.yaml"
+ });
+ const err = await t.throwsAsync(ui5Module.getSpecifications());
+
+ t.regex(err.message,
+ new RegExp("^Failed to parse configuration for project application.a.id at 'ui5-test-corrupt.yaml'.*"),
+ "Threw with parsing error");
+});
+
+test("Empty configuration in file", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-empty.yaml"
+ });
+ const res = await ui5Module.getSpecifications();
+
+ t.deepEqual(res, {
+ project: null,
+ extensions: []
+ }, "Returned no project or extensions");
+});
+
+test("No configuration", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: fixturesPath, // does not contain a ui5.yaml
+ });
+ const res = await ui5Module.getSpecifications();
+
+ t.deepEqual(res, {
+ project: null,
+ extensions: []
+ }, "Returned no project or extensions");
+});
+
test("Incorrect config path", async (t) => {
const ui5Module = new Module({
id: "application.a.id",
diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js
index fdbbe1f41..ea0a3e198 100644
--- a/test/lib/graph/ProjectGraph.js
+++ b/test/lib/graph/ProjectGraph.js
@@ -364,6 +364,20 @@ test("getTransitiveDependencies", async (t) => {
], "Should store and return correct transitive dependencies for library.a");
});
+test("getTransitiveDependencies: Unknown project", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+
+ const error = t.throws(() => {
+ graph.getTransitiveDependencies("library.x");
+ });
+ t.is(error.message,
+ "Failed to get transitive dependencies for project library.x: Unable to find project in project graph",
+ "Should throw with expected error message");
+});
+
test("declareDependency: Unknown source", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
@@ -485,7 +499,6 @@ test("declareDependency: Already declared as optional, now non-optional", async
"Should declare dependency as non-optional");
});
-
test("getDependencies: Project without dependencies", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
@@ -775,7 +788,7 @@ test("traverseBreadthFirst: Detect cycle", async (t) => {
const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {}));
t.is(error.message,
- "Detected cyclic dependency chain: library.a* -> library.b -> library.a*",
+ "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*",
"Should throw with expected error message");
});
@@ -1000,7 +1013,7 @@ test("traverseDepthFirst: Detect cycle", async (t) => {
const error = await t.throwsAsync(graph.traverseDepthFirst(() => {}));
t.is(error.message,
- "Detected cyclic dependency chain: library.a* -> library.b -> library.a*",
+ "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*",
"Should throw with expected error message");
});
@@ -1020,7 +1033,7 @@ test("traverseDepthFirst: Cycle which does not occur in BFS", async (t) => {
const error = await t.throwsAsync(graph.traverseDepthFirst(() => {}));
t.is(error.message,
- "Detected cyclic dependency chain: library.a -> library.b* -> library.c -> library.b*",
+ "Detected cyclic dependency chain: library.a -> *library.b* -> library.c -> *library.b*",
"Should throw with expected error message");
});
diff --git a/test/lib/graph/Workspace.js b/test/lib/graph/Workspace.js
new file mode 100644
index 000000000..af79ca3d4
--- /dev/null
+++ b/test/lib/graph/Workspace.js
@@ -0,0 +1,473 @@
+import path from "node:path";
+import {fileURLToPath} from "node:url";
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import Module from "../../../lib/graph/Module.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const libraryD = path.join(__dirname, "..", "..", "fixtures", "library.d");
+const libraryE = path.join(__dirname, "..", "..", "fixtures", "library.e");
+const collectionLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a");
+const collectionLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b");
+const collectionLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection", "library.c");
+const collectionBLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.a");
+const collectionBLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.b");
+const collectionBLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.c");
+
+function createWorkspaceConfig({dependencyManagement}) {
+ return {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "workspace-name"
+ },
+ dependencyManagement
+ };
+}
+
+test.beforeEach(async (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
+ };
+
+ t.context.Workspace = await esmock("../../../lib/graph/Workspace.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub().withArgs("graph:Workspace").returns(t.context.log)
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.ProjectGraph);
+});
+
+test("Basic resolution", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d", "library.e"], "Correct project name keys");
+
+ const libE = projectNameMap.get("library.e");
+ t.true(libE instanceof Module, "library.e value is instance of Module");
+ t.is(libE.getVersion(), "1.0.0", "Correct version for library.e");
+ t.is(libE.getPath(), libraryE, "Correct path for library.e");
+
+ const libD = projectNameMap.get("library.d");
+ t.true(libD instanceof Module, "library.d value is instance of Module");
+ t.is(libD.getVersion(), "1.0.0", "Correct version for library.d");
+ t.is(libD.getPath(), libraryD, "Correct path for library.d");
+
+ t.is(await workspace.getModuleByProjectName("library.d"), libD,
+ "getModuleByProjectName returns correct module for library.d");
+ t.is(await workspace.getModuleByNodeId("library.d"), libD,
+ "getModuleByNodeId returns correct module for library.d");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.d", "library.e"], "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Basic resolution: package.json is missing name field", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.context.sinon.stub(workspace, "_readPackageJson")
+ .resolves({
+ version: "1.0.0",
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.is(err.message,
+ `Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`,
+ "Threw with expected error message");
+});
+
+test("Basic resolution: package.json is missing version field", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.context.sinon.stub(workspace, "_readPackageJson")
+ .resolves({
+ name: "Package",
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.is(err.message,
+ `Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`,
+ "Threw with expected error message");
+});
+
+test("Package workspace resolution: Static patterns", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/collection"
+ }]
+ }
+ })
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c"],
+ "Correct project name keys");
+
+ const libA = projectNameMap.get("library.a");
+ t.true(libA instanceof Module, "library.a value is instance of Module");
+ t.is(libA.getVersion(), "1.0.0", "Correct version for library.a");
+ t.is(libA.getPath(), collectionLibraryA, "Correct path for library.a");
+
+ const libB = projectNameMap.get("library.b");
+ t.true(libB instanceof Module, "library.b value is instance of Module");
+ t.is(libB.getVersion(), "1.0.0", "Correct version for library.b");
+ t.is(libB.getPath(), collectionLibraryB, "Correct path for library.b");
+
+ const libC = projectNameMap.get("library.c");
+ t.true(libC instanceof Module, "library.c value is instance of Module");
+ t.is(libC.getVersion(), "1.0.0", "Correct version for library.c");
+ t.is(libC.getPath(), collectionLibraryC, "Correct path for library.c");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c"],
+ "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Package workspace resolution: Dynamic patterns", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/collection.b"
+ }]
+ }
+ })
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"],
+ "Correct project name keys");
+
+ const libA = projectNameMap.get("library.a");
+ t.true(libA instanceof Module, "library.a value is instance of Module");
+ t.is(libA.getVersion(), "1.0.0", "Correct version for library.a");
+ t.is(libA.getPath(), collectionBLibraryA, "Correct path for library.a");
+
+ const libB = projectNameMap.get("library.b");
+ t.true(libB instanceof Module, "library.b value is instance of Module");
+ t.is(libB.getVersion(), "1.0.0", "Correct version for library.b");
+ t.is(libB.getPath(), collectionBLibraryB, "Correct path for library.b");
+
+ const libC = projectNameMap.get("library.c");
+ t.true(libC instanceof Module, "library.c value is instance of Module");
+ t.is(libC.getVersion(), "1.0.0", "Correct version for library.c");
+ t.is(libC.getPath(), collectionBLibraryC, "Correct path for library.c");
+
+ const libD = projectNameMap.get("library.d");
+ t.true(libD instanceof Module, "library.d value is instance of Module");
+ t.is(libD.getVersion(), "1.0.0", "Correct version for library.d");
+ t.is(libD.getPath(), libraryD, "Correct path for library.d");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"],
+ "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Package workspace resolution: Nested workspaces", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.xyz"
+ }]
+ }
+ })
+ });
+
+ const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson")
+ .rejects(new Error("Test does not provide for more package mocks"))
+ .onCall(0).resolves({
+ name: "First Package",
+ version: "1.0.0",
+ ui5: {
+ workspaces: [
+ "workspace-a",
+ "workspace-b"
+ ]
+ }
+ }).onCall(1).resolves({
+ name: "Second Package",
+ version: "1.0.0",
+ workspaces: [
+ "workspace-c",
+ "workspace-d"
+ ]
+ }).onCall(2).resolves({
+ name: "Third Package",
+ version: "1.0.0"
+ }).onCall(3).resolves({
+ name: "Fourth Package",
+ version: "1.0.0",
+ }).onCall(4).resolves({
+ name: "Fifth Package",
+ version: "1.0.0",
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ // All workspaces. Should not resolve to any module
+ t.is(readPackageJsonStub.callCount, 5, "readPackageJson got called five times");
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Package workspace resolution: Recursive workspaces", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.xyz"
+ }]
+ }
+ })
+ });
+
+ const basePath = path.join(__dirname, "../../fixtures/library.xyz");
+ const workspaceAPath = path.join(basePath, "workspace-a");
+
+ const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson");
+ readPackageJsonStub.withArgs(basePath).resolves({
+ name: "Base Package",
+ version: "1.0.0",
+ workspaces: [
+ "workspace-a"
+ ]
+ });
+ readPackageJsonStub.withArgs(workspaceAPath).resolves({
+ name: "Workspace A Package",
+ version: "1.0.0",
+ workspaces: [
+ ".."
+ ]
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ // All workspaces. Should not resolve to any module
+ // Recursive workspace definition should not lead to another readPackageJson call
+ t.is(readPackageJsonStub.callCount, 2, "readPackageJson got called two times");
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("No resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {}
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+
+ t.falsy(await workspace.getModuleByProjectName("library.e"),
+ "getModuleByProjectName yields no result for library.e");
+ t.falsy(await workspace.getModuleByNodeId("library.e"),
+ "getModuleByNodeId yields no result for library.e");
+});
+
+test("Empty dependencyManagement configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {}
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Empty resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: []
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Missing path in resolution", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{}]
+ }
+ })
+ });
+
+ await t.throwsAsync(workspace._getResolvedModules(), {
+ message: "Missing property 'path' in dependency resolution configuration of workspace workspace-name"
+ }, "Threw with expected error message");
+});
+
+test("Invalid specVersion", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: {
+ specVersion: "project/1.0",
+ metadata: {
+ name: "workspace-name"
+ }
+ }
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.true(
+ err.message.includes(`Unsupported "specVersion"`),
+ "Threw with expected error message");
+});
+
+test("Invalid resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/does-not-exist"
+ }]
+ }
+ })
+ });
+
+ const absPath = path.join(__dirname, "../../fixtures/does-not-exist");
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.true(
+ err.message.startsWith(`Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/does-not-exist to ${absPath}: ENOENT:`),
+ "Threw with expected error message");
+});
+
+test("Resolves extension only", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/extension.a"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 1, "Added one entry to Module ID to module map");
+ t.deepEqual(Array.from(moduleIdMap.keys()), ["extension.a"],
+ "Expected entry in Module ID to module map");
+});
+
+test("Resolution does not lead to a project", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ // Using a directory with a package.json but no ui5.yaml
+ path: "../../fixtures/init-library"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Missing parameters", (t) => {
+ t.throws(() => {
+ new t.context.Workspace({
+ configuration: {metadata: {name: "config-a"}}
+ });
+ }, {
+ message: "Could not create Workspace: Missing or empty parameter 'cwd'"
+ }, "Threw with expected error message");
+
+ t.throws(() => {
+ new t.context.Workspace({
+ cwd: "cwd"
+ });
+ }, {
+ message: "Could not create Workspace: Missing or empty parameter 'configuration'"
+ }, "Threw with expected error message");
+});
diff --git a/test/lib/graph/graph.integration.js b/test/lib/graph/graph.integration.js
new file mode 100644
index 000000000..4f27781e9
--- /dev/null
+++ b/test/lib/graph/graph.integration.js
@@ -0,0 +1,281 @@
+import test from "ava";
+import {fileURLToPath} from "node:url";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import Workspace from "../../../lib/graph/Workspace.js";
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
+const libraryHPath = path.join(fixturesPath, "library.h");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.npmProviderConstructorStub = sinon.stub();
+ class MockNpmProvider {
+ constructor(params) {
+ t.context.npmProviderConstructorStub(params);
+ }
+ }
+
+ t.context.MockNpmProvider = MockNpmProvider;
+
+ t.context.projectGraphBuilderStub = sinon.stub().resolves("graph");
+ t.context.enrichProjectGraphStub = sinon.stub();
+ t.context.graph = await esmock.p("../../../lib/graph/graph.js", {
+ "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider,
+ "../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub,
+ "../../../lib/graph/helpers/ui5Framework.js": {
+ enrichProjectGraph: t.context.enrichProjectGraphStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.graph);
+});
+
+test.serial("graphFromPackageDependencies with workspace object", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "dolphin"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace object not matching workspaceName", async (t) => {
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ await t.throwsAsync(graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "other",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }), {
+ message: "The provided workspace name 'other' does not match the provided workspace configuration 'default'"
+ }, "Threw with expected error message");
+});
+
+test.serial("graphFromPackageDependencies with workspace file", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: libraryHPath,
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace file at custom path", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ workspaceConfigPath: path.join(libraryHPath, "ui5-workspace.yaml")
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with inactive workspace file at custom path", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ workspaceConfigPath: path.join(libraryHPath, "custom-ui5-workspace.yaml")
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], null,
+ "projectGraphBuilder got called with no workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: null
+ }, "enrichProjectGraph got called with correct options");
+});
diff --git a/test/lib/graph/graph.js b/test/lib/graph/graph.js
index 76a419f7a..e5bf55fad 100644
--- a/test/lib/graph/graph.js
+++ b/test/lib/graph/graph.js
@@ -3,19 +3,22 @@ import {fileURLToPath} from "node:url";
import path from "node:path";
import sinonGlobal from "sinon";
import esmock from "esmock";
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
test.beforeEach(async (t) => {
const sinon = t.context.sinon = sinonGlobal.createSandbox();
t.context.npmProviderConstructorStub = sinon.stub();
- class DummyNpmProvider {
+ class MockNpmProvider {
constructor(params) {
t.context.npmProviderConstructorStub(params);
}
}
+ t.context.createWorkspaceStub = sinon.stub().returns("workspace");
- t.context.DummyNpmProvider = DummyNpmProvider;
+ t.context.MockNpmProvider = MockNpmProvider;
t.context.dependencyTreeProviderStub = sinon.stub();
class DummyDependencyTreeProvider {
@@ -28,8 +31,9 @@ test.beforeEach(async (t) => {
t.context.projectGraphBuilderStub = sinon.stub().resolves("graph");
t.context.enrichProjectGraphStub = sinon.stub();
t.context.graph = await esmock.p("../../../lib/graph/graph.js", {
- "../../../lib/graph/providers/NodePackageDependencies.js": t.context.DummyNpmProvider,
+ "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider,
"../../../lib/graph/providers/DependencyTree.js": t.context.DummyDependencyTreeProvider,
+ "../../../lib/graph/helpers/createWorkspace.js": t.context.createWorkspaceStub,
"../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub,
"../../../lib/graph/helpers/ui5Framework.js": {
enrichProjectGraph: t.context.enrichProjectGraphStub
@@ -44,8 +48,8 @@ test.afterEach.always((t) => {
test.serial("graphFromPackageDependencies", async (t) => {
const {
- npmProviderConstructorStub,
- projectGraphBuilderStub, enrichProjectGraphStub, DummyNpmProvider
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
} = t.context;
const {graphFromPackageDependencies} = t.context.graph;
@@ -58,6 +62,54 @@ test.serial("graphFromPackageDependencies", async (t) => {
t.is(res, "graph");
+ t.is(createWorkspaceStub.callCount, 0, "createWorkspace did not get called");
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], undefined,
+ "projectGraphBuilder got called with an empty workspace");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: undefined
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromPackageDependencies with workspace name", async (t) => {
+ const {
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: undefined,
+ }, "createWorkspace called with correct parameters");
+
t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
cwd: path.join(__dirname, "..", "..", "..", "cwd"),
@@ -66,14 +118,144 @@ test.serial("graphFromPackageDependencies", async (t) => {
}, "Created NodePackageDependencies provider instance with correct parameters");
t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
- t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof DummyNpmProvider,
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
"projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], "workspace",
+ "projectGraphBuilder got called with correct workspace instance");
t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
"enrichProjectGraph got called with graph");
t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
- versionOverride: "versionOverride"
+ versionOverride: "versionOverride",
+ workspace: "workspace"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromPackageDependencies with workspace object", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceConfiguration: "workspaceConfiguration"
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ configPath: "ui5-workspace.yaml",
+ name: undefined,
+ configObject: "workspaceConfiguration"
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfiguration: "workspaceConfiguration"
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: "workspaceConfiguration"
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with workspace path and workspace name", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfigPath: "workspaceConfigurationPath"
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "workspaceConfigurationPath",
+ configObject: undefined
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with empty workspace", async (t) => {
+ const {
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ // Simulate no workspace config found
+ createWorkspaceStub.resolves(null);
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: undefined,
+ }, "createWorkspace called with correct parameters");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], null,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: null
}, "enrichProjectGraph got called with correct options");
});
@@ -208,3 +390,21 @@ test.serial("usingObject: Do not resolve framework dependencies", async (t) => {
t.is(res, "graph");
t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called");
});
+
+test.serial("utils: readDependencyConfigFile", async (t) => {
+ const {graphFromStaticFile} = t.context.graph;
+ const res = await graphFromStaticFile._utils.readDependencyConfigFile(
+ path.join(fixturesPath, "application.h"), "projectDependencies.yaml");
+
+ t.deepEqual(res, {
+ id: "static-application.a",
+ path: path.join(fixturesPath, "application.a"),
+ version: "0.0.1",
+ dependencies: [{
+ id: "static-library.e",
+ path: path.join(fixturesPath, "library.e"),
+ version: "0.0.1",
+ }],
+ }, "Returned correct file content");
+});
+
diff --git a/test/lib/graph/graphFromObject.js b/test/lib/graph/graphFromObject.js
index 7efa41072..0e4c90011 100644
--- a/test/lib/graph/graphFromObject.js
+++ b/test/lib/graph/graphFromObject.js
@@ -81,8 +81,8 @@ test("Application Cycle A: Traverse project graph breadth first with cycles", as
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*",
+ "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());
@@ -143,8 +143,8 @@ test("Application Cycle A: Traverse project graph depth first with cycles", asyn
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*",
+ "Detected cyclic dependency chain: *application.cycle.a* -> component.cycle.a " +
+ "-> *application.cycle.a*",
"Threw with expected error message");
});
@@ -157,8 +157,8 @@ test("Application Cycle B: Traverse project graph depth first with cycles", asyn
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*",
+ "Detected cyclic dependency chain: application.cycle.b -> *module.d* " +
+ "-> module.e -> *module.d*",
"Threw with expected error message");
});
diff --git a/test/lib/graph/helpers/createWorkspace.js b/test/lib/graph/helpers/createWorkspace.js
new file mode 100644
index 000000000..db8a43005
--- /dev/null
+++ b/test/lib/graph/helpers/createWorkspace.js
@@ -0,0 +1,296 @@
+import test from "ava";
+import {fileURLToPath} from "node:url";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures");
+const libraryHPath = path.join(fixturesPath, "library.h");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.workspaceConstructorStub = sinon.stub();
+ class MockWorkspace {
+ constructor(params) {
+ t.context.workspaceConstructorStub(params);
+ }
+ }
+ t.context.MockWorkspace = MockWorkspace;
+
+ t.context.createWorkspace = await esmock.p("../../../../lib/graph/helpers/createWorkspace", {
+ "../../../../lib/graph/Workspace.js": t.context.MockWorkspace
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("createWorkspace: Missing parameter 'configObject' or 'configPath'", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ }));
+ t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Missing parameter 'cwd'", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+ t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Missing parameter 'name' if 'configPath' is set", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+ t.is(err.message, "createWorkspace: Parameter 'configPath' implies parameter 'name', but it's empty",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Using object", async (t) => {
+ const {
+ workspaceConstructorStub,
+ MockWorkspace,
+ createWorkspace
+ } = t.context;
+
+ const res = await createWorkspace({
+ cwd: "cwd",
+ configObject: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: "cwd",
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using invalid object", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ configObject: {
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }));
+ t.is(err.message, "Invalid workspace configuration: Missing or empty property 'metadata.name'",
+ "Threw with validation error");
+});
+
+test("createWorkspace: Using file", async (t) => {
+ const {createWorkspace, MockWorkspace, workspaceConstructorStub} = t.context;
+
+ const res = await createWorkspace({
+ cwd: "cwd",
+ name: "default",
+ configPath: path.join(libraryHPath, "ui5-workspace.yaml")
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using invalid file", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ name: "default",
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+
+ t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error");
+});
+
+test("createWorkspace: Using missing file", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const res = await createWorkspace({
+ cwd: path.join(fixturesPath, "library.d"),
+ name: "default",
+ configPath: "ui5-workspace.yaml"
+ });
+
+ t.is(res, null, "Returned no workspace");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("createWorkspace: Using missing file and non-default name", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: path.join(fixturesPath, "library.d"),
+ name: "special",
+ configPath: "ui5-workspace.yaml"
+ }));
+
+ const filePath = path.join(fixturesPath, "library.d", "ui5-workspace.yaml");
+ t.true(err.message.startsWith(
+ `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("createWorkspace: Using non-default file and non-default name", async (t) => {
+ const {createWorkspace, workspaceConstructorStub, MockWorkspace} = t.context;
+
+ const res = await createWorkspace({
+ cwd: path.join(fixturesPath, "library.h"),
+ name: "library-d",
+ configPath: "custom-ui5-workspace.yaml"
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "library-d"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using non-default file and non-default name which is not in file", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: path.join(fixturesPath, "library.h"),
+ name: "special",
+ configPath: "custom-ui5-workspace.yaml"
+ }));
+
+ t.is(err.message, `Could not find a workspace named 'special' in custom-ui5-workspace.yaml`,
+ "Threw with expected error message");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("readWorkspaceConfigFile", async (t) => {
+ const {createWorkspace} = t.context;
+ const res = await createWorkspace._readWorkspaceConfigFile(
+ path.join(libraryHPath, "ui5-workspace.yaml"), false);
+ t.deepEqual(res,
+ [{
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default",
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d",
+ }]
+ },
+ }, {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "all-libraries",
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d",
+ }, {
+ path: "../library.e",
+ }, {
+ path: "../library.f",
+ }],
+ },
+ }], "Read workspace configuration file correctly");
+});
+
+test("readWorkspaceConfigFile: Throws for missing file", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(fixturesPath, "library.d", "other-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath));
+ t.true(err.message.startsWith(
+ `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message");
+});
+
+test("readWorkspaceConfigFile: Validation errors", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "invalid-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true));
+ t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error");
+});
+
+test("readWorkspaceConfigFile: Not a YAML", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "corrupt-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true));
+ t.true(err.message.includes(`Failed to parse workspace configuration at ${filePath}`),
+ "Threw with parsing error");
+});
+
+test("readWorkspaceConfigFile: Empty file", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "empty-ui5-workspace.yaml");
+ const res = await createWorkspace._readWorkspaceConfigFile(filePath, true);
+ t.deepEqual(res, [], "No workspace configuration returned");
+});
diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js
index 23c71028c..f40a6aae4 100644
--- a/test/lib/graph/helpers/ui5Framework.js
+++ b/test/lib/graph/helpers/ui5Framework.js
@@ -9,7 +9,9 @@ import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d");
const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e");
+const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f");
test.beforeEach(async (t) => {
const sinon = t.context.sinon = sinonGlobal.createSandbox();
@@ -48,7 +50,7 @@ test.afterEach.always((t) => {
esmock.purge(t.context.ui5Framework);
});
-test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => {
+test.serial("enrichProjectGraph", async (t) => {
const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context;
const dependencyTree = {
@@ -104,8 +106,14 @@ test.serial("ui5Framework translator should throw an error when framework versio
], "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");
+ const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0];
+ t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata,
+ "Correct libraryMetadata provided to ProjectProcessor");
+ t.is(projectProcessorConstructorArgs.graph._rootProjectName,
+ "fake-root-of-application.a-framework-dependency-graph",
+ "Correct graph provided to ProjectProcessor");
+ t.falsy(projectProcessorConstructorArgs.workspace,
+ "No workspace provided to ProjectProcessor");
t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times");
t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0],
@@ -127,7 +135,7 @@ test.serial("ui5Framework translator should throw an error when framework versio
], "Traversed graph in correct order");
});
-test.serial("enrichProjectGraph (with versionOverride)", async (t) => {
+test.serial("enrichProjectGraph: With versionOverride", async (t) => {
const {
sinon, ui5Framework, utils,
Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
@@ -417,6 +425,40 @@ test.serial("enrichProjectGraph should throw for framework project with dependen
"Threw with expected error message");
});
+test.serial("enrichProjectGraph should throw for incorrect framework name", async (t) => {
+ const {ui5Framework, sinon} = t.context;
+ const dependencyTree = {
+ id: "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);
+
+ sinon.stub(projectGraph.getRoot(), "getFrameworkName").returns("Pony5");
+ const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph));
+ t.is(err.message, `Unknown framework.name "Pony5" for project application.a. Must be "OpenUI5" or "SAPUI5"`,
+ "Threw with expected error message");
+});
+
test.serial("enrichProjectGraph should ignore root project without framework configuration", async (t) => {
const {ui5Framework} = t.context;
const dependencyTree = {
@@ -921,4 +963,486 @@ test.serial("utils.declareFrameworkDependenciesInGraph: No deprecation warnings
], `Root project has correct dependencies`);
});
-// TODO test: ProjectProcessor
+test.serial("ProjectProcessor: Add project to graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+});
+
+test.serial("ProjectProcessor: Add same project twice", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+});
+
+test.serial("ProjectProcessor: Project already in graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns("project"),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 0, "graph#addProject never got called");
+});
+
+test.serial("ProjectProcessor: Add project with dependencies to graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called twice");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryDProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryEProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with additional dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [], // Dependency to library.d is only declared in workspace-resolved library.e
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with additional, unknown dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.xyz"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "Unable to find dependency library.xyz, required by project library.e " +
+ "(resolved via workspace name workspace) " +
+ "in current set of libraries. Try adding it temporarily to the root project's dependencies"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with cyclic dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.e" // Cyclic dependency in workspace project
+ }])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " +
+ "library.e -> *library.d* -> *library.d*"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with distant cyclic dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.f"
+ }])
+ };
+ const libraryFProjectMock = {
+ getName: () => "library.f",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.e" // Cyclic dependency in workspace project
+ }])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ .onThirdCall().resolves({
+ project: libraryFProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: ["library.f"],
+ optionalDependencies: []
+ },
+ "library.f": {
+ id: "lib.f.id",
+ version: "1.0.0",
+ path: libraryFPath,
+ dependencies: [],
+ optionalDependencies: []
+ },
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " +
+ "library.e -> *library.d* -> library.f -> *library.d*"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Project missing in metadata", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "lib.x": {}
+ },
+ graph: graphMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("lib.a"), {
+ message: "Failed to find library lib.a in dist packages metadata.json"
+ }, "Threw with expected error message");
+});
diff --git a/test/lib/graph/projectGraphBuilder.js b/test/lib/graph/projectGraphBuilder.js
index 51489cbb9..df042ec74 100644
--- a/test/lib/graph/projectGraphBuilder.js
+++ b/test/lib/graph/projectGraphBuilder.js
@@ -249,6 +249,29 @@ test("Node depends on itself", async (t) => {
"Threw with expected error message");
});
+test("Cyclic dependencies", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies
+ .onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "project-2"
+ }),
+ ])
+ .onSecondCall().resolves([
+ createNode({
+ id: "id1",
+ name: "project-1"
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.deepEqual(graph.getDependencies("project-1"), ["project-2"], "Cyclic dependency has been added");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added");
+});
+
test("Nested node with same id is ignored", async (t) => {
t.context.getRootNode.resolves(createNode({
id: "id1",
@@ -263,7 +286,7 @@ test("Nested node with same id is ignored", async (t) => {
t.context.getDependencies.onSecondCall().resolves([
createNode({
id: "id1",
- name: "project-3"
+ name: "project-3" // name will be ignored, since the first "id1" node is being used
}),
]);
const graph = await projectGraphBuilder(t.context.provider);
diff --git a/test/lib/graph/providers/NodePackageDependencies.integration.js b/test/lib/graph/providers/NodePackageDependencies.integration.js
index 8aba7f989..842442cb1 100644
--- a/test/lib/graph/providers/NodePackageDependencies.integration.js
+++ b/test/lib/graph/providers/NodePackageDependencies.integration.js
@@ -14,6 +14,7 @@ const applicationFPath = path.join(__dirname, "..", "..", "..", "fixtures", "app
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 libraryDOverridePath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d-adtl-deps");
import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js";
import NodePackageDependenciesProvider from "../../../../lib/graph/providers/NodePackageDependencies.js";
@@ -27,18 +28,18 @@ test.afterEach.always((t) => {
});
function testGraphCreationBfs(...args) {
- return _testGraphCreation(...args, true);
+ return _testGraphCreation(true, ...args);
}
function testGraphCreationDfs(...args) {
- return _testGraphCreation(...args, false);
+ return _testGraphCreation(false, ...args);
}
-async function _testGraphCreation(t, npmProvider, expectedOrder, bfs) {
+async function _testGraphCreation(bfs, t, npmProvider, expectedOrder, workspace) {
if (bfs === undefined) {
throw new Error("Test error: Parameter 'bfs' must be specified");
}
- const projectGraph = await projectGraphBuilder(npmProvider);
+ const projectGraph = await projectGraphBuilder(npmProvider, workspace);
const callbackStub = t.context.sinon.stub().resolves();
if (bfs) {
await projectGraph.traverseBreadthFirst(callbackStub);
@@ -67,6 +68,37 @@ test("AppA: project with collection dependency", async (t) => {
]);
});
+test("AppA: project with workspace overrides", async (t) => {
+ const workspace = {
+ getName: () => "workspace name",
+ getModuleByNodeId: t.context.sinon.stub().resolves(undefined).onFirstCall().resolves({
+ // This version of library.d has an additional dependency to library.f,
+ // which in turn has a dependency to library.g
+ getPath: () => libraryDOverridePath,
+ getVersion: () => "1.0.0",
+ })
+ };
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationAPath
+ });
+ const graph = await testGraphCreationDfs(t, npmProvider, [
+ "library.g", // Added through workspace override of library.d
+ "library.a",
+ "library.b",
+ "library.c",
+ "library.f", // Added through workspace override of library.d
+ "library.d",
+ "application.a",
+ ], workspace);
+
+ t.is(workspace.getModuleByNodeId.callCount, 2, "Workspace#getModuleByNodeId got called twice");
+ t.is(workspace.getModuleByNodeId.getCall(0).args[0], "library.d",
+ "Workspace#getModuleByNodeId got called with correct argument on first call");
+ t.is(workspace.getModuleByNodeId.getCall(1).args[0], "collection",
+ "Workspace#getModuleByNodeId got called with correct argument on second call");
+ t.is(graph.getProject("library.d").getVersion(), "2.0.0", "Version from override is used");
+});
+
test("AppC: project with dependency with optional dependency resolved through root project", async (t) => {
const npmProvider = new NodePackageDependenciesProvider({
cwd: applicationCPath
@@ -188,7 +220,7 @@ test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => {
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*`);
+ `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) => {