fix: linked strategy fixes for scoped packages, aliases, and peer deps#8996
Merged
owlstronaut merged 7 commits intonpm:latestfrom Feb 20, 2026
Merged
Conversation
Contributor
|
@manzoorwanijk could you get the CI passing before I do a deep-dive on this? |
Shrinkwrap packages always have a name; no fallback is needed
Contributor
Author
|
@owlstronaut Thank you for looking into it. I have updated the test coverage that should make CI happy. |
owlstronaut
approved these changes
Feb 20, 2026
Contributor
owlstronaut
left a comment
There was a problem hiding this comment.
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!
This was referenced Feb 20, 2026
[BUG] linked install: TypeError: Cannot read properties of undefined (reading 'isProjectRoot')
#7067
Closed
Contributor
Author
|
@owlstronaut Thank you for shipping this. I was wondering whether it’s possible to backport the changes to v10. |
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
Contributor
Author
|
I have created #9011 to backport the changes to v10. Is there anything else that needs to be done for the backport? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
We're looking at using
install-strategy=linkedin 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
@scope/prefix innode_modulesbecause the symlink name came from the folder basename instead of the package name."prettier": "npm:custom-prettier@3.0.3") in workspace projects were getting symlinked under the real package name instead of the alias, sorequire('prettier')would fail.legacy-peer-deps = true, peer dependencies weren't being placed alongside packages in the store, sorequire()calls for peer deps failed from within the store.strict-peer-deps = true, the linked strategy could crash withCannot read properties of undefinedwhen store entries or parent nodes were missing for excluded peer deps.Root cause
assignCommonPropertieswas using a singleresult.namefor both consumer-facing symlinks and store-internal paths. For workspaces,node.namecomes from the folder basename (missing the scope). For aliases,node.packageNamegives the real name but we need the alias for the consumer symlink.Separately,
legacy-peer-depstells the arborist to skip creating peer dep edges entirely, so the isolated reifier never saw them and never placed them in the store. Andstrict-peer-depscan cause nodes to be excluded from the tree while still being referenced by edges, leading to undefined lookups.Changes
result.name(consumer-facing: alias or scoped workspace name) andresult.packageName(store-internal: real package name frompackage.json). Store paths (getKey,treeHash,generateChild,processEdges,processDeps) usepackageName; consumer symlinks keep usingname.legacyPeerDepsis enabled, resolve missing peer dep edges from the tree vianode.resolve()so they still get symlinked in the store.fromandtargetnodes inprocessEdges/processDepsto prevent crashes withstrict-peer-deps.idealTree.children.get(ws)inreify.jssince the isolated tree uses an array forchildren, not a Map.recursive: trueformkdirSync, scoped workspace glob support.legacyPeerDeps.References
Fixes #6122