Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
Found while testing #9439.
Under --install-strategy=linked, each external package is extracted into a content-addressed store entry keyed by name@version-hash.
For scoped packages the key contains the unescaped scope slash, so the entry nests one extra directory level: node_modules/.store/@scope/pkg@version-hash/node_modules/@scope/pkg.
When a scoped dependency is updated (or otherwise re-resolved to a new hash), the orphan-cleanup pass that runs after a linked install fails to remove the stale store entry.
The old node_modules/.store/@scope/pkg@oldversion-hash/ directory is left behind indefinitely.
The store accumulates dead scoped entries on every update, which never happens for unscoped packages.
Note this is specifically the cleanup granularity bug.
If the last package under a scope is orphaned, an empty @scope directory is also left behind.
Expected Behavior
After a linked install, store entries that are no longer referenced by the ideal tree should be removed, including scoped ones.
Updating @scope/pkg from one version to another should leave exactly one store entry for that package, just like the unscoped case.
Empty @scope directories should not be left behind in node_modules/.store/.
Steps To Reproduce
mkdir scoped-store-repro && cd scoped-store-repro
npm init -y
npm install @isaacs/cliui@8.0.2 --install-strategy=linked
ls node_modules/.store/@isaacs/
# cliui@8.0.2-<hash>
npm install @isaacs/cliui@9.0.0 --install-strategy=linked
ls node_modules/.store/@isaacs/
# cliui@8.0.2-<hash> ❌ stale entry still present
# cliui@9.0.0-<hash>
For contrast, an unscoped package is cleaned up correctly:
mkdir unscoped-store-repro && cd unscoped-store-repro
npm init -y
npm install abbrev@2.0.0 --install-strategy=linked
ls node_modules/.store/ | grep abbrev
# abbrev@2.0.0-<hash>
npm install abbrev@3.0.0 --install-strategy=linked
ls node_modules/.store/ | grep abbrev
# abbrev@3.0.0-<hash> ✅ old abbrev@2.0.0 entry removed
The same result happens if instead of installing a new version directly, the version is bumped in package.json and npm install --install-strategy=linked is re-run.
A regression test mirroring the existing unscoped test in workspaces/arborist/test/isolated-mode.js ("orphaned store entries are cleaned up on dependency update"), but using a scoped dependency, fails on the assertion that the old store entry is removed.
Environment
- npm: 11.16.0
- Node.js: v24.15.0
- OS Name: macOS (Darwin 25.5.0)
- System Model Name: Mac
- npm config:
install-strategy = "linked"
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
Found while testing #9439.
Under
--install-strategy=linked, each external package is extracted into a content-addressed store entry keyed byname@version-hash.For scoped packages the key contains the unescaped scope slash, so the entry nests one extra directory level:
node_modules/.store/@scope/pkg@version-hash/node_modules/@scope/pkg.When a scoped dependency is updated (or otherwise re-resolved to a new hash), the orphan-cleanup pass that runs after a linked install fails to remove the stale store entry.
The old
node_modules/.store/@scope/pkg@oldversion-hash/directory is left behind indefinitely.The store accumulates dead scoped entries on every update, which never happens for unscoped packages.
Note this is specifically the cleanup granularity bug.
If the last package under a scope is orphaned, an empty
@scopedirectory is also left behind.Expected Behavior
After a linked install, store entries that are no longer referenced by the ideal tree should be removed, including scoped ones.
Updating
@scope/pkgfrom one version to another should leave exactly one store entry for that package, just like the unscoped case.Empty
@scopedirectories should not be left behind innode_modules/.store/.Steps To Reproduce
For contrast, an unscoped package is cleaned up correctly:
The same result happens if instead of installing a new version directly, the version is bumped in
package.jsonandnpm install --install-strategy=linkedis re-run.A regression test mirroring the existing unscoped test in
workspaces/arborist/test/isolated-mode.js("orphaned store entries are cleaned up on dependency update"), but using a scoped dependency, fails on the assertion that the old store entry is removed.Environment