Skip to content

fix(arborist): validate peerOptional conflicts in no-save mutations - #9641

Merged
owlstronaut merged 1 commit into
release/v11from
backport/v11/9605
Jun 24, 2026
Merged

fix(arborist): validate peerOptional conflicts in no-save mutations#9641
owlstronaut merged 1 commit into
release/v11from
backport/v11/9605

Conversation

@owlstronaut

Copy link
Copy Markdown

Backport of #9605 to release/v11.

…9605)

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`.

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.

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.

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.

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>
(cherry picked from commit 2aa1c7c)
@owlstronaut
owlstronaut requested review from a team as code owners June 24, 2026 17:38
@owlstronaut
owlstronaut merged commit e601d4a into release/v11 Jun 24, 2026
16 checks passed
@owlstronaut
owlstronaut deleted the backport/v11/9605 branch June 24, 2026 17:43
@github-actions github-actions Bot mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants