Skip to content

Commit f5bf3c1

Browse files
committed
ProjectGraph: Only resolve optional dependencies if the target can be reached from the root project
Checking for a non-optional dependency is not sufficient since that can originate from a project which is optional itself (i.e. has no non-optional dependency leading to it).
1 parent 81bdb27 commit f5bf3c1

File tree

2 files changed

+97
-14
lines changed

2 files changed

+97
-14
lines changed

lib/graph/ProjectGraph.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -229,30 +229,30 @@ class ProjectGraph {
229229
}
230230

231231
/**
232-
* Transforms any optional dependencies in the graph for which the target is referenced by
233-
* at least one non-optional project
234-
* into a non-optional dependency.
232+
* Transforms any optional dependencies declared in the graph to non-optional dependency, if the target
233+
* can already be reached from the root project.
235234
*
236235
* @public
237236
*/
238-
resolveOptionalDependencies() {
239-
// TODO: If a project is referenced as non-optional by an *optional* dependency, it should still be optional
237+
async resolveOptionalDependencies() {
240238
if (!this._shouldResolveOptionalDependencies) {
241239
log.verbose(`Skipping resolution of optional dependencies since none have been declared`);
242240
return;
243241
}
244242
log.verbose(`Resolving optional dependencies...`);
243+
244+
// First collect all projects that are currently reachable from the root project (=all non-optional projects)
245245
const resolvedProjects = new Set;
246-
for (const [, dependencies] of Object.entries(this._adjList)) {
247-
for (let i = dependencies.length - 1; i >= 0; i--) {
248-
resolvedProjects.add(dependencies[i]);
249-
}
250-
}
246+
await this.traverseBreadthFirst(({project}) => {
247+
resolvedProjects.add(project.getName());
248+
});
249+
251250
for (const [projectName, dependencies] of Object.entries(this._optAdjList)) {
252251
for (let i = dependencies.length - 1; i >= 0; i--) {
253252
const targetProjectName = dependencies[i];
254253
if (resolvedProjects.has(targetProjectName)) {
255-
// Resolve optional dependency
254+
// Target node is already reachable in the graph
255+
// => Resolve optional dependency
256256
log.verbose(`Resolving optional dependency from ${projectName} to ${targetProjectName}...`);
257257

258258
if (this._adjList[targetProjectName].includes(projectName)) {

test/lib/graph/ProjectGraph.js

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ test("resolveOptionalDependencies", async (t) => {
463463
graph.declareDependency("library.d", "library.b");
464464
graph.declareDependency("library.d", "library.c");
465465

466-
graph.resolveOptionalDependencies();
466+
await graph.resolveOptionalDependencies();
467467

468468
t.is(graph.isOptionalDependency("library.a", "library.b"), false,
469469
"library.a should have no optional dependency to library.b anymore");
@@ -478,7 +478,6 @@ test("resolveOptionalDependencies", async (t) => {
478478
]);
479479
});
480480

481-
482481
test("resolveOptionalDependencies: Optional dependency has not been resolved", async (t) => {
483482
const {ProjectGraph} = t.context;
484483
const graph = new ProjectGraph({
@@ -493,7 +492,7 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a
493492
graph.declareOptionalDependency("library.a", "library.c");
494493
graph.declareDependency("library.a", "library.d");
495494

496-
graph.resolveOptionalDependencies();
495+
await graph.resolveOptionalDependencies();
497496

498497
t.is(graph.isOptionalDependency("library.a", "library.b"), true,
499498
"Dependency from library.a to library.b should still be optional");
@@ -507,6 +506,90 @@ test("resolveOptionalDependencies: Optional dependency has not been resolved", a
507506
]);
508507
});
509508

509+
test("resolveOptionalDependencies: Dependency of optional dependency has not been resolved", async (t) => {
510+
const {ProjectGraph} = t.context;
511+
const graph = new ProjectGraph({
512+
rootProjectName: "library.a"
513+
});
514+
graph.addProject(await createProject("library.a"));
515+
graph.addProject(await createProject("library.b"));
516+
graph.addProject(await createProject("library.c"));
517+
graph.addProject(await createProject("library.d"));
518+
519+
graph.declareOptionalDependency("library.a", "library.b");
520+
graph.declareOptionalDependency("library.a", "library.c");
521+
graph.declareDependency("library.b", "library.c");
522+
523+
await graph.resolveOptionalDependencies();
524+
525+
t.is(graph.isOptionalDependency("library.a", "library.b"), true,
526+
"Dependency from library.a to library.b should still be optional");
527+
528+
t.is(graph.isOptionalDependency("library.a", "library.c"), true,
529+
"Dependency from library.a to library.c should still be optional");
530+
531+
await traverseDepthFirst(t, graph, [
532+
"library.a"
533+
]);
534+
});
535+
536+
test("resolveOptionalDependencies: Cyclic optional dependency is not resolved", async (t) => {
537+
const {ProjectGraph} = t.context;
538+
const graph = new ProjectGraph({
539+
rootProjectName: "library.a"
540+
});
541+
graph.addProject(await createProject("library.a"));
542+
graph.addProject(await createProject("library.b"));
543+
graph.addProject(await createProject("library.c"));
544+
545+
graph.declareDependency("library.a", "library.b");
546+
graph.declareDependency("library.a", "library.c");
547+
graph.declareDependency("library.c", "library.b");
548+
graph.declareOptionalDependency("library.b", "library.c");
549+
550+
await graph.resolveOptionalDependencies();
551+
552+
t.is(graph.isOptionalDependency("library.b", "library.c"), true,
553+
"Dependency from library.b to library.c should still be optional");
554+
555+
await traverseDepthFirst(t, graph, [
556+
"library.b",
557+
"library.c",
558+
"library.a"
559+
]);
560+
});
561+
562+
test("resolveOptionalDependencies: Resolves transitive optional dependencies", async (t) => {
563+
const {ProjectGraph} = t.context;
564+
const graph = new ProjectGraph({
565+
rootProjectName: "library.a"
566+
});
567+
graph.addProject(await createProject("library.a"));
568+
graph.addProject(await createProject("library.b"));
569+
graph.addProject(await createProject("library.c"));
570+
graph.addProject(await createProject("library.d"));
571+
572+
graph.declareDependency("library.a", "library.b");
573+
graph.declareOptionalDependency("library.a", "library.c");
574+
graph.declareDependency("library.b", "library.c");
575+
graph.declareDependency("library.c", "library.d");
576+
graph.declareOptionalDependency("library.a", "library.d");
577+
578+
await graph.resolveOptionalDependencies();
579+
580+
t.is(graph.isOptionalDependency("library.a", "library.c"), false,
581+
"Dependency from library.a to library.c should not be optional anymore");
582+
583+
t.is(graph.isOptionalDependency("library.a", "library.d"), false,
584+
"Dependency from library.a to library.d should not be optional anymore");
585+
586+
await traverseDepthFirst(t, graph, [
587+
"library.d",
588+
"library.c",
589+
"library.b",
590+
"library.a"
591+
]);
592+
});
510593

511594
test("traverseBreadthFirst", async (t) => {
512595
const {ProjectGraph} = t.context;

0 commit comments

Comments
 (0)