diff --git a/lib/build/ProjectBuilder.js b/lib/build/ProjectBuilder.js
index 2fc76a9ba..8faba9cbb 100644
--- a/lib/build/ProjectBuilder.js
+++ b/lib/build/ProjectBuilder.js
@@ -162,7 +162,7 @@ class ProjectBuilder {
});
// Count total number of projects to build based on input
- const requestedProjects = this._graph.getAllProjects().map((p) => p.getName()).filter(function(projectName) {
+ const requestedProjects = this._graph.getProjectNames().filter(function(projectName) {
return filterProject(projectName);
});
@@ -264,18 +264,16 @@ class ProjectBuilder {
}
async _createRequiredBuildContexts(requestedProjects) {
- const allProjects = this._graph.getAllProjects();
- const requiredProjects = new Set(allProjects.filter((project) => {
- return requestedProjects.includes(project.getName());
+ const requiredProjects = new Set(this._graph.getProjectNames().filter((projectName) => {
+ return requestedProjects.includes(projectName);
}));
const projectBuildContexts = new Map();
- for (const project of requiredProjects) {
- const projectName = project.getName();
+ for (const projectName of requiredProjects) {
log.verbose(`Creating build context for project ${projectName}...`);
const projectBuildContext = this._buildContext.createProjectContext({
- project,
+ project: this._graph.getProject(projectName),
log
});
@@ -299,8 +297,7 @@ class ProjectBuilder {
return;
}
// Add dependency to list of projects to build
- const depProject = this._graph.getProject(depName);
- requiredProjects.add(depProject);
+ requiredProjects.add(depName);
});
}
}
@@ -322,7 +319,7 @@ class ProjectBuilder {
);
if (includedDependencies.length) {
- if (includedDependencies.length === this._graph.getAllProjects().length - 1) {
+ if (includedDependencies.length === this._graph.getSize() - 1) {
log.info(` Including all dependencies`);
} else {
log.info(` Requested dependencies:`);
diff --git a/lib/build/TaskRunner.js b/lib/build/TaskRunner.js
index aac5eeb8e..89409bf5d 100644
--- a/lib/build/TaskRunner.js
+++ b/lib/build/TaskRunner.js
@@ -450,7 +450,7 @@ class TaskRunner {
// Add transitive dependencies to set of required dependencies
const requiredDependencies = new Set(requiredDirectDependencies);
for (const projectName of requiredDirectDependencies) {
- this._graph.getAllDependencies(projectName).forEach((depName) => {
+ this._graph.getTransitiveDependencies(projectName).forEach((depName) => {
requiredDependencies.add(depName);
});
}
diff --git a/lib/build/helpers/composeProjectList.js b/lib/build/helpers/composeProjectList.js
index 77e6b1b8f..ae4db6dbb 100644
--- a/lib/build/helpers/composeProjectList.js
+++ b/lib/build/helpers/composeProjectList.js
@@ -13,14 +13,13 @@ async function getFlattenedDependencyTree(graph) {
const dependencyMap = Object.create(null);
const rootName = graph.getRoot().getName();
- await graph.traverseDepthFirst(({project, getDependencies}) => {
+ await graph.traverseDepthFirst(({project, dependencies}) => {
if (project.getName() === rootName) {
// Skip root project
return;
}
const projectDeps = [];
- getDependencies().forEach((dep) => {
- const depName = dep.getName();
+ dependencies.forEach((depName) => {
projectDeps.push(depName);
if (dependencyMap[depName]) {
projectDeps.push(...dependencyMap[depName]);
diff --git a/lib/graph/ProjectGraph.js b/lib/graph/ProjectGraph.js
index a6bb22462..18ddaf546 100644
--- a/lib/graph/ProjectGraph.js
+++ b/lib/graph/ProjectGraph.js
@@ -20,11 +20,11 @@ class ProjectGraph {
}
this._rootProjectName = rootProjectName;
- this._projects = Object.create(null); // maps project name to instance
- this._adjList = Object.create(null); // maps project name to edges/dependencies
- this._optAdjList = Object.create(null); // maps project name to optional dependencies
+ 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._extensions = Object.create(null); // maps extension name to instance
+ this._extensions = new Map(); // maps extension name to instance
this._sealed = false;
this._hasUnresolvedOptionalDependencies = false; // Performance optimization flag
@@ -38,7 +38,7 @@ class ProjectGraph {
* @returns {@ui5/project/specifications/Project} Root project
*/
getRoot() {
- const rootProject = this._projects[this._rootProjectName];
+ const rootProject = this._projects.get(this._rootProjectName);
if (!rootProject) {
throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`);
}
@@ -50,15 +50,11 @@ class ProjectGraph {
*
* @public
* @param {@ui5/project/specifications/Project} project Project which should be added to the graph
- * @param {boolean} [ignoreDuplicates=false] Whether an error should be thrown when a duplicate project is added
*/
- addProject(project, ignoreDuplicates) {
+ addProject(project) {
this._checkSealed();
const projectName = project.getName();
- if (this._projects[projectName]) {
- if (ignoreDuplicates) {
- return;
- }
+ if (this._projects.has(projectName)) {
throw new Error(
`Failed to add project ${projectName} to graph: A project with that name has already been added`);
}
@@ -69,9 +65,9 @@ class ProjectGraph {
`Failed to add project ${projectName} to graph: Project name must not be integer-like`);
}
log.verbose(`Adding project: ${projectName}`);
- this._projects[projectName] = project;
- this._adjList[projectName] = [];
- this._optAdjList[projectName] = [];
+ this._projects.set(projectName, project);
+ this._adjList.set(projectName, new Set());
+ this._optAdjList.set(projectName, new Set());
}
/**
@@ -83,17 +79,37 @@ class ProjectGraph {
* project instance or undefined if the project is unknown to the graph
*/
getProject(projectName) {
- return this._projects[projectName];
+ return this._projects.get(projectName);
}
/**
* Get all projects in the graph
*
* @public
- * @returns {@ui5/project/specifications/Project[]}
+ * @returns {Iterable.<@ui5/project/specifications/Project>}
+ */
+ getProjects() {
+ return this._projects.values();
+ }
+
+ /**
+ * Get names of all projects in the graph
+ *
+ * @public
+ * @returns {string[]} Names of all projects
+ */
+ getProjectNames() {
+ return Array.from(this._projects.keys());
+ }
+
+ /**
+ * Get the number of projects in the graph
+ *
+ * @public
+ * @returns {integer} Count of projects in the graph
*/
- getAllProjects() {
- return Object.values(this._projects);
+ getSize() {
+ return this._projects.size;
}
/**
@@ -105,7 +121,7 @@ class ProjectGraph {
addExtension(extension) {
this._checkSealed();
const extensionName = extension.getName();
- if (this._extensions[extensionName]) {
+ if (this._extensions.has(extensionName)) {
throw new Error(
`Failed to add extension ${extensionName} to graph: ` +
`An extension with that name has already been added`);
@@ -116,7 +132,7 @@ class ProjectGraph {
throw new Error(
`Failed to add extension ${extensionName} to graph: Extension name must not be integer-like`);
}
- this._extensions[extensionName] = extension;
+ this._extensions.set(extensionName, extension);
}
/**
@@ -126,17 +142,27 @@ class ProjectGraph {
* Extension instance or undefined if the extension is unknown to the graph
*/
getExtension(extensionName) {
- return this._extensions[extensionName];
+ return this._extensions.get(extensionName);
}
/**
* Get all extensions in the graph
*
* @public
- * @returns {@ui5/project/specifications/Extension[]}
+ * @returns {Iterable.<@ui5/project/specifications/Extension>}
+ */
+ getExtensions() {
+ return this._extensions.values();
+ }
+
+ /**
+ * Get names of all extensions in the graph
+ *
+ * @public
+ * @returns {string[]} Names of all extensions
*/
- getAllExtensions() {
- return Object.values(this._extensions);
+ getExtensionNames() {
+ return Array.from(this._extensions.keys());
}
/**
@@ -195,11 +221,11 @@ class ProjectGraph {
* @param {string} toProjectName Name of project on which the other depends
*/
_declareDependency(map, fromProjectName, toProjectName) {
- if (!this._projects[fromProjectName]) {
+ if (!this._projects.has(fromProjectName)) {
throw new Error(
`Unable to find depending project with name ${fromProjectName} in project graph`);
}
- if (!this._projects[toProjectName]) {
+ if (!this._projects.has(toProjectName)) {
throw new Error(
`Unable to find dependency project with name ${toProjectName} in project graph`);
}
@@ -207,10 +233,11 @@ class ProjectGraph {
throw new Error(
`A project can't depend on itself`);
}
- if (map[fromProjectName].includes(toProjectName)) {
+ const adjacencies = map.get(fromProjectName);
+ if (adjacencies.has(toProjectName)) {
log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`);
} else {
- map[fromProjectName].push(toProjectName);
+ adjacencies.add(toProjectName);
}
}
@@ -222,13 +249,13 @@ class ProjectGraph {
* @returns {string[]} Names of all direct dependencies
*/
getDependencies(projectName) {
- const adjacencies = this._adjList[projectName];
+ const adjacencies = this._adjList.get(projectName);
if (!adjacencies) {
throw new Error(
`Failed to get dependencies for project ${projectName}: ` +
`Unable to find project in project graph`);
}
- return adjacencies;
+ return Array.from(adjacencies);
}
/**
@@ -238,11 +265,11 @@ class ProjectGraph {
* @param {string} projectName Name of the project to retrieve the dependencies of
* @returns {string[]} Names of all direct and transitive dependencies
*/
- getAllDependencies(projectName) {
+ getTransitiveDependencies(projectName) {
const dependencies = new Set();
const processDependency = (depName) => {
- const adjacencies = this._adjList[depName];
+ const adjacencies = this._adjList.get(depName);
adjacencies.forEach((depName) => {
if (!dependencies.has(depName)) {
dependencies.add(depName);
@@ -264,18 +291,18 @@ class ProjectGraph {
* @returns {boolean} True if the dependency is currently optional
*/
isOptionalDependency(fromProjectName, toProjectName) {
- const adjacencies = this._adjList[fromProjectName];
+ const adjacencies = this._adjList.get(fromProjectName);
if (!adjacencies) {
throw new Error(
`Failed to determine whether dependency from ${fromProjectName} to ${toProjectName} ` +
`is optional: ` +
`Unable to find project with name ${fromProjectName} in project graph`);
}
- if (adjacencies.includes(toProjectName)) {
+ if (adjacencies.has(toProjectName)) {
return false;
}
- const optAdjacencies = this._optAdjList[fromProjectName];
- if (optAdjacencies.includes(toProjectName)) {
+ const optAdjacencies = this._optAdjList.get(fromProjectName);
+ if (optAdjacencies.has(toProjectName)) {
return true;
}
return false;
@@ -288,6 +315,7 @@ class ProjectGraph {
* @public
*/
async resolveOptionalDependencies() {
+ this._checkSealed();
if (!this._hasUnresolvedOptionalDependencies) {
log.verbose(`Skipping resolution of optional dependencies since none have been declared`);
return;
@@ -295,33 +323,32 @@ class ProjectGraph {
log.verbose(`Resolving optional dependencies...`);
// First collect all projects that are currently reachable from the root project (=all non-optional projects)
- const resolvedProjects = new Set;
+ const resolvedProjects = new Set();
await this.traverseBreadthFirst(({project}) => {
resolvedProjects.add(project.getName());
});
let unresolvedOptDeps = false;
- for (const [projectName, optDependencies] of Object.entries(this._optAdjList)) {
- for (let i = optDependencies.length - 1; i >= 0; i--) {
- const targetProjectName = optDependencies[i];
- if (resolvedProjects.has(targetProjectName)) {
+ for (const [fromProjectName, optDependencies] of this._optAdjList) {
+ for (const toProjectName of optDependencies) {
+ if (resolvedProjects.has(toProjectName)) {
// Target node is already reachable in the graph
// => Resolve optional dependency
- log.verbose(`Resolving optional dependency from ${projectName} to ${targetProjectName}...`);
+ log.verbose(`Resolving optional dependency from ${fromProjectName} to ${toProjectName}...`);
- if (this._adjList[targetProjectName].includes(projectName)) {
+ if (this._adjList.get(toProjectName).has(fromProjectName)) {
log.verbose(
- ` Cyclic optional dependency detected: ${targetProjectName} already has a non-optional ` +
- `dependency to ${projectName}`);
+ ` Cyclic optional dependency detected: ${toProjectName} already has a non-optional ` +
+ `dependency to ${fromProjectName}`);
log.verbose(
- ` Optional dependency from ${projectName} to ${targetProjectName} ` +
+ ` Optional dependency from ${fromProjectName} to ${toProjectName} ` +
`will not be declared as it would introduce a cycle`);
unresolvedOptDeps = true;
} else {
- this.declareDependency(projectName, targetProjectName);
+ this.declareDependency(fromProjectName, toProjectName);
// This optional dependency has now been resolved
// => Remove it from the list of optional dependencies
- optDependencies.splice(i, 1);
+ optDependencies.delete(toProjectName);
}
} else {
unresolvedOptDeps = true;
@@ -340,26 +367,14 @@ class ProjectGraph {
* @async
* @callback @ui5/project/graph/ProjectGraph~traversalCallback
* @param {object} parameters Parameters passed to the callback
- * @param {@ui5/project/specifications/Project} parameters.project The project that is currently visited
- * @param {@ui5/project/graph/ProjectGraph~getDependencies} parameters.getDependencies
- * Function to access the dependencies of the project that is currently visited.
- * @returns {Promise} Must return a promise on which the graph traversal will wait
- */
-
- /**
- * Helper function available in the
- * [traversalCallback]{@link @ui5/project/graph/ProjectGraph~traversalCallback} to access the
- * dependencies of the corresponding project in the current graph.
- *
- * Note that transitive dependencies can't be accessed this way. Projects should rather add a direct
- * dependency to projects they need access to.
- *
- * @public
- * @function @ui5/project/graph/ProjectGraph~getDependencies
- * @returns {Array.<@ui5/project/specifications/Project>} Direct dependencies of the visited project
+ * @param {@ui5/project/specifications/Project} parameters.project
+ * Project that is currently visited
+ * @param {string[]} parameters.dependencies
+ * Array containing the names of all direct dependencies of the project
+ * @returns {Promise|undefined} If a promise is returned,
+ * 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.
@@ -409,9 +424,7 @@ class ProjectGraph {
await callback({
project: this.getProject(projectName),
- getDependencies: () => {
- return dependencies.map(($) => this.getProject($));
- }
+ dependencies
});
})();
}));
@@ -437,7 +450,7 @@ class ProjectGraph {
if (!this.getProject(startName)) {
throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`);
}
- return this._traverseDepthFirst(startName, {}, [], callback);
+ return this._traverseDepthFirst(startName, Object.create(null), [], callback);
}
async _traverseDepthFirst(projectName, visited, predecessors, callback) {
@@ -455,9 +468,7 @@ class ProjectGraph {
await callback({
project: this.getProject(projectName),
- getDependencies: () => {
- return dependencies.map(($) => this.getProject($));
- }
+ dependencies
});
})();
}
@@ -480,8 +491,12 @@ class ProjectGraph {
projectGraph.seal();
}
mergeMap(this._projects, projectGraph._projects);
- mergeMap(this._adjList, projectGraph._adjList);
mergeMap(this._extensions, projectGraph._extensions);
+ mergeMap(this._adjList, projectGraph._adjList);
+ mergeMap(this._optAdjList, projectGraph._optAdjList);
+
+ this._hasUnresolvedOptionalDependencies =
+ this._hasUnresolvedOptionalDependencies || projectGraph._hasUnresolvedOptionalDependencies;
} catch (err) {
throw new Error(
`Failed to join project graph with root project ${projectGraph._rootProjectName} into ` +
@@ -608,11 +623,16 @@ class ProjectGraph {
}
function mergeMap(target, source) {
- for (const [key, value] of Object.entries(source)) {
- if (target[key]) {
+ for (const [key, value] of source) {
+ if (target.has(key)) {
throw new Error(`Failed to merge map: Key '${key}' already present in target set`);
}
- target[key] = value;
+ if (value instanceof Set) {
+ // Shallow-clone any Sets
+ target.set(key, new Set(value));
+ } else {
+ target.set(key, value);
+ }
}
}
diff --git a/test/lib/build/ProjectBuilder.js b/test/lib/build/ProjectBuilder.js
index d7b8f332b..05f249550 100644
--- a/test/lib/build/ProjectBuilder.js
+++ b/test/lib/build/ProjectBuilder.js
@@ -31,11 +31,12 @@ test.beforeEach(async (t) => {
};
},
isSealed: sinon.stub().returns(true),
- getAllProjects: sinon.stub().returns([
- getMockProject("library", "a"),
- getMockProject("library", "b"),
- getMockProject("library", "c"),
+ getProjectNames: sinon.stub().returns([
+ "project.a",
+ "project.b",
+ "project.c",
]),
+ getSize: sinon.stub().returns(3),
getDependencies: sinon.stub().returns([]).withArgs("project.a").returns(["project.b"]),
traverseBreadthFirst: async (start, callback) => {
if (callback) {
@@ -65,7 +66,9 @@ test.beforeEach(async (t) => {
project: getMockProject("library", "c")
});
},
- getProject: sinon.stub().returns(getMockProject("project", "b"))
+ getProject: sinon.stub().callsFake((projectName) => {
+ return getMockProject(...projectName.split("."));
+ })
};
t.context.ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js");
diff --git a/test/lib/build/TaskRunner.js b/test/lib/build/TaskRunner.js
index 04c89da1e..f0683227f 100644
--- a/test/lib/build/TaskRunner.js
+++ b/test/lib/build/TaskRunner.js
@@ -102,7 +102,7 @@ test.beforeEach(async (t) => {
},
getExtension: sinon.stub().returns(t.context.customTask),
traverseBreadthFirst: sinon.stub(),
- getAllDependencies: sinon.stub().returns(["dep.a", "dep.b", "dep.c"])
+ getTransitiveDependencies: sinon.stub().returns(["dep.a", "dep.b", "dep.c"])
};
t.context.logger = {
diff --git a/test/lib/graph/ProjectGraph.js b/test/lib/graph/ProjectGraph.js
index db79e47ba..fdbbe1f41 100644
--- a/test/lib/graph/ProjectGraph.js
+++ b/test/lib/graph/ProjectGraph.js
@@ -161,23 +161,6 @@ test("addProject: Add duplicate", async (t) => {
t.is(res, project1, "Should return correct project");
});
-test("addProject: Add duplicate with ignoreDuplicates", async (t) => {
- const {ProjectGraph} = t.context;
- const graph = new ProjectGraph({
- rootProjectName: "my root project"
- });
- const project1 = await createProject("application.a");
- graph.addProject(project1);
-
- const project2 = await createProject("application.a");
- t.notThrows(() => {
- graph.addProject(project2, true);
- }, "Should not throw when adding duplicates");
-
- const res = graph.getProject("application.a");
- t.is(res, project1, "Should return correct project");
-});
-
test("addProject: Add project with integer-like name", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
@@ -202,7 +185,7 @@ test("getProject: Project is not in graph", (t) => {
t.is(res, undefined, "Should return undefined");
});
-test("getAllProjects", async (t) => {
+test("getProjects", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "my root project"
@@ -213,10 +196,45 @@ test("getAllProjects", async (t) => {
const project2 = await createProject("application.b");
graph.addProject(project2);
- const res = graph.getAllProjects();
- t.deepEqual(res, [
+ const res = graph.getProjects();
+ t.deepEqual(Array.from(res), [
project1, project2
- ], "Should return all projects in a flat array");
+ ], "Should return an iterable for all projects");
+});
+
+test("getProjectNames", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.b");
+ graph.addProject(project2);
+
+ const res = graph.getProjectNames();
+ t.deepEqual(res, [
+ "application.a", "application.b"
+ ], "Should return all project names in a flat array");
+});
+
+test("getSize", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.b");
+ graph.addProject(project2);
+
+ // Extensions should not influence graph size
+ const extension1 = await createExtension("extension.a");
+ graph.addExtension(extension1);
+
+ t.is(graph.getSize(), 2, "Should return correct project count");
});
test("add-/getExtension", async (t) => {
@@ -274,7 +292,7 @@ test("getExtension: Project is not in graph", (t) => {
t.is(res, undefined, "Should return undefined");
});
-test("getAllExtensions", async (t) => {
+test("getExtensions", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "my root project"
@@ -284,10 +302,10 @@ test("getAllExtensions", async (t) => {
const extension2 = await createExtension("extension.b");
graph.addExtension(extension2);
- const res = graph.getAllExtensions();
- t.deepEqual(res, [
+ const res = graph.getExtensions();
+ t.deepEqual(Array.from(res), [
extension1, extension2
- ], "Should return all extensions in a flat array");
+ ], "Should return an iterable for all extensions");
});
test("declareDependency / getDependencies", async (t) => {
@@ -321,7 +339,7 @@ test("declareDependency / getDependencies", async (t) => {
"Should declare dependency as non-optional");
});
-test("getAllDependencies", async (t) => {
+test("getTransitiveDependencies", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "my root project"
@@ -338,7 +356,7 @@ test("getAllDependencies", async (t) => {
graph.declareDependency("library.a", "library.d");
graph.declareDependency("library.d", "library.e");
- t.deepEqual(graph.getAllDependencies("library.a"), [
+ t.deepEqual(graph.getTransitiveDependencies("library.a"), [
"library.b",
"library.c",
"library.d",
@@ -671,7 +689,7 @@ test("resolveOptionalDependencies: Resolves transitive optional dependencies", a
]);
});
-test("traverseBreadthFirst", async (t) => {
+test("traverseBreadthFirst: Async", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "library.a"
@@ -681,10 +699,47 @@ test("traverseBreadthFirst", async (t) => {
graph.declareDependency("library.a", "library.b");
- await traverseBreadthFirst(t, graph, [
+ const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise");
+ resolve();
+ }, 100);
+ });
+ });
+ await graph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
"library.a",
- "library.b"
- ]);
+ "library.b",
+ ], "Traversed graph in correct order, starting with library.a");
+});
+
+test("traverseBreadthFirst: Sync", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().returns();
+ await graph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.a",
+ "library.b",
+ ], "Traversed graph in correct order, starting with library.a");
});
test("traverseBreadthFirst: No project visited twice", async (t) => {
@@ -782,7 +837,7 @@ test("traverseBreadthFirst: Custom start node", async (t) => {
], "Traversed graph in correct order, starting with library.b");
});
-test("traverseBreadthFirst: getDependencies callback", async (t) => {
+test("traverseBreadthFirst: dependencies parameter", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "library.a"
@@ -802,9 +857,7 @@ test("traverseBreadthFirst: getDependencies callback", async (t) => {
const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
const dependencies = callbackStub.getCalls().map((call) => {
- return call.args[0].getDependencies().map((dep) => {
- return dep.getName();
- });
+ return call.args[0].dependencies;
});
t.deepEqual(callbackCalls, [
@@ -861,7 +914,7 @@ test("traverseBreadthFirst: Dependency declaration order is followed", async (t)
]);
});
-test("traverseDepthFirst", async (t) => {
+test("traverseDepthFirst: Async", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "library.a"
@@ -871,10 +924,47 @@ test("traverseDepthFirst", async (t) => {
graph.declareDependency("library.a", "library.b");
- await traverseDepthFirst(t, graph, [
+ const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise");
+ resolve();
+ }, 100);
+ });
+ });
+ await graph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
"library.b",
- "library.a"
- ]);
+ "library.a",
+ ], "Traversed graph in correct order, starting with library.b");
+});
+
+test("traverseDepthFirst: Sync", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().returns();
+ await graph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.b",
+ "library.a",
+ ], "Traversed graph in correct order, starting with library.b");
});
test("traverseDepthFirst: No project visited twice", async (t) => {
@@ -971,7 +1061,7 @@ test("traverseDepthFirst: Custom start node", async (t) => {
], "Traversed graph in correct order, starting with library.b");
});
-test("traverseDepthFirst: getDependencies callback", async (t) => {
+test("traverseDepthFirst: dependencies parameter", async (t) => {
const {ProjectGraph} = t.context;
const graph = new ProjectGraph({
rootProjectName: "library.a"
@@ -991,9 +1081,7 @@ test("traverseDepthFirst: getDependencies callback", async (t) => {
const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
const dependencies = callbackStub.getCalls().map((call) => {
- return call.args[0].getDependencies().map((dep) => {
- return dep.getName();
- });
+ return call.args[0].dependencies;
});
t.deepEqual(callbackCalls, [
@@ -1074,22 +1162,31 @@ test("join", async (t) => {
graph2.addProject(await createProject("theme.b"));
graph2.addProject(await createProject("theme.c"));
graph2.addProject(await createProject("theme.d"));
+ graph2.addProject(await createProject("theme.e"));
graph2.declareDependency("theme.a", "theme.d");
graph2.declareDependency("theme.a", "theme.c");
graph2.declareDependency("theme.b", "theme.a"); // This causes theme.b to not appear
+ graph2.declareOptionalDependency("theme.a", "theme.e");
const extensionB = await createExtension("extension.b");
graph2.addExtension(extensionB);
-
graph1.join(graph2);
+
+ t.true(graph1._hasUnresolvedOptionalDependencies,
+ "Graph has unresolved optional dependencies taken over from graph2");
+
graph1.declareDependency("library.d", "theme.a");
+ graph1.declareDependency("library.d", "theme.e");
+
+ graph1.resolveOptionalDependencies();
await traverseDepthFirst(t, graph1, [
"library.b",
"library.c",
"theme.d",
"theme.c",
+ "theme.e",
"theme.a",
"library.d",
"library.a",
@@ -1097,6 +1194,33 @@ test("join", async (t) => {
t.is(graph1.getExtension("extension.a"), extensionA, "Should return correct extension");
t.is(graph1.getExtension("extension.b"), extensionB, "Should return correct joined extension");
+
+ // graph2 remained unmodified
+ await traverseDepthFirst(t, graph2, [
+ "theme.d",
+ "theme.c",
+ "theme.a",
+ ]);
+});
+
+test("join: Preserves hasUnresolvedOptionalDependencies flag", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "theme.a"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph1.addProject(await createProject("library.b"));
+ graph1.declareOptionalDependency("library.a", "library.b");
+
+ graph1.join(graph2);
+
+ t.true(graph1._hasUnresolvedOptionalDependencies,
+ "graph1 still has unresolved optional dependencies");
+ t.false(graph2._hasUnresolvedOptionalDependencies,
+ "graph2 still does not have unresolved optional dependencies");
});
test("join: Seals incoming graph", (t) => {
@@ -1219,6 +1343,9 @@ test("Seal/isSealed", async (t) => {
}, {
message: expectedSealMsg
});
+ await t.throwsAsync(graph.resolveOptionalDependencies(), {
+ message: expectedSealMsg
+ });
const graph2 = new ProjectGraph({
diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js
index bc608d0cd..b176ec157 100644
--- a/test/lib/graph/helpers/ui5Framework.js
+++ b/test/lib/graph/helpers/ui5Framework.js
@@ -231,7 +231,7 @@ test.serial("generateDependencyTree should skip framework project without versio
const projectGraph = await projectGraphBuilder(provider);
await ui5Framework.enrichProjectGraph(projectGraph);
- t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged");
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
});
test.serial("generateDependencyTree should skip framework project with version and framework config", async (t) => {
@@ -263,7 +263,7 @@ test.serial("generateDependencyTree should skip framework project with version a
const projectGraph = await projectGraphBuilder(provider);
await ui5Framework.enrichProjectGraph(projectGraph);
- t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged");
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
});
test.serial("generateDependencyTree should throw for framework project with dependency missing in graph", async (t) => {
@@ -316,7 +316,7 @@ test.serial("generateDependencyTree should ignore root project without framework
const projectGraph = await projectGraphBuilder(provider);
await ui5Framework.enrichProjectGraph(projectGraph);
- t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged");
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
});
test.serial("utils.shouldIncludeDependency", (t) => {