Skip to content

fix(deps-resolver): prefer locked peer contexts during resolution by default#12083

Merged
zkochan merged 3 commits into
pnpm:mainfrom
sharmila-oai:dev/eacc-1813-prefer-locked-peer-resolution
Jun 2, 2026
Merged

fix(deps-resolver): prefer locked peer contexts during resolution by default#12083
zkochan merged 3 commits into
pnpm:mainfrom
sharmila-oai:dev/eacc-1813-prefer-locked-peer-resolution

Conversation

@sharmila-oai

@sharmila-oai sharmila-oai commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an opt-in workspace setting in pnpm-workspace.yaml:

peerResolutionMode: prefer-locked

During writable resolution, pnpm reuses each dependency instance's previous
compatible peer providers when those providers are still present in the
current dependency graph.

The default resolution behavior is unchanged.

Why

#12075 fixed optional-peer candidate
selection: pnpm no longer discards a compatible optional-peer version merely
because it came from the lockfile.

This PR addresses a separate source of lockfile churn. A writable install could
still replace one valid peer context with another valid peer context even when
the existing provider remained present and satisfied the peer range.

Public reproduction:
https://github.com/sharmila-oai/pnpm-optional-peer-lockfile-repro

The nested reproduction starts with two valid vitest@3.2.4 contexts:

context-low  -> vitest@3.2.4(jsdom@26.1.0)
context-high -> vitest@3.2.4(jsdom@27.4.0)

Running a writable lockfile regeneration should retain both contexts:

./reproduce-nested-context.sh

Behavior

pnpm reuses a locked peer provider only when:

  • The provider is still present in the current dependency graph.
  • The provider still satisfies the peer range.

Current manifest choices remain authoritative. In particular, pnpm does not
replace:

  • A newly added direct peer provider.
  • An explicitly updated direct peer provider.
  • A changed nested provider.
  • A direct provider installed through an alias.

The reuse pass runs only when the dependency tree contains locked peer contexts,
so fresh installs do not pay for a second peer-resolution pass.

Tradeoff

This change favors lockfile stability over reducing the number of peer
contexts. A writable install may retain multiple compatible peer contexts where
a fresh install would select one.

Implementation

The resolver performs its normal peer-resolution pass first. When the
dependency tree contains locked peer contexts, it performs a second pass that
may reuse compatible provider paths from the lockfile while respecting current
manifest choices.

pacquet now mirrors this behavior. Its lockfile-reuse path rebuilds child
dependencies from the package manifest and skips peer dependencies recorded in
the snapshot, so the peer pass derives each dependency instance's peer context.

Validation

  • Added resolver tests for compatible locked providers, incompatible providers,
    newly declared providers, explicitly updated providers, changed nested
    providers, workspace-root providers, and aliases.
  • Added pnpm CLI and pacquet end-to-end coverage for preserving multiple valid
    peer contexts during writable lockfile regeneration.
  • Ran cargo fmt --check.
  • Ran cargo test -p pacquet-resolving-deps-resolver.
  • Ran cargo clippy -p pacquet-resolving-deps-resolver --all-targets -- --deny warnings.

Written by an agent (Codex, GPT-5).

Sent by Codex

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds an opt-in peerResolutionMode: 'prefer-locked' configuration option that preserves compatible peer dependency contexts from the lockfile during dependency resolution. The feature spans configuration contracts, lockfile schema, the full install pipeline, resolver data models, and peer resolution orchestration with extensive test coverage.

Changes

Peer Resolution Mode Feature

Layer / File(s) Summary
Configuration contracts and types
config/reader/src/Config.ts, config/reader/src/types.ts, config/reader/src/configFileKey.ts, config/reader/src/localConfig.ts, lockfile/types/src/index.ts, workspace/state/src/types.ts, config/reader/test/fixtures/settings-in-workspace-yaml/pnpm-workspace.yaml, config/reader/test/index.ts, workspace/state/test/createWorkspaceState.test.ts
Config and LockfileSettings interfaces add optional peerResolutionMode?: 'prefer-locked'; configuration key registry and workspace state extend to include the setting; test fixtures and assertions verify end-to-end configuration flow.
Lockfile settings detection and verification
lockfile/settings-checker/src/getOutdatedLockfileSetting.ts, deps/status/src/checkDepsStatus.ts, deps/status/test/checkDepsStatus.test.ts
Settings checker detects changes to peerResolutionMode by comparing lockfile value against current config; dependency status checks invoke the updated checker and validate mode mismatches trigger upToDate: false.
Command and installer option propagation
installing/commands/src/install.ts, installing/commands/src/installDeps.ts, installing/commands/src/recursive.ts, installing/deps-installer/src/getPeerDependencyIssues.ts, installing/deps-installer/src/install/extendInstallOptions.ts, installing/deps-installer/src/install/index.ts
Install commands and installer options extend Pick<Config> to include peerResolutionMode; mode is threaded through install defaults and passed to dependency resolution, lockfile settings computation, and peer-issue detection.
Resolver data model for locked-peer tracking
installing/deps-resolver/src/resolveDependencies.ts, installing/deps-resolver/src/resolveDependencyTree.ts
Dependency tree nodes, pending nodes, and resolved package addresses gain optional lockedPeerContext and previousDepPath fields; resolution context carries peerResolutionMode; InfoFromLockfile exposes computed locked-peer context so downstream resolution accesses ancestor peer bindings from the lockfile.
Peer resolution orchestration and locked-peer handling
installing/deps-resolver/src/index.ts, installing/deps-resolver/src/resolvePeers.ts
Dependency resolver orchestrates two-phase peer resolution when peerResolutionMode === 'prefer-locked': first resolves peers normally, then re-resolves using the first result's provider paths as locked references; resolvePeers now accepts optional resolvedPeerProviderPaths and returns pathsByNodeId; resolvePeersOfNode iterates locked peer contexts, filters by provider availability, applies precedence rules, and forces peer path selections to match previously locked choices when compatible.
Integration tests for locked-peer mode and frozen lockfile
installing/deps-resolver/test/resolvePeers.ts, installing/deps-installer/test/install/frozenLockfile.ts, installing/deps-installer/test/install/peerDependencies.ts
Tests verify locked-peer context persistence across installs, new providers override locked choices when declared/updated, compatible providers are reused during writable lockfile regeneration, frozen-lockfile install rejects when mode changes, and locked providers are not reused outside their compatible peer-range constraint.
Release notes and changesets
.changeset/prefer-locked-peer-contexts.md, .changeset/steady-optional-peers.md
Changesets document minor-release updates for configuration and resolver packages with notes on locked-peer context preservation behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • pnpm/pnpm#12018: Both PRs change peer-resolution behavior in installing/deps-resolver/src/resolvePeers.ts — one adds peerResolutionMode: prefer-locked with locked-peer context threading, the other adjusts cycle detection to prevent hangs in aliased mutual peer cycles.
  • pnpm/pnpm#12081: Both PRs modify installing/deps-resolver/src/resolvePeers.ts peer-resolution reuse logic — one introduces prefer-locked via locked peer context propagation, the other prevents reusing inherited parents that would break peer-diamond constraints.

Suggested reviewers

  • zkochan

Poem

🐰 A prefer-locked peer now hops through lockfiles so true,
Reusing past contexts when resolution's brand new,
With paths tracked and filtered through each dependency tree,
Peer diamonds stay stable—how lovely to see!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title describes a specific technical change (prefer locked peer contexts during resolution), but the raw_summary and pr_objectives indicate this is actually an opt-in feature controlled by peerResolutionMode setting, not a default behavior change. Revise the title to accurately reflect that this adds an opt-in peerResolutionMode: prefer-locked setting, not a default behavior change. For example: 'feat: add opt-in peerResolutionMode: prefer-locked for peer context reuse' or similar.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

@sharmila-oai Sure, I'll review the changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov-commenter

codecov-commenter commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.60%. Comparing base (32c07bf) to head (014e813).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12083      +/-   ##
==========================================
+ Coverage   87.47%   87.60%   +0.12%     
==========================================
  Files         262      267       +5     
  Lines       29903    30661     +758     
==========================================
+ Hits        26158    26860     +702     
- Misses       3745     3801      +56     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.077 ± 0.118 1.967 2.282 1.01 ± 0.06
pacquet@main 2.047 ± 0.047 1.988 2.143 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.0772624199800003,
      "stddev": 0.11849315197522589,
      "median": 2.01989419918,
      "user": 2.5687213399999993,
      "system": 3.39235668,
      "min": 1.96733244718,
      "max": 2.28191351218,
      "times": [
        2.08880134218,
        2.13602444518,
        2.27886497718,
        1.96733244718,
        2.00344372518,
        2.02562534518,
        2.28191351218,
        2.01416305318,
        1.9986625511800002,
        1.9777928011800001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0473134317799997,
      "stddev": 0.046681763003528345,
      "median": 2.04260490168,
      "user": 2.5511548399999997,
      "system": 3.375646379999999,
      "min": 1.98828289018,
      "max": 2.14333927618,
      "times": [
        2.00897739418,
        2.02926991718,
        1.99959600118,
        2.03834110418,
        2.0838893181800002,
        2.08487363718,
        2.14333927618,
        2.04969608018,
        2.04686869918,
        1.98828289018
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 659.7 ± 29.1 637.3 734.4 1.00
pacquet@main 666.4 ± 33.2 640.8 740.4 1.01 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6596570533,
      "stddev": 0.02909180236721938,
      "median": 0.6503970971999999,
      "user": 0.3627385799999999,
      "system": 1.33001574,
      "min": 0.6372703867,
      "max": 0.7343900587000001,
      "times": [
        0.7343900587000001,
        0.6603160477000001,
        0.6677771487,
        0.6552617207,
        0.6732703137,
        0.6388227587,
        0.6416491017,
        0.6455324737,
        0.6422805227,
        0.6372703867
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6664036192,
      "stddev": 0.03315757926977948,
      "median": 0.6517157587,
      "user": 0.35928408,
      "system": 1.3379215399999997,
      "min": 0.6408113187000001,
      "max": 0.7403821037,
      "times": [
        0.7134042357,
        0.6466469097,
        0.6519496767,
        0.6544466597,
        0.6514818407,
        0.6665245317,
        0.6498222017,
        0.6485667137000001,
        0.6408113187000001,
        0.7403821037
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.317 ± 0.029 2.287 2.367 1.00
pacquet@main 2.318 ± 0.027 2.281 2.357 1.00 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.316507052200001,
      "stddev": 0.028907013830705815,
      "median": 2.3112749595000004,
      "user": 3.7125234,
      "system": 3.1484871199999995,
      "min": 2.2869352600000004,
      "max": 2.3665862950000003,
      "times": [
        2.2869352600000004,
        2.2914401250000003,
        2.2927635320000004,
        2.3355269560000003,
        2.3665862950000003,
        2.2963666860000003,
        2.342886548,
        2.2870459490000004,
        2.339335938,
        2.326183233
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.3175844057000003,
      "stddev": 0.027356044952991383,
      "median": 2.3203221660000004,
      "user": 3.7167439,
      "system": 3.14601492,
      "min": 2.281446829,
      "max": 2.3569658930000004,
      "times": [
        2.308186661,
        2.336341378,
        2.3569658930000004,
        2.2869611450000003,
        2.281446829,
        2.2832035100000003,
        2.3302543260000004,
        2.310390006,
        2.3394512790000004,
        2.34264303
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.495 ± 0.021 1.474 1.543 1.00
pacquet@main 1.543 ± 0.083 1.490 1.774 1.03 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.49499924244,
      "stddev": 0.020907610781396545,
      "median": 1.4892796276400002,
      "user": 1.7020975799999998,
      "system": 1.84631832,
      "min": 1.47438009714,
      "max": 1.5434113091400001,
      "times": [
        1.49108247814,
        1.48238134714,
        1.49166677614,
        1.5189466361400001,
        1.5434113091400001,
        1.4964384211400001,
        1.4874767771400002,
        1.48483584414,
        1.47438009714,
        1.4793727381400001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.54313268774,
      "stddev": 0.08301783201421883,
      "median": 1.52032892664,
      "user": 1.70889858,
      "system": 1.8954199200000001,
      "min": 1.4902726701400002,
      "max": 1.77396015514,
      "times": [
        1.51277412114,
        1.4904336771400002,
        1.77396015514,
        1.5280677521400001,
        1.5473707311400002,
        1.51818151614,
        1.53435858414,
        1.52247633714,
        1.4902726701400002,
        1.51343133314
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12083
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,316.51 ms
(-1.41%)Baseline: 2,349.67 ms
2,819.60 ms
(82.16%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,495.00 ms
(-1.88%)Baseline: 1,523.59 ms
1,828.31 ms
(81.77%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,077.26 ms
(+0.41%)Baseline: 2,068.82 ms
2,482.59 ms
(83.67%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
659.66 ms
(+0.78%)Baseline: 654.52 ms
785.43 ms
(83.99%)
🐰 View full continuous benchmarking report in Bencher

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

@sharmila-oai Sure, I'll review the latest changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
installing/deps-resolver/src/resolvePeers.ts (1)

705-723: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep aliased direct providers authoritative during locked-context reuse.

This check only protects explicit requests and brand-new declared providers. An already-installed direct alias still falls through, so prefer-locked can reuse the locked provider and override the current direct alias on a later writable install. That regresses the aliased-direct-provider precedence this resolver already relies on.

🩹 Suggested fix
   for (const source of ctx.currentProviderSources) {
     if ([...source.directNodeIdsByAlias].some(([alias, directNodeId]) =>
       directNodeId === peerNodeId &&
-      (source.explicitlyRequestedDirectDependencies.has(alias) ||
+      (alias !== peerName ||
+        source.explicitlyRequestedDirectDependencies.has(alias) ||
         (source.declaredDirectDependencies.has(alias) &&
           ctx.dependenciesTree.get(peerNodeId)?.previousDepPath == null)
       ))) return true
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@installing/deps-resolver/src/resolvePeers.ts` around lines 705 - 723, The
check in hasCurrentPeerProviderThatMustWin wrongly ignores already-installed
direct aliases by only treating declared direct providers as authoritative when
previousDepPath == null; to fix, remove the previousDepPath == null guard so
that any alias present in source.declaredDirectDependencies (or
source.explicitlyRequestedDirectDependencies) wins; update the condition inside
the loop that currently reads
(source.explicitlyRequestedDirectDependencies.has(alias) ||
(source.declaredDirectDependencies.has(alias) &&
ctx.dependenciesTree.get(peerNodeId)?.previousDepPath == null)) to instead be
(source.explicitlyRequestedDirectDependencies.has(alias) ||
source.declaredDirectDependencies.has(alias)), keeping the rest of the loop and
function logic unchanged so installed aliased direct providers remain
authoritative.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@installing/deps-resolver/src/resolvePeers.ts`:
- Around line 705-723: The check in hasCurrentPeerProviderThatMustWin wrongly
ignores already-installed direct aliases by only treating declared direct
providers as authoritative when previousDepPath == null; to fix, remove the
previousDepPath == null guard so that any alias present in
source.declaredDirectDependencies (or
source.explicitlyRequestedDirectDependencies) wins; update the condition inside
the loop that currently reads
(source.explicitlyRequestedDirectDependencies.has(alias) ||
(source.declaredDirectDependencies.has(alias) &&
ctx.dependenciesTree.get(peerNodeId)?.previousDepPath == null)) to instead be
(source.explicitlyRequestedDirectDependencies.has(alias) ||
source.declaredDirectDependencies.has(alias)), keeping the rest of the loop and
function logic unchanged so installed aliased direct providers remain
authoritative.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11ad61d6-80f8-45c6-8176-0ef8e2d99b0d

📥 Commits

Reviewing files that changed from the base of the PR and between d3c4f94 and b30ec6d.

📒 Files selected for processing (3)
  • installing/deps-installer/test/install/peerDependencies.ts
  • installing/deps-resolver/src/resolvePeers.ts
  • installing/deps-resolver/test/resolvePeers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • installing/deps-resolver/test/resolvePeers.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Compile & Lint
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)

Files:

  • installing/deps-installer/test/install/peerDependencies.ts
  • installing/deps-resolver/src/resolvePeers.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/deps-installer/test/install/peerDependencies.ts
  • installing/deps-resolver/src/resolvePeers.ts
🔇 Additional comments (2)
installing/deps-installer/test/install/peerDependencies.ts (1)

2118-2119: LGTM!

Also applies to: 2133-2134, 2137-2159

installing/deps-resolver/src/resolvePeers.ts (1)

114-161: LGTM!

Also applies to: 382-389

@sharmila-oai

sharmila-oai commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

The latest follow-up commit 5df742233 addresses CodeRabbit's aliased direct-provider finding. It preserves an already-installed direct alias during peerResolutionMode: prefer-locked re-resolution while keeping the rule narrow, so ordinary same-name locked-provider reuse still works. A focused resolver regression covers the case.


Written by an agent (Codex, GPT-5).

Sent by Codex

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

@sharmila-oai Sure, I'll review the latest changes now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sharmila-oai sharmila-oai force-pushed the dev/eacc-1813-prefer-locked-peer-resolution branch from c17c613 to 38bbb6d Compare June 2, 2026 02:08
@sharmila-oai sharmila-oai marked this pull request as ready for review June 2, 2026 02:09
@sharmila-oai sharmila-oai requested a review from zkochan as a code owner June 2, 2026 02:09
Copilot AI review requested due to automatic review settings June 2, 2026 02:09
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Add opt-in prefer-locked peer resolution mode to reduce lockfile churn

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add opt-in peerResolutionMode: prefer-locked setting for lockfile stability
• Preserve compatible locked optional peer versions during re-resolution
• Reuse existing peer contexts when providers remain available and valid
• Respect current manifest choices: new/updated/aliased providers take precedence
• Record peer resolution mode in lockfile and workspace state for consistency
Diagram
flowchart LR
  A["Config Setting<br/>peerResolutionMode"] --> B["Lockfile Settings<br/>& Workspace State"]
  C["Initial Peer<br/>Resolution"] --> D["Second Pass:<br/>Reuse Compatible<br/>Locked Contexts"]
  D --> E["Respect Current<br/>Manifest Choices"]
  E --> F["Stable Lockfile<br/>with Multiple<br/>Peer Contexts"]
  B --> D

Loading

Grey Divider

File Changes

1. pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs 🐞 Bug fix +13/-14

Support weighted version selectors in optional peer hoisting

pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs


2. pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs 🧪 Tests +23/-0

Add test for weighted optional peer version selector

pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs


3. pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs 🧪 Tests +84/-2

Add regression test for locked optional peer preservation

pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs


View more (30)
4. config/reader/src/Config.ts ⚙️ Configuration changes +1/-0

Add peerResolutionMode configuration option

config/reader/src/Config.ts


5. config/reader/src/configFileKey.ts ⚙️ Configuration changes +1/-0

Register peer-resolution-mode as excluded pnpm key

config/reader/src/configFileKey.ts


6. config/reader/src/localConfig.ts 📝 Documentation +1/-1

Document peer-resolution-mode as workspace setting

config/reader/src/localConfig.ts


7. config/reader/src/types.ts ⚙️ Configuration changes +1/-0

Define peer-resolution-mode type as prefer-locked

config/reader/src/types.ts


8. config/reader/test/index.ts 🧪 Tests +1/-0

Test reading peerResolutionMode from workspace yaml

config/reader/test/index.ts


9. deps/status/src/checkDepsStatus.ts ✨ Enhancement +2/-0

Track peerResolutionMode in dependency status checks

deps/status/src/checkDepsStatus.ts


10. deps/status/test/checkDepsStatus.test.ts 🧪 Tests +30/-0

Test peerResolutionMode change detection in status

deps/status/test/checkDepsStatus.test.ts


11. installing/commands/src/install.ts ✨ Enhancement +1/-0

Pass peerResolutionMode through install command options

installing/commands/src/install.ts


12. installing/commands/src/installDeps.ts ✨ Enhancement +1/-0

Include peerResolutionMode in install dependencies options

installing/commands/src/installDeps.ts


13. installing/commands/src/recursive.ts ✨ Enhancement +1/-0

Add peerResolutionMode to recursive install options

installing/commands/src/recursive.ts


14. installing/deps-installer/src/getPeerDependencyIssues.ts ✨ Enhancement +2/-0

Pass peerResolutionMode to peer dependency issue detection

installing/deps-installer/src/getPeerDependencyIssues.ts


15. installing/deps-installer/src/install/extendInstallOptions.ts ✨ Enhancement +2/-0

Add peerResolutionMode to strict install options

installing/deps-installer/src/install/extendInstallOptions.ts


16. installing/deps-installer/src/install/index.ts ✨ Enhancement +4/-0

Record peerResolutionMode in lockfile settings

installing/deps-installer/src/install/index.ts


17. installing/deps-installer/test/install/autoInstallPeers.ts 🧪 Tests +51/-1

Add end-to-end test for locked optional peer preservation

installing/deps-installer/test/install/autoInstallPeers.ts


18. installing/deps-installer/test/install/frozenLockfile.ts 🧪 Tests +15/-0

Test frozen lockfile rejection on peerResolutionMode change

installing/deps-installer/test/install/frozenLockfile.ts


19. installing/deps-installer/test/install/peerDependencies.ts 🧪 Tests +67/-0

Add comprehensive prefer-locked peer resolution tests

installing/deps-installer/test/install/peerDependencies.ts


20. installing/deps-resolver/src/hoistPeers.ts 🐞 Bug fix +2/-1

Support weighted selectors in optional peer hoisting

installing/deps-resolver/src/hoistPeers.ts


21. installing/deps-resolver/src/index.ts ✨ Enhancement +26/-6

Track declared and explicitly requested dependencies for prefer-locked

installing/deps-resolver/src/index.ts


22. installing/deps-resolver/src/resolveDependencies.ts ✨ Enhancement +49/-1

Capture locked peer context and previous dep paths

installing/deps-resolver/src/resolveDependencies.ts


23. installing/deps-resolver/src/resolveDependencyTree.ts ✨ Enhancement +4/-0

Pass peerResolutionMode through resolution context

installing/deps-resolver/src/resolveDependencyTree.ts


24. installing/deps-resolver/src/resolvePeers.ts ✨ Enhancement +100/-7

Implement prefer-locked peer provider reuse logic

installing/deps-resolver/src/resolvePeers.ts


25. installing/deps-resolver/test/hoistPeers.test.ts 🧪 Tests +13/-0

Test weighted version selector in optional peer hoisting

installing/deps-resolver/test/hoistPeers.test.ts


26. installing/deps-resolver/test/resolvePeers.ts 🧪 Tests +278/-1

Add comprehensive locked peer provider preference tests

installing/deps-resolver/test/resolvePeers.ts


27. lockfile/settings-checker/src/getOutdatedLockfileSetting.ts ✨ Enhancement +6/-0

Detect peerResolutionMode changes in lockfile validation

lockfile/settings-checker/src/getOutdatedLockfileSetting.ts


28. lockfile/types/src/index.ts ✨ Enhancement +1/-0

Add peerResolutionMode to lockfile settings type

lockfile/types/src/index.ts


29. workspace/state/src/types.ts ✨ Enhancement +1/-0

Include peerResolutionMode in workspace state settings

workspace/state/src/types.ts


30. workspace/state/test/createWorkspaceState.test.ts 🧪 Tests +2/-0

Test peerResolutionMode persistence in workspace state

workspace/state/test/createWorkspaceState.test.ts


31. .changeset/prefer-locked-peer-contexts.md 📝 Documentation +13/-0

Document prefer-locked peer resolution feature

.changeset/prefer-locked-peer-contexts.md


32. .changeset/steady-optional-peers.md 📝 Documentation +6/-0

Document optional peer preservation bug fix

.changeset/steady-optional-peers.md


33. config/reader/test/fixtures/settings-in-workspace-yaml/pnpm-workspace.yaml 🧪 Tests +1/-0

Add peerResolutionMode to test workspace configuration

config/reader/test/fixtures/settings-in-workspace-yaml/pnpm-workspace.yaml


Grey Divider

Qodo Logo

@sharmila-oai

Copy link
Copy Markdown
Contributor Author

@zkochan this follow-up is ready for review. It is rebased onto the latest
approved #12075 head.

This adds the opt-in peerResolutionMode: prefer-locked workspace setting.
The default behavior is unchanged. When enabled, writable resolution reuses
compatible peer providers already recorded for each dependency instance when
those providers remain available. The tradeoff is explicit: more stable
lockfiles, with potentially less aggressive peer-context deduplication.

Known landing requirement: pacquet still needs prior lockfile
dependency-instance plumbing before this can merge. I kept that visible in the
PR description rather than adding a Rust option that would not have the
requested effect.

@codex review


Written by an agent (Codex, GPT-5).

Sent by Codex

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in peerResolutionMode: prefer-locked setting that reduces lockfile churn by reusing previously locked, still-compatible peer provider contexts during writable resolution, while preserving precedence for newly introduced/updated providers. The branch also includes the prerequisite fix to preserve weighted (lockfile-seeded) optional peer candidates during hoisting.

Changes:

  • Introduce peerResolutionMode: prefer-locked plumbing across config, workspace state, lockfile settings, installer, and resolver, including frozen-lockfile mismatch detection.
  • Add a second “prefer locked providers” peer-resolution pass that can reuse compatible locked peer provider paths when still present in the current graph.
  • Fix/port optional-peer hoisting to consider weighted preferred-version selectors (lockfile-seeded), with regression tests in both TypeScript and pacquet (Rust).

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
workspace/state/test/createWorkspaceState.test.ts Verifies workspace state persists peerResolutionMode.
workspace/state/src/types.ts Adds peerResolutionMode to the set of lockfile-affecting workspace-state keys.
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs Adds Rust regression test for preserving weighted optional-peer preferred versions.
pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs Adds Rust unit test for weighted version selectors in optional-peer hoisting.
pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs Updates Rust optional-peer hoisting to handle weighted selectors.
lockfile/types/src/index.ts Extends lockfile settings type with peerResolutionMode.
lockfile/settings-checker/src/getOutdatedLockfileSetting.ts Treats settings.peerResolutionMode changes as lockfile-setting mismatches.
installing/deps-resolver/test/resolvePeers.ts Adds resolver tests covering locked peer provider reuse/precedence rules.
installing/deps-resolver/test/hoistPeers.test.ts Adds unit test for weighted selector handling in optional-peer hoisting.
installing/deps-resolver/src/resolvePeers.ts Implements locked peer provider reuse logic + tracks pathsByNodeId for the second pass.
installing/deps-resolver/src/resolveDependencyTree.ts Propagates lockedPeerContext and previousDepPath into the dependency tree for peer resolution.
installing/deps-resolver/src/resolveDependencies.ts Records prior depPath/peer context from lockfile when peerResolutionMode is enabled; tracks “must-win” providers.
installing/deps-resolver/src/index.ts Wires peerResolutionMode, computes declared/explicit direct deps, and performs 2-pass peer resolution in prefer-locked mode.
installing/deps-resolver/src/hoistPeers.ts Fixes optional-peer hoisting to consider weighted preferred-version selectors.
installing/deps-installer/test/install/peerDependencies.ts Adds installer-level tests for lockfile setting persistence and provider-precedence behavior.
installing/deps-installer/test/install/frozenLockfile.ts Adds frozen-lockfile mismatch test for settings.peerResolutionMode.
installing/deps-installer/test/install/autoInstallPeers.ts Adds end-to-end regression test for preserving weighted optional-peer versions.
installing/deps-installer/src/install/index.ts Records peerResolutionMode in lockfile settings and participates in lockfile mismatch checking.
installing/deps-installer/src/install/extendInstallOptions.ts Adds peerResolutionMode to install options defaults/types.
installing/deps-installer/src/getPeerDependencyIssues.ts Plumbs peerResolutionMode into peer-issue computation context.
installing/commands/src/recursive.ts Exposes peerResolutionMode through recursive command options.
installing/commands/src/installDeps.ts Exposes peerResolutionMode through install-deps options.
installing/commands/src/install.ts Exposes peerResolutionMode through install command options.
deps/status/test/checkDepsStatus.test.ts Adds deps-status test for detecting peerResolutionMode workspace-state changes.
deps/status/src/checkDepsStatus.ts Includes peerResolutionMode in lockfile up-to-date assertions.
config/reader/test/index.ts Asserts peerResolutionMode is read from workspace YAML settings.
config/reader/test/fixtures/settings-in-workspace-yaml/pnpm-workspace.yaml Fixture adds peerResolutionMode: prefer-locked.
config/reader/src/types.ts Registers peer-resolution-mode as a valid config key/value.
config/reader/src/localConfig.ts Documents peer-resolution-mode among resolution-strategy keys (comment table).
config/reader/src/configFileKey.ts Adds peer-resolution-mode to excluded pnpm keys list.
config/reader/src/Config.ts Extends Config type with peerResolutionMode.
.changeset/steady-optional-peers.md Changeset for optional-peer preservation fix (prerequisite commit).
.changeset/prefer-locked-peer-contexts.md Changeset describing the new opt-in prefer-locked peer resolution mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread installing/deps-resolver/src/resolvePeers.ts Outdated
Comment thread installing/deps-resolver/src/resolvePeers.ts Outdated
@zkochan zkochan force-pushed the dev/eacc-1813-prefer-locked-peer-resolution branch from 38bbb6d to e34262c Compare June 2, 2026 16:28
… default

Peer dependency resolution now reuses the peer contexts already recorded in
the lockfile when those providers remain present in the dependency graph and
still satisfy the peer ranges, instead of collapsing compatible contexts on a
writable re-resolution. Current manifest choices stay authoritative: newly
added, explicitly updated, or aliased direct providers, changed nested
providers, and locked versions that no longer satisfy the range still win.

The pacquet lockfile-reuse path is fixed to match: a reused node now derives
its children from the package manifest, skipping resolved peers recorded in
the snapshot, so the peer pass re-derives each occurrence's context instead of
baking in the first occurrence's.

Adds end-to-end coverage to both the pnpm CLI and pacquet.
Copilot AI review requested due to automatic review settings June 2, 2026 18:32
@zkochan zkochan force-pushed the dev/eacc-1813-prefer-locked-peer-resolution branch from e34262c to 27c2d4b Compare June 2, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

installing/deps-installer/test/install/peerDependencies.ts:2133

  • This installer test is about precedence in the “locked peer context reuse” mode, but it doesn’t enable the opt-in setting. If the resolver behavior is meant to be opt-in, pass peerResolutionMode: 'prefer-locked' in the options here so the test remains aligned with the intended contract.
  const project = prepareEmpty()
  const opts = testDefaults({ strictPeerDependencies: false })

installing/deps-installer/test/install/peerDependencies.ts:2150

  • This installer test expects preservation of multiple compatible peer contexts, but it doesn’t enable the PR’s opt-in “prefer locked” mode. If the feature is intended to be opt-in, set peerResolutionMode: 'prefer-locked' in the install options so the test is specifically exercising that mode rather than implicitly changing default behavior expectations.
  const project = prepareEmpty()
  const opts = testDefaults({ strictPeerDependencies: false })

Comment thread installing/deps-resolver/src/index.ts
Comment thread pnpm/test/install/preferLockedPeerContexts.ts
Comment thread installing/deps-installer/test/install/peerDependencies.ts
@zkochan zkochan changed the title [deps-resolver] Add opt-in locked peer-context reuse [deps-resolver] Prefer locked peer contexts during resolution by default Jun 2, 2026
@zkochan zkochan changed the title [deps-resolver] Prefer locked peer contexts during resolution by default fix(deps-resolver): prefer locked peer contexts during resolution by default Jun 2, 2026
sharmila-oai and others added 2 commits June 2, 2026 12:13
…locked-context reuse

Limit locked peer-context reuse to stable leaf providers (those whose locked
depPath has no peer suffix of their own). A provider that carries its own peer
suffix is context-dependent — and self-referential for cyclic transitive peers
— so reusing it could re-expand cyclic suffixes across installs and break the
deterministic resolution from pnpm#8155. Also skip a provider that has already
resolved to a different path in the current pass, which would otherwise leave
pathsByNodeId pointing at a depPath absent from the dependency graph.
Copilot AI review requested due to automatic review settings June 2, 2026 19:37
@zkochan zkochan enabled auto-merge (squash) June 2, 2026 19:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

@zkochan zkochan disabled auto-merge June 2, 2026 20:02
@zkochan zkochan merged commit 1c73e83 into pnpm:main Jun 2, 2026
27 checks passed
zkochan added a commit that referenced this pull request Jun 14, 2026
…des (#12320)

* fix: prevent a pinned locked peer provider from leaking to sibling nodes

When the locked-peer-context pinning introduced in #12083 runs for
a node that has no child dependencies, parentPkgs aliases the parent's
object, so writing the pinned provider into it exposed the provider to every
sibling resolved afterwards. Sibling order follows resolution completion
order, so optional peers of siblings resolved nondeterministically and
"pnpm dedupe --check" failed intermittently in CI.

Copy parentPkgs before pinning so the pin stays scoped to the node and its
own subtree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: copy parentPkgs only before the first pin write

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
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.

4 participants