SIGN IN SIGN UP

fix(arborist): invalid filterNode crash under the linked strategy (#9637)

In continuation of our exploration of using `install-strategy=linked` in
the [Gutenberg
monorepo](https://github.com/WordPress/gutenberg/pull/75814), which
powers the WordPress Block Editor.

Under `install-strategy=linked`, several common installs failed with
`npm error invalid filterNode: outside idealTree/actualTree`, with no
workaround besides dropping the linked strategy. Hoisted handled all of
them. This fixes two distinct paths that both produced that error.

## Why

A linked reify diffs the ideal tree against a synthesized actual wrapper
(`#linkedActualForDiff`) rather than `this.actualTree`. `Diff.calculate`
rejects any filter node whose root is neither the ideal nor the actual
it was given, so a filter node taken from the real `this.actualTree` is
"outside" the diff and throws.

Two places fed it such nodes:

- `--workspaces=false` and `-w <ws> --include-workspace-root` go through
the `includeRootDeps` branch of `_diffTrees()`, which collected root-dep
edge targets from both `this.idealTree` and `this.actualTree`. The
actual-side targets are rooted at the real actual tree, not the wrapper,
so they tripped the guard. The sibling `includeWorkspaces` branch
already accounted for this; the root-dep branch did not.

- A global install with a per-call `installStrategy: 'linked'`
re-engaged the linked path even though the constructor normalizes global
installs to `shallow` (the linked layout is unsupported for globals).
Re-installing an already-present global package then hit the global
explicit-request branch, which pushes actual-side nodes, and tripped the
same guard. Suppressing the crash there was worse: the isolated reifier
does not materialize the global layout and removed the package instead.

## How

`_diffTrees()` now iterates only the ideal tree for root-dep filter
nodes when the linked wrapper is in use, matching the existing
workspace-node handling. The ideal-side nodes are sufficient to scope
the diff, and the post-reify orphan sweep continues to prune deps
removed from the manifest.

`reify()` now honors the constructor's global-to-shallow normalization
when deriving the `linked` flag, so a global install never engages the
linked path regardless of a per-call `installStrategy`. Global installs
fall back to shallow, which materializes and upgrades packages
correctly. No change to the global explicit-request branch is needed
once global is never linked.

## References

Fixes #9614
Part of #9608
M
Manzoor Wani committed
2b976b521e6cf3804a79bd99d17757358d8bfa6b
Parent: 0ffce98
Committed by GitHub <noreply@github.com> on 6/24/2026, 6:40:47 PM