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
Contributor

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

3 participants