From b9083535669dddedec0b58d9a15ba95457cfe5b3 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Wed, 15 Apr 2026 19:04:30 +0100 Subject: [PATCH] fix(arborist): do not install inert optional extraneous shared dependencies (#9221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shared dependency of two or more umet optional dependencies, where no other package in the tree also depends on that shared package, does not need to be installed. This removes packages currently marked as extraneous from trees where one or more failed-optional (platform-specific) packages share a transitive dependency, with `@emnapi/runtime` being a good example of this. Before: ``` $ node /path/to/cli/bin/npm-cli.js install sharp@0.35.0-rc.2 added 9 packages in 325ms $ npm ls --all ├─┬ @emnapi/runtime@1.9.2 extraneous │ └── tslib@2.8.1 deduped ├─┬ @img/sharp-wasm32@0.35.0-rc.2 extraneous │ └── @emnapi/runtime@1.9.2 deduped ├─┬ sharp@0.35.0-rc.2 │ ├── @img/colour@1.1.0 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-darwin-arm64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-darwin-x64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-freebsd-wasm32@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-darwin-arm64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-darwin-x64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-arm@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-arm64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-ppc64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-riscv64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-s390x@1.3.0-rc.4 │ ├── @img/sharp-libvips-linux-x64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linuxmusl-arm64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linuxmusl-x64@1.3.0-rc.4 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-arm@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-arm64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-ppc64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-riscv64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-s390x@0.35.0-rc.2 │ ├─┬ @img/sharp-linux-x64@0.35.0-rc.2 │ │ └── @img/sharp-libvips-linux-x64@1.3.0-rc.4 deduped │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linuxmusl-arm64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linuxmusl-x64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-webcontainers-wasm32@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-arm64@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-ia32@0.35.0-rc.2 │ ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-x64@0.35.0-rc.2 │ ├── detect-libc@2.1.2 │ └── semver@7.7.4 └── tslib@2.8.1 extraneous $ du -s node_modules/ 28824 node_modules/ ``` After: ``` $ node /path/to/cli/bin/npm-cli.js install sharp@0.35.0-rc.2 added 6 packages in 1s $ npm ls --all └─┬ sharp@0.35.0-rc.2 ├── @img/colour@1.1.0 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-darwin-arm64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-darwin-x64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-freebsd-wasm32@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-darwin-arm64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-darwin-x64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-arm@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-arm64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-ppc64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-riscv64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linux-s390x@1.3.0-rc.4 ├── @img/sharp-libvips-linux-x64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linuxmusl-arm64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-libvips-linuxmusl-x64@1.3.0-rc.4 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-arm@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-arm64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-ppc64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-riscv64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linux-s390x@0.35.0-rc.2 ├─┬ @img/sharp-linux-x64@0.35.0-rc.2 │ └── @img/sharp-libvips-linux-x64@1.3.0-rc.4 deduped ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linuxmusl-arm64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-linuxmusl-x64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-webcontainers-wasm32@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-arm64@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-ia32@0.35.0-rc.2 ├── UNMET OPTIONAL DEPENDENCY @img/sharp-win32-x64@0.35.0-rc.2 ├── detect-libc@2.1.2 └── semver@7.7.4 $ du -s node_modules/ 18336 node_modules/ ``` ## References Fixes https://github.com/npm/cli/issues/8832 (cherry picked from commit 1d058b0cc7161fa728cba2020a265a81e0ec7cdf) --- workspaces/arborist/lib/optional-set.js | 2 +- workspaces/arborist/test/optional-set.js | 41 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/optional-set.js b/workspaces/arborist/lib/optional-set.js index 021a0ef72aa17..891961fe2cd95 100644 --- a/workspaces/arborist/lib/optional-set.js +++ b/workspaces/arborist/lib/optional-set.js @@ -26,7 +26,7 @@ const optionalSet = node => { // now that we've hit the boundary, gather the rest of the nodes in // the optional section that don't have dependents outside the set. - return gatherDepSet(set, edge => !set.has(edge.to)) + return gatherDepSet(set, edge => !set.has(edge.to) && !edge.from?.inert) } module.exports = optionalSet diff --git a/workspaces/arborist/test/optional-set.js b/workspaces/arborist/test/optional-set.js index cf40bd382af1c..27719254eae06 100644 --- a/workspaces/arborist/test/optional-set.js +++ b/workspaces/arborist/test/optional-set.js @@ -89,3 +89,44 @@ t.equal(setM.has(nodeN), true, 'set m includes n') const setB = optionalSet(nodeB) t.equal(setB.size, 1, 'gathering from b is only b') t.equal(setB.has(nodeB), true, 'set b includes b') + +// tree (OPT opt-p, OPT opt-q) +// +-- OPT opt-p (PROD shared-dep) +// +-- OPT opt-q (PROD shared-dep) +// +-- shared-dep () +const sharedTree = new Node({ + path: '/path/to/shared-tree', + pkg: { + optionalDependencies: { + 'opt-p': '', + 'opt-q': '', + }, + }, + children: [ + { pkg: { name: 'opt-p', version: '1.0.0', dependencies: { 'shared-dep': '' } } }, + { pkg: { name: 'opt-q', version: '1.0.0', dependencies: { 'shared-dep': '' } } }, + { pkg: { name: 'shared-dep', version: '1.0.0' } }, + ], +}) + +calcDepFlags(sharedTree) + +const nodeOptP = sharedTree.children.get('opt-p') +const nodeOptQ = sharedTree.children.get('opt-q') +const nodeSharedDep = sharedTree.children.get('shared-dep') + +// Simulate opt-p failing platform check and being marked inert first +const setOptP = optionalSet(nodeOptP) +// shared-dep is excluded because opt-q (not yet inert) also depends on it +t.equal(setOptP.has(nodeOptP), true, 'set opt-p includes opt-p') +t.equal(setOptP.has(nodeSharedDep), false, 'set opt-p excludes shared-dep (opt-q is not inert)') +for (const n of setOptP) { + n.inert = true +} + +// Simulate opt-q failing platform check second (opt-p is already inert) +const setOptQ = optionalSet(nodeOptQ) +// shared-dep now has no active external dependents and is included +t.equal(setOptQ.has(nodeOptQ), true, 'set opt-q includes opt-q') +t.equal(setOptQ.has(nodeSharedDep), true, 'set opt-q includes shared-dep (opt-p is inert)') +t.equal(setOptQ.size, 2, 'set opt-q has two nodes: opt-q and shared-dep')