SIGN IN SIGN UP

fix(arborist): correct dev/prod dep flags for workspaces under the linked strategy (#9655)

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`, `npm query` reports the wrong
`dev`/`prod` flags for workspaces and their dependencies. In a workspace
project the entire non-root tree is flagged `dev`, so `:is(.prod)`
returns almost nothing and `:is(.dev)` returns almost everything — the
opposite of the hoisted strategy. This breaks tooling that classifies
dependencies via `npm query`, e.g. a license checker that selects
`.prod` dependencies.

## Why

Two compounding defects, both exercised only by the linked layout.

First, the linked strategy does not symlink undeclared workspaces into
the root's `node_modules`, so the root's `workspace` edges resolve to
`null`. `calcDepFlags` walks outward from the root via edges, dead-ends
immediately, and never reaches any workspace or its transitive deps,
leaving them at their default `dev=true`.

Second, the `node.isLink` branch in `calcDepFlags` assigned target flags
unconditionally (`target.dev = link.dev`), unlike every other flag in
that file which is only ever unset (true to false). When a target is
reachable through more than one link — the norm under linked, where each
workspace's own `node_modules` links to a shared target — the last link
visited could overwrite an already-correct `dev=false` back to `true`.

## How

Make the `calcDepFlags` link branch monotonic: only unset flags,
matching the edge walk below it, and queue the target on first visit so
its own deps are still walked. A target reachable through multiple links
now keeps the most permissive flags regardless of visit order.

In `loadActual`, when the install strategy is linked, synthesize the
missing root-to-workspace links from the already-loaded workspace
targets so the root's workspace edges resolve and flags propagate. The
synthesis is gated to linked because under hoisted an unresolved
workspace edge is a genuinely missing symlink that reify must recreate,
not synthesize. Workspaces already linked into the root `node_modules`
are skipped.

This targets the path used by `npm query` and non-lockfile `npm sbom`,
which force a filesystem read of the actual tree. Commands that load
from the hidden lockfile (`npm ls`, `npm outdated`, `npm audit
signatures`) are unchanged; their separate, pre-existing linked flag gap
is left for a follow-up.

## References

Fixes #9100
M
Manzoor Wani committed
f9e3a80a3bb8617bf4e8a9fef20c3ecd4f8d7928
Parent: 6a5bf26
Committed by GitHub <noreply@github.com> on 6/25/2026, 6:05:38 PM