Skip to content

fix(lockfile): resolve optional peers like pnpm 11 - #1793

Merged
jdx merged 2 commits into
mainfrom
fix/optional-peer-parity
Oct 20, 2026
Merged

jdx merged 2 commits into
mainfrom
fix/optional-peer-parity

Conversation

@jdx

@jdx jdx commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

aube-written pnpm-lock.yaml files leave out optional peers that pnpm 11 resolves, so Dependabot (which runs pnpm) rewrites snapshot keys it did not need to touch. This is difference #2 of discussion #1779. Difference #1 (suffix order) is in #1792 and #3 (unused catalog entries) in #1788.

I ran pnpm@11.27.1 install --lockfile-only on small fixtures to establish the rules, and the aube output now matches pnpm's snapshots: byte for byte on all of them (and on the importer rows):

  1. Optional peers resolve from anywhere in the graph (when dedupePeerDependents is on, the default). If a satisfying version is already resolved anywhere in the workspace, even in an unrelated importer's subtree, pnpm binds it. With autoInstallPeers: false, vite@7.1.0 in one importer gets (jiti@2.7.0) when only another importer's c12 pulls in jiti. aube only looked at ancestors, the package's own deps and the workspace root. This also covers debug's supports-color, which pnpm 11 binds when chalk brings it in.
  2. A resolved optional peer is listed under optionalDependencies: in the snapshot, not dependencies:.
  3. Compatible variants of the same package merge across importers. Importer a resolving vite with jiti and importer b with yaml both end up as vite@7.1.0(jiti@2.5.1)(yaml@2.8.1). Variants whose peers conflict (different jiti versions) stay separate.

Before (importer apps/web and apps/other share vite, only web has jiti and yaml):

apps/other: vite 7.1.0(@types/node@24.3.0)(tsx@4.20.5)
apps/web:   vite 7.1.0(@types/node@24.3.0)(jiti@2.5.1)(tsx@4.20.5)(yaml@2.8.1)

After (same as pnpm 11):

apps/other: vite 7.1.0(@types/node@24.3.0)(jiti@2.5.1)(tsx@4.20.5)(yaml@2.8.1)
apps/web:   vite 7.1.0(@types/node@24.3.0)(jiti@2.5.1)(tsx@4.20.5)(yaml@2.8.1)

With dedupePeerDependents: false pnpm 11 leaves those peers unresolved, and so does aube.

Migration

Existing aube-written lockfiles shift once on the next resolve for any package whose optional peers are now resolved. Lockfiles are read as-is, so frozen installs of old lockfiles keep working.

Not covered yet

Running both tools fresh on the repro repo (speakeasy-api/gram at e537882348) still leaves differences. They come from optional peers a package gets only because a descendant needs them, e.g. @ai-sdk/react@4.0.138(react@19.3.0)(zod@4.6.7), where zod is only a peer of its child @ai-sdk/mcp, and @testing-library/react@14.0.0(@types/react@…). pnpm hoists those resolved optional peers into the importer's scope so every package below sees them; aube's ancestor-propagation gate does not. I did not change that because I could not pin the rule down from output alone without risking regressions. This is a follow-up.

Validation

  • Compared snapshots: against pnpm@11.27.1 on fixtures covering: optional peer provided by another importer's subtree, by the root, by a cousin dep, version conflicts across importers, three importers with partial overlap, dedupePeerDependents off, and required peers across importers (already matching).
  • The old unit test optional_peer_is_not_bound_via_graph_scan asserted the opposite of pnpm 11's behavior (which I checked: pnpm 11.27.1 binds supports-color@7.2.0 for debug when chalk@4 is in the graph). It is replaced by tests for the new behavior, plus a variant-merge test.
  • cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt; bats peer_deps, peers, install, update, prune, optional_platform, pnpm_monorepo_dedupePeers.

AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5; version: unavailable.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved optional peer dependency resolution across the dependency graph when deduplication is enabled.
    • Compatible optional peers are now consolidated across variants, while conflicting versions remain separate.
    • Resolved optional peers are recorded correctly, and unresolved peers continue to be reported when no satisfying version is available.
    • Clarified peer dependency resolution behavior in relevant output.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M5D1PN1G6J2PEWVHKN2Y0PP3
Entire-Checkpoint: 01M5D1XBC4VR1JDY0KPSYGFTXM
Entire-Checkpoint: 01M5D1YHYRXQBGTXSR3A0PAX36
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Central YAML (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f7ae9a84-b0e9-417e-9ee8-dfcd74b8b9ad
📥 Commits

Reviewing files that changed from the base of the PR and between 1280945 and 12090d1.

📒 Files selected for processing: 2
  • crates/aube-resolver/src/peer_context.rs
  • crates/aube-resolver/src/resolve/driver.rs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 2 remain after this review.

📝 Walkthrough

Optional peers can now resolve from satisfying versions elsewhere in the dependency graph when dedupe_peer_dependents is enabled. The resolver records resolved optional peers in optionalDependencies and merges compatible optional peer bindings across variants.

Changes: Optional peer resolution

Layer / File(s) Summary
Graph-wide optional peer lookup
crates/aube-resolver/src/peer_context.rs, crates/aube-resolver/src/resolve/driver.rs
When dedupe_peer_dependents is enabled, optional peers can resolve from satisfying versions elsewhere in the graph. Resolved peers are recorded in optionalDependencies unless already declared as ordinary dependencies. Tests cover cousin resolution and the disabled setting.
Compatible peer variant merging
crates/aube-resolver/src/peer_context.rs
Compatible variants absorb missing optional peer bindings before equivalence grouping. Conflicting resolutions remain separate. Tests cover compatible absorption, conflicts, and cross-importer folding.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: rubnogueira

Merge Risk: ⚪ Minimal · up to 12090

No actionable merge-blocking issue was found in the optional-peer resolution and variant-merging changes. The PR is mergeable after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: resolving optional peers in lockfiles to match pnpm 11 behavior.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 2 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; all previous findings are addressed and no new actionable issue remains.

Summary

This PR brings optional-peer resolution closer to pnpm 11.

  • Fresh lockfiles bind optional peers found elsewhere in the workspace.

All three previous findings are addressed. No new actionable issue was found.

Reviews (2) · Last reviewed commit: "fix(lockfile): merge main and address op..." · Reviewed by Greptile

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M5D2YMQBASH10R41KZB08S1Q
@jdx
jdx merged commit e1e9cff into main Oct 20, 2026
29 checks passed
@jdx
jdx deleted the fix/optional-peer-parity branch October 20, 2026 17:47
@github-actions

Copy link
Copy Markdown

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: graph on jdx-perf-v1-ubuntu24.04-x64-aube-rust1.97.1-img5263c143, install on jdx-perf-v1-ubuntu24.04-x64-aube-rust1.97.1-img5263c143, run on jdx-perf-v1-ubuntu24.04-x64-aube-rust1.97.1-img5263c143, startup on jdx-perf-v1-ubuntu24.04-x64-aube-rust1.97.1-img5263c143, tree on jdx-perf-v1-ubuntu24.04-x64-aube-rust1.97.1-img5263c143

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

12090d165e44 vs 1280945e08e4 · measured on the runner, not pushed to the history.

jdx added a commit that referenced this pull request Oct 21, 2026
… like pnpm 11 (#1805)

<!-- entire-trail-link-start -->
https://entire.com/gh/aubepkg/aube/trails/189
<!-- entire-trail-link-end -->

Follow-up to #1793 for difference #2 of discussion
#1779. aube-written
`pnpm-lock.yaml` snapshots left out peers that pnpm 11 carries on a
package because one of its *dependencies* needs them, so Dependabot
(which runs pnpm) rewrote those keys.

## Problem

pnpm 11 puts a peer resolved from above into every ancestor's snapshot
key, even when the ancestor declares peers of its own. aube only did
that for ancestors with no declared peers, and it also dropped a flat
peer when an own peer's nested segment mentioned the same name.

With `autoInstallPeers: false` and `@ai-sdk/react@4.0.138`, `ai@7.0.135`
and `zod@^4` in one importer:

```
pnpm 11.27.1: @ai-sdk/react@4.0.138(react@19.3.1)(zod@4.6.7)
aube before:  @ai-sdk/react@4.0.138(react@19.3.1)
```

`zod` is only a peer of its child `@ai-sdk/mcp`. The same applies to
`@testing-library/react@14.0.0(@types/react@…)(react-dom@…)(react@…)`,
where `@types/react` comes from the child `@types/react-dom`.

## Changes

- A package that declares peers now also takes the peers its
dependencies resolved from above.
- Edges to a package's own declared peers don't bubble their peers
upward.
- A peer only nested in another segment (`(ai@7(zod@4))`) is still
listed flat when a descendant needs it.
- `transitivePeerDependencies:` lists those inherited peers (`zod`,
`@types/react`), as pnpm does.

Existing aube-written lockfiles shift once on the next resolve for
affected packages. Lockfiles are read as-is, so frozen installs keep
working.

## Validation

Fresh `pnpm@11.27.1 install --lockfile-only` vs `aube install
--lockfile-only`, `snapshots:` identical on four small fixtures
(descendant required peer, descendant `@types/react`, older
`@ai-sdk/react`, optional-peer variant). On the speakeasy-api/gram repro
the `@ai-sdk/*` and `@assistant-ui/*` descendant-peer diffs are gone.
Still open there: optional peers hoisted into a nested peer's scope
(`@assistant-ui/react(...)(ai)(immer)(use-sync-external-store)`),
`ajv-formats`/`@inquirer/*` optional-peer handling, and resolution picks
like `@babel/runtime`.

The bats expectation in `peer_deps.bats` now includes `@types/react`,
which pnpm 11.27.1 writes for that fixture. Full bats suite passes
except one test needing `bun`, which isn't installed here.

*AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5;
version: unavailable.*

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved peer dependency resolution for nested dependency paths.
* Correctly propagates peer requirements through packages that declare
peers and their ancestors.
* Prevents duplicate, self-referencing, and incorrectly inherited peer
dependencies.
  * Preserves transitive peer dependency information during resolution.
* Ensures references use consistent resolved paths after peer-context
changes.
* Applies consistent handling for long inherited peer dependency paths.
  * Updated lockfile validation for pnpm 11 dependency metadata changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

1 participant