From d397d60f51c6735781ec6487690c036ed90d7ec6 Mon Sep 17 00:00:00 2001 From: Milan Meva Date: Mon, 21 Jul 2025 15:22:16 -0400 Subject: [PATCH 1/2] fix: peer edge crash due to no parent or detached node --- .../arborist/lib/arborist/build-ideal-tree.js | 5 + .../arborist/build-ideal-tree.js.test.cjs | 309 ++++++++++++++++++ .../test/arborist/build-ideal-tree.js | 10 + .../registry-mocks/content/test/a.json | 146 +++++++++ .../registry-mocks/content/test/a/a-1.0.0.tgz | Bin 0 -> 784 bytes .../registry-mocks/content/test/a/a-1.1.0.tgz | Bin 0 -> 346 bytes .../registry-mocks/content/test/b.json | 95 ++++++ .../registry-mocks/content/test/b/b-1.0.0.tgz | Bin 0 -> 277 bytes .../registry-mocks/content/test/b/b-1.1.0.tgz | Bin 0 -> 277 bytes .../registry-mocks/content/test/c.json | 95 ++++++ .../registry-mocks/content/test/c/c-1.0.0.tgz | Bin 0 -> 276 bytes .../registry-mocks/content/test/c/c-1.1.0.tgz | Bin 0 -> 276 bytes 12 files changed, 660 insertions(+) create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.1.0.tgz create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/b.json create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.0.0.tgz create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz create mode 100644 workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 1edd0b643b60d..5b2e4c83db110 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -1306,6 +1306,11 @@ This is a one-time fix-up, please be patient... .sort(({ name: a }, { name: b }) => localeCompare(a, b)) for (const edge of peerEdges) { + // if node is detached/removed from the tree, or has no parent, + // then we can't place the peer dep, so skip it. + if (!node.parent) { + break + } // already placed this one, and we're happy with it. if (edge.valid && edge.to) { continue diff --git a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs index 855539521b9df..f76c505e89ff2 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs @@ -74672,6 +74672,315 @@ exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts metadeps Array [] ` +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > default result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > force result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + +exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts peerDep replacement of top level dep with different version resulting detached top level dep > strict result 1`] = ` +ArboristNode { + "children": Map { + "@test/a" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/a", + "spec": "^1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/b", + "name": "@test/a", + "spec": "1.1.0", + "type": "peer", + }, + }, + "edgesOut": Map { + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "peerOptional", + }, + "@test/c" => EdgeOut { + "name": "@test/c", + "spec": "1.1.0", + "to": null, + "type": "peerOptional", + }, + "lodash" => EdgeOut { + "name": "lodash", + "spec": "^4.17.0", + "to": null, + "type": "peerOptional", + }, + "uniq" => EdgeOut { + "name": "uniq", + "spec": "^1.0.0", + "to": null, + "type": "peerOptional", + }, + }, + "location": "node_modules/@test/a", + "name": "@test/a", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/a", + "resolved": "http://localhost:4873/@test/a/-/a-1.1.0.tgz", + "version": "1.1.0", + }, + "@test/b" => ArboristNode { + "dev": true, + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "@test/b", + "spec": "1.1.0", + "type": "dev", + }, + EdgeIn { + "from": "node_modules/@test/a", + "name": "@test/b", + "spec": "1.1.0", + "type": "peerOptional", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "1.1.0", + "to": "node_modules/@test/a", + "type": "peer", + }, + }, + "location": "node_modules/@test/b", + "name": "@test/b", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep/node_modules/@test/b", + "resolved": "http://localhost:4873/@test/b/-/b-1.1.0.tgz", + "version": "1.1.0", + }, + }, + "edgesOut": Map { + "@test/a" => EdgeOut { + "name": "@test/a", + "spec": "^1.1.0", + "to": "node_modules/@test/a", + "type": "dev", + }, + "@test/b" => EdgeOut { + "name": "@test/b", + "spec": "1.1.0", + "to": "node_modules/@test/b", + "type": "dev", + }, + }, + "isProjectRoot": true, + "location": "", + "name": "tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", + "path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-more-peer-dep-conflicts-peerDep-replacement-of-top-level-dep-with-different-version-resulting-detached-top-level-dep", +} +` + exports[`test/arborist/build-ideal-tree.js TAP more peer dep conflicts prod dep directly on conflicted peer, full peer set, newer > force result 1`] = ` ArboristNode { "children": Map { diff --git a/workspaces/arborist/test/arborist/build-ideal-tree.js b/workspaces/arborist/test/arborist/build-ideal-tree.js index 32bc6b25ed39c..24961be79ac32 100644 --- a/workspaces/arborist/test/arborist/build-ideal-tree.js +++ b/workspaces/arborist/test/arborist/build-ideal-tree.js @@ -1655,6 +1655,16 @@ t.test('more peer dep conflicts', async t => { error: false, resolvable: true, }, + 'peerDep replacement of top level dep with different version resulting detached top level dep': { + pkg: { + description: 'a@ -> (PeerOptional(b, c, dep, dep)) b -> ( Peer(a) ) c -> ( Peer(a) )', + devDependencies: { + '@test/a': '^1.1.0', + '@test/b': '1.1.0', + }, + }, + error: false, + resolvable: true }, }) createRegistry(t, true) diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json new file mode 100644 index 0000000000000..94cd8577573fb --- /dev/null +++ b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a.json @@ -0,0 +1,146 @@ +{ + "name": "@test/a", + "versions": { + "1.2.0": { + "name": "@test/a", + "version": "1.2.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.21", + "underscore": "^1.13.1", + "@test/b": "1.2.0", + "@test/c": "1.2.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "underscore": { + "optional": true + } + }, + "_id": "@test/a@1.2.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-k7WYu8tdQY1aq8QV+7YEGcoSYXrdCACqnabuvNC8Tpwvpk/MF25CeX4nei6eliVaHqHxzNzAr60ne2TgsEoz2Q==", + "shasum": "b609076c847a018b144ab68c953817847195535a", + "tarball": "http://localhost:4873/@test/a/-/a-1.2.0.tgz" + }, + "contributors": [] + }, + "1.1.0": { + "name": "@test/a", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.0", + "uniq": "^1.0.0", + "@test/b": "1.1.0", + "@test/c": "1.1.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "uniq": { + "optional": true + } + }, + "_id": "@test/a@1.1.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-qlfAcmAKeohHKBVVAnwsiDs+URz5jCPYlXe+srdxX6Nzhl9W6FX9kV5Lm6XahBhB+H/c+eRi+ghAE8YcdzmFIA==", + "shasum": "0d9b53f67e05d388195ad096f61fe2c1c6f0ff8d", + "tarball": "http://localhost:4873/@test/a/-/a-1.1.0.tgz" + }, + "contributors": [] + }, + "1.0.0": { + "name": "@test/a", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "lodash": "^4.17.0", + "uniq": "^1.0.0", + "@test/b": "1.0.0", + "@test/c": "1.0.0" + }, + "peerDependenciesMeta": { + "@test/b": { + "optional": true + }, + "@test/c": { + "optional": true + }, + "lodash": { + "optional": true + }, + "uniq": { + "optional": true + } + }, + "_id": "@test/a@1.0.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-BRD01XQTy4WW2PrMdV0ZvHdqlY6v0FY3kyvEIv4v0n7apOHQwuQqjdL4iWnApfEwD0o0mVSbQs5s6DibNmDnMg==", + "shasum": "a1ec39760cf04261fff44b23582f1bafba0b14ff", + "tarball": "http://localhost:4873/@test/a/-/a-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-07-31T16:24:31.780Z", + "created": "2025-07-29T12:59:32.758Z", + "1.2.0": "2025-07-29T13:15:20.477Z", + "1.1.0": "2025-07-31T16:24:09.634Z", + "1.0.0": "2025-07-31T16:24:31.780Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_rev": "44-1c1667b80cb416cc", + "_id": "@test/a", + "readme": "ERROR: No README data found!", + "_attachments": {} +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/a/a-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..00df7811e9df7c50ee5af3ab371d4e9ec110dfe5 GIT binary patch literal 784 zcmV+r1MmDFiwFP!00002|0_sL&Q45E)h~e1dRfK!c??tr1_lOZCMF8l#0?A#3=IuT z6c~(6%?u0;&CEb-14Bbo6C(u%0|q*P(&CcDA_fBkDtOYsz(4^`4-rt!rJ$gcmzbNX zWTl|wP?B0)qMxXw0}?DtEh^5;&jX1Y>KW)6Ktxkgi<65o3re6e5W(EUOfWAqFD11? zFRK_L3Q<_BWTgPIL_t9bWI9M$YH~)tLX47YQBi)8l|o*=0!TukxF9t-Gc7YUB}Pd> zO-&)SBC|xnP>D-Hp%!dfc4}pLeo+d@xae50NMdP8Mt%{(6*-y7sd>dZS(1qG$jyv#zd zFd{4zU;&nd8b)yOWP~`}8|YU1rj{fktVfs!5l~Q2$_IyaVvdrPLP=3+Dp_PDV{r0rO);-YWrioNo~=CnXH}oWtW`$8k~hsd5f|XQ zOGQ_-R;n=3>*2D8{l9gdE}wmcVa>%?mPzxH+;6wq*4(`~?Q~y$a!r1%Z(PaS-Br2_ z32N`6bRrnWkLc51|~)dl5q~|wy}fl zKr7Vmj_j0%e?lE9q)K@ioc#Q9ckgN;=i-S*b$g~WXLPb>A!43o@Eb=&&M|}JWSk@C zc^=-8^Ko_z2`2xb#(7~A#65&WMA)7OqPGD6)1o3)06X*K{7A5)0AI*DWpv;?5`V|%(b*NI#xhv0>DD`U?Vv-FkzS08e4!i5H7&gB-Nv$a>8IZ z1X?N&oDBfhvD%zguf~?4-0fW?qVZE>yD1h*l6Eo3)y;X!{Hl&XnyNDTW3rmaUQkWC zBrO$v8gXGt;iln_`z+DU9u@CB&*p}@= zE7bpv90O%wtdKx?%gOIf_hB#9Mn2Kpmsqc(v+TqmV%IeAlg~8GIj}|Bp%7h%4KNg< zZ5P0h{Q}#VWMGIVI1&-zEB+DQDgbWfK)eTjktnA513%01g+esWW-n^2u~@64 zrpQ5>@nx*0UA>OQmcqz;$U^{l?w-p?tsE@*bqFr>U>)QHL{G{*8q!M+vl-CNBoMp; zIE>vkw10I$=XQ7ZqmkRRa^bkevr%N@c*xD|WpQcmN1@y>IQzMoCkj{O$!fC7(6<%K b56I)ud46@M*u=!d#DC)*^d^D^00;m8&#;E< literal 0 HcmV?d00001 diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/b/b-1.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..96f29aea98300ef7dbeda715145d09dbae29c457 GIT binary patch literal 277 zcmV+w0qXuAiwFP!00002|Lu@HPs1<}Kt1PIoIG?w=~+#JlnDtKm>DU9u@CB&*p}@= zE7bpv90O%wtdKx?%gOIf_hB#9Mn2Kpmsqc(v+TqmV%IeAlg~8GIj}|Bp%7h%4KNg< zZ5P0h{Q}#VWMGIVI1&-zEB+DQDgbWfK)eTjktnA513%01g+esWW-n?{W3if|3dX08 zOp${$kwS%!8*tZh@O;rG^CdtW;39jNg#Ly za2UI7X#eVh&h75*MHde bACSkR^Ze>iv5AR^iT}nsf^k%y00;m8(>{ls literal 0 HcmV?d00001 diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json new file mode 100644 index 0000000000000..e26765f61e416 --- /dev/null +++ b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c.json @@ -0,0 +1,95 @@ +{ + "name": "@test/c", + "versions": { + "1.0.0": { + "name": "@test/c", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.0.0" + }, + "_id": "@test/c@1.0.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-ikGDvMXxzqHgCkIycVNWmpfDs6G/aA7i3AY1Or+T+hO+2G/t+rfIxnLgIc4p10K7GM2ZxcipK9Z7U6LAtTO0iw==", + "shasum": "96b5a6fa92f8713c240686e9f3dfd5c00df7497e", + "tarball": "http://localhost:4873/@test/c/-/c-1.0.0.tgz" + }, + "contributors": [] + }, + "1.1.0": { + "name": "@test/c", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.1.0" + }, + "_id": "@test/c@1.1.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-BNxNmwGwAhVxA8RQpog/wy/NNZfa5ruskwZePlKfu1zpLVtsrjO8zGau6C/c8iIw9mwrVqBAeBuFpUwJhLTAZA==", + "shasum": "01db72391f551fd7944adbf0f54eaebc389b90c4", + "tarball": "http://localhost:4873/@test/c/-/c-1.1.0.tgz" + }, + "contributors": [] + }, + "1.2.0": { + "name": "@test/c", + "version": "1.2.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "peerDependencies": { + "@test/a": "1.2.0" + }, + "_id": "@test/c@1.2.0", + "_nodeVersion": "22.14.0", + "_npmVersion": "11.4.2", + "dist": { + "integrity": "sha512-pAHdEr8mb8mXuWPQL0mbkyHVulhzVWQ3HvpO9OBZ0azF56p9cr1+hVy/CajxPdEr/Crx6iBfjpoaNYscVPbvMg==", + "shasum": "e915f26882f8bbde7e238c02908bbc5625cf3c4e", + "tarball": "http://localhost:4873/@test/c/-/c-1.2.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2025-07-29T13:03:42.407Z", + "created": "2025-07-29T13:03:29.009Z", + "1.0.0": "2025-07-29T13:03:29.009Z", + "1.1.0": "2025-07-29T13:03:36.559Z", + "1.2.0": "2025-07-29T13:03:42.407Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.2.0" + }, + "_rev": "9-19d0ff85a20ff576", + "_id": "@test/c", + "readme": "ERROR: No README data found!", + "_attachments": {} +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..65661f1328219064d1afe678f6b785d960e62b77 GIT binary patch literal 276 zcmV+v0qg!BiwFP!00002|Lu@HPs1<}Kt1PIoIG?w=~*NhGF1WwW=6_j*#~t?Y|D0` z70Q1{j)5{TR!E?{<>Ys#`>>a4C!c8PORTriS$1X+v1=Om$!D6T1$NnXC`8v`0}O>| zTLBE&FR+hE28MWsBM}k4;veCy0^n8-#CzbYL@_NDzsT~1LNv~1FKVo@SWQs{Ytj3DX%*@REKi&b+;}wMf2mk=TU4-@k literal 0 HcmV?d00001 diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/test/c/c-1.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..cdbee7603400109631258f080e603f707b094414 GIT binary patch literal 276 zcmV+v0qg!BiwFP!00002|Lu@HPs1<}Kt1PIoIG?w=~*NhGF1WwW=6_j*#~t?Y|D0` z70Q1{j)5{TR!E?{<>Ys#`>>a4C!c8PORTriS$1X+v1=Om$!D6T1$NnXC`8v`0}O>| zTLBE&FR+hE28MWsBM}k4;veCy0^n8-#CzbYL@_NDzsT~1LNv~1FKSU^v6`X^#;1== zk%KhT%UDf^dK-%^g^~A=hXC;0J(rQ%I9T(W5M1cNI>-r#o|Jhsq}Lo43!sBZAb15Z zPTh8Ne04$RcJ~jHk^8i9;k3oGQDozE$nD)#ap~wMq1-Sy`?;AX3fJVxYO>1Ew-w6| a$kWl)SdA5%nVFgSf4l>yw8I?$2mk;q)`Cd@ literal 0 HcmV?d00001 From a679c4d3289a180c517dbda3eab0165f2d03089c Mon Sep 17 00:00:00 2001 From: Milan Meva Date: Thu, 31 Jul 2025 16:37:05 -0400 Subject: [PATCH 2/2] add comment about the behaviour --- workspaces/arborist/lib/arborist/build-ideal-tree.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 5b2e4c83db110..c0f31008d6496 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -1306,8 +1306,9 @@ This is a one-time fix-up, please be patient... .sort(({ name: a }, { name: b }) => localeCompare(a, b)) for (const edge of peerEdges) { - // if node is detached/removed from the tree, or has no parent, - // then we can't place the peer dep, so skip it. + // node.parent gets mutated during loop execution due to recursive #nodeFromEdge calls. + // When a compatible peer is found (e.g. a@1.1.0 replaces a@1.2.0), the original node loses its parent. + // if node is detached/removed from the tree, or has no parent, so no need to check remaining edgesOut for that node. if (!node.parent) { break }