Skip to content

fix(arborist): materialize installed transitive optional deps under linked#9660

Closed
manzoorwanijk wants to merge 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-transitive-optional-unmet
Closed

fix(arborist): materialize installed transitive optional deps under linked#9660
manzoorwanijk wants to merge 1 commit into
npm:latestfrom
manzoorwanijk:fix/linked-transitive-optional-unmet

Conversation

@manzoorwanijk

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, an installed transitive optional dependency (a non-root package's optionalDependencies entry, which lives as a sibling in node_modules/.store/<key>/node_modules/<dep>) was reported as UNMET OPTIONAL DEPENDENCY by npm ls and omitted from npm sbom, even though it is installed and resolves at runtime. Hoisted is unaffected, and a root-declared optional dep works because it is symlinked at the top level.

Why

loadActual resolves store siblings through #findMissingEdges(), which walks up ancestor node_modules for edges that still need a target. It skipped any edge where notMissing was true. An unresolved optional edge reports edge.missing === false (Edge.error is null, not 'MISSING', for an optional edge with no target) and edge.to === null, so the old condition treated it as satisfied and never loaded the installed store sibling. The edge stayed unresolved, which npm ls renders as UNMET and npm sbom never reaches.

The bug surfaces only on the FS-scan path. When the hidden lockfile validates, loadVirtual replay masks it; it reappears whenever the hidden lockfile is absent or stale, or forceActual is set.

How

#findMissingEdges() now walks any edge with no resolved target (edge.to === null), instead of only those flagged missing. The walk still loads a node only when the package is actually present in an ancestor node_modules, so a genuinely uninstalled optional dep stays unresolved and a present-but-wrong-version resolves to INVALID — matching node's resolution and the hoisted strategy.

References

Fixes #9622

@manzoorwanijk manzoorwanijk changed the title fix(arborist): materialize installed transitive optional deps in the linked actual tree fix(arborist): materialize installed transitive optional deps under linked Jun 25, 2026
@manzoorwanijk manzoorwanijk marked this pull request as ready for review June 25, 2026 12:27
@manzoorwanijk manzoorwanijk requested review from a team as code owners June 25, 2026 12:27
@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

Already completed via #9654

@manzoorwanijk manzoorwanijk deleted the fix/linked-transitive-optional-unmet branch June 25, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] install-strategy=linked: extension-added optional dependency reported UNMET by npm ls though installed

1 participant