Skip to content

Commit b335763

Browse files
nicolo-ribaudoevocateur
authored andcommittedJul 18, 2019
fix(package-graph): Flatten cycles to avoid skipping packages (#2185)
1 parent e1b3d62 commit b335763

37 files changed

+512
-52
lines changed
 
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
+---+
2+
| A |
3+
+---+
4+
|
5+
v
6+
+---+ +---+ +---+
7+
| E | --> | B | <-- | G |
8+
+---+ +---+ +---+
9+
^ | ^
10+
| v |
11+
+---+ +---+ +---+
12+
| D | <-- | C | --> | F |
13+
+---+ +---+ +---+
14+
15+
Cycles:
16+
B -> C -> D -> E -> B
17+
B -> C -> F -> G -> B
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": "1.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "cycle-intersection"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "b",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"c": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "c",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"d": "^1.0.0",
6+
"f": "^1.0.0"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "d",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"e": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "e",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "f",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"g": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "g",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0"
6+
}
7+
}

‎__fixtures__/cycle-parent/graph.txt

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
https://github.com/lerna/lerna/issues/2107#issuecomment-511987691
2+
3+
+---+
4+
/----> | B |
5+
| +---+
6+
|
7+
+---+ +---+ -----\
8+
| A | --> | C | |
9+
+---+ +---+ <-\ |
10+
| | |
11+
| +---+ --/ |
12+
\----> | D | |
13+
+---+ <----/

‎__fixtures__/cycle-parent/lerna.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": "1.0.0"
3+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "cycle-parent"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0",
6+
"c": "^1.0.0",
7+
"d": "^1.0.0"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "b",
3+
"version": "1.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "c",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"d": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "d",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"c": "^1.0.0"
6+
}
7+
}

‎__fixtures__/cycle-separate/graph.txt

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
+---+ +---+
2+
| A | --> | B | <----\
3+
+---+ +---+ |
4+
| | |
5+
v v |
6+
+---+ +---+ +---+
7+
| H | | C | --> | D |
8+
+---+ +---+ +---+
9+
| |
10+
v |
11+
+---+ |
12+
| E | <----\ |
13+
+---+ | |
14+
| | |
15+
v | |
16+
+---+ +---+ |
17+
| F | --> | G | <----/
18+
+---+ +---+
19+
20+
Cycles:
21+
B -> C -> D -> B
22+
E -> F -> G -> E
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": "1.0.0"
3+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "cycle-separate"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0",
6+
"h": "^1.0.0"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "b",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"c": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "c",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"d": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "d",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"b": "^1.0.0",
6+
"g": "^1.0.0"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "e",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"f": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "f",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"g": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "g",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"e": "^1.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "h",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"e": "^1.0.0"
6+
}
7+
}

‎__fixtures__/toposort/graph.txt

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
+------------------+ +---------+ -----\
3+
| cycle-extraneous | --> | cycle-1 | |
4+
+------------------+ +---------+ <-\ |
5+
| |
6+
+---------+ --/ |
7+
| cycle-2 | |
8+
+---------+ <----/
9+
10+
+--------+
11+
+-------+ --> | dag-2a | ---> +-------+
12+
| dag-3 | +--------+ | dag-1 |
13+
+-------+ ------------------> +-------+
14+
^
15+
+--------+ |
16+
| dag-2b | --/
17+
+--------+
18+
19+
+------------+
20+
| standalone |
21+
+------------+

‎__fixtures__/toposort/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"name": "independent"
2+
"name": "toposort"
33
}

‎commands/exec/__tests__/exec-command.test.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -206,24 +206,20 @@ describe("ExecCommand", () => {
206206
it("warns when cycles are encountered", async () => {
207207
const testDir = await initFixture("toposort");
208208

209-
await lernaExec(testDir)("ls");
209+
await lernaExec(testDir)("ls", "--concurrency", "1");
210210

211211
const [logMessage] = loggingOutput("warn");
212212
expect(logMessage).toMatch("Dependency cycles detected, you should fix these!");
213213
expect(logMessage).toMatch("package-cycle-1 -> package-cycle-2 -> package-cycle-1");
214-
expect(logMessage).toMatch("package-cycle-2 -> package-cycle-1 -> package-cycle-2");
215-
expect(logMessage).toMatch(
216-
"package-cycle-extraneous -> package-cycle-1 -> package-cycle-2 -> package-cycle-1"
217-
);
218214

219215
expect(calledInPackages()).toEqual([
220216
"package-dag-1",
221217
"package-standalone",
222218
"package-dag-2a",
223219
"package-dag-2b",
224-
"package-dag-3",
225220
"package-cycle-1",
226221
"package-cycle-2",
222+
"package-dag-3",
227223
"package-cycle-extraneous",
228224
]);
229225
});

‎commands/run/__tests__/run-command.test.js

+30-8
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe("RunCommand", () => {
174174
it("runs scripts in lexical (not topological) order", async () => {
175175
const testDir = await initFixture("toposort");
176176

177-
await lernaRun(testDir)("env", "--no-sort");
177+
await lernaRun(testDir)("env", "--concurrency", "1", "--no-sort");
178178

179179
expect(output.logged().split("\n")).toEqual([
180180
"package-cycle-1",
@@ -191,7 +191,7 @@ describe("RunCommand", () => {
191191
it("optionally streams output", async () => {
192192
const testDir = await initFixture("toposort");
193193

194-
await lernaRun(testDir)("env", "--no-sort", "--stream");
194+
await lernaRun(testDir)("env", "--concurrency", "1", "--no-sort", "--stream");
195195

196196
expect(ranInPackagesStreaming(testDir)).toMatchInlineSnapshot(`
197197
Array [
@@ -212,28 +212,50 @@ describe("RunCommand", () => {
212212
it("warns when cycles are encountered", async () => {
213213
const testDir = await initFixture("toposort");
214214

215-
await lernaRun(testDir)("env");
215+
await lernaRun(testDir)("env", "--concurrency", "1");
216216

217217
const [logMessage] = loggingOutput("warn");
218218
expect(logMessage).toMatch("Dependency cycles detected, you should fix these!");
219219
expect(logMessage).toMatch("package-cycle-1 -> package-cycle-2 -> package-cycle-1");
220-
expect(logMessage).toMatch("package-cycle-2 -> package-cycle-1 -> package-cycle-2");
221-
expect(logMessage).toMatch(
222-
"package-cycle-extraneous -> package-cycle-1 -> package-cycle-2 -> package-cycle-1"
223-
);
224220

225221
expect(output.logged().split("\n")).toEqual([
226222
"package-dag-1",
227223
"package-standalone",
228224
"package-dag-2a",
229225
"package-dag-2b",
230-
"package-dag-3",
231226
"package-cycle-1",
232227
"package-cycle-2",
228+
"package-dag-3",
233229
"package-cycle-extraneous",
234230
]);
235231
});
236232

233+
it("works with intersected cycles", async () => {
234+
const testDir = await initFixture("cycle-intersection");
235+
236+
await lernaRun(testDir)("env", "--concurrency", "1");
237+
238+
const [logMessage] = loggingOutput("warn");
239+
expect(logMessage).toMatch("Dependency cycles detected, you should fix these!");
240+
expect(logMessage).toMatch("b -> c -> d -> e -> b");
241+
expect(logMessage).toMatch("f -> g -> (nested cycle: b -> c -> d -> e -> b) -> f");
242+
243+
expect(output.logged().split("\n")).toEqual(["f", "b", "e", "d", "c", "g", "a"]);
244+
});
245+
246+
it("works with separate cycles", async () => {
247+
const testDir = await initFixture("cycle-separate");
248+
249+
await lernaRun(testDir)("env", "--concurrency", "1");
250+
251+
const [logMessage] = loggingOutput("warn");
252+
expect(logMessage).toMatch("Dependency cycles detected, you should fix these!");
253+
expect(logMessage).toMatch("b -> c -> d -> b");
254+
expect(logMessage).toMatch("e -> f -> g -> e");
255+
256+
expect(output.logged().split("\n")).toEqual(["e", "g", "f", "h", "b", "d", "c", "a"]);
257+
});
258+
237259
it("should throw an error with --reject-cycles", async () => {
238260
const testDir = await initFixture("toposort");
239261

‎commands/version/__tests__/version-command.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,35 @@ describe("VersionCommand", () => {
642642
expect(patch).toMatchSnapshot();
643643
});
644644

645+
it("versions all packages with cycles", async () => {
646+
const testDir = await initFixture("cycle-parent");
647+
648+
await gitTag(testDir, "v1.0.0");
649+
650+
await Promise.all(
651+
["a", "b", "c", "d"].map(n => fs.outputFile(path.join(testDir, "packages", n, "index.js"), "hello"))
652+
);
653+
await gitAdd(testDir, ".");
654+
await gitCommit(testDir, "feat: hello");
655+
656+
collectUpdates.mockImplementationOnce(collectUpdatesActual);
657+
658+
await lernaVersion(testDir)("major", "--yes");
659+
660+
const patch = await showCommit(testDir, "--name-only");
661+
expect(patch).toMatchInlineSnapshot(`
662+
"v2.0.0
663+
664+
HEAD -> master, tag: v2.0.0
665+
666+
lerna.json
667+
packages/a/package.json
668+
packages/b/package.json
669+
packages/c/package.json
670+
packages/d/package.json"
671+
`);
672+
});
673+
645674
describe("with relative file: specifiers", () => {
646675
const setupChanges = async (cwd, pkgRoot = "packages") => {
647676
await gitTag(cwd, "v1.0.0");

‎core/package-graph/__tests__/package-graph.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ describe("PackageGraph", () => {
133133

134134
expect(node.prereleaseId).toBe("rc");
135135
});
136+
137+
describe(".toString()", () => {
138+
it("returns the node's name", () => {
139+
const node = new PackageGraph([
140+
new Package({ name: "pkg-name", version: "0.1.2" }, "/path/to/pkg-name"),
141+
]).get("pkg-name");
142+
143+
expect(node.toString()).toBe("pkg-name");
144+
});
145+
});
136146
});
137147

138148
describe(".get()", () => {

‎core/package-graph/index.js

+201-9
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,129 @@ class PackageGraphNode {
5555
satisfies({ gitCommittish, gitRange, fetchSpec }) {
5656
return semver.satisfies(this.version, gitCommittish || gitRange || fetchSpec);
5757
}
58+
59+
/**
60+
* Returns a string representation of this node (its name)
61+
*
62+
* @returns {String}
63+
*/
64+
toString() {
65+
return this.name;
66+
}
67+
}
68+
69+
let lastCollapsedNodeId = 0;
70+
71+
/**
72+
* Represents a cyclic collection of nodes in a PackageGraph.
73+
* It is meant to be used as a black box, where the only exposed
74+
* information are the connections to the other nodes of the graph.
75+
* It can contains either `PackageGraphNode`s or other `CyclicPackageGraphNode`s.
76+
*/
77+
class CyclicPackageGraphNode extends Map {
78+
constructor() {
79+
super();
80+
81+
this.localDependencies = new Map();
82+
this.localDependents = new Map();
83+
84+
Object.defineProperties(this, {
85+
// immutable properties
86+
name: {
87+
enumerable: true,
88+
value: `(cycle) ${(lastCollapsedNodeId += 1)}`,
89+
},
90+
isCycle: {
91+
value: true,
92+
},
93+
});
94+
}
95+
96+
/**
97+
* @returns {String} Returns a representation of a cycle, like like `A -> B -> C -> A`.
98+
*/
99+
toString() {
100+
const parts = Array.from(this, ([key, node]) =>
101+
node.isCycle ? `(nested cycle: ${node.toString()})` : key
102+
);
103+
104+
// start from the origin
105+
parts.push(parts[0]);
106+
107+
return parts.reverse().join(" -> ");
108+
}
109+
110+
/**
111+
* Flattens a CyclicPackageGraphNode (which can have multiple level of cycles).
112+
*
113+
* @returns {PackageGraphNode[]}
114+
*/
115+
flatten() {
116+
const result = [];
117+
118+
for (const node of this.values()) {
119+
if (node.isCycle) {
120+
result.push(...node.flatten());
121+
} else {
122+
result.push(node);
123+
}
124+
}
125+
126+
return result;
127+
}
128+
129+
/**
130+
* Checks if a given node is contained in this cycle (or in a nested one)
131+
*
132+
* @param {String} name The name of the package to search in this cycle
133+
* @returns {Boolean}
134+
*/
135+
contains(name) {
136+
for (const [currentName, currentNode] of this) {
137+
if (currentNode.isCycle) {
138+
if (currentNode.contains(name)) {
139+
return true;
140+
}
141+
} else if (currentName === name) {
142+
return true;
143+
}
144+
}
145+
return false;
146+
}
147+
148+
/**
149+
* Adds a graph node, or a nested cycle, to this group.
150+
*
151+
* @param {PackageGraphNode|CyclicPackageGraphNode} node
152+
*/
153+
insert(node) {
154+
this.set(node.name, node);
155+
this.unlink(node);
156+
157+
for (const [dependencyName, dependencyNode] of node.localDependencies) {
158+
if (!this.contains(dependencyName)) {
159+
this.localDependencies.set(dependencyName, dependencyNode);
160+
}
161+
}
162+
163+
for (const [dependentName, dependentNode] of node.localDependents) {
164+
if (!this.contains(dependentName)) {
165+
this.localDependents.set(dependentName, dependentNode);
166+
}
167+
}
168+
}
169+
170+
/**
171+
* Remove pointers to candidate node from internal collections.
172+
* @param {PackageGraphNode|CyclicPackageGraphNode} candidateNode instance to unlink
173+
*/
174+
unlink(candidateNode) {
175+
// remove incoming edges ("indegree")
176+
this.localDependencies.delete(candidateNode.name);
177+
178+
// remove outgoing edges ("outdegree")
179+
this.localDependents.delete(candidateNode.name);
180+
}
58181
}
59182

60183
/**
@@ -190,7 +313,10 @@ class PackageGraph extends Map {
190313
}
191314

192315
/**
193-
* Return a tuple of cycle paths and nodes, which have been removed from the graph.
316+
* Return a tuple of cycle paths and nodes.
317+
*
318+
* @deprecated Use collapseCycles instead.
319+
*
194320
* @param {!boolean} rejectCycles Whether or not to reject cycles
195321
* @returns [Set<String[]>, Set<PackageGraphNode>]
196322
*/
@@ -238,19 +364,71 @@ class PackageGraph extends Map {
238364
currentNode.localDependents.forEach(visits([currentName]));
239365
});
240366

241-
if (cyclePaths.size) {
242-
const cycleMessage = ["Dependency cycles detected, you should fix these!"]
243-
.concat(Array.from(cyclePaths).map(cycle => cycle.join(" -> ")))
244-
.join("\n");
367+
reportCycles(Array.from(cyclePaths, cycle => cycle.join(" -> ")), rejectCycles);
245368

246-
if (rejectCycles) {
247-
throw new ValidationError("ECYCLE", cycleMessage);
369+
return [cyclePaths, cycleNodes];
370+
}
371+
372+
/**
373+
* Returns the cycles of this graph. If two cycles share some elements, they will
374+
* be returned as a single cycle.
375+
*
376+
* @param {!boolean} rejectCycles Whether or not to reject cycles
377+
* @returns Set<CyclicPackageGraphNode>
378+
*/
379+
collapseCycles(rejectCycles) {
380+
const cyclePaths = [];
381+
const nodeToCycle = new Map();
382+
const cycles = new Set();
383+
384+
const walkStack = [];
385+
386+
function visits(baseNode, dependentNode) {
387+
if (nodeToCycle.has(baseNode)) {
388+
return;
389+
}
390+
391+
let topLevelDependent = dependentNode;
392+
while (nodeToCycle.has(topLevelDependent)) {
393+
topLevelDependent = nodeToCycle.get(topLevelDependent);
394+
}
395+
396+
if (
397+
topLevelDependent === baseNode ||
398+
(topLevelDependent.isCycle && topLevelDependent.has(baseNode.name))
399+
) {
400+
const cycle = new CyclicPackageGraphNode();
401+
402+
walkStack.forEach(nodeInCycle => {
403+
nodeToCycle.set(nodeInCycle, cycle);
404+
cycle.insert(nodeInCycle);
405+
cycles.delete(nodeInCycle);
406+
});
407+
408+
cycles.add(cycle);
409+
cyclePaths.push(cycle.toString());
410+
411+
return;
248412
}
249413

250-
log.warn("ECYCLE", cycleMessage);
414+
if (walkStack.indexOf(topLevelDependent) === -1) {
415+
// eslint-disable-next-line no-use-before-define
416+
visitWithStack(baseNode, topLevelDependent);
417+
}
251418
}
252419

253-
return [cyclePaths, cycleNodes];
420+
function visitWithStack(baseNode, currentNode = baseNode) {
421+
walkStack.push(currentNode);
422+
currentNode.localDependents.forEach(visits.bind(null, baseNode));
423+
walkStack.pop();
424+
}
425+
426+
this.forEach(currentNode => visitWithStack(currentNode));
427+
cycles.forEach(collapsedNode => visitWithStack(collapsedNode));
428+
429+
reportCycles(cyclePaths, rejectCycles);
430+
431+
return cycles;
254432
}
255433

256434
/**
@@ -291,4 +469,18 @@ class PackageGraph extends Map {
291469
}
292470
}
293471

472+
function reportCycles(paths, rejectCycles) {
473+
if (!paths.length) {
474+
return;
475+
}
476+
477+
const cycleMessage = ["Dependency cycles detected, you should fix these!"].concat(paths).join("\n");
478+
479+
if (rejectCycles) {
480+
throw new ValidationError("ECYCLE", cycleMessage);
481+
}
482+
483+
log.warn("ECYCLE", cycleMessage);
484+
}
485+
294486
module.exports = PackageGraph;

‎utils/listable/__tests__/listable-format.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ pkg-1
242242
expect(loggingOutput("warn")).toContainEqual(expect.stringContaining("pkg-2 -> pkg-3 -> pkg-2"));
243243
expect(text).toMatchInlineSnapshot(`
244244
pkg-2
245-
pkg-1
246245
pkg-3 (PRIVATE)
246+
pkg-1
247247
`);
248248
});
249249
});

‎utils/query-graph/query-graph.js

+12-27
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,23 @@ class QueryGraph {
2626
this.graph = new PackageGraph(packages, options.graphType);
2727

2828
// Evaluate cycles
29-
[this.cyclePaths, this.cycleNodes] = this.graph.partitionCycles(options.rejectCycles);
30-
31-
if (this.cyclePaths.size) {
32-
// Find the cyclical package with the most dependents. Will be evaluated before other cyclical packages
33-
this.cyclicalPackageWithMostDependents = Array.from(this.cycleNodes)
34-
.sort((a, b) => b.localDependents.size - a.localDependents.size)
35-
.shift();
36-
}
29+
this.cycles = this.graph.collapseCycles(options.rejectCycles);
3730
}
3831

3932
_getNextLeaf() {
4033
return Array.from(this.graph.values()).filter(node => node.localDependencies.size === 0);
4134
}
4235

4336
_getNextCycle() {
44-
// If the cyclical package with the most dependents is still in the graph, we return it
45-
if (this.graph.has(this.cyclicalPackageWithMostDependents.name)) {
46-
return [this.graph.get(this.cyclicalPackageWithMostDependents.name)];
37+
const cycle = Array.from(this.cycles).find(cycleNode => cycleNode.localDependencies.size === 0);
38+
39+
if (!cycle) {
40+
return [];
4741
}
4842

49-
// Otherwise, return any package that does not depend on the package referenced above
50-
return Array.from(this.graph.values()).filter(
51-
node => !node.localDependencies.has(this.cyclicalPackageWithMostDependents)
52-
);
53-
}
43+
this.cycles.delete(cycle);
5444

55-
_onlyCyclesLeft() {
56-
// Check if every remaining package is a package from the cycleNodes graph
57-
return Array.from(this.graph.values()).every(node =>
58-
Array.from(this.cycleNodes).some(cycleNode => cycleNode.name === node.name)
59-
);
45+
return cycle.flatten();
6046
}
6147

6248
getAvailablePackages() {
@@ -67,12 +53,7 @@ class QueryGraph {
6753
return availablePackages;
6854
}
6955

70-
// Or, get the next cyclical packages
71-
if (this.cyclePaths.size && this._onlyCyclesLeft()) {
72-
return this._getNextCycle();
73-
}
74-
75-
return [];
56+
return this._getNextCycle();
7657
}
7758

7859
markAsTaken(name) {
@@ -81,6 +62,10 @@ class QueryGraph {
8162

8263
markAsDone(candidateNode) {
8364
this.graph.remove(candidateNode);
65+
66+
for (const cycle of this.cycles) {
67+
cycle.unlink(candidateNode);
68+
}
8469
}
8570
}
8671

0 commit comments

Comments
 (0)
Please sign in to comment.