Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.

Commit 1d68907

Browse files
committed
Allow --force to override conflicted peerOptional
With a dependency graph like this: ``` root -> (a, b@1) a -> PEEROPTIONAL(b@2) ``` We do not install the peerOptional dependency by default, so even though `b@2` is included in the peerSet of `a`, it is not added to the tree. Then, the `b@1` dependency is added to satisfy root's direct dependency on it, causing the `a -> b@2` edge to become invalid. We then try to resolve the `a -> b@2` edge, and find that we cannot place it anywhere, causing an `ERESOLVE` error. However, because `b@2` is no longer a part of a peerSet sourced on the `root` node, we miss the chance to detect that it should be overridden, resulting in an `ERESOLVE` failure even when `--force` is used. This commit adds the check for `this[_force]` prior to crashing with ERESOLVE, so that cases that avoid our earlier heuristics still accept the invalid resolution when `--force` is in effect. Fix: #226 Fix: npm/cli#2504 PR-URL: #228 Credit: @isaacs Close: #228 Reviewed-by: @ruyadorno
1 parent 828990c commit 1d68907

10 files changed

+1092
-7
lines changed

lib/arborist/build-ideal-tree.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ This is a one-time fix-up, please be patient...
806806
// a virtual root of whatever brought in THIS node.
807807
// so we VR the node itself if the edge is not a peer
808808
const source = edge.peer ? peerSource : node
809+
809810
const virtualRoot = this[_virtualRoot](source, true)
810811
// reuse virtual root if we already have one, but don't
811812
// try to do the override ahead of time, since we MAY be able
@@ -827,8 +828,7 @@ This is a one-time fix-up, please be patient...
827828
// +-- z@1
828829
// But if x and y are loaded in the same virtual root, then they will
829830
// be forced to agree on a version of z.
830-
const required = edge.type === 'peerOptional' ? new Set()
831-
: new Set([edge.from])
831+
const required = new Set([edge.from])
832832
const parent = edge.peer ? virtualRoot : null
833833
const dep = vrDep && vrDep.satisfies(edge) ? vrDep
834834
: await this[_nodeFromEdge](edge, parent, null, required)
@@ -1218,8 +1218,25 @@ This is a one-time fix-up, please be patient...
12181218
break
12191219
}
12201220

1221-
if (!target)
1222-
this[_failPeerConflict](edge)
1221+
// if we can't find a target, that means that the last placed checked
1222+
// (and all the places before it) had a copy already. if we're in
1223+
// --force mode, then the user has explicitly said that they're ok
1224+
// with conflicts. This can only occur in --force mode in the case
1225+
// when a node was added to the tree with a peerOptional dep that we
1226+
// ignored, and then later, that edge became invalid, and we fail to
1227+
// resolve it. We will warn about it in a moment.
1228+
if (!target) {
1229+
if (this[_force]) {
1230+
// we know that there is a dep (not the root) which is the target
1231+
// of this edge, or else it wouldn't have been a conflict.
1232+
target = edge.to.resolveParent
1233+
canPlace = KEEP
1234+
} else
1235+
this[_failPeerConflict](edge)
1236+
} else {
1237+
// it worked, so we clearly have no peer conflicts at this point.
1238+
this[_peerConflict] = null
1239+
}
12231240

12241241
this.log.silly(
12251242
'placeDep',
@@ -1230,9 +1247,6 @@ This is a one-time fix-up, please be patient...
12301247
`want: ${edge.spec || '*'}`
12311248
)
12321249

1233-
// it worked, so we clearly have no peer conflicts at this point.
1234-
this[_peerConflict] = null
1235-
12361250
// Can only get KEEP here if the original edge was valid,
12371251
// and we're checking for an update but it's already up to date.
12381252
if (canPlace === KEEP) {
@@ -1418,6 +1432,7 @@ This is a one-time fix-up, please be patient...
14181432
})
14191433
const entryEdge = peerEntryEdge || edge
14201434
const source = this[_peerSetSource].get(dep)
1435+
14211436
isSource = isSource || target === source
14221437
// if we're overriding the source, then we care if the *target* is
14231438
// ours, even if it wasn't actually the original source, since we

tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js

+736
Large diffs are not rendered by default.

test/arborist/build-ideal-tree.js

+105
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,7 @@ t.test('more peer dep conflicts', t => {
15711571
},
15721572
error: true,
15731573
},
1574+
15741575
'prod dep directly on conflicted peer, older': {
15751576
pkg: {
15761577
dependencies: {
@@ -1580,6 +1581,7 @@ t.test('more peer dep conflicts', t => {
15801581
},
15811582
error: true,
15821583
},
1584+
15831585
'prod dep directly on conflicted peer, full peer set, newer': {
15841586
pkg: {
15851587
dependencies: {
@@ -1592,6 +1594,7 @@ t.test('more peer dep conflicts', t => {
15921594
},
15931595
error: true,
15941596
},
1597+
15951598
'prod dep directly on conflicted peer, full peer set, older': {
15961599
pkg: {
15971600
dependencies: {
@@ -1604,6 +1607,7 @@ t.test('more peer dep conflicts', t => {
16041607
},
16051608
error: true,
16061609
},
1610+
16071611
'prod dep directly on conflicted peer, meta peer set, older': {
16081612
pkg: {
16091613
dependencies: {
@@ -1615,6 +1619,7 @@ t.test('more peer dep conflicts', t => {
16151619
},
16161620
error: true,
16171621
},
1622+
16181623
'dep indirectly on conflicted peer': {
16191624
pkg: {
16201625
dependencies: {
@@ -1624,6 +1629,7 @@ t.test('more peer dep conflicts', t => {
16241629
},
16251630
error: true,
16261631
},
1632+
16271633
'collision forcing duplication, order 1': {
16281634
pkg: {
16291635
dependencies: {
@@ -1634,6 +1640,7 @@ t.test('more peer dep conflicts', t => {
16341640
error: false,
16351641
resolvable: true,
16361642
},
1643+
16371644
'collision forcing duplication, order 2': {
16381645
pkg: {
16391646
dependencies: {
@@ -1644,6 +1651,7 @@ t.test('more peer dep conflicts', t => {
16441651
error: false,
16451652
resolvable: true,
16461653
},
1654+
16471655
'collision forcing duplication via add, order 1': {
16481656
pkg: {
16491657
dependencies: {
@@ -1654,6 +1662,7 @@ t.test('more peer dep conflicts', t => {
16541662
error: false,
16551663
resolvable: true,
16561664
},
1665+
16571666
'collision forcing duplication via add, order 2': {
16581667
pkg: {
16591668
dependencies: {
@@ -1664,6 +1673,7 @@ t.test('more peer dep conflicts', t => {
16641673
error: false,
16651674
resolvable: true,
16661675
},
1676+
16671677
'collision forcing metadep duplication, order 1': {
16681678
pkg: {
16691679
dependencies: {
@@ -1674,6 +1684,7 @@ t.test('more peer dep conflicts', t => {
16741684
error: false,
16751685
resolvable: true,
16761686
},
1687+
16771688
'collision forcing metadep duplication, order 2': {
16781689
pkg: {
16791690
dependencies: {
@@ -1684,6 +1695,7 @@ t.test('more peer dep conflicts', t => {
16841695
error: false,
16851696
resolvable: true,
16861697
},
1698+
16871699
'direct collision forcing metadep duplication, order 1': {
16881700
pkg: {
16891701
dependencies: {
@@ -1694,6 +1706,7 @@ t.test('more peer dep conflicts', t => {
16941706
error: false,
16951707
resolvable: true,
16961708
},
1709+
16971710
'direct collision forcing metadep duplication, order 2': {
16981711
pkg: {
16991712
dependencies: {
@@ -1704,6 +1717,7 @@ t.test('more peer dep conflicts', t => {
17041717
error: false,
17051718
resolvable: true,
17061719
},
1720+
17071721
'dep with conflicting peers': {
17081722
pkg: {
17091723
dependencies: {
@@ -1714,6 +1728,7 @@ t.test('more peer dep conflicts', t => {
17141728
// but it is a conflict in a peerSet that the root is sourcing.
17151729
error: true,
17161730
},
1731+
17171732
'metadeps with conflicting peers': {
17181733
pkg: {
17191734
dependencies: {
@@ -1722,6 +1737,7 @@ t.test('more peer dep conflicts', t => {
17221737
},
17231738
error: false,
17241739
},
1740+
17251741
'metadep conflict that warns because source is target': {
17261742
pkg: {
17271743
dependencies: {
@@ -1732,6 +1748,7 @@ t.test('more peer dep conflicts', t => {
17321748
error: false,
17331749
resolvable: false,
17341750
},
1751+
17351752
'metadep conflict triggering the peerConflict code path': {
17361753
pkg: {
17371754
dependencies: {
@@ -1743,6 +1760,7 @@ t.test('more peer dep conflicts', t => {
17431760
resolvable: false,
17441761
},
17451762
})
1763+
17461764
t.jobs = cases.length
17471765
t.plan(cases.length)
17481766

@@ -2494,3 +2512,90 @@ t.test('do not ERESOLVE on peerOptionals that are ignored anyway', t => {
24942512
})
24952513
}
24962514
})
2515+
2516+
t.test('allow ERESOLVE to be forced when not in the source', async t => {
2517+
const types = [
2518+
'peerDependencies',
2519+
'optionalDependencies',
2520+
'devDependencies',
2521+
'dependencies',
2522+
]
2523+
2524+
// in these tests, the deps are both of the same type. b has a peerOptional
2525+
// dep on peer, and peer is a direct dependency of the root.
2526+
t.test('both direct and peer of the same type', t => {
2527+
t.plan(types.length)
2528+
const pj = type => ({
2529+
name: '@isaacs/conflicted-peer-optional-from-dev-dep',
2530+
version: '1.2.3',
2531+
[type]: {
2532+
'@isaacs/conflicted-peer-optional-from-dev-dep-peer': '1',
2533+
'@isaacs/conflicted-peer-optional-from-dev-dep-b': '',
2534+
},
2535+
})
2536+
2537+
for (const type of types) {
2538+
t.test(type, async t => {
2539+
const path = t.testdir({
2540+
'package.json': JSON.stringify(pj(type)),
2541+
})
2542+
t.matchSnapshot(await printIdeal(path, { force: true }), 'use the force')
2543+
t.rejects(printIdeal(path), { code: 'ERESOLVE' }, 'no force')
2544+
})
2545+
}
2546+
})
2547+
2548+
// in these, the peer is a peer dep of the root, and b is a different type
2549+
t.test('peer is peer, b is some other type', t => {
2550+
t.plan(types.length - 1)
2551+
const pj = type => ({
2552+
name: '@isaacs/conflicted-peer-optional-from-dev-dep',
2553+
version: '1.2.3',
2554+
peerDependencies: {
2555+
'@isaacs/conflicted-peer-optional-from-dev-dep-b': '',
2556+
},
2557+
[type]: {
2558+
'@isaacs/conflicted-peer-optional-from-dev-dep-peer': '1',
2559+
},
2560+
})
2561+
for (const type of types) {
2562+
if (type === 'peerDependencies')
2563+
continue
2564+
t.test(type, async t => {
2565+
const path = t.testdir({
2566+
'package.json': JSON.stringify(pj(type)),
2567+
})
2568+
t.matchSnapshot(await printIdeal(path, { force: true }), 'use the force')
2569+
t.rejects(printIdeal(path), { code: 'ERESOLVE' }, 'no force')
2570+
})
2571+
}
2572+
})
2573+
2574+
// in these, b is a peer dep, and peer is some other type
2575+
t.test('peer is peer, b is some other type', t => {
2576+
t.plan(types.length - 1)
2577+
const pj = type => ({
2578+
name: '@isaacs/conflicted-peer-optional-from-dev-dep',
2579+
version: '1.2.3',
2580+
peerDependencies: {
2581+
'@isaacs/conflicted-peer-optional-from-dev-dep-peer': '1',
2582+
},
2583+
[type]: {
2584+
'@isaacs/conflicted-peer-optional-from-dev-dep-b': '',
2585+
},
2586+
})
2587+
for (const type of types) {
2588+
if (type === 'peerDependencies')
2589+
continue
2590+
t.test(type, async t => {
2591+
const path = t.testdir({
2592+
'package.json': JSON.stringify(pj(type)),
2593+
})
2594+
t.matchSnapshot(await printIdeal(path, { force: true }), 'use the force')
2595+
t.rejects(printIdeal(path), { code: 'ERESOLVE' }, 'no force')
2596+
})
2597+
}
2598+
})
2599+
2600+
t.end()
2601+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@isaacs/conflicted-peer-optional-from-dev-dep-b",
3+
"version": "1.0.0",
4+
"peerDependencies": {
5+
"@isaacs/conflicted-peer-optional-from-dev-dep-peer": "2"
6+
},
7+
"peerDependenciesMeta": {
8+
"@isaacs/conflicted-peer-optional-from-dev-dep-peer": {
9+
"optional": true
10+
}
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@isaacs/conflicted-peer-optional-from-dev-dep-peer",
3+
"version": "1.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@isaacs/conflicted-peer-optional-from-dev-dep-peer",
3+
"version": "2.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"_id": "@isaacs/conflicted-peer-optional-from-dev-dep-b",
3+
"name": "@isaacs/conflicted-peer-optional-from-dev-dep-b",
4+
"dist-tags": {
5+
"latest": "1.0.0"
6+
},
7+
"versions": {
8+
"1.0.0": {
9+
"name": "@isaacs/conflicted-peer-optional-from-dev-dep-b",
10+
"version": "1.0.0",
11+
"peerDependencies": {
12+
"@isaacs/conflicted-peer-optional-from-dev-dep-peer": "2"
13+
},
14+
"peerDependenciesMeta": {
15+
"@isaacs/conflicted-peer-optional-from-dev-dep-peer": {
16+
"optional": true
17+
}
18+
},
19+
"_id": "@isaacs/[email protected]",
20+
"_nodeVersion": "15.3.0",
21+
"_npmVersion": "7.5.3",
22+
"dist": {
23+
"integrity": "sha512-2rStqBkn1QSXBjweS4lHUPH9jt4ToFqkw2nUbWA4fZBxOaYS1nkS/ST70xRlQxRW+3UdTFGc+5xk187sHlMT/A==",
24+
"shasum": "63f188dba7ec8a3e23b09540230ba8014ee760d2",
25+
"tarball": "https://registry.npmjs.org/@isaacs/conflicted-peer-optional-from-dev-dep-b/-/conflicted-peer-optional-from-dev-dep-b-1.0.0.tgz",
26+
"fileCount": 1,
27+
"unpackedSize": 299,
28+
"npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgJI1ACRA9TVsSAnZWagAAbrMP+QHkDuGH+7Upc7vgCNGn\nh/BJ9oweduRP3C8DW4xviYVqUVYJdRtfN9NjA8DOA6RroecCaRONhiMPybz3\nD53u7QfKMAowj9z5KckriR8/RJ6v9EbFZWA3re/c3O6lNR8sZ0fPlw3VeOgq\nxuofI21+n3SQNDlD2N/fK8YRGkqgx4QI90IF0gb5q1k56cFP7DAqAHnRaxk9\nZ60SA8iv/lYu60+bGrnozv4H1qn6VM9m2hm/284H+HkGMOCBXSLpPLu6N1oJ\nNBy0rrp+5nhHHizWxEtTeI7xMds59p9IjMXMHSyVjnKakPYqLXeyda/dceCM\naqx5KFVyiJV+fcnqJgjgjbOLnE4+Xaz3PcSrYOfThSPgAvQBeOWk9vh0Wmuf\nMHpoONZt7NSI3ZBEbJuELpaFRO94bqaZgLqJIxDd8CiYmnbBQnOTWdzWFmCU\nQxFkwkOdQFE2oGkRRocRuWK2oD+aNegW48/tR2ckIp8apKOz9xGyL7suWmdO\n6RjCoq+9LFQJLb4q3J+ebRKD4GP0vXWHbNr/pEEPdgJI3X3q0s3jn5ig2gvU\nLj/oEkXoiPgeOvqX2n53tRtWen8Zw/hRtk06tMBUPzdg5LbGON24F7aklIrU\nCdMZwtbk/LwKXU+8bJlhTl63YB8+qoYKC7WnCqxcTvFyxZobT7FPTnGSO0wE\nHGT/\r\n=wnoR\r\n-----END PGP SIGNATURE-----\r\n"
29+
},
30+
"_npmUser": {
31+
"name": "isaacs",
32+
"email": "[email protected]"
33+
},
34+
"directories": {},
35+
"maintainers": [
36+
{
37+
"name": "isaacs",
38+
"email": "[email protected]"
39+
}
40+
],
41+
"_npmOperationalInternal": {
42+
"host": "s3://npm-registry-packages",
43+
"tmp": "tmp/conflicted-peer-optional-from-dev-dep-b_1.0.0_1613008191679_0.24514239505031288"
44+
},
45+
"_hasShrinkwrap": false
46+
}
47+
},
48+
"time": {
49+
"created": "2021-02-11T01:49:51.620Z",
50+
"1.0.0": "2021-02-11T01:49:51.832Z",
51+
"modified": "2021-02-11T01:49:54.757Z"
52+
},
53+
"maintainers": [
54+
{
55+
"name": "isaacs",
56+
"email": "[email protected]"
57+
}
58+
],
59+
"readme": "ERROR: No README data found!",
60+
"readmeFilename": ""
61+
}

0 commit comments

Comments
 (0)