feat(pacquet): configurational dependencies support#12285
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📜 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). (8)
🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🧠 Learnings (5)📚 Learning: 2026-05-20T19:40:55.051ZApplied to files:
📚 Learning: 2026-05-22T00:08:44.646ZApplied to files:
📚 Learning: 2026-05-20T23:07:58.444ZApplied to files:
📚 Learning: 2026-05-20T23:23:40.606ZApplied to files:
📚 Learning: 2026-06-06T18:58:37.156ZApplied to files:
🔇 Additional comments (2)
📝 WalkthroughWalkthroughAdds end-to-end support for pnpm-style configDependencies: an env lockfile document, resolver + env-installer crate to materialize config deps into node_modules/.pnpm-config, updateConfig hook execution, workspace manifest edits for configDependencies, CLI wiring for install/add, reporter events, hashing helpers, and tests. ChangesConfig Dependencies and UpdateConfig Hooks
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Micro-Benchmark ResultsLinux |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 10.0703398826,
"stddev": 0.3695879452585777,
"median": 9.9612328988,
"user": 2.98987604,
"system": 3.2126489200000004,
"min": 9.7823590063,
"max": 11.0286917353,
"times": [
11.0286917353,
10.0631564833,
10.206252415300002,
9.833150057300001,
10.1382513913,
9.7823590063,
9.8593093143,
9.8569628283,
9.8308299793,
10.104435615300002
]
},
{
"command": "pacquet@main",
"mean": 9.8407284748,
"stddev": 0.04094713702659372,
"median": 9.836526048800001,
"user": 2.96658284,
"system": 3.22110982,
"min": 9.7913178493,
"max": 9.9349875023,
"times": [
9.863381028300001,
9.856237098300001,
9.7913178493,
9.9349875023,
9.843603509300001,
9.853953671300001,
9.829448588300002,
9.816601789300002,
9.8022200243,
9.8155336873
]
},
{
"command": "pnpr@HEAD",
"mean": 5.34748012,
"stddev": 0.09197809226576023,
"median": 5.3269097493,
"user": 2.4538090399999994,
"system": 2.93300292,
"min": 5.2656684263,
"max": 5.5957745593,
"times": [
5.3781132563,
5.3017191413,
5.3341543403000005,
5.3237631363,
5.3308129813,
5.3179716343,
5.2656684263,
5.5957745593,
5.2967673623,
5.3300563623
]
},
{
"command": "pnpr@main",
"mean": 5.4008227164,
"stddev": 0.12903773415225914,
"median": 5.3261791252999995,
"user": 2.4007877400000006,
"system": 2.93641702,
"min": 5.3049559183,
"max": 5.6615578453,
"times": [
5.3071346393,
5.3049559183,
5.3083550093,
5.4202088113,
5.3193353873,
5.6615578453,
5.3051098033,
5.3330228633,
5.4789871473,
5.5695597393
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6376648414600002,
"stddev": 0.016250354529034897,
"median": 0.6336530353600001,
"user": 0.36915541999999996,
"system": 1.3066455599999998,
"min": 0.6200019038600001,
"max": 0.6618892288600001,
"times": [
0.6608509148600001,
0.6374716018600001,
0.6618892288600001,
0.6230288748600001,
0.65432576486,
0.6255236538600001,
0.6200019038600001,
0.6411438988600001,
0.6225781038600001,
0.62983446886
]
},
{
"command": "pacquet@main",
"mean": 0.6470982485600001,
"stddev": 0.02398907430867578,
"median": 0.6447370388600001,
"user": 0.36442882,
"system": 1.32245006,
"min": 0.6220172378600001,
"max": 0.7076022058600001,
"times": [
0.6478445448600001,
0.6220172378600001,
0.7076022058600001,
0.6314737978600001,
0.6521056838600001,
0.6336059378600001,
0.65740846086,
0.6426388018600001,
0.6294505388600001,
0.6468352758600001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.81573911686,
"stddev": 0.09476172319908027,
"median": 0.7731097758600001,
"user": 0.38832732,
"system": 1.3547038599999999,
"min": 0.7477432648600001,
"max": 1.01468801586,
"times": [
0.9704157098600001,
0.7477432648600001,
0.7938157888600001,
0.7626292198600001,
0.79188836786,
0.7692619168600001,
1.01468801586,
0.7769576348600001,
0.7634113578600001,
0.7665798918600001
]
},
{
"command": "pnpr@main",
"mean": 0.7816775815600002,
"stddev": 0.0753041469068164,
"median": 0.7572575558600001,
"user": 0.38313762,
"system": 1.31286346,
"min": 0.71439316086,
"max": 0.9330668608600001,
"times": [
0.7609093788600001,
0.74275477586,
0.9330668608600001,
0.7623581638600001,
0.7291183118600001,
0.7621902278600001,
0.9092706048600001,
0.7536057328600001,
0.7491085978600001,
0.71439316086
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 9.18372792668,
"stddev": 0.030325245383382844,
"median": 9.17665877688,
"user": 3.4763640800000006,
"system": 3.2513979999999996,
"min": 9.142910991379999,
"max": 9.228005377379999,
"times": [
9.19217019438,
9.18219419938,
9.14634440238,
9.16669302138,
9.21100277938,
9.17053942438,
9.228005377379999,
9.142910991379999,
9.171123354379999,
9.22629552238
]
},
{
"command": "pacquet@main",
"mean": 9.19254601208,
"stddev": 0.041014226837257584,
"median": 9.19165280288,
"user": 3.49179308,
"system": 3.2322397999999994,
"min": 9.12915414438,
"max": 9.25190539838,
"times": [
9.23934662538,
9.15139198538,
9.12915414438,
9.18557664838,
9.197728957379999,
9.25190539838,
9.22333033338,
9.17795934338,
9.21880576738,
9.150260917379999
]
},
{
"command": "pnpr@HEAD",
"mean": 5.151704176680001,
"stddev": 0.12496576171536285,
"median": 5.096387287380001,
"user": 2.29415588,
"system": 2.8213420999999994,
"min": 5.06056963638,
"max": 5.415765660380001,
"times": [
5.33498004638,
5.10470428238,
5.08807029238,
5.067939869380001,
5.06056963638,
5.066545777380001,
5.415765660380001,
5.1122572143800005,
5.1885541363800005,
5.07765485138
]
},
{
"command": "pnpr@main",
"mean": 5.1759807884799995,
"stddev": 0.1598132596643247,
"median": 5.109305944880001,
"user": 2.24577958,
"system": 2.8028398,
"min": 5.01501094738,
"max": 5.467345814380001,
"times": [
5.12878238638,
5.44623044438,
5.06179022438,
5.089829503380001,
5.01501094738,
5.467345814380001,
5.07071246238,
5.23635332038,
5.084777170380001,
5.158975611380001
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.3676391154,
"stddev": 0.012370554676851364,
"median": 1.3686475786,
"user": 1.5073400399999997,
"system": 1.76222866,
"min": 1.3488408901000002,
"max": 1.3914848871,
"times": [
1.3695124211,
1.3677827361000001,
1.3776871871,
1.3659673321,
1.3914848871,
1.3518691431,
1.3488408901000002,
1.3596149371000001,
1.3719253051000002,
1.3717063151000002
]
},
{
"command": "pacquet@main",
"mean": 1.3790689703,
"stddev": 0.02489673445760907,
"median": 1.3714326766,
"user": 1.5032406399999998,
"system": 1.7775510599999997,
"min": 1.3533942541000001,
"max": 1.4462532521000002,
"times": [
1.4462532521000002,
1.3688837551000002,
1.3690529531,
1.3708155571,
1.3533942541000001,
1.3798362811,
1.3720497961,
1.3805047051000001,
1.3798317831,
1.3700673661
]
},
{
"command": "pnpr@HEAD",
"mean": 0.673614452,
"stddev": 0.03208226599473443,
"median": 0.6608489416,
"user": 0.33028574,
"system": 1.2703459599999998,
"min": 0.6480463281,
"max": 0.7471574401,
"times": [
0.7471574401,
0.7150858261,
0.6725994191,
0.6480463281,
0.6525269151,
0.6614090811,
0.6602888021,
0.6532134391,
0.6685469981,
0.6572702711
]
},
{
"command": "pnpr@main",
"mean": 0.6745691866,
"stddev": 0.05443455429539115,
"median": 0.6543729106,
"user": 0.32672614,
"system": 1.2651306599999999,
"min": 0.6466950461,
"max": 0.8255512431,
"times": [
0.6906937651,
0.6527167501,
0.6512316831,
0.6466950461,
0.6604459001,
0.8255512431,
0.6531815651,
0.6574611571,
0.6555642561,
0.6521505001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.970603273139999,
"stddev": 0.033540979187953526,
"median": 4.97800098634,
"user": 1.6709779999999999,
"system": 1.86960394,
"min": 4.92679980434,
"max": 5.01748349034,
"times": [
4.9958842383399995,
4.94484278534,
4.9385026733399995,
5.004340398339999,
5.01748349034,
4.98803872234,
4.99306590634,
4.9679632503399995,
4.92911146234,
4.92679980434
]
},
{
"command": "pacquet@main",
"mean": 4.95601024484,
"stddev": 0.019013810957542274,
"median": 4.96054214634,
"user": 1.6821370999999998,
"system": 1.8909357399999998,
"min": 4.92186073934,
"max": 4.9864182993399995,
"times": [
4.931596616339999,
4.9559568583399995,
4.96618139034,
4.94321749634,
4.96396418534,
4.95902062234,
4.92186073934,
4.96982257034,
4.96206367034,
4.9864182993399995
]
},
{
"command": "pnpr@HEAD",
"mean": 0.69024275944,
"stddev": 0.05415720988906426,
"median": 0.66738631534,
"user": 0.32244429999999996,
"system": 1.30604564,
"min": 0.64852599634,
"max": 0.83520502234,
"times": [
0.71231490334,
0.83520502234,
0.66804870634,
0.66519598434,
0.66460769834,
0.66267762934,
0.66672392434,
0.64852599634,
0.68388329134,
0.69524443834
]
},
{
"command": "pnpr@main",
"mean": 0.65779551844,
"stddev": 0.023071785532128155,
"median": 0.65096137734,
"user": 0.3182223999999999,
"system": 1.26003974,
"min": 0.63747758834,
"max": 0.71907115634,
"times": [
0.71907115634,
0.64667868734,
0.66350442734,
0.65562614134,
0.65089713134,
0.64241276334,
0.64779050034,
0.63747758834,
0.6634711653400001,
0.65102562334
]
}
]
} |
|
| Branch | pr/12285 |
| Testbed | pacquet |
🚨 1 Alert
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | Latency seconds (s) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 9.18 s(+37.34%)Baseline: 6.69 s | 8.02 s (114.45%) |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 9,183.73 ms(+37.34%)Baseline: 6,686.85 ms | 8,024.22 ms (114.45%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 4,970.60 ms(-0.87%)Baseline: 5,014.36 ms | 6,017.23 ms (82.61%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,367.64 ms(-2.58%)Baseline: 1,403.86 ms | 1,684.63 ms (81.18%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 10,070.34 ms(+14.49%)Baseline: 8,795.58 ms | 10,554.69 ms (95.41%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 637.66 ms(-2.11%)Baseline: 651.39 ms | 781.67 ms (81.58%) |
Ports pnpm's `@pnpm/installing.env-installer` to pacquet: resolves and
installs `configDependencies` declared in pnpm-workspace.yaml into
node_modules/.pnpm-config/<name> via the global virtual store, recording
them in the env lockfile (the first YAML document of pnpm-lock.yaml).
New `pacquet-env-installer` crate:
- resolve_and_install_config_deps: clean-specifier resolution plus
migration of the old inline (`version+integrity`) and object
(`{ tarball?, integrity }`) formats
- one level of optional subdeps with os/cpu/libc platform fields and
host filtering; exact-version-only enforcement
- env-lockfile pruning of stale packages/snapshots on re-resolve
- pnpm error codes (ERR_PNPM_BAD_CONFIG_DEP, CONFIG_DEP_OPTIONAL_NOT_EXACT,
ENV_LOCKFILE_CORRUPTED, FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE, ...)
Supporting changes:
- graph-hasher: calc_leaf_global_virtual_store_path and
calc_global_virtual_store_path_with_subdeps
- lockfile: EnvLockfile type + multi-document read/write; the wanted
lockfile writer now preserves the env document
- reporter: pnpm:installing-config-deps event
Verified with 5 integration tests against the mocked registry plus
graph-hasher and env-lockfile unit tests.
Draft / work in progress. Still to land: the updateConfig pnpmfile hook
primitive (plugin hooks), wiring config-dep install into the install
command at config finalization, `add --config`, and the remaining
ported pnpm tests.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12285 +/- ##
==========================================
- Coverage 87.55% 87.53% -0.03%
==========================================
Files 280 288 +8
Lines 33664 34816 +1152
==========================================
+ Hits 29476 30475 +999
- Misses 4188 4341 +153 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Code Review by Qodo
1. Config dep path traversal
|
PR Summary by Qodofeat(pacquet): end-to-end support for pnpm-style configDependencies WalkthroughsDescription• Resolve and install workspace configDependencies into node_modules/.pnpm-config before main install. • Persist config deps in the env lockfile (first pnpm-lock.yaml document) and preserve it on rewrites. • Add updateConfig pnpmfile hook chaining (plugin + root pnpmfile) and support pacquet add --config. Diagramgraph TD
A["pacquet CLI"] --> B["Config finalization"] --> C["Env installer"] --> D["Env lockfile"] --> E["pnpm-lock.yaml"]
B --> F["updateConfig hooks"] --> G["Config (mutated)"] --> H["Package manager install"] --> E
C --> I[("Global virtual store")]
C --> J[".pnpm-config links"]
subgraph Legend
direction LR
_svc[Service/Module] ~~~ _db[(Data/Store)]
end
High-Level AssessmentThe following are alternative approaches to this PR: 1. Avoid leaking StoreDir by changing installer APIs
2. Integrate config-deps install into package-manager pipeline
3. Persist hook delta as an explicit typed structure
Recommendation: The PR’s approach (a dedicated env-installer + explicit config-finalization phase + JSON round-trip through WorkspaceSettings) is the best fit for pnpm parity and for ensuring the env document is written before the main lockfile path runs. The main follow-up to consider is removing the StoreDir leak by evolving downstream APIs to accept non-'static borrows once the surrounding installer plumbing is ready for that refactor. File ChangesEnhancement (29)
Bug fix (1)
Tests (7)
Other (2)
|
|
Code review by qodo was updated up to the latest commit e2b0afb |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
pacquet/crates/cli/tests/config_dependencies.rs (1)
37-44: ⚡ Quick winAdd pre-assert diagnostics for non-
assert_eq!checks.Several
assert!checks run without nearby logging of the inspected values/paths, which makes CI failures harder to debug.As per coding guidelines, "Follow test-logging guidance — log before non-
assert_eq!assertions,dbg!complex structures, skip logging for simple scalarassert_eq!."Also applies to: 66-69, 98-102, 128-137, 165-168
🤖 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/cli/tests/config_dependencies.rs` around lines 37 - 44, Add diagnostic logging before each non-assert_eq! assertion so CI failures show inspected values: e.g., print the computed workspace path and the `installed` path and the `lockfile` contents before the assert! calls in config_dependencies.rs (around the checks using variables `installed`, `lockfile`, and `workspace`), using dbg! or eprintln! with clear labels; apply the same pattern to the other non-assert_eq! blocks noted (the checks around the ranges that reference the pnpm lockfile, `configDependencies`, and package names) so tests emit the values being checked just prior to assertion.Source: Coding guidelines
🤖 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.
Inline comments:
In `@pacquet/crates/cli/src/cli_args.rs`:
- Around line 265-299: The config-deps phase is using the CLI cwd (`dir`)
instead of the workspace/lockfile root, so change calls to
config_deps::install_config_deps and config_deps::run_update_config_hooks to use
the same root that State::init uses (i.e. use
cfg.workspace_dir.as_deref().unwrap_or(dir.as_path()) as the directory argument)
rather than `&dir`, keeping the rest of the call sites (reporter generics and
frozen_lockfile) unchanged so workspace-level `.pnpm-config` and env lockfile
are read/written consistently with State::init before calling args.run.
In `@pacquet/crates/cli/src/cli_args/add.rs`:
- Around line 131-140: The code computes root_dir from
state.manifest.path().parent() which picks the package directory; instead
resolve --config against the workspace root by preferring
state.config.workspace_dir.as_deref() and only falling back to the manifest
parent (or "."), then pass that root_dir into
config_deps::add_config_dependency::<Reporter>; update the root_dir construction
to use state.config.workspace_dir.as_deref().map_or_else(||
state.manifest.path().parent().map_or_else(|| PathBuf::from("."),
Path::to_path_buf), |ws| PathBuf::from(ws)) so configDependencies are
stored/modified at the workspace level.
In `@pacquet/crates/cli/src/config_deps.rs`:
- Around line 227-266: The hook-output handling currently ignores keys removed
by hooks (e.g., current.get("catalogs") skip and config_delta omits missing
keys), so fixes must: (1) when detecting current.get("catalogs") !=
input.get("catalogs") and the output has removed "catalogs", explicitly set
config.catalogs to an empty catalog representation (e.g., Some(empty Vec/Map)
instead of leaving the manifest value) via the same code path that assigns
config.catalogs (update the block that references current.get("catalogs") and
config.catalogs); and (2) change config_delta(input, output) to include keys
that existed in input but are absent in output by inserting a sentinel
(Value::Null) for general deleted keys and special-case "catalogs" to insert the
explicit empty catalogs Value so delta_settings (deserialized into
WorkspaceSettings) and delta_settings.apply_to(config, &base_dir) can clear
fields rather than leaving old values. Ensure you reference and update the
functions/variables config_delta, current.get("catalogs"), config.catalogs,
delta_settings, and WorkspaceSettings::apply_to.
In `@pacquet/crates/env-installer/src/errors.rs`:
- Around line 58-60: DownloadTarball currently annotates a crate-local
diagnostic code which overrides codes from the inner TarballError; remove the
variant-level #[diagnostic(code(...))] (or otherwise stop assigning a new
diagnostic code) on the DownloadTarball enum variant so the underlying
TarballError's diagnostic code/messages are preserved and surfaced to callers;
keep the display message and the source wrapper
(DownloadTarball(#[error(source)] TarballError)) intact so only the
diagnostic-code override is removed.
In `@pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs`:
- Around line 183-184: The code silently ignores pkg_key parse failures (let
Ok(key) = pkg_key(name, version) else { return }) which aborts migration without
signaling an error; change the function to return a Result and replace the
pattern with let key = pkg_key(name, version)? so failures propagate, then
update callers (the places that invoke this routine around the earlier call
sites referenced) to use the ? operator (as suggested for the call sites at
lines 51 and 67) so errors bubble up instead of being swallowed before inserting
into env_lockfile.root_importer_mut().config_dependencies.
In `@pacquet/crates/env-installer/src/tests.rs`:
- Around line 391-396: The test currently checks CONFIG_DEP_EVENTS (locked via
CONFIG_DEP_EVENTS.lock().unwrap()) using contains(), which allows wrong orders
or duplicates; replace the loose check with a strict order-and-count assertion
by comparing the collected vector (first) exactly to the expected sequence
vec![InstallingConfigDepsStatus::Started, InstallingConfigDepsStatus::Done]
using assert_eq! so the test enforces order and no duplicates for the
installing-config-deps event stream.
In `@pacquet/crates/workspace-manifest-writer/src/lib.rs`:
- Around line 130-134: The current code always writes pnpm-workspace.yaml even
when edit::add_config_dependency returns false (no change); change the flow to
check the boolean result from edit::add_config_dependency and, if it indicates
no modification, return early (propagating no error) instead of calling
fs::write; keep the existing error mapping for the edit failure
(UpdateWorkspaceManifestError::Edit { path, source }) and only call
fs::write(manifest.into_text()) with its UpdateWorkspaceManifestError::Write
mapping when the edit returned true (i.e., the manifest was actually modified).
---
Nitpick comments:
In `@pacquet/crates/cli/tests/config_dependencies.rs`:
- Around line 37-44: Add diagnostic logging before each non-assert_eq! assertion
so CI failures show inspected values: e.g., print the computed workspace path
and the `installed` path and the `lockfile` contents before the assert! calls in
config_dependencies.rs (around the checks using variables `installed`,
`lockfile`, and `workspace`), using dbg! or eprintln! with clear labels; apply
the same pattern to the other non-assert_eq! blocks noted (the checks around the
ranges that reference the pnpm lockfile, `configDependencies`, and package
names) so tests emit the values being checked just prior to assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 595f9ebc-e6a3-4122-9e8e-e0530844a9c3
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (39)
Cargo.tomlpacquet/crates/cli/Cargo.tomlpacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/cli/src/config_deps.rspacquet/crates/cli/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/config/Cargo.tomlpacquet/crates/config/src/lib.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/Cargo.tomlpacquet/crates/env-installer/src/errors.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/env-installer/src/lib.rspacquet/crates/env-installer/src/options.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/env-installer/src/prune.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/env-installer/src/tests.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/lib.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/package-manager/src/install.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/reporter/src/lib.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/workspace-manifest-writer/src/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). (5)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (3)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/cli/src/lib.rspacquet/crates/env-installer/src/prune.rspacquet/crates/cli/src/cli_args.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/package-manager/src/install.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/lockfile/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/env-installer/src/errors.rspacquet/crates/reporter/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/workspace-manifest-writer/src/tests.rspacquet/crates/env-installer/src/options.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/env-installer/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/cli/src/config_deps.rspacquet/crates/config/src/lib.rspacquet/crates/hooks/src/lib.rspacquet/crates/env-installer/src/tests.rs
pacquet/**/Cargo.toml
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in[workspace.dependencies]in the rootCargo.tomlbefore adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it
Files:
pacquet/crates/config/Cargo.tomlpacquet/crates/env-installer/Cargo.tomlpacquet/crates/cli/Cargo.toml
pacquet/**/tests/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — usetempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq!assertions,dbg!complex structures, skip logging for simple scalarassert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer#[cfg_attr(target_os = "windows", ignore = "...")](or matching#[cfg(unix)]gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests withinstaand carefully review diffs when intentional changes alter snapshots; accept withcargo insta reviewonly after careful review
Tests that need the mocked registry should startpnprthroughpacquet-testing-utils;cargo test/cargo nextest runshould not require a separatejust registry-mock launchstep
Files:
pacquet/crates/hooks/tests/node_hooks.rspacquet/crates/cli/tests/config_dependencies.rs
🧠 Learnings (5)
📚 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/cli/src/lib.rspacquet/crates/env-installer/src/prune.rspacquet/crates/cli/src/cli_args.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/package-manager/src/install.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/lockfile/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/env-installer/src/errors.rspacquet/crates/reporter/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/workspace-manifest-writer/src/tests.rspacquet/crates/env-installer/src/options.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/env-installer/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/cli/src/config_deps.rspacquet/crates/config/src/lib.rspacquet/crates/hooks/src/lib.rspacquet/crates/env-installer/src/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/cli/src/lib.rspacquet/crates/env-installer/src/prune.rspacquet/crates/cli/src/cli_args.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/package-manager/src/install.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/lockfile/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/env-installer/src/errors.rspacquet/crates/reporter/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/workspace-manifest-writer/src/tests.rspacquet/crates/env-installer/src/options.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/env-installer/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/cli/src/config_deps.rspacquet/crates/config/src/lib.rspacquet/crates/hooks/src/lib.rspacquet/crates/env-installer/src/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/cli/src/lib.rspacquet/crates/env-installer/src/prune.rspacquet/crates/cli/src/cli_args.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/package-manager/src/install.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/lockfile/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/env-installer/src/errors.rspacquet/crates/reporter/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/workspace-manifest-writer/src/tests.rspacquet/crates/env-installer/src/options.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/env-installer/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/cli/src/config_deps.rspacquet/crates/config/src/lib.rspacquet/crates/hooks/src/lib.rspacquet/crates/env-installer/src/tests.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/cli/src/lib.rspacquet/crates/env-installer/src/prune.rspacquet/crates/cli/src/cli_args.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile/src/resolution.rspacquet/crates/package-manager/src/install.rspacquet/crates/graph-hasher/src/lib.rspacquet/crates/lockfile/src/lib.rspacquet/crates/hooks/src/node_runtime.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/env-installer/src/parse_integrity.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rspacquet/crates/env-installer/src/errors.rspacquet/crates/reporter/src/lib.rspacquet/crates/hooks/src/finder.rspacquet/crates/hooks/tests/node_hooks.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/workspace-manifest-writer/src/tests.rspacquet/crates/env-installer/src/options.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/graph-hasher/src/global_virtual_store_path.rspacquet/crates/env-installer/src/lib.rspacquet/crates/cli/tests/config_dependencies.rspacquet/crates/lockfile/src/env_lockfile.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/env-installer/src/resolve_optional_subdeps.rspacquet/crates/cli/src/config_deps.rspacquet/crates/config/src/lib.rspacquet/crates/hooks/src/lib.rspacquet/crates/env-installer/src/tests.rs
📚 Learning: 2026-05-20T23:23:40.606Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11788
File: pacquet/crates/lockfile/src/snapshot_dep_ref.rs:52-56
Timestamp: 2026-05-20T23:23:40.606Z
Learning: In the pacquet lockfile crate, the link target is intentionally represented as a raw `Link(String)` (not a `LinkTarget` newtype) for both `SnapshotDepRef::Link` and `ImporterDepVersion::Link`. pnpm upstream preserves `link:` targets verbatim with only a prefix check (e.g., `refToRelative`), so do not suggest introducing a `LinkTarget` newtype in a bug-fix/targeted PR. If type branding is ever desired, it should be done together for both variants in a separate dedicated refactor PR rather than as part of a small fix.
Applied to files:
pacquet/crates/lockfile/src/resolution.rspacquet/crates/lockfile/src/lib.rspacquet/crates/lockfile/src/env_lockfile/tests.rspacquet/crates/lockfile/src/save_lockfile.rspacquet/crates/lockfile/src/yaml_documents.rspacquet/crates/lockfile/src/env_lockfile.rs
🔇 Additional comments (28)
pacquet/crates/config/Cargo.toml (1)
14-14: LGTM!pacquet/crates/config/src/workspace_yaml.rs (1)
75-75: LGTM!pacquet/crates/hooks/src/lib.rs (1)
77-91: LGTM!pacquet/crates/env-installer/src/lib.rs (1)
17-34: LGTM!pacquet/crates/env-installer/src/resolve_optional_subdeps.rs (1)
20-154: LGTM!pacquet/crates/env-installer/src/prune.rs (1)
16-48: LGTM!pacquet/crates/hooks/src/node_runtime.rs (1)
149-159: LGTM!pacquet/crates/lockfile/src/lib.rs (1)
3-3: LGTM!Also applies to: 26-26
pacquet/crates/lockfile/src/env_lockfile.rs (1)
1-149: LGTM!pacquet/crates/lockfile/src/yaml_documents.rs (1)
16-16: LGTM!Also applies to: 21-21, 42-59
pacquet/crates/lockfile/src/save_lockfile.rs (1)
1-4: LGTM!Also applies to: 57-90
pacquet/crates/lockfile/src/env_lockfile/tests.rs (1)
1-104: LGTM!pacquet/crates/hooks/tests/node_hooks.rs (1)
465-545: LGTM!pacquet/crates/workspace-manifest-writer/src/tests.rs (1)
204-243: LGTM!pacquet/crates/lockfile/src/resolution.rs (1)
323-323: LGTM!pacquet/crates/env-installer/Cargo.toml (1)
1-47: LGTM!pacquet/crates/env-installer/src/parse_integrity.rs (1)
1-51: LGTM!Cargo.toml (1)
28-28: LGTM!pacquet/crates/cli/Cargo.toml (1)
18-43: LGTM!pacquet/crates/cli/src/lib.rs (1)
2-2: LGTM!pacquet/crates/env-installer/src/options.rs (1)
1-73: LGTM!pacquet/crates/env-installer/src/install_config_deps.rs (1)
1-532: LGTM!pacquet/crates/graph-hasher/src/global_virtual_store_path.rs (1)
23-27: LGTM!Also applies to: 95-138
pacquet/crates/graph-hasher/src/lib.rs (1)
25-28: LGTM!pacquet/crates/graph-hasher/src/global_virtual_store_path/tests.rs (1)
1-4: LGTM!Also applies to: 6-6, 307-363
pacquet/crates/hooks/src/finder.rs (1)
1-4: LGTM!Also applies to: 25-78
pacquet/crates/package-manager/src/install.rs (1)
440-449: LGTM!pacquet/crates/package-manager/src/install_package_from_registry/tests.rs (1)
109-109: LGTM!
|
Code review by qodo was updated up to the latest commit f72f9eb |
|
Code review by qodo was updated up to the latest commit 3f8994e |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
pacquet/crates/cli/src/config_deps.rs (1)
262-273:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle deleted hook keys in
config_delta(not only modified/present ones).
config_deltaonly iterates keys present inoutput, so hook deletions (e.g., removingoverrides) are dropped and never reachWorkspaceSettings::apply_to. That leaves stale config values afterupdateConfig.Proposed minimal fix
fn config_delta(input: &Value, output: &Value) -> Value { let (Some(input_obj), Some(output_obj)) = (input.as_object(), output.as_object()) else { return output.clone(); }; let mut delta = serde_json::Map::new(); for (key, value) in output_obj { if input_obj.get(key) != Some(value) { delta.insert(key.clone(), value.clone()); } } + for key in input_obj.keys() { + if key != "catalogs" && !output_obj.contains_key(key) { + delta.insert(key.clone(), Value::Null); + } + } Value::Object(delta) }🤖 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/cli/src/config_deps.rs` around lines 262 - 273, The config_delta function only considers keys present in output, so deletions in input (e.g., removed hook keys like "overrides") are lost; modify config_delta to iterate the union of keys from both input.as_object() and output.as_object(), and for each key insert output's value when present or Value::Null when the key exists in input but not in output (so WorkspaceSettings::apply_to can detect deletions), keeping the existing behavior for unchanged values by comparing input_obj.get(key) to output_obj.get(key).
🤖 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.
Duplicate comments:
In `@pacquet/crates/cli/src/config_deps.rs`:
- Around line 262-273: The config_delta function only considers keys present in
output, so deletions in input (e.g., removed hook keys like "overrides") are
lost; modify config_delta to iterate the union of keys from both
input.as_object() and output.as_object(), and for each key insert output's value
when present or Value::Null when the key exists in input but not in output (so
WorkspaceSettings::apply_to can detect deletions), keeping the existing behavior
for unchanged values by comparing input_obj.get(key) to output_obj.get(key).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 030f000d-bf2f-4ef0-af77-e48fa8a8ba1d
📒 Files selected for processing (11)
pacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/add.rspacquet/crates/cli/src/config_deps.rspacquet/crates/env-installer/src/errors.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/env-installer/src/resolve_and_install_config_deps.rspacquet/crates/env-installer/src/tests.rspacquet/crates/workspace-manifest-writer/src/edit.rspacquet/crates/workspace-manifest-writer/src/lib.rspacquet/crates/workspace-manifest-writer/src/model.rspacquet/crates/workspace-manifest-writer/src/tests.rs
🚧 Files skipped from review as they are similar to previous changes (8)
- pacquet/crates/workspace-manifest-writer/src/lib.rs
- pacquet/crates/cli/src/cli_args/add.rs
- pacquet/crates/workspace-manifest-writer/src/tests.rs
- pacquet/crates/cli/src/cli_args.rs
- pacquet/crates/env-installer/src/errors.rs
- pacquet/crates/env-installer/src/resolve_and_install_config_deps.rs
- pacquet/crates/workspace-manifest-writer/src/edit.rs
- pacquet/crates/env-installer/src/install_config_deps.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). (7)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/workspace-manifest-writer/src/model.rspacquet/crates/env-installer/src/tests.rspacquet/crates/cli/src/config_deps.rs
🧠 Learnings (4)
📚 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/workspace-manifest-writer/src/model.rspacquet/crates/env-installer/src/tests.rspacquet/crates/cli/src/config_deps.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/workspace-manifest-writer/src/model.rspacquet/crates/env-installer/src/tests.rspacquet/crates/cli/src/config_deps.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/workspace-manifest-writer/src/model.rspacquet/crates/env-installer/src/tests.rspacquet/crates/cli/src/config_deps.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/workspace-manifest-writer/src/model.rspacquet/crates/env-installer/src/tests.rspacquet/crates/cli/src/config_deps.rs
🔇 Additional comments (2)
pacquet/crates/env-installer/src/tests.rs (1)
390-396: LGTM!Also applies to: 466-507
pacquet/crates/workspace-manifest-writer/src/model.rs (1)
19-23: LGTM!Also applies to: 32-44, 59-60, 68-84
|
Code review by qodo was updated up to the latest commit 6e284b5 |
…e-manager binary (#12292) pnpm can be made to download and execute a native binary through two **repository-controlled** inputs, neither of which was authenticated before this change: 1. **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` (in `pnpm-workspace.yaml`) opts in to pnpm's Rust install engine, and pnpm spawns the platform binary `@pacquet/<platform>-<arch>` during `pnpm install`. 2. **package-manager version switch** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. This is **on by default** (`onFail` defaults to `download`) and also covers `pnpm self-update` and `pnpm with`. In both cases the repository also controls the lockfile integrity and the registry the bytes are fetched from (via `.npmrc`), so matching the lockfile integrity proves nothing — it matches the hash the attacker wrote. A cloned, untrusted repository could therefore execute an arbitrary native binary just by running a normal pnpm command. ## Fix (corepack-style registry-signature verification) pnpm now verifies the **npm registry signature** of the bytes it is about to spawn, **over the installed integrity**, against npm's public signing keys that **ship embedded in the pnpm CLI** (exactly as corepack does). If the bytes on disk were substituted or tampered with, npm's real signature does not validate over them. - New reusable `verifyInstalledPackageSignatures()` in `@pnpm/deps.security.signatures` verifies `name@version:integrity` against `dist.signatures` using the embedded keys. - Because the keys are **embedded** (not fetched), a registry the user did not vouch for cannot supply its own keypair to forge a signature. The signed packument is fetched from the **configured** registry, so an **npm mirror works transparently** — it proxies the same signed packument, with no configuration. There is intentionally **no runtime override or off-switch** for the keys. - **pacquet** (`installing/commands`): verifies the `pacquet` shim and the host platform binary. It **fails the command** if the signature does not verify or cannot be checked (e.g. registry unreachable); the only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform. - **pnpm engine** (`engine/pm/commands`): verifies `pnpm`, `@pnpm/exe`, and the host platform binary, **only on a store cache miss** (an actual download), so it adds no network round trip to every command. It **fails closed** — any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary. ## Keeping the embedded keys fresh The embedded keys live in a generated file. `deps/security/signatures/scripts/update-npm-signing-keys.mjs` keeps them in sync with npm's keys endpoint (`pnpm check:npm-signing-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate, so a key rotation cannot silently break verification — a stale key set blocks the release until refreshed. ## Pacquet parity pacquet gained `configDependencies` support on `main` (#12285), but it has **no install-engine-spawn sink** — pacquet *is* the engine, and it does not select/spawn an alternate engine from `configDependencies` (its only config-dependency code-execution path is `updateConfig` plugin pnpmfiles, which it shares with pnpm and which this advisory does not cover). So CAND-PNPM-097 has no pacquet-side analog; no pacquet code change is needed.
Ports pnpm's
configDependenciesfeature to pacquet end-to-end: config dependencies are resolved and installed ahead of regular deps intonode_modules/.pnpm-config/<name>via the global virtual store, recorded in the env lockfile (the first YAML document ofpnpm-lock.yaml), theirupdateConfigplugin hooks run before the main install (includingpatchedDependencies/catalogsinjection), andpnpm add --configmanages them.The whole path runs via
pacquet install/pacquet add --configand is covered by tests against the mocked registry.What's implemented
Resolve + install (
pacquet-env-installer) — clean-specifier resolution + migration of the old inline (version+integrity) / object ({ tarball?, integrity }) formats; one level of optional subdeps withos/cpu/libcplatform fields + host filtering (exact-version-only); env-lockfile pruning of stale entries; pnpm error codes (ERR_PNPM_BAD_CONFIG_DEP,CONFIG_DEP_OPTIONAL_NOT_EXACT,ENV_LOCKFILE_CORRUPTED,FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE, …).updateConfigpnpmfile hook (pacquet-hooks+pacquet-config) — newupdateConfigtrait method + Node-worker dispatch;is_plugin_name+ plugin-pnpmfile resolution (pnpm-plugin-*/@pnpm/plugin-*/@scope/pnpm-plugin-*) from.pnpm-config; fullConfiground-trip viaWorkspaceSettings(addedSerialize), applying only hook-changed keys so.npmrc/CLI values aren't clobbered;catalog/catalogsare seeded into the hook input and captured back intoConfig::catalogs(the install prefers it over re-reading the manifest), so a hook can inject catalogs acatalog:dependency then resolves against.pnpm add --config(pacquet-cli+pacquet-workspace-manifest-writer) — resolves + installs the dep and writes the clean specifier intopnpm-workspace.yaml'sconfigDependenciesblock with a format-preserving edit (extends the previously catalog-only writer).Wiring (
pacquet-cli) — config-dep install +updateConfighooks run at config finalization, before the main install.Supporting —
graph-hasher:calc_leaf_global_virtual_store_path+calc_global_virtual_store_path_with_subdeps;lockfile:EnvLockfilemulti-document read/write + env-doc preservation in the wanted-lockfile writer;reporter:pnpm:installing-config-depsevent.Tests
env-installer integration (7, incl. old-format migration and frozen-up-to-date), env-lockfile + GVS units, hooks (
updateConfig+ plugin matchers, 4), manifest-writer config-dep (4), and CLI e2e (5): config dep installs + links + env lockfile; repeat install no-op;updateConfigflipsnodeLinker;updateConfiginjects a catalog acatalog:dep resolves against;add --configwrites the yaml block and installs. Each push passes the pre-push checks (fmt, taplo,cargo doc, dylint Perfectionist).Notes
Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
New Features
Tests