Skip to content

fix: correct dev/prod dep flags for workspaces under the linked strategy - #9666

Merged
owlstronaut merged 1 commit into
release/v11from
backport/v11/9655
Jun 25, 2026
Merged

fix: correct dev/prod dep flags for workspaces under the linked strategy#9666
owlstronaut merged 1 commit into
release/v11from
backport/v11/9655

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Backport of #9655 to release/v11.

…nked strategy (#9655)

In continuation of our exploration of using `install-strategy=linked` in
the [Gutenberg
monorepo](WordPress/gutenberg#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

(cherry picked from commit f9e3a80)
@github-actions
github-actions Bot requested review from a team as code owners June 25, 2026 18:06
@owlstronaut owlstronaut changed the title fix(arborist): correct dev/prod dep flags for workspaces under the linked strategy Jun 25, 2026
@owlstronaut
owlstronaut merged commit f503b07 into release/v11 Jun 25, 2026
21 of 34 checks passed
@owlstronaut
owlstronaut deleted the backport/v11/9655 branch June 25, 2026 18:11
@github-actions github-actions Bot mentioned this pull request Jun 25, 2026
owlstronaut pushed a commit that referenced this pull request Jun 25, 2026
The `ls --install-strategy=linked` tests for undeclared and declared-but-missing workspaces passed without ever entering the undeclared-workspace branch of `filterLinkedStrategyEdges` in lib/commands/ls.js, because their mock filesystems omitted the hidden `node_modules/.package-lock.json` that a real linked install writes. Without that hidden lockfile, loadActual resolves the workspace root edges via the workspace globs instead of marking them missing, so the `edge.missing` guard is never satisfied and the branch stays uncovered.

This surfaced as a global coverage gate failure (ls.js branch below 100%) on the first PR after the actual-tree workspace changes in #9666, because the Test matrix only runs on PRs and not on direct pushes to release/v11.

Add the hidden lockfile to both tests so they reproduce a real linked install: the undeclared workspace now resolves as a missing root edge and is correctly skipped, and the declared-but-missing workspace resolves as missing and is still reported as UNMET DEPENDENCY. This exercises both branches and restores 100% coverage. No production code changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants