fix(arborist): resolve sibling override sets via common ancestor (#9110) · npm/cli@21ea382 · GitHub
Skip to content

Commit 21ea382

Browse files
fix(arborist): resolve sibling override sets via common ancestor (#9110)
1 parent 51365b1 commit 21ea382

5 files changed

Lines changed: 190 additions & 16 deletions

File tree

lib/utils/explain-dep.js

Lines changed: 26 additions & 13 deletions

tap-snapshots/test/lib/utils/explain-dep.js.test.cjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@
55
* Make sure to inspect the output below. Do not ignore changes!
66
*/
77
'use strict'
8+
exports[`test/lib/utils/explain-dep.js TAP basic > circular dependency does not recurse infinitely 1`] = `
9+
cycle-a@1.0.0
10+
node_modules/cycle-a
11+
cycle-a@"1.x" from cycle-b@2.0.0
12+
node_modules/cycle-b
13+
cycle-b@"2.x" from cycle-a@1.0.0
14+
node_modules/cycle-a
15+
cycle-a@"1.x" from cycle-b@2.0.0
16+
node_modules/cycle-b
17+
`
18+
19+
exports[`test/lib/utils/explain-dep.js TAP basic > circular dependency from other side 1`] = `
20+
cycle-b@2.0.0
21+
node_modules/cycle-b
22+
cycle-b@"2.x" from cycle-a@1.0.0
23+
node_modules/cycle-a
24+
cycle-a@"1.x" from cycle-b@2.0.0
25+
node_modules/cycle-b
26+
cycle-b@"2.x" from cycle-a@1.0.0
27+
node_modules/cycle-a
28+
`
29+
830
exports[`test/lib/utils/explain-dep.js TAP basic > ellipses test one 1`] = `
931
manydep@1.0.0
1032
manydep@"1.0.0" from prod-dep@1.2.3
@@ -21,6 +43,11 @@ manydep@1.0.0
2143
6 more (optdep, extra-neos, deep-dev, peer, the root project, a package with a pretty long name)
2244
`
2345

46+
exports[`test/lib/utils/explain-dep.js TAP basic > explainEdge without seen parameter 1`] = `
47+
some-dep@"1.x" from parent-pkg@2.0.0
48+
node_modules/parent-pkg
49+
`
50+
2451
exports[`test/lib/utils/explain-dep.js TAP basic bundled > explain color deep 1`] = `
2552
bundle-of-joy@1.0.0 [4m[36mbundled[39m[24m[2m[22m
2653
[2mnode_modules/bundle-of-joy[22m

test/lib/utils/explain-dep.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { resolve } = require('node:path')
22
const t = require('tap')
3-
const { explainNode, printNode } = require('../../../lib/utils/explain-dep.js')
3+
const { explainNode, printNode, explainEdge } = require('../../../lib/utils/explain-dep.js')
44
const { cleanCwd } = require('../../fixtures/clean-snapshot')
55

66
t.cleanSnapshot = (str) => cleanCwd(str)
@@ -268,6 +268,47 @@ t.test('basic', async t => {
268268
})
269269
}
270270

271+
// Regression test for https://github.com/npm/cli/issues/9109
272+
// Circular dependency graphs (common in linked strategy store nodes) should not cause infinite recursion in explainNode.
273+
const cycleA = {
274+
name: 'cycle-a',
275+
version: '1.0.0',
276+
location: 'node_modules/cycle-a',
277+
dependents: [],
278+
}
279+
const cycleB = {
280+
name: 'cycle-b',
281+
version: '2.0.0',
282+
location: 'node_modules/cycle-b',
283+
dependents: [{
284+
type: 'prod',
285+
name: 'cycle-b',
286+
spec: '2.x',
287+
from: cycleA,
288+
}],
289+
}
290+
cycleA.dependents = [{
291+
type: 'prod',
292+
name: 'cycle-a',
293+
spec: '1.x',
294+
from: cycleB,
295+
}]
296+
t.matchSnapshot(explainNode(cycleA, Infinity, noColor), 'circular dependency does not recurse infinitely')
297+
t.matchSnapshot(explainNode(cycleB, Infinity, noColor), 'circular dependency from other side')
298+
299+
// explainEdge called without seen parameter (covers default seen = new Set() branch on explainEdge and explainFrom)
300+
t.matchSnapshot(explainEdge({
301+
type: 'prod',
302+
name: 'some-dep',
303+
spec: '1.x',
304+
from: {
305+
name: 'parent-pkg',
306+
version: '2.0.0',
307+
location: 'node_modules/parent-pkg',
308+
dependents: [],
309+
},
310+
}, 2, noColor), 'explainEdge without seen parameter')
311+
271312
// make sure that we show the last one if it's the only one that would
272313
// hit the ...
273314
cases.manyDeps.dependents.pop()

workspaces/arborist/lib/override-set.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,29 @@ class OverrideSet {
195195
}
196196
}
197197

198-
// The override sets are incomparable. Neither one contains the other.
199-
log.silly('Conflicting override sets', first, second)
198+
// The override sets are incomparable (e.g. siblings like the "react" and "react-dom" children of the root override set). Check if they have semantically conflicting rules before treating this as an error.
199+
if (this.haveConflictingRules(first, second)) {
200+
log.silly('Conflicting override sets', first, second)
201+
return undefined
202+
}
203+
204+
// The override sets are structurally incomparable but have compatible rules. Fall back to their nearest common ancestor so the node still has a valid override set.
205+
return this.findCommonAncestor(first, second)
206+
}
207+
208+
static findCommonAncestor (first, second) {
209+
const firstAncestors = []
210+
for (const ancestor of first.ancestry()) {
211+
firstAncestors.push(ancestor)
212+
}
213+
for (const secondAnc of second.ancestry()) {
214+
for (const firstAnc of firstAncestors) {
215+
if (firstAnc.isEqual(secondAnc)) {
216+
return firstAnc
217+
}
218+
}
219+
}
220+
return null
200221
}
201222

202223
static doOverrideSetsConflict (first, second) {

workspaces/arborist/test/override-set.js

Lines changed: 72 additions & 0 deletions

0 commit comments

Comments
 (0)