Skip to content

fix(arborist): record the linked .store layout in the hidden lockfile#9630

Merged
owlstronaut merged 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-hidden-lockfile-store-layout
Jun 24, 2026
Merged

fix(arborist): record the linked .store layout in the hidden lockfile#9630
owlstronaut merged 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-hidden-lockfile-store-layout

Conversation

@manzoorwanijk

@manzoorwanijk manzoorwanijk commented Jun 24, 2026

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, the hidden lockfile node_modules/.package-lock.json recorded the hoisted logical layout (node_modules/<pkg>) instead of the actual on-disk .store/symlink layout. The hidden lockfile is meant to cache what loadActual() finds on disk so the actual tree can be validated cheaply, but because it recorded the wrong layout it was rejected on every reload, so it never served as a cache and misrepresented the installed layout.

Why

A linked reify swaps idealTree for the isolated tree, materializes the .store/symlink layout, then swaps the logical tree back before saving. The hidden lockfile was serialized from that logical tree, so it listed packages at their hoisted paths. On the next load, assertNoNewer() walked the real node_modules (the root symlink plus .store/) and could not reconcile it with the hoisted entries, throwing missing from lockfile, so loadActual() always fell back to a full filesystem scan.

How

reify.js serializes the hidden lockfile from the isolated tree, which mirrors the on-disk layout, while package-lock.json still comes from the logical tree. It records every store package directory and symlink, adds an entry for each .store/<key> container directory (these are the fsParents loadVirtual() needs so a store package can resolve its sibling deps), includes the workspace directories, and skips tree-only undeclared-workspace self-links that are never materialized on disk.

assertNoNewer() additionally validates the directories the plain node_modules walk cannot reach under the linked strategy: a store package's deps live as symlinked siblings under .store/<key>/node_modules (and .store is skipped as a dot-dir), and an undeclared workspace is not symlinked into the root node_modules at all. These directories are derived from the lockfile entries. A workspace directory is only walked when it is not the target of a link entry, so the hoisted strategy keeps its existing, stricter validation unchanged — a stale workspace symlink that points at the wrong target still surfaces as a missing entry and rejects the cache.

References

Fixes #9612
Part of #9608

@manzoorwanijk manzoorwanijk force-pushed the fix/linked-hidden-lockfile-store-layout branch from f6abc1b to 658c010 Compare June 24, 2026 13:29
@manzoorwanijk manzoorwanijk marked this pull request as ready for review June 24, 2026 13:33
@manzoorwanijk manzoorwanijk requested review from a team as code owners June 24, 2026 13:33
@manzoorwanijk manzoorwanijk force-pushed the fix/linked-hidden-lockfile-store-layout branch from 658c010 to f723efa Compare June 24, 2026 14:36
@owlstronaut owlstronaut merged commit 6968015 into npm:latest Jun 24, 2026
23 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Backport to release/v11 failed.

This usually means the cherry-pick had conflicts. Please create a manual backport:

git fetch origin release/v11
git checkout -b backport/v11/9630 origin/release/v11
git cherry-pick -x 696801574984ad19ffaa9a7200d7e752920a018d
# resolve any conflicts, then:
git push origin backport/v11/9630
Error details
Command failed: git cherry-pick -x 696801574984ad19ffaa9a7200d7e752920a018d
error: could not apply 696801574... fix(arborist): record the linked .store layout in the hidden lockfile (#9630)
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"

@manzoorwanijk manzoorwanijk deleted the fix/linked-hidden-lockfile-store-layout branch June 24, 2026 17:49
@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

Creating a manual backport for this...

@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

Here you go #9642

owlstronaut pushed a commit that referenced this pull request Jun 24, 2026
… (backport #9630) (#9642)

Backport of #9630 to `release/v11`.

Under `install-strategy=linked`, the hidden lockfile
`node_modules/.package-lock.json` recorded the hoisted logical layout
instead of the on-disk `.store`/symlink layout, so it was rejected on
every reload and never served as the actual-tree cache it is meant to
be.

## How

`reify.js` serializes the hidden lockfile from the isolated tree, which
mirrors the on-disk layout, while `package-lock.json` still comes from
the logical tree. `assertNoNewer()` additionally validates the store
package node_modules and undeclared-workspace subtrees that the plain
`node_modules` walk cannot reach under the linked strategy, gated so the
hoisted strategy keeps its existing, stricter validation.

The original commit's change to `test/arborist/reify-npm-extension.js`
was dropped because the `.npm-extension` feature does not exist on
`release/v11`.

## References

Backport of #9630
Part of #9608
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] install-strategy=linked: hidden lockfile (node_modules/.package-lock.json) records the hoisted layout, not the linked layout

2 participants