Skip to content

fix: hang on cyclic aliased peer dependency#12018

Merged
zkochan merged 3 commits into
mainfrom
fix/11999
May 28, 2026
Merged

fix: hang on cyclic aliased peer dependency#12018
zkochan merged 3 commits into
mainfrom
fix/11999

Conversation

@zkochan

@zkochan zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

  • pnpm i nuxt@npm:nuxt-nightly@5x (and similar aliased installs) hung at 0% CPU during peer resolution after resolved N, reused 0, downloaded N, added 0.
  • resolvePeers.calculateDepPath only short-circuited cycles whose members included currentAlias. When two peers form a mutual cycle (e.g. vite@vitejs/devtools) and both hit the findHit cache instead of running their own calculateDepPath, the cycle surfaced at a level where no participant could break it — a sibling's calculateDepPath saw the cycle in the cycles argument but kept awaiting pathsByNodeIdPromises on cyclic peer node IDs.
  • The fix expands cyclicPeerAliases to also include any cycle that intersects the current call's pending peers, so awaiting siblings emit the name@version peer id and the cached promise gets released.
  • Pacquet's resolve_peers walks synchronously with an in_progress set and returns already-realized DepPath values from find_hit, so the deadlock does not occur there. A pacquet regression test locks in that the aliased-install + transitive-mutual-peer scenario terminates with the expected graph entries.

Closes #11999.

Test plan

  • New regression test aliased install with a transitive mutual-peer cycle should not hang in cyclicPeerDeterminism.ts — verified to fail with a 15 s timeout without the pnpm-side fix and to pass with it.
  • New pacquet regression test aliased_install_with_transitive_mutual_peer_cycle_terminates in pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs — exercises the same shape against resolve_importer and asserts the aliased root + auto-installed peers + leaf graph entries.
  • pnpm i nuxt@npm:nuxt-nightly@5x completes in ~2 s instead of hanging.
  • pnpm i nuxt-nightly@5x (the un-aliased baseline) still completes in ~2 s.
  • pacquet install of the same nuxt@npm:nuxt-nightly@5x package.json completes successfully.
  • All 93 tests in @pnpm/installing.deps-resolver pass.
  • All 61 tests in pacquet-resolving-deps-resolver pass (incl. the new regression test).
  • All 67 enabled tests in installing/deps-installer/test/install/peerDependencies.ts pass.
  • cyclicPeerDeterminism.ts, deepRecursive.ts, and install should not hang on circular peer dependencies in misc.ts all pass.

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a hang during peer dependency resolution for aliased installs that create transitive mutual peer cycles by short‑circuiting cyclic sibling resolutions to avoid deadlocks (regression for issue #11999).
  • Tests
    • Added regression tests to verify installs and peer resolution complete correctly for aliased packages involved in mutual peer cycles.

Review Change Stack

…they surface

When two peers form a mutual cycle (e.g. vite ↔ @vitejs/devtools) and both hit
`findHit` cache instead of running their own `calculateDepPath`, the cycle
detected at the parent level had no participant to short-circuit it. Sibling
resolutions at that level now drop their cyclic peers into the
`name@version` form so the cached path promises can release.

Closes #11999.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ef3c4d6-95aa-4562-8e02-c119b09c4077

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc1300 and 883c6a7.

📒 Files selected for processing (1)
  • installing/deps-resolver/src/resolvePeers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • installing/deps-resolver/src/resolvePeers.ts
📜 Recent 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). (9)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

Fixes a hang in peer-dependency resolution when aliased installs create transitive mutual peer cycles by expanding cycle detection to include pending peer aliases; adds Jest and Rust regression tests and a changeset documenting the patch release.

Changes

Cyclic Aliased Peer Resolution

Layer / File(s) Summary
Peer-cycle alias detection fix and Jest regression test
installing/deps-resolver/src/resolvePeers.ts, installing/deps-installer/test/install/cyclicPeerDeterminism.ts
Expands calculateDepPath cycle detection to treat cycles as cyclic when they include currentAlias or any pending peer alias. Adds a Jest regression test that constructs an aliased install (aaReal@1.0.0) with transitive mutual peers (xy) and asserts the install completes and lockfile retains the aliased resolution.
Rust resolver regression test
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
Adds aliased_fake_result helper and a regression test mirroring the JS scenario, asserting resolve_importer terminates and produces expected resolved graph entries (aliased root present, auto-installed missing peers, aliased dep-path prefix matches real package id, both cycle leaves present).
Release notes
.changeset/fix-cyclic-aliased-peer-hang.md
Patch changeset documenting the hang fix for transitive mutual peer cycles in aliased installs (issue #11999).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 A cycle of peers, so tangled and deep,
Where aliases wander in circles they keep,
I sniffed the pending names hiding in thread—
Short-circuited the loop, and unblocked the dread. 🥕
Now installs hop forward, no more hangs to sweep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: resolving a hang issue when cyclic aliased peer dependencies occur during installation.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/11999

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

…er install

Pacquet's `resolve_peers` walks synchronously with an `in_progress` set
and `find_hit` returns already-realized `DepPath` values, so the cross-
cache cycle from #11999 doesn't deadlock here. Add a regression
test using the same shape — aliased root `a@npm:a-real`, two siblings
that each pull half of an `x` ↔ `y` mutual-peer pair, peer-back to the
aliased root — so the parity is locked in.
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.09      6.1±0.22ms   710.3 KB/sec    1.00      5.6±0.19ms   777.3 KB/sec

@zkochan zkochan marked this pull request as ready for review May 28, 2026 10:57
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Fix hang on cyclic aliased peer dependency resolution

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Fix hang in peer resolution for aliased installs with transitive mutual-peer cycles
• Expand cycle detection to include pending peers, not just current alias
• Add regression tests in pnpm and pacquet to prevent future regressions
• Cycles detected at parent level now properly short-circuit via sibling resolutions
Diagram
flowchart LR
  A["Aliased Install<br/>a@npm:a-real"] -->|depends on| B["Sibling b-real"]
  A -->|depends on| C["Sibling c-real"]
  B -->|depends on| X["Package x"]
  C -->|depends on| Y["Package y"]
  X -->|peer cycle| Y
  Y -->|peer cycle| X
  B -->|peer-depends on| A
  C -->|peer-depends on| A
  X -->|auto-installed| Root["Root Level"]
  Y -->|auto-installed| Root
  Root -->|cycle detection<br/>now includes pending peers| Fix["Cycle Short-circuit"]

Loading

Grey Divider

File Changes

1. installing/deps-resolver/src/resolvePeers.ts 🐞 Bug fix +9/-2

Expand cycle detection to include pending peers

• Expand cyclicPeerAliases detection to include cycles intersecting pending peers
• Add pendingPeerAliases set to track peers being awaited in current resolution
• Update cycle detection logic to short-circuit when cycle members are in pending peers
• Add detailed comment explaining the cross-cache cycle deadlock scenario

installing/deps-resolver/src/resolvePeers.ts


2. installing/deps-installer/test/install/cyclicPeerDeterminism.ts 🧪 Tests +103/-0

Add regression test for aliased cyclic peer hang

• Add regression test for aliased install with transitive mutual-peer cycle
• Create test scenario with aliased root pulling siblings with mutual-peer dependencies
• Mock npm registry with five packages forming the cycle pattern
• Verify lockfile contains expected aliased dependency entry

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


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

Add pacquet regression test for aliased peer cycles

• Add aliased_fake_result helper function for creating aliased package results
• Add regression test aliased_install_with_transitive_mutual_peer_cycle_terminates
• Verify aliased root, auto-installed peers, and mutual-peer leaves appear in graph
• Ensure pacquet's synchronous resolution handles the scenario without deadlock

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


View more (1)
4. .changeset/fix-cyclic-aliased-peer-hang.md 📝 Documentation +6/-0

Changelog entry for cyclic aliased peer hang fix

• Document fix for pnpm hanging on aliased installs with mutual peer cycles
• Reference issue #11999 and example command pnpm i nuxt@npm:nuxt-nightly@5x
• Explain root cause: cycles hitting findHit cache without proper short-circuit
• Mark as patch-level changes for @pnpm/installing.deps-resolver and pnpm

.changeset/fix-cyclic-aliased-peer-hang.md


Grey Divider

Qodo Logo

@github-actions

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.114 ± 0.149 1.963 2.508 1.04 ± 0.08
pacquet@main 2.042 ± 0.063 1.954 2.136 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.11435775132,
      "stddev": 0.14856382552826278,
      "median": 2.10603139152,
      "user": 2.7016877599999995,
      "system": 3.2587299599999993,
      "min": 1.9633633340200003,
      "max": 2.5076455810200002,
      "times": [
        2.11214681902,
        2.02489671902,
        2.10934864202,
        2.12564033102,
        2.0438145370200003,
        2.1027141410200003,
        2.12751677502,
        2.5076455810200002,
        2.02649063402,
        1.9633633340200003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.04187280572,
      "stddev": 0.0628812740769117,
      "median": 2.0476228705199997,
      "user": 2.71750186,
      "system": 3.2550897599999997,
      "min": 1.9541448220200002,
      "max": 2.13569589002,
      "times": [
        2.13569589002,
        1.9541448220200002,
        1.99689717002,
        2.05790255302,
        2.10346437402,
        1.9753709080200001,
        1.9786397210200002,
        2.08735473602,
        2.0919146950200003,
        2.03734318802
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 683.1 ± 13.6 672.0 713.6 1.00
pacquet@main 705.2 ± 31.2 668.7 759.5 1.03 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.68309063694,
      "stddev": 0.01364711768224537,
      "median": 0.6780006111400001,
      "user": 0.36889757999999995,
      "system": 1.35358026,
      "min": 0.6720011611400001,
      "max": 0.7136484751400001,
      "times": [
        0.7136484751400001,
        0.6872831921400001,
        0.67895859014,
        0.68128746514,
        0.67704263214,
        0.67231011214,
        0.67277063914,
        0.67601565214,
        0.6720011611400001,
        0.69958845014
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7052314787400001,
      "stddev": 0.031239755478996597,
      "median": 0.69468873314,
      "user": 0.36832107999999997,
      "system": 1.3581152600000002,
      "min": 0.6686941591400001,
      "max": 0.7594529001400001,
      "times": [
        0.7517586891400001,
        0.7594529001400001,
        0.6686941591400001,
        0.6836533921400001,
        0.6875939281400001,
        0.71114044214,
        0.68804991914,
        0.7245429081400001,
        0.7013275471400001,
        0.6761009021400001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.368 ± 0.031 2.311 2.405 1.00 ± 0.02
pacquet@main 2.367 ± 0.032 2.302 2.422 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.3676020200800005,
      "stddev": 0.03053077079885649,
      "median": 2.36630651268,
      "user": 3.90672468,
      "system": 3.01635742,
      "min": 2.31083567118,
      "max": 2.40491830418,
      "times": [
        2.40491830418,
        2.3326913411800003,
        2.3955082131800003,
        2.31083567118,
        2.35544186018,
        2.3923060511800003,
        2.39681082918,
        2.36684341618,
        2.36576960918,
        2.35489490518
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.36658683928,
      "stddev": 0.03187844118965516,
      "median": 2.3639317121800003,
      "user": 3.9154443799999994,
      "system": 3.03868112,
      "min": 2.30228466718,
      "max": 2.42205585018,
      "times": [
        2.30228466718,
        2.42205585018,
        2.39894894118,
        2.37767615518,
        2.36008164918,
        2.36778177518,
        2.35962829218,
        2.3772496591800003,
        2.35267766218,
        2.34748374118
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.511 ± 0.016 1.496 1.549 1.00
pacquet@main 1.552 ± 0.049 1.509 1.670 1.03 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.51112777728,
      "stddev": 0.016467763991670065,
      "median": 1.5059646888800002,
      "user": 1.7220167599999996,
      "system": 1.79260488,
      "min": 1.49601282638,
      "max": 1.5486514463800003,
      "times": [
        1.5295626313800001,
        1.5118939013800001,
        1.51275446238,
        1.5486514463800003,
        1.4987482793800002,
        1.49601282638,
        1.50111406938,
        1.51063760738,
        1.5006107783800002,
        1.5012917703800002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.55209041458,
      "stddev": 0.04898651921074414,
      "median": 1.5385126908800002,
      "user": 1.7279414599999998,
      "system": 1.8254855799999998,
      "min": 1.50908131638,
      "max": 1.6695398703800002,
      "times": [
        1.55878838138,
        1.5484929173800002,
        1.6695398703800002,
        1.50908131638,
        1.51352954338,
        1.5247082743800002,
        1.52014144138,
        1.5976956393800001,
        1.5285324643800002,
        1.5503942973800002
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12018
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,367.60 ms
(+1.12%)Baseline: 2,341.38 ms
2,809.66 ms
(84.27%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,511.13 ms
(+2.84%)Baseline: 1,469.43 ms
1,763.32 ms
(85.70%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,114.36 ms
(+3.24%)Baseline: 2,048.01 ms
2,457.61 ms
(86.03%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
683.09 ms
(+1.59%)Baseline: 672.43 ms
806.91 ms
(84.65%)
🐰 View full continuous benchmarking report in Bencher

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs (1)

833-865: ⚡ Quick win

Trim prose that re-narrates the test flow.

The new doc/inline comments mostly restate setup and assertions already visible in the test body. Please keep only the minimal non-obvious rationale to reduce doc drift.

Proposed simplification
-/// Regression test for <https://github.com/pnpm/pnpm/issues/11999>.
-///
-/// The TypeScript fix (`installing/deps-resolver/src/resolvePeers.ts`)
-/// broadens which cycles `calculateDepPath` short-circuits. Pacquet's
-/// `resolve_peers` walks synchronously with an `in_progress` set, so
-/// the deadlock that hit pnpm does not occur here — but the scenario
-/// has to keep terminating with a graph entry for the aliased root and
-/// for each pair of mutually-peer-depending leaves.
-///
-/// Layout (from the upstream bug): an aliased install `a@npm:a-real`
-/// pulls in `b-real` and `c-real`. Each of those depends on one half
-/// of a mutual-peer pair (`x` ↔ `y`) and peer-depends on the aliased
-/// root (`a@npm:a-real`). The hoist loop auto-installs `x` and `y` at
-/// the importer level, where their cycle surfaces.
+/// Regression guard for <https://github.com/pnpm/pnpm/issues/11999>:
+/// aliased root + transitive mutual-peer cycle must terminate and keep expected graph entries.
 #[tokio::test]
 async fn aliased_install_with_transitive_mutual_peer_cycle_terminates() {
     let mut table = HashMap::new();
-    // Root install: `a@npm:a-real@1.0.0`.

As per coding guidelines: “Tests are documentation. Do not duplicate them in prose.”

Also applies to: 868-917

🤖 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 `@pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs` around
lines 833 - 865, Condense the long doc comment that precedes the #[tokio::test]
regression test for issue 11999 (the block that describes the aliased install
scenario) so it no longer restates the test setup and assertions; keep a single
short sentence stating the test's purpose (regression for `#11999`) and, if
needed, one brief non-obvious rationale or the minimal layout summary (1–2
lines). Apply the same trimming to the subsequent comment lines that cover the
test body (the section noted as also applying to 868–917). Edit the comment
block surrounding the test (the prose before the test attribute) rather than the
test code or helper functions like aliased_fake_result; remove redundant
narrative while preserving the issue reference and any essential context.
🤖 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.

Nitpick comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs`:
- Around line 833-865: Condense the long doc comment that precedes the
#[tokio::test] regression test for issue 11999 (the block that describes the
aliased install scenario) so it no longer restates the test setup and
assertions; keep a single short sentence stating the test's purpose (regression
for `#11999`) and, if needed, one brief non-obvious rationale or the minimal
layout summary (1–2 lines). Apply the same trimming to the subsequent comment
lines that cover the test body (the section noted as also applying to 868–917).
Edit the comment block surrounding the test (the prose before the test
attribute) rather than the test code or helper functions like
aliased_fake_result; remove redundant narrative while preserving the issue
reference and any essential context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cc30ec62-8f8f-465f-bd37-544e86f335c0

📥 Commits

Reviewing files that changed from the base of the PR and between b1fa2d5 and 9fc1300.

📒 Files selected for processing (4)
  • .changeset/fix-cyclic-aliased-peer-hang.md
  • installing/deps-installer/test/install/cyclicPeerDeterminism.ts
  • installing/deps-resolver/src/resolvePeers.ts
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📜 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). (2)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{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-resolver/src/resolvePeers.ts
  • installing/deps-installer/test/install/cyclicPeerDeterminism.ts
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
🧠 Learnings (7)
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/fix-cyclic-aliased-peer-hang.md
📚 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-resolver/src/resolvePeers.ts
  • installing/deps-installer/test/install/cyclicPeerDeterminism.ts
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
🔇 Additional comments (3)
.changeset/fix-cyclic-aliased-peer-hang.md (1)

1-7: LGTM!

installing/deps-installer/test/install/cyclicPeerDeterminism.ts (1)

120-221: LGTM!

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

558-567: LGTM!

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.29%. Comparing base (2a0032e) to head (883c6a7).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12018      +/-   ##
==========================================
+ Coverage   88.21%   88.29%   +0.08%     
==========================================
  Files         228      228              
  Lines       28777    28942     +165     
==========================================
+ Hits        25385    25555     +170     
+ Misses       3392     3387       -5     

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

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

# Scenario main HEAD
1 Isolated linker: fresh restore, hot cache + hot store 2.221s ± 0.117s 2.218s ± 0.058s
2 Isolated linker: fresh add new dep, hot cache + hot store 5.808s ± 0.122s 5.632s ± 0.067s
3 Isolated linker: fresh install, hot cache + hot store 5.642s ± 0.05s 5.621s ± 0.055s
4 Isolated linker: fresh restore, cold cache + cold store 4.688s ± 0.109s 4.681s ± 0.086s
5 Isolated linker: fresh install, cold cache + cold store 8.116s ± 0.279s 7.986s ± 0.077s
6 GVS linker: fresh restore, hot cache + hot store 1.022s ± 0.032s 1.006s ± 0.009s

Run 26570835797 · 10 runs per scenario · triggered by @zkochan

@zkochan zkochan merged commit 39101f5 into main May 28, 2026
25 checks passed
@zkochan zkochan deleted the fix/11999 branch May 28, 2026 11:30
@isaacs

isaacs commented May 28, 2026

Copy link
Copy Markdown

Thanks! 🙏

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.

pnpm hangs when encountering a cyclical aliased peer dependency

3 participants