feat(install): add --dry-run option (npm-style preview)#12449
Conversation
…ting Implements npm-style `pnpm install --dry-run`: it runs a full dependency resolution and reports what a real install would add/remove/update, but writes nothing to disk (no lockfile, no node_modules, no workspace-state file, no metadata cache) and always exits 0. Mechanism: - The install command sets the existing `lockfileCheck` callback plus `lockfileOnly`, which together make the installer resolve fully while suppressing every write, and hand back the wanted lockfile before and after resolution for comparison. - The frozen/headless fast path is disabled whenever `lockfileCheck` is set so a check-only install always resolves and never materializes anything. - The before/after lockfiles are diffed (reusing the dedupe diff engine, now exported as `calcDedupeCheckIssues`) and rendered into a human report. `--dry-run` with a configured pnpr server is rejected, since that path resolves and links through the server. Resolves #7340 --- Written by an agent (Claude Code, claude-opus-4-8).
Mirrors the pnpm-side `install --dry-run`: resolve fully but write nothing, then report what a real install would change, and exit 0. - `--dry-run` forces the fresh-resolve path (so the would-be lockfile is always computed) and reuses the lockfile-only plumbing to skip node_modules / .modules.yaml / workspace-state, while additionally skipping the pnpm-lock.yaml write in InstallWithFreshLockfile. - New `dry_run` module diffs the existing on-disk lockfile against the freshly-resolved one (importer direct deps + packages) and renders a report printed to stdout. Other Install constructions (add/remove/update, pnpr resolve) pass dry_run: false. --- Written by an agent (Claude Code, claude-opus-4-8).
Code Review by Qodo
1.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements Changespnpm install --dry-run (TypeScript)
pacquet install --dry-run (Rust)
Sequence Diagram(s)sequenceDiagram
participant User
participant pnpmCLI as pnpm install CLI
participant handler as install.handler
participant dryRunInstall
participant installDeps
participant calcDedupeCheckIssues
rect rgba(100, 149, 237, 0.5)
Note over User,calcDedupeCheckIssues: TypeScript --dry-run path
User->>pnpmCLI: pnpm install --dry-run
pnpmCLI->>handler: opts.dryRun = true
handler->>dryRunInstall: route check-only flow
dryRunInstall->>dryRunInstall: lockfileOnly=true, disable optimistic repeat
dryRunInstall->>installDeps: run with dryRun options
installDeps-->>dryRunInstall: DryRunInstallResult{original, wanted}
dryRunInstall->>calcDedupeCheckIssues: diff lockfiles
calcDedupeCheckIssues-->>dryRunInstall: DedupeCheckIssues (or empty)
dryRunInstall-->>handler: formatted report string
handler-->>User: print report, exit 0 (no disk writes)
end
sequenceDiagram
participant User
participant pacquetCLI as pacquet install CLI
participant Install
participant InstallWithFreshLockfile
participant diff_lockfiles
participant render
rect rgba(144, 238, 144, 0.5)
Note over User,render: Rust --dry-run path
User->>pacquetCLI: pacquet install --dry-run
pacquetCLI->>Install: dry_run=true
Install->>Install: resolve_only=true, frozen_path=false
Install->>Install: capture existing_wanted_lockfile
Install->>InstallWithFreshLockfile: lockfile_only=true, dry_run=true
InstallWithFreshLockfile->>InstallWithFreshLockfile: resolve in-memory
InstallWithFreshLockfile->>InstallWithFreshLockfile: skip lockfile verification record
InstallWithFreshLockfile-->>Install: wanted_lockfile (memory only)
Install->>diff_lockfiles: old=existing, new=resolved
diff_lockfiles-->>Install: LockfileDiff
Install->>render: format diff
render-->>Install: report string
Install-->>User: print report, exit 0 (skip materialization)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
PR Summary by Qodofeat(install): add npm-style --dry-run preview to pnpm install (and pacquet) WalkthroughsDescription• Add pnpm install --dry-run to resolve fully and preview changes without disk writes. • Diff before/after wanted lockfiles and render an npm-style report; always exits 0. • Port the same --dry-run behavior to pacquet with dedicated diff/report logic and tests. Diagramgraph TD
A["pnpm install.ts"] --> B["deps-installer mutateModules"] --> C["lockfileCheck callback"] --> D["dedupe check diff"] --> E["issues renderer"]
F["pacquet install args"] --> G["package-manager install"] --> H["dry_run diff+report"]
High-Level AssessmentThe following are alternative approaches to this PR: 1. Catch-and-render DedupeCheckIssuesError
2. Introduce a dedicated lockfile-diff API shared by install/dedupe/dry-run
3. Implement dry-run as `--frozen-lockfile --lockfile-only` validator
Recommendation: The PR’s approach is the best fit for the stated npm-style semantics: it forces a real resolution, guarantees no writes via existing lockfile-only plumbing, and produces a concrete diff report while always exiting 0. Exporting File ChangesEnhancement (5)
Bug fix (1)
Refactor (10)
Tests (5)
Documentation (2)
Other (3)
|
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12449 +/- ##
==========================================
+ Coverage 88.00% 88.03% +0.02%
==========================================
Files 308 309 +1
Lines 41395 41580 +185
==========================================
+ Hits 36431 36605 +174
- Misses 4964 4975 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pacquet/crates/cli/src/cli_args/install.rs (1)
413-431:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
--dry-runis silently ignored on the pnpr path instead of being rejectedWhen
pnpr_serveris configured,InstallArgs::rundelegates toinstall_via_pnprwithout guardingdry_run, and both pnprInstallcalls forcedry_run: false(Line 697 and Line 825). This breaks the dry-run contract and can perform real writes even though the user explicitly asked for preview-only behavior.Suggested fix
@@ - if let Some(pnpr_server) = config.pnpr_server.as_deref() { + if dry_run && config.pnpr_server.is_some() { + return Err(miette::miette!( + "`--dry-run` cannot be used with `pnprServer`; disable pnpr server or remove `--dry-run`." + )); + } + if let Some(pnpr_server) = config.pnpr_server.as_deref() { return install_via_pnpr::<Reporter>( &state, pnpr_server,As per coding guidelines, “Keep pnpm and pacquet in sync for any user-visible change … behavior … must be implemented on both sides,” and this PR’s objective explicitly requires rejecting pnpr-server configurations for
--dry-run.Also applies to: 697-698, 825-826
🤖 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/cli_args/install.rs` around lines 413 - 431, The `--dry-run` flag is being silently ignored when pnpr_server is configured, allowing actual writes despite the user's explicit preview-only request. Add a guard check before the `install_via_pnpr` call in the pnpr_server branch that rejects the operation if `dry_run` is true, raising an error to inform the user that dry-run is not supported with pnpr configurations. This check should be placed before delegating to `install_via_pnpr` around line 413-431. The hardcoded `dry_run: false` values in the pnpr Install calls at lines 697-698 and 825-826 are manifestations of this issue and do not require direct changes once the root-cause guard is implemented at the entry point.Source: Coding guidelines
🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/dry_run/tests.rs (1)
3-26: ⚡ Quick winAdd direct unit coverage for
diff_lockfiles, not just renderer output.These tests validate
render_dry_run_report, but they don’t assertdiff_lockfilesbehavior (importer add/remove/update and package key add/remove). A focused diff test here would catch parity regressions earlier.As per coding guidelines, behavior changes should be covered with targeted tests, and this feature’s core behavior lives in diff construction.
🤖 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/package-manager/src/dry_run/tests.rs` around lines 3 - 26, Add direct unit tests for the `diff_lockfiles` function to validate its core behavior rather than relying only on `render_dry_run_report` output validation. Create new test functions that call `diff_lockfiles` with specific lockfile inputs and directly assert the resulting `LockfileDiff` structure for importer add/remove/update operations and package additions/removals. This ensures the diff construction logic is covered independently from the renderer, catching parity regressions earlier as per coding guidelines.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/package-manager/src/install.rs`:
- Around line 1318-1320: The issue is that diff_lockfiles is using the lockfile
variable which may have been previously modified to include a synthesized
fallback from virtual_store_dir/lock.yaml, causing the dry-run diff baseline to
be incorrect and potentially hiding real changes to pnpm-lock.yaml. To fix this,
ensure that the render_dry_run_report call with diff_lockfiles uses the original
on-disk lockfile state rather than the potentially modified lockfile variable.
Store the original lockfile state before any fallback synthesis occurs, and pass
that original state to diff_lockfiles so the comparison accurately reflects what
would actually be written to disk, matching pnpm's dry-run behavior exactly.
---
Outside diff comments:
In `@pacquet/crates/cli/src/cli_args/install.rs`:
- Around line 413-431: The `--dry-run` flag is being silently ignored when
pnpr_server is configured, allowing actual writes despite the user's explicit
preview-only request. Add a guard check before the `install_via_pnpr` call in
the pnpr_server branch that rejects the operation if `dry_run` is true, raising
an error to inform the user that dry-run is not supported with pnpr
configurations. This check should be placed before delegating to
`install_via_pnpr` around line 413-431. The hardcoded `dry_run: false` values in
the pnpr Install calls at lines 697-698 and 825-826 are manifestations of this
issue and do not require direct changes once the root-cause guard is implemented
at the entry point.
---
Nitpick comments:
In `@pacquet/crates/package-manager/src/dry_run/tests.rs`:
- Around line 3-26: Add direct unit tests for the `diff_lockfiles` function to
validate its core behavior rather than relying only on `render_dry_run_report`
output validation. Create new test functions that call `diff_lockfiles` with
specific lockfile inputs and directly assert the resulting `LockfileDiff`
structure for importer add/remove/update operations and package
additions/removals. This ensures the diff construction logic is covered
independently from the renderer, catching parity regressions earlier as per
coding guidelines.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d7fe4ff5-ffb7-4795-aba3-f86fcd4f55fb
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (25)
.changeset/dry-run-install.mdconfig/reader/src/Config.tsinstalling/commands/package.jsoninstalling/commands/src/install.tsinstalling/commands/src/prune.tsinstalling/commands/test/install.tsinstalling/commands/tsconfig.jsoninstalling/dedupe/check/src/dedupeDiffCheck.tsinstalling/dedupe/check/src/index.tsinstalling/deps-installer/src/install/index.tspacquet/crates/cli/src/cli_args/install.rspacquet/crates/cli/src/cli_args/install/tests.rspacquet/crates/cli/tests/dry_run.rspacquet/crates/package-manager/src/add.rspacquet/crates/package-manager/src/dry_run.rspacquet/crates/package-manager/src/dry_run/tests.rspacquet/crates/package-manager/src/install.rspacquet/crates/package-manager/src/install/tests.rspacquet/crates/package-manager/src/install_with_fresh_lockfile.rspacquet/crates/package-manager/src/lib.rspacquet/crates/package-manager/src/remove.rspacquet/crates/package-manager/src/update.rspatching/commands/src/patchCommit.tspatching/commands/src/patchRemove.tspnpr/crates/pnpr/src/resolver/resolve.rs
Address review on the dry-run diff: - Diff against the actual on-disk pnpm-lock.yaml (captured before the synthesized-from-current fallback), so a real install creating pnpm-lock.yaml is reported instead of hidden as "no changes". - Diff each dependency group (prod/dev/optional) independently so a dependency moving between groups registers as a change, matching pnpm's per-field diff. Specifiers stay uncompared, matching pnpm (which keeps them in a separate map outside its diff fields). - Unit tests for the group-move and specifier-only cases. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit c964340 |
The package-level diff now walks the v9 `snapshots:` map (the peer-aware dependency wiring a real install rewrites) instead of the `packages:` name@version metadata keys, matching pnpm's `dedupeDiffCheck` whose in-memory `packages` map is depPath-keyed. This catches peer-variant re-resolutions (snapshot key changes) and snapshot wiring changes (`dependencies`/`optionalDependencies`) that the previous key-set diff missed. Adds an `updated_packages` group to the report and unit tests for the wiring-change case. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit b6cb9ba |
Replaces the reuse of the dedupe-oriented `lockfileCheck` callback with a proper `dryRun` install option, mirroring the pacquet implementation. - `StrictInstallOptions.dryRun`: resolve fully, write nothing, and return the before/after wanted lockfiles in the install result's `dryRunResult`. - A shared `isCheckOnlyInstall` helper folds `dryRun` and `lockfileCheck` into one "resolve-only, no writes" concept, so the frozen/headless fast paths are skipped for both (removes the ad-hoc `lockfileCheck == null` guards). - `dryRunResult` is threaded up through the install result types; the command layer diffs it (still via the lockfile-diff engine) and renders the report. `installDeps` now returns the dry-run result; callers that returned its promise from void handlers now await it. `lockfileCheck` stays as the mechanism behind `dedupe --check`. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 4e49843 |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
installing/commands/src/installDeps.ts (1)
412-428:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate manifest writes during dry-run.
opts.dryRunnow flows intoinstallDeps(), but these branches still writepackage.json/pnpm-workspace.yamlwheneveropts.save !== false. Treat dry-run like no-save for manifest and workspace-manifest persistence so the preview path cannot modify project files. Based on PR objectives, dry-run must write nothing to disk.🛡️ Skip manifest persistence when dry-run is active
- if (opts.save !== false) { + if (opts.save !== false && opts.dryRun !== true) { // Only pick entries when we'll actually persist. Otherwise the // info log would claim we added entries the workspace manifest // never saw, and the next install would re-prompt or fail- if (opts.save !== false) { + if (opts.save !== false && opts.dryRun !== true) { const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations) if (opts.update === true) {Also applies to: 443-470
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@installing/commands/src/installDeps.ts` around lines 412 - 428, The manifest persistence logic in the block starting at line 412 checks `opts.save !== false` before writing manifests but does not account for dry-run mode. Modify the condition that gates the Promise.all block containing writeProjectManifest and updateWorkspaceManifest to also check that opts.dryRun is not active, ensuring manifests are only persisted when both save is enabled and dry-run is false. The same pattern should also be applied to the similar manifest write block at lines 443-470.installing/deps-installer/src/install/index.ts (1)
184-185:⚠️ Potential issue | 🟠 Major | ⚡ Quick winApply
isCheckOnlyInstall()before every dry-run side-effect path.The new helper skips several materialization paths, but dry-run can still reach pnpr delegation, verification-cache writes, merge-branch lockfile cleanup, and resolver non-dry-run behavior. This makes the exported
dryRunoption depend on command-layerlockfileOnlyand can still modify disk under some configs. Gate these paths withisCheckOnlyInstall(opts)and pass that flag through to the resolver. Based on PR objectives, dry-run must write nothing to disk.🛡️ Reuse the check-only flag for remaining side-effect gates
const opts = extendOptions(maybeOpts) + const checkOnlyInstall = isCheckOnlyInstall(opts) + if (opts.pnprServer && checkOnlyInstall) { + throw new PnpmError('CONFIG_CONFLICT_CHECK_ONLY_WITH_PNPR_SERVER', + 'Cannot use a check-only install with a configured pnpr server because the pnpr install path resolves and links through the server') + } + // When a pnpr server is configured, use server-side resolution. The pnpr server- const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0 + const cacheActive = !checkOnlyInstall && opts.cacheDir != null && opts.resolutionVerifiers.length > 0 const wantedLockfilePath = cacheActive ? path.resolve(ctx.lockfileDir, await getWantedLockfileName({ useGitBranchLockfile: opts.useGitBranchLockfile, mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, })) : undefined verifyLockfilePromise = verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, { - cacheDir: opts.cacheDir, + cacheDir: checkOnlyInstall ? undefined : opts.cacheDir, lockfilePath: wantedLockfilePath, })- if (opts.mergeGitBranchLockfiles) { + if (opts.mergeGitBranchLockfiles && !checkOnlyInstall) { await cleanGitBranchLockfiles(ctx.lockfileDir) }- dryRun: opts.lockfileOnly, + dryRun: opts.lockfileOnly || isInstallationOnlyForLockfileCheck,Also applies to: 325-327, 425-440, 502-503, 1560-1560
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@installing/deps-installer/src/install/index.ts` around lines 184 - 185, The dry-run functionality currently allows side effects to reach several code paths including pnpr delegation, verification-cache writes, merge-branch lockfile cleanup, and resolver behavior. Guard all these side-effect paths with `isCheckOnlyInstall(opts)` to ensure dry-run mode writes nothing to disk. Specifically, wrap the pnpr delegation path (the `installViaPnprServer` call), verification-cache write operations, merge-branch lockfile cleanup, and resolver invocations with checks using `isCheckOnlyInstall(opts)`, and pass this flag through to the resolver so it also respects the dry-run constraint. This ensures the `dryRun` option properly isolates disk modifications regardless of command-layer `lockfileOnly` configuration.
🤖 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 `@installing/commands/src/install.ts`:
- Around line 453-462: The renderDryRunReport function currently treats
undefined dryRunResult as "up to date" which masks stale workspace lockfiles.
Make the dryRunResult parameter required (non-optional) in renderDryRunReport
instead of allowing it to be undefined, and ensure the workspace recursive path
in installDeps function properly forwards and returns the DryRunInstallResult so
that all dry-run comparisons are accurately calculated and reported. This
ensures stale workspace lockfiles are detected and reported as changes rather
than being silently ignored.
---
Outside diff comments:
In `@installing/commands/src/installDeps.ts`:
- Around line 412-428: The manifest persistence logic in the block starting at
line 412 checks `opts.save !== false` before writing manifests but does not
account for dry-run mode. Modify the condition that gates the Promise.all block
containing writeProjectManifest and updateWorkspaceManifest to also check that
opts.dryRun is not active, ensuring manifests are only persisted when both save
is enabled and dry-run is false. The same pattern should also be applied to the
similar manifest write block at lines 443-470.
In `@installing/deps-installer/src/install/index.ts`:
- Around line 184-185: The dry-run functionality currently allows side effects
to reach several code paths including pnpr delegation, verification-cache
writes, merge-branch lockfile cleanup, and resolver behavior. Guard all these
side-effect paths with `isCheckOnlyInstall(opts)` to ensure dry-run mode writes
nothing to disk. Specifically, wrap the pnpr delegation path (the
`installViaPnprServer` call), verification-cache write operations, merge-branch
lockfile cleanup, and resolver invocations with checks using
`isCheckOnlyInstall(opts)`, and pass this flag through to the resolver so it
also respects the dry-run constraint. This ensures the `dryRun` option properly
isolates disk modifications regardless of command-layer `lockfileOnly`
configuration.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e262d2e4-b99e-4211-a8ab-9e524e042d7a
📒 Files selected for processing (7)
installing/commands/src/add.tsinstalling/commands/src/dedupe.tsinstalling/commands/src/install.tsinstalling/commands/src/installDeps.tsinstalling/commands/src/update/index.tsinstalling/deps-installer/src/install/extendInstallOptions.tsinstalling/deps-installer/src/install/index.ts
✅ Files skipped from review due to trivial changes (1)
- installing/commands/src/add.ts
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": 4.21226762788,
"stddev": 0.1600202559451862,
"median": 4.16461524488,
"user": 3.9960526,
"system": 3.5617277799999996,
"min": 4.02421643338,
"max": 4.49032058638,
"times": [
4.2269652863800005,
4.32395347038,
4.02421643338,
4.0783009773800005,
4.49032058638,
4.07995786738,
4.087860028380001,
4.383120083380001,
4.10226520338,
4.325716342380001
]
},
{
"command": "pacquet@main",
"mean": 4.14918284558,
"stddev": 0.14947491424895396,
"median": 4.096980497880001,
"user": 3.9515578,
"system": 3.5372433799999996,
"min": 3.9947684203800002,
"max": 4.5109396653800005,
"times": [
4.09553513138,
4.053216622380001,
3.9947684203800002,
4.07559700938,
4.18727395438,
4.5109396653800005,
4.28065114238,
4.06409104238,
4.09842586438,
4.13132960338
]
},
{
"command": "pnpr@HEAD",
"mean": 2.15502554608,
"stddev": 0.09916187754885875,
"median": 2.13443844038,
"user": 2.6588359999999995,
"system": 3.04636618,
"min": 2.03373399538,
"max": 2.32575818538,
"times": [
2.2688718523799998,
2.16127512038,
2.0984458513799997,
2.03373399538,
2.14896182038,
2.0899632113799997,
2.11991506038,
2.25869302938,
2.04463733438,
2.32575818538
]
},
{
"command": "pnpr@main",
"mean": 2.1603548679799998,
"stddev": 0.12237318257349977,
"median": 2.16041859788,
"user": 2.6015556999999996,
"system": 3.0056674799999996,
"min": 1.98565003838,
"max": 2.38614739938,
"times": [
2.20245420638,
2.05638622238,
1.98565003838,
2.25784558238,
2.11838298938,
2.23870257438,
2.09800459038,
2.03630046238,
2.22367461438,
2.38614739938
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6220306105,
"stddev": 0.011963004710281694,
"median": 0.6229404656,
"user": 0.35907142,
"system": 1.32441656,
"min": 0.6031680961000001,
"max": 0.6395582541000001,
"times": [
0.6115014831000001,
0.6149002161000001,
0.6241994201000001,
0.6296264561000001,
0.6031680961000001,
0.6216815111,
0.6395582541000001,
0.6096353621,
0.6347071321000001,
0.6313281741000001
]
},
{
"command": "pacquet@main",
"mean": 0.6332479241000001,
"stddev": 0.023299471480440436,
"median": 0.6245711631,
"user": 0.37919442,
"system": 1.31826026,
"min": 0.6165416671,
"max": 0.6907386261,
"times": [
0.6190231541000001,
0.6234899821000001,
0.6260234021000001,
0.6430415211000001,
0.6169503641,
0.6165416671,
0.6193488041,
0.6516693761000001,
0.6256523441,
0.6907386261
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6690965217,
"stddev": 0.012676275978345307,
"median": 0.6692010406000001,
"user": 0.38609492,
"system": 1.34511296,
"min": 0.6530029151000001,
"max": 0.6904213531000001,
"times": [
0.6781803021000001,
0.6559626641,
0.6661716011000001,
0.6637317251000001,
0.6530029151000001,
0.6722304801000001,
0.6806436551000001,
0.6537288071,
0.6904213531000001,
0.6768917141
]
},
{
"command": "pnpr@main",
"mean": 0.6846030869,
"stddev": 0.017797612593057056,
"median": 0.6858700841,
"user": 0.37668492,
"system": 1.35934376,
"min": 0.6556859241,
"max": 0.7207105731000001,
"times": [
0.7207105731000001,
0.6833490861000001,
0.6887964051000001,
0.6881435641000001,
0.6851620581000001,
0.6815682381000001,
0.6865781101,
0.6610035701000001,
0.6950333401000001,
0.6556859241
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.24702607264,
"stddev": 0.04885676168108486,
"median": 4.25686886974,
"user": 3.8042719999999997,
"system": 3.3904595800000004,
"min": 4.16313962824,
"max": 4.32916092024,
"times": [
4.27984116724,
4.32916092024,
4.24656869724,
4.28565540424,
4.16313962824,
4.21070288024,
4.19933219424,
4.2207199522400005,
4.26716904224,
4.26797084024
]
},
{
"command": "pacquet@main",
"mean": 4.25493320784,
"stddev": 0.036976327280891334,
"median": 4.26569125274,
"user": 3.7550787,
"system": 3.4214877800000005,
"min": 4.17674198124,
"max": 4.29544271224,
"times": [
4.21255936824,
4.17674198124,
4.26248010824,
4.29544271224,
4.2839656262400005,
4.28396180624,
4.23210756824,
4.2706904022400005,
4.2680935702400005,
4.26328893524
]
},
{
"command": "pnpr@HEAD",
"mean": 2.16648909444,
"stddev": 0.07168253730570763,
"median": 2.15527133324,
"user": 2.5640302000000004,
"system": 2.98579118,
"min": 2.09329547824,
"max": 2.3051042602400003,
"times": [
2.10041860424,
2.16329853724,
2.17320344424,
2.1321554702400003,
2.10859676524,
2.09329547824,
2.14724412924,
2.27760346724,
2.16397078824,
2.3051042602400003
]
},
{
"command": "pnpr@main",
"mean": 2.2402290068399995,
"stddev": 0.17138406304400167,
"median": 2.15012803624,
"user": 2.5531553000000002,
"system": 3.0158982799999996,
"min": 2.06104426624,
"max": 2.48433324124,
"times": [
2.06831743924,
2.4401319792400002,
2.13582101824,
2.36484040624,
2.11425960124,
2.44451019624,
2.48433324124,
2.12459686624,
2.16443505424,
2.06104426624
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.45872451616,
"stddev": 0.0182214366793883,
"median": 1.45826905786,
"user": 1.47087904,
"system": 1.7762791,
"min": 1.43151340486,
"max": 1.49145383086,
"times": [
1.45953043586,
1.47355435486,
1.4771733828600002,
1.45700767986,
1.49145383086,
1.44860985086,
1.46191407886,
1.43151340486,
1.44647549786,
1.4400126448600001
]
},
{
"command": "pacquet@main",
"mean": 1.44655124736,
"stddev": 0.112297678614344,
"median": 1.41705880436,
"user": 1.42010124,
"system": 1.7644368,
"min": 1.3618677618600001,
"max": 1.7419938508600001,
"times": [
1.43885957886,
1.7419938508600001,
1.4743613988600002,
1.45774530986,
1.4655926948600002,
1.3678289328600002,
1.3618677618600001,
1.39319586386,
1.36880905186,
1.3952580298600001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6713675588600001,
"stddev": 0.03398846500277617,
"median": 0.6601346273600001,
"user": 0.34064214,
"system": 1.298981,
"min": 0.6522886048600001,
"max": 0.7663621888600001,
"times": [
0.6655507068600001,
0.6699659458600001,
0.6548412908600001,
0.6522886048600001,
0.7663621888600001,
0.6710302488600001,
0.6598275488600001,
0.65952488086,
0.6604417058600001,
0.6538424668600001
]
},
{
"command": "pnpr@main",
"mean": 0.66114302806,
"stddev": 0.01187685133313228,
"median": 0.6619593938600001,
"user": 0.34340033999999997,
"system": 1.2955269,
"min": 0.6447275868600001,
"max": 0.6854135438600001,
"times": [
0.6642865288600001,
0.6612230288600001,
0.6626957588600001,
0.64587237486,
0.6447275868600001,
0.6854135438600001,
0.6568135758600001,
0.6715598538600001,
0.6553989988600001,
0.6634390298600001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.07373477354,
"stddev": 0.06964622147138776,
"median": 3.05259201914,
"user": 1.83069344,
"system": 2.01602088,
"min": 3.01350193564,
"max": 3.25670076064,
"times": [
3.04062946964,
3.05705741864,
3.01350193564,
3.04812661964,
3.06685486464,
3.10666796064,
3.04008552464,
3.25670076064,
3.08094349164,
3.02677968964
]
},
{
"command": "pacquet@main",
"mean": 3.07129199654,
"stddev": 0.06991823042131683,
"median": 3.05343734764,
"user": 1.82205814,
"system": 2.0091297799999994,
"min": 3.01793402864,
"max": 3.26281408364,
"times": [
3.05007746164,
3.04474985564,
3.04031316964,
3.06554524864,
3.03037539764,
3.01793402864,
3.0567972336399998,
3.26281408364,
3.0869254866399998,
3.05738799964
]
},
{
"command": "pnpr@HEAD",
"mean": 0.65135332844,
"stddev": 0.008626985280428713,
"median": 0.64942442564,
"user": 0.33592083999999994,
"system": 1.30336288,
"min": 0.64078132564,
"max": 0.6699297696400001,
"times": [
0.6699297696400001,
0.6549340476400001,
0.65531798564,
0.64818192064,
0.64607656964,
0.64675745464,
0.6425924576400001,
0.64078132564,
0.65066693064,
0.65829482264
]
},
{
"command": "pnpr@main",
"mean": 0.6601961093400001,
"stddev": 0.018319807896529934,
"median": 0.66266749714,
"user": 0.32824894,
"system": 1.29826708,
"min": 0.63751094564,
"max": 0.70379259464,
"times": [
0.66424171064,
0.64370533764,
0.66440859364,
0.66274093964,
0.66399511164,
0.64484182964,
0.63751094564,
0.66259405464,
0.65412997564,
0.70379259464
]
}
]
} |
|
| Branch | pr/12449 |
| Testbed | pacquet |
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 | 4,247.03 ms(+1.84%)Baseline: 4,170.37 ms | 5,004.44 ms (84.87%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 3,073.73 ms(+2.85%)Baseline: 2,988.48 ms | 3,586.17 ms (85.71%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,458.72 ms(+11.10%)Baseline: 1,313.00 ms | 1,575.60 ms (92.58%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,212.27 ms(+6.65%)Baseline: 3,949.75 ms | 4,739.70 ms (88.87%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 622.03 ms(+0.20%)Baseline: 620.80 ms | 744.97 ms (83.50%) |
|
| Branch | pr/12449 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,166.49 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 651.35 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 671.37 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,155.03 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 669.10 ms |
A direct dependency whose manifest specifier no longer matches the lockfile is something a real install would rewrite, so --dry-run now reports it (instead of "up to date") while still exiting 0 and writing nothing. The importer diff keys on each direct dependency's `specifier` rather than its resolved version: the in-memory resolved-version fields are cleared for specifier-mismatched deps (they're about to be re-resolved), and for a direct dependency the resolved version only changes when the specifier does. Both stacks updated; pnpm gates this behind a new `includeImporterSpecifiers` option on the lockfile-diff helper that `dedupe --check` leaves off. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit b87f89a |
… closed The workspace recursive install returned without forwarding the dry-run lockfile comparison, so a workspace `install --dry-run` could mask stale lockfiles as "up to date". Thread `dryRunResult` through the shared workspace-lockfile path (recursive -> recursiveInstallThenUpdateWorkspaceState -> installDeps), and make `renderDryRunReport` require a result. When the installer can't produce one (e.g. a workspace without a shared lockfile), fail closed with `ERR_PNPM_DRY_RUN_UNSUPPORTED` rather than reporting no changes. Adds a workspace dry-run regression test. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit aff90d1 |
The specifier-only importer diff missed a dependency moving between dependency groups (e.g. dev -> prod) when its specifier was unchanged, since the lockfile `specifiers` map is flat across groups. Diff importers by the per-group dependency fields *and* `specifiers`: the per-group fields catch group moves, `specifiers` (compared last so it wins the render on a collision) catches specifier-only edits. Adds a group-move regression test. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 8b15544 |
A plain install persists auto-policy updates (e.g. minimumReleaseAgeExclude) to pnpm-workspace.yaml, and --update rewrites package.json, both gated only on `opts.save !== false`. A dry-run could therefore still write a manifest. Gate every project/workspace manifest write on `!opts.dryRun` across the single-project, recursive workspace, and add/update paths so --dry-run truly writes nothing. --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 78b676d |
… add/update/remove - pacquet: reject `install --dry-run` when a pnpr server is configured (ERR_PNPM_CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER), matching pnpm — the pnpr path resolves/links through the server and can't be no-write. - pnpm: when no lockfile comparison can be produced, print an informational message and exit 0 instead of throwing, honoring the documented "--dry-run always exits 0" contract. - pnpm: `dry-run` is a real config key, so force `dryRun: false` in the add/update/remove/dedupe install paths — a config-level `dry-run` must not silently turn those into no-op check-only runs (matches pacquet, which already sets `dry_run: false` on those paths). --- Written by an agent (Claude Code, claude-opus-4-8).
|
Code review by qodo was updated up to the latest commit 2e974cc |
Fast-forward was not possible: both sides diverged from 7c97d9e (2 local commits removing redundant Arc usage, 15 commits from main). The merge is conflict-free — of main's new commits, only #12449 (install --dry-run) touched install_with_fresh_lockfile.rs, in a region disjoint from the compat-extender change.
Description
Adds a
--dry-runoption topnpm installwith npm-style preview semantics: it runs a full dependency resolution and reports what a real install would add/remove/update, but writes nothing to disk (no lockfile, nonode_modules, no.modules.yaml, no workspace-state file) and always exits 0.When the lockfile is already up to date it prints
Dry run complete. pnpm-lock.yaml is up to date; a real install would make no changes.Resolves #7340.
Why this shape
An earlier attempt (#12270, now closed) implemented
--dry-runas--frozen-lockfile --lockfile-only— i.e. a fail-on-drift lockfile validator. That collides with the well-established meaning of--dry-runacross npm/yarn ("preview, never fail") and duplicated existing behaviour (pnpm install --frozen-lockfile --lockfile-onlyalready does that). This PR implements the intuitive preview meaning instead.How it works (pnpm)
lockfileCheckcallback (resolve fully, skip the lockfile write, hand back the before/after wanted lockfile) pluslockfileOnly(skipnode_modules, the workspace-state file, and metadata-cache writes).lockfileCheckis set, so a check-only install always resolves and never materialises anything.calcDedupeCheckIssues) and rendered into the report.--dry-runwith a configured pnpr server is rejected (that path resolves/links through the server).Pacquet
Ported in the second commit —
pacquet install --dry-runforces the fresh-resolve path, skips every write (a newdry_runflag onInstallWithFreshLockfileskips thepnpm-lock.yamlsave), and a newdry_runmodule diffs the existing lockfile against the freshly-resolved one and prints the same report.Tests
Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
Release Notes
--dry-runtoinstallto resolve dependencies and report what would change without writingpnpm-lock.yamlor creatingnode_modules(no lockfile updates; exits successfully).dryRunhelp/inline documentation to match the preview behavior.