Skip to content

Comments

fix: linked strategy fixes for scoped packages, aliases, and peer deps#8996

Merged
owlstronaut merged 7 commits intonpm:latestfrom
manzoorwanijk:fix/install-strategy-linked-scoped-packages
Feb 20, 2026
Merged

fix: linked strategy fixes for scoped packages, aliases, and peer deps#8996
owlstronaut merged 7 commits intonpm:latestfrom
manzoorwanijk:fix/install-strategy-linked-scoped-packages

Conversation

@manzoorwanijk
Copy link
Contributor

@manzoorwanijk manzoorwanijk commented Feb 19, 2026

We're looking at using install-strategy=linked in the Gutenberg monorepo (~200 workspace packages), which powers the WordPress Block Editor. While testing it in a PR, we ran into several issues with the linked strategy that this PR fixes.

Summary

  1. Scoped workspace packages were losing their @scope/ prefix in node_modules because the symlink name came from the folder basename instead of the package name.
  2. Aliased packages (e.g. "prettier": "npm:custom-prettier@3.0.3") in workspace projects were getting symlinked under the real package name instead of the alias, so require('prettier') would fail.
  3. With legacy-peer-deps = true, peer dependencies weren't being placed alongside packages in the store, so require() calls for peer deps failed from within the store.
  4. With strict-peer-deps = true, the linked strategy could crash with Cannot read properties of undefined when store entries or parent nodes were missing for excluded peer deps.

Root cause

assignCommonProperties was using a single result.name for both consumer-facing symlinks and store-internal paths. For workspaces, node.name comes from the folder basename (missing the scope). For aliases, node.packageName gives the real name but we need the alias for the consumer symlink.

Separately, legacy-peer-deps tells the arborist to skip creating peer dep edges entirely, so the isolated reifier never saw them and never placed them in the store. And strict-peer-deps can cause nodes to be excluded from the tree while still being referenced by edges, leading to undefined lookups.

Changes

  • Split proxy identity into result.name (consumer-facing: alias or scoped workspace name) and result.packageName (store-internal: real package name from package.json). Store paths (getKey, treeHash, generateChild, processEdges, processDeps) use packageName; consumer symlinks keep using name.
  • When legacyPeerDeps is enabled, resolve missing peer dep edges from the tree via node.resolve() so they still get symlinked in the store.
  • Guard against undefined from and target nodes in processEdges/processDeps to prevent crashes with strict-peer-deps.
  • Guard idealTree.children.get(ws) in reify.js since the isolated tree uses an array for children, not a Map.
  • Test fixture updates: recursive: true for mkdirSync, scoped workspace glob support.
  • New tests for scoped workspace packages, aliased packages in workspaces, and peer deps with legacyPeerDeps.

References

Fixes #6122

@manzoorwanijk manzoorwanijk requested a review from a team as a code owner February 19, 2026 09:22
@manzoorwanijk manzoorwanijk marked this pull request as draft February 19, 2026 10:12
@manzoorwanijk manzoorwanijk changed the title fix: Use package name instead of directory name for linked strategy symlinks fix: linked strategy fixes for scoped packages, aliases, and peer deps Feb 19, 2026
@manzoorwanijk manzoorwanijk marked this pull request as ready for review February 19, 2026 10:37
@owlstronaut owlstronaut self-assigned this Feb 19, 2026
@owlstronaut
Copy link
Contributor

@manzoorwanijk could you get the CI passing before I do a deep-dive on this?

@manzoorwanijk
Copy link
Contributor Author

@owlstronaut Thank you for looking into it. I have updated the test coverage that should make CI happy.

Copy link
Contributor

@owlstronaut owlstronaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @manzoorwanijk , this looks great! I created some experiments here to run and they all seem good as well. Appreciate your work on this!

@owlstronaut owlstronaut merged commit a2154cd into npm:latest Feb 20, 2026
16 checks passed
@manzoorwanijk
Copy link
Contributor Author

manzoorwanijk commented Feb 20, 2026

@owlstronaut Thank you for shipping this.

I was wondering whether it’s possible to backport the changes to v10.
I would be more than happy to spin up a PR.

@manzoorwanijk manzoorwanijk deleted the fix/install-strategy-linked-scoped-packages branch February 20, 2026 23:33
manzoorwanijk added a commit to manzoorwanijk/npm-cli that referenced this pull request Feb 23, 2026
npm#8996)

We're looking at using `install-strategy=linked` in the [Gutenberg
monorepo](https://github.com/WordPress/gutenberg) (~200 workspace
packages), which powers the WordPress Block Editor. While [testing it in
a PR](WordPress/gutenberg#75213), we ran into
several issues with the linked strategy that this PR fixes.

## Summary

1. Scoped workspace packages were losing their `@scope/` prefix in
`node_modules` because the symlink name came from the folder basename
instead of the package name.
2. Aliased packages (e.g. `"prettier": "npm:custom-prettier@3.0.3"`) in
workspace projects were getting symlinked under the real package name
instead of the alias, so `require('prettier')` would fail.
3. With `legacy-peer-deps = true`, peer dependencies weren't being
placed alongside packages in the store, so `require()` calls for peer
deps failed from within the store.
4. With `strict-peer-deps = true`, the linked strategy could crash with
`Cannot read properties of undefined` when store entries or parent nodes
were missing for excluded peer deps.

## Root cause

`assignCommonProperties` was using a single `result.name` for both
consumer-facing symlinks and store-internal paths. For workspaces,
`node.name` comes from the folder basename (missing the scope). For
aliases, `node.packageName` gives the real name but we need the alias
for the consumer symlink.

Separately, `legacy-peer-deps` tells the arborist to skip creating peer
dep edges entirely, so the isolated reifier never saw them and never
placed them in the store. And `strict-peer-deps` can cause nodes to be
excluded from the tree while still being referenced by edges, leading to
undefined lookups.

## Changes

- Split proxy identity into `result.name` (consumer-facing: alias or
scoped workspace name) and `result.packageName` (store-internal: real
package name from `package.json`). Store paths (`getKey`, `treeHash`,
`generateChild`, `processEdges`, `processDeps`) use `packageName`;
consumer symlinks keep using `name`.
- When `legacyPeerDeps` is enabled, resolve missing peer dep edges from
the tree via `node.resolve()` so they still get symlinked in the store.
- Guard against undefined `from` and `target` nodes in
`processEdges`/`processDeps` to prevent crashes with `strict-peer-deps`.
- Guard `idealTree.children.get(ws)` in `reify.js` since the isolated
tree uses an array for `children`, not a Map.
- Test fixture updates: `recursive: true` for `mkdirSync`, scoped
workspace glob support.
- New tests for scoped workspace packages, aliased packages in
workspaces, and peer deps with `legacyPeerDeps`.

## References

Fixes npm#6122
@manzoorwanijk
Copy link
Contributor Author

I have created #9011 to backport the changes to v10. Is there anything else that needs to be done for the backport?

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] Import error with install-strategy=linked and namespaced packages in a workspace

2 participants