fix: prefer existing tree nodes for peerOptional deps (#9249)#9283
fix: prefer existing tree nodes for peerOptional deps (#9249)#9283owlstronaut merged 3 commits intonpm:latestfrom
Conversation
When a peerOptional edge conflicts, search descendants for a satisfying node before fetching from the registry. This prevents extraneous packages from blocking hoisting of required deps.
| if (!dep && edge.type === 'peerOptional') { | ||
| dep = this.#findHoistableNode(edge.from.resolveParent || edge.from, edge) | ||
| } |
There was a problem hiding this comment.
| if (!dep && edge.type === 'peerOptional') { | |
| dep = this.#findHoistableNode(edge.from.resolveParent || edge.from, edge) | |
| } | |
| // Skip the shortcut when the user has signaled an explicit re-fetch intent (npm update by name, explicit request, or audit fix), so we honor those signals rather than silently keeping the existing node. | |
| const skipExistingShortcut = this[_updateNames].includes(edge.name) | |
| || this.#explicitRequests.has(edge) | |
| || (edge.to && this.auditReport?.isVulnerable(edge.to)) | |
| if (!dep && edge.type === 'peerOptional' && !skipExistingShortcut) { | |
| dep = this.#findHoistableNode(edge.from.resolveParent || edge.from, edge) | |
| } |
I think if we add this check, we get to keep the dedupe behavior for normal installs while still letting npm update, npm audit fix, and explicit installs do their thing.
There was a problem hiding this comment.
This is only in problemEdges tho, wouldn't there be other chances earlier to apply requested updates?
Aside: I noticed there is a test that peerOptionals are installed when explicitly requested, and that still passes npm install a-peer-optional.
Tho I noticed that even before this change the test would fail on re-install, or when package-lock or node_modules was already present. That seems bad, npm's only respecting the explciit request when it's not building from a lock.
There was a problem hiding this comment.
Oh, I see those count as problems and land here.
| // A peerOptional conflict can be resolved by finding an existing | ||
| // node in the tree that satisfies the edge, avoiding a registry | ||
| // fetch that may introduce an extraneous package. |
There was a problem hiding this comment.
| // A peerOptional conflict can be resolved by finding an existing | |
| // node in the tree that satisfies the edge, avoiding a registry | |
| // fetch that may introduce an extraneous package. | |
| // A peerOptional conflict can be resolved by finding an existing node in the tree that satisfies the edge, avoiding a registry fetch that may introduce an extraneous package. |
we try to break only at natural boundaries for screen reader accessibility
|
@owlstronaut Do you have ideas for alternative fixes? I really want a solution that fixes existing lockfiles, that have the old peerOptional conflict resolution, is an extraneous dependency that will be pruned. This is causing friction updating to npm 11 (I know, I'm behind the curve). |
…, and audit fixes
9970730 to
9b96929
Compare
| t.ok(peerOptEdge, 'ts-jest has peerOptional edge for jest-util') | ||
| t.not(peerOptEdge.to?.version, undefined, 'peerOptional jest-util resolved') |
There was a problem hiding this comment.
| t.ok(peerOptEdge, 'ts-jest has peerOptional edge for jest-util') | |
| t.not(peerOptEdge.to?.version, undefined, 'peerOptional jest-util resolved') | |
| t.equal(peerOptEdge.to?.version, '30.0.0', 'peerOptional jest-util refetched to @30, not deduped to @29') |
This would pass even without the !skipExistingShortcut guard
9b96929 to
13270e2
Compare
What do you think about running a dedupe pass when nodes are deleted by idealTreePrune? |
That seems like a promising direction. I'm worried that'll get expensive on big trees, but I'm open to a dedupe pass given we could profile the performance appropriately |
|
This usually means the cherry-pick had conflicts. Please create a manual backport: git fetch origin release/v11
git checkout -b backport/v11/9283 origin/release/v11
git cherry-pick -x 0629fbf736eafcb555428d96bd86a69f8e791d70
# resolve any conflicts, then:
git push origin backport/v11/9283Error details |
When a peerOptional edge conflicts, search descendants for a satisfying node before fetching from the registry. This prevents extraneous packages from blocking hoisting of required deps. This fixes 1/2 or 1/3 of #9249. Before this change a clean install would resolve `nm/jest-util@30` when resolving the conflict at nm/jest-util between ts-jest's jest-util@^29||^30, and expect's ^28, which had been placed at root. `#nodeFromEdge` would create a brand new node, matching greatest ^30. A subequent install would mark nm/jest-util@30 as extraneous and prune it. This tree is valid, but ts-jest's peerOptional jest-util is unsatisfied, while compatible jest-util are installed and duplicated. This change reduces duplication and can prevent peerOptionals from actively installing. 1. Now during initial installs npm will prefer hoisting a dependency to de-dupe a peerOptional conflict over creating a new extraneous edge. 2. It doesn't solve the problem if there's no compatible version in the sub-tree. npm will still use `#nodeFromEdge` and install an extraneous edge. 3. It doesn't fix installs from lockfiles generated before this fix. I think this is okay, because the trees are techincally valid, just not optimal. I think a better solution to all three issues would be: * During problemEdge conflict resolution, npm would hoist nm/jest-util@28 under expect, without replacing it with anything. ts-jest's peerOptional jest-util would be unsatisfied. This creates the same tree as npm's second installs that prune extraneous. * Check for any dependencies that can be hosited. This can run during the initial install on problemEdge conflict resoultion, and in pruneIdealTree on any nodes that are removed. I think this solves all three issues. I didn't implement it because I couldn't find a way to resolve the conflict by leaving a hole in the tree.. (cherry picked from commit 0629fbf)
When a peerOptional edge conflicts, search descendants for a satisfying node before fetching from the registry. This prevents extraneous packages from blocking hoisting of required deps.
This fixes 1/2 or 1/3 of #9249. Before this change a clean install would resolve
nm/jest-util@30when resolving the conflict at nm/jest-util between ts-jest's jest-util@^29||^30, and expect's ^28, which had been placed at root.#nodeFromEdgewould create a brand new node, matching greatest ^30. A subequent install would mark nm/jest-util@30 as extraneous and prune it.This tree is valid, but ts-jest's peerOptional jest-util is unsatisfied, while compatible jest-util are installed and duplicated.
This change reduces duplication and can prevent peerOptionals from actively installing.
#nodeFromEdgeand install an extraneous edge.I think a better solution to all three issues would be:
I think this solves all three issues. I didn't implement it because I couldn't find a way to resolve the conflict by leaving a hole in the tree..