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