-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Is there an existing issue for this?
- I have searched the existing issues
This issue exists in the latest npm version
- I am using the latest npm
Current Behavior
When using --install-strategy=linked, updating or removing a dependency leaves the old .store/ directory behind. Each install creates a new store entry keyed by name@version-<treeHash>, but the previous one is never cleaned up. Over time, node_modules/.store/ accumulates orphaned directories that waste disk space.
This is particularly wasteful because each store entry contains the package and all of its transitive dependencies as nested directories. A single orphaned entry for something like express can be a significant directory tree.
The problem occurs when:
- A dependency is upgraded (e.g. chalk@4 -> chalk@5)
- A dependency is removed from package.json
- The dependency tree is restructured (same version but different
treeHash)
Expected Behavior
Old .store/ entries that are no longer referenced by any symlink should be removed during install, similar to how the hoisted and shallow strategies remove packages that are no longer needed.
Steps To Reproduce
mkdir /tmp/test-orphan && cd /tmp/test-orphan
npm init -y
# First install
npm install chalk@4 --install-strategy=linked
ls node_modules/.store/
# Shows: chalk@4.1.2-<hash>/ and transitive dep entries
# Update to new version
npm install chalk@5 --install-strategy=linked
ls node_modules/.store/
# Shows both: chalk@4.1.2-<oldhash>/ (orphan) AND chalk@5.4.1-<newhash>/
# The old entry is never cleaned up, even with explicit prune
npm prune --install-strategy=linked
ls node_modules/.store/
# chalk@4.1.2-<oldhash>/ is still thereAdditional Context
Related to #6100. The hoisted and shallow strategies don't have this problem because loadActual() can see all packages as direct children of root, so the diff correctly produces REMOVE actions for packages no longer needed. The linked strategy's flat .store/ layout is invisible to the diff's allChildren() traversal of the actual tree, so orphaned entries never get REMOVE actions.
Environment
- npm: 11.11.0
- Node.js: v22.20.0
- OS Name: macOS