fix(arborist): validate peerOptional conflicts in no-save mutations (#9605)
## Problem Arborist currently treats `save=false` as a signal that invalid `peerOptional` edges from the lockfile can be trusted and skipped. That is correct for non-mutating lockfile reads such as `npm ci`, but it is wrong for commands that still mutate the ideal tree and write a new lockfile while leaving `package.json` unchanged. For example, `npm update` runs with `save=false`, but it can still update package versions in `package-lock.json`. If one of those updates introduces or exposes an invalid optional peer relationship, Arborist can finish successfully and write a lockfile that a later plain `npm install` rejects with `ERESOLVE`. ## Fix This separates two concepts that were previously conflated: - `#requestedTreeMutation`: whether the command explicitly requested an add, remove, or update operation. - `#mutateTree`: whether the ideal-tree build has actually changed the tree during placement. Invalid `peerOptional` policy now uses `#requestedTreeMutation` instead of `#mutateTree`: - `save=false` + explicit add/rm/update: invalid `peerOptional` edges are treated as resolver problems. - `save=false` + no requested mutation: invalid `peerOptional` edges remain trusted, preserving the existing `npm ci`-style behavior. The requeue path also uses `#requestedTreeMutation`, so a `save=false` update can reprocess an already-seen node when a later placement invalidates one of its `peerOptional` edges. ## Behavior Changed This fixes bad lockfile output for `save=false` commands that mutate the dependency graph, including: - `npm update` - `npm install <pkg> --no-save` - other explicit add/rm/update Arborist builds with `save=false` Those commands should not silently produce an ideal tree or lockfile containing invalid `peerOptional` edges that a subsequent install rejects. ## Behavior Preserved Non-mutating `save=false` builds continue to trust the lockfile. In those cases, an invalid `peerOptional` edge is still ignored rather than forcing re-resolution, preserving the behavior needed by `npm ci`-style lockfile reads. Fixes #9604. ## Tests Added regression coverage for: - `npm update`-style `save=false` mutation rejecting an invalid `peerOptional` graph instead of writing a bad lockfile. - `npm install <pkg> --no-save`-style mutation rejecting the same class of invalid graph. - `save=false` update requeueing an already-seen node when later placement invalidates its `peerOptional` edge. - non-mutating `save=false` builds continuing to ignore invalid `peerOptional` problem edges. Validated with: ```sh git diff --check npm exec -- tap # from workspaces/arborist npm run lint --workspace=@npmcli/arborist ``` Co-authored-by: Dale Lakes <6843636+spitfire55@users.noreply.github.com>
D
Dale Lakes committed
2aa1c7cd25399c49f12aab684341cc1743ad1635
Parent: 690bf17
Committed by GitHub <noreply@github.com>
on 6/24/2026, 5:28:46 PM