Skip to content

fix(arborist): clean up orphaned scoped store entries in linked strategy#9441

Merged
owlstronaut merged 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-orphan-scoped-store-entries
Jun 3, 2026
Merged

fix(arborist): clean up orphaned scoped store entries in linked strategy#9441
owlstronaut merged 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-orphan-scoped-store-entries

Conversation

@manzoorwanijk
Copy link
Copy Markdown
Contributor

In continuation of our exploration of using install-strategy=linked in the Gutenberg monorepo, which powers the WordPress Block Editor.

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.
The store key itself spans two path segments.

When a scoped dependency is updated or removed, the orphan-cleanup pass in #cleanOrphanedStoreEntries failed to remove the stale store entry, so the store accumulated dead scoped entries on every update.
This never happened for unscoped packages.

The root cause is that the cleanup made a single-segment assumption in two places.
When collecting valid keys from the ideal tree, loc.split('/')[2] returned just @scope instead of the full @scope/pkg@version-hash key.
When enumerating on-disk entries, the non-recursive readdir(storeDir) returned the @scope directory as a single entry and never descended into the per-version subdirectories inside it.
The orphan filter then compared @scope on disk against @scope in the valid set, so the scope directory was always considered valid and never swept, no matter how many stale versions it contained.

Fixed by reconstructing the full two-segment key for scoped entries when collecting valid keys, and by descending one level into each @scope directory when enumerating on-disk entries so the real per-version entries are listed and compared as full keys.
Both sides are compared in forward-slash form, matching how ideal-tree locations are already normalized, so resolve reconstructs the on-disk path correctly on POSIX and Windows.

Removing the last scoped orphan under a scope would leave an empty @scope directory behind, so after removing orphans the pass now prunes any scope directory that has become empty.

References

Fixes #9440

@manzoorwanijk manzoorwanijk marked this pull request as ready for review May 30, 2026 09:42
@manzoorwanijk manzoorwanijk requested review from a team as code owners May 30, 2026 09:43
@manzoorwanijk manzoorwanijk force-pushed the fix/linked-orphan-scoped-store-entries branch from 6fd575b to 9a7e31e Compare June 2, 2026 04:54
@owlstronaut owlstronaut merged commit 275bc69 into npm:latest Jun 3, 2026
18 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

🎉 Backport to release/v11 created: #9484

@manzoorwanijk manzoorwanijk deleted the fix/linked-orphan-scoped-store-entries branch June 4, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Orphaned scoped store entries are not cleaned up under install-strategy=linked

2 participants