feat(install): add --dry-run option#12270
Conversation
Code Review by Qodo
Context used✅ Tickets:
🎫 dry-run option for install 1. pnpr breaks dry-run
|
|
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:
📝 WalkthroughWalkthroughAdds a ChangesDry-run feature for install command
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds --dry-run support for pnpm install and pacquet install, validating lockfile freshness without installing packages.
Changes:
- Introduces
--dry-runCLI flag (Rustpacquet+ TSpnpm) and wires it tofrozen-lockfile+lockfile-onlybehavior. - Threads a
dryRunoption through headless install plumbing. - Adds unit/integration tests covering parsing and lockfile-staleness behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm/dev/pnpm-workspace.yaml | Adds a workspace override intended to link a dependency locally. |
| pacquet/crates/cli/tests/dry_run.rs | New integration tests for pacquet install --dry-run success/failure behavior. |
| pacquet/crates/cli/src/cli_args/install/tests.rs | Adds parsing test for the new --dry-run flag. |
| pacquet/crates/cli/src/cli_args/install.rs | Adds dry_run flag and maps it to --frozen-lockfile --lockfile-only. |
| installing/deps-restorer/src/index.ts | Adds dryRun option to HeadlessOptions and passes through to installs. |
| installing/deps-installer/test/install/dryRun.ts | Adds tests asserting “dry-run-like” behavior (via frozen+lockfile-only). |
| installing/commands/test/install.ts | Adds pnpm install --dry-run test for stale lockfile failure. |
| installing/commands/src/installDeps.ts | Extends InstallDepsOptions typing to include frozenLockfile. |
| installing/commands/src/install.ts | Adds --dry-run option, config support, and conflict check with --resolution-only. |
| config/reader/src/Config.ts | Promotes dryRun to a supported config option (removes “might not be supported” comment). |
| .changeset/dry-run-install.md | Changeset documenting the new --dry-run behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
df87034 to
607117d
Compare
607117d to
66d4a1b
Compare
|
Code review by qodo was updated up to the latest commit 66d4a1b |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
installing/deps-restorer/src/index.ts:1
- A normalized boolean
headlessOpts.dryRunis computed here, but later code forwardsopts.dryRundirectly. To keep behavior consistent and avoid accidentalundefinedpropagation, prefer using the normalized value (orBoolean(opts.dryRun)) everywhere you passdryRundownstream.
import { promises as fs } from 'node:fs'
installing/deps-restorer/src/index.ts:1
- This changes
dryRunfrom an explicit boolean (false) to potentiallyundefined. If downstream logic distinguishesundefinedfromfalse(or uses strict boolean checks), this can cause unintended behavior. Pass a definite boolean (e.g.dryRun: opts.dryRun === trueor reuse the earlier normalized value) to preserve the prior semantics.
import { promises as fs } from 'node:fs'
66d4a1b to
cf44411
Compare
|
Code review by qodo was updated up to the latest commit cf44411 |
cf44411 to
640b3b0
Compare
|
Code review by qodo was updated up to the latest commit 640b3b0 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
installing/deps-restorer/src/index.ts:1
opts.dryRunis optional, but this call now forwards it as-is. Ifpruneexpects a strict boolean (the previous code passedfalse), this can become a type error or change runtime behavior. Prefer normalizing to a boolean here (e.g.,Boolean(opts.dryRun)) to keep the contract consistent.
import { promises as fs } from 'node:fs'
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": 9.945341445919999,
"stddev": 0.11741516209588951,
"median": 9.913862849520001,
"user": 3.1136955800000004,
"system": 3.37122912,
"min": 9.81106969902,
"max": 10.18063951202,
"times": [
9.94507818902,
10.03599916002,
9.88763056202,
9.87890624902,
9.93796182502,
9.88976387402,
10.06881526002,
9.81755012902,
9.81106969902,
10.18063951202
]
},
{
"command": "pacquet@main",
"mean": 9.86520509862,
"stddev": 0.050838178806918946,
"median": 9.853005370520002,
"user": 2.9709813800000004,
"system": 3.3080621199999998,
"min": 9.79970765502,
"max": 9.95676016202,
"times": [
9.83200978402,
9.85443987802,
9.84532888202,
9.95676016202,
9.82150184602,
9.87957272402,
9.851570863020001,
9.79970765502,
9.94688976902,
9.86426942302
]
},
{
"command": "pnpr@HEAD",
"mean": 5.410972537520001,
"stddev": 0.12114923368395995,
"median": 5.36463355902,
"user": 2.47690148,
"system": 2.978618419999999,
"min": 5.30487737202,
"max": 5.6982544840200005,
"times": [
5.33554118602,
5.42076472002,
5.6982544840200005,
5.54292100502,
5.36269410302,
5.34832399702,
5.30487737202,
5.40119460802,
5.36657301502,
5.32858088502
]
},
{
"command": "pnpr@main",
"mean": 5.345286166519999,
"stddev": 0.04883508237456016,
"median": 5.344445561020001,
"user": 2.45264788,
"system": 2.9708365199999998,
"min": 5.27953799802,
"max": 5.46527800102,
"times": [
5.31638297902,
5.31680785502,
5.27953799802,
5.34925765002,
5.36750107302,
5.3414353210200005,
5.46527800102,
5.34762985202,
5.34745580102,
5.32157513502
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6419319654800001,
"stddev": 0.01739310893059641,
"median": 0.63967202508,
"user": 0.36381927999999997,
"system": 1.3260347999999997,
"min": 0.6187823315800001,
"max": 0.6757170515800001,
"times": [
0.6757170515800001,
0.62807579958,
0.64210065058,
0.62942566858,
0.63011981258,
0.6450421375800001,
0.66371098658,
0.6187823315800001,
0.64910181658,
0.63724339958
]
},
{
"command": "pacquet@main",
"mean": 0.6884931805800001,
"stddev": 0.10373359859476472,
"median": 0.64816161508,
"user": 0.3653211799999999,
"system": 1.3467700999999999,
"min": 0.6364152655800001,
"max": 0.9792931715800001,
"times": [
0.66538649258,
0.6364152655800001,
0.64704085658,
0.6468855625800001,
0.64201925058,
0.9792931715800001,
0.67689954358,
0.64842671158,
0.6478965185800001,
0.69466843258
]
},
{
"command": "pnpr@HEAD",
"mean": 0.78657758218,
"stddev": 0.057485964018396674,
"median": 0.7665493160800001,
"user": 0.38990718,
"system": 1.3392351999999998,
"min": 0.72781495858,
"max": 0.9215407935800001,
"times": [
0.7661770545800001,
0.8032284335800001,
0.7874759625800001,
0.76557107558,
0.75535868358,
0.9215407935800001,
0.83864535458,
0.7330419275800001,
0.72781495858,
0.76692157758
]
},
{
"command": "pnpr@main",
"mean": 0.7582633080800001,
"stddev": 0.047599493936848446,
"median": 0.74171553958,
"user": 0.38762198000000003,
"system": 1.3279283,
"min": 0.7208355725800001,
"max": 0.88566175558,
"times": [
0.74142858858,
0.74200249058,
0.74469011858,
0.73198412158,
0.7322611705800001,
0.88566175558,
0.74115164158,
0.7750682625800001,
0.7208355725800001,
0.7675493585800001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 9.20415763596,
"stddev": 0.037766784126559136,
"median": 9.204446440759998,
"user": 3.5303544799999997,
"system": 3.28898342,
"min": 9.14876748626,
"max": 9.27083942726,
"times": [
9.18472146126,
9.153134120259999,
9.24548242326,
9.21175093226,
9.14876748626,
9.21255618726,
9.19415492326,
9.27083942726,
9.197141949259999,
9.22302744926
]
},
{
"command": "pacquet@main",
"mean": 9.18962239736,
"stddev": 0.03456067828380239,
"median": 9.17662721526,
"user": 3.51406978,
"system": 3.28609462,
"min": 9.13960809326,
"max": 9.25609583526,
"times": [
9.17172158426,
9.17437157726,
9.17888285326,
9.216207643259999,
9.25609583526,
9.13960809326,
9.21227021526,
9.21675466726,
9.16311138326,
9.16720012126
]
},
{
"command": "pnpr@HEAD",
"mean": 5.231956451159999,
"stddev": 0.2070765134911823,
"median": 5.15669674576,
"user": 2.3122893799999997,
"system": 2.8544574199999997,
"min": 5.03632365226,
"max": 5.57137615126,
"times": [
5.21498150126,
5.49621880926,
5.47588039226,
5.57137615126,
5.06480151426,
5.23440696226,
5.09841199026,
5.05043609926,
5.03632365226,
5.07672743926
]
},
{
"command": "pnpr@main",
"mean": 5.16660529186,
"stddev": 0.1209047645051234,
"median": 5.124124655259999,
"user": 2.28854868,
"system": 2.86555462,
"min": 5.063190350259999,
"max": 5.46523642726,
"times": [
5.46523642726,
5.10109964426,
5.088263641259999,
5.094539794259999,
5.23913551326,
5.14714966626,
5.08734399726,
5.063190350259999,
5.151033583259999,
5.22906030126
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.4032059216000001,
"stddev": 0.024002108173536655,
"median": 1.4012061261,
"user": 1.5673464200000002,
"system": 1.78311664,
"min": 1.3702187206,
"max": 1.4448521996,
"times": [
1.3741414026,
1.4009046646,
1.3995860146,
1.4037738306,
1.4054467756,
1.3702187206,
1.3915958246,
1.4448521996,
1.4015075876,
1.4400321956000002
]
},
{
"command": "pacquet@main",
"mean": 1.3993016033999999,
"stddev": 0.033035516997678364,
"median": 1.3993141636000002,
"user": 1.54927902,
"system": 1.8146001400000003,
"min": 1.3616879066,
"max": 1.4738539806,
"times": [
1.3916789616,
1.3616879066,
1.3667364246,
1.3663378516,
1.4030066266,
1.4218609946,
1.4738539806,
1.3956217006,
1.4088539916,
1.4033775956
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6841485704,
"stddev": 0.01598001613839885,
"median": 0.6782918246,
"user": 0.34513541999999997,
"system": 1.2969448399999999,
"min": 0.6706247286000001,
"max": 0.7224148196000001,
"times": [
0.7224148196000001,
0.6833339536,
0.6764907886,
0.6972709456,
0.6912033236,
0.6800928606000001,
0.6718807196000001,
0.6706247286000001,
0.6745042196000001,
0.6736693446
]
},
{
"command": "pnpr@main",
"mean": 0.6853422984,
"stddev": 0.021340555555859196,
"median": 0.6815400271000001,
"user": 0.33500571999999995,
"system": 1.28146904,
"min": 0.6525011116,
"max": 0.7197533746000001,
"times": [
0.6950652246000001,
0.6843178316,
0.6667558166,
0.7149720956000001,
0.6787622226000001,
0.6764339866000001,
0.6525011116,
0.7197533746000001,
0.6961698446000001,
0.6686914756000001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.979749742659999,
"stddev": 0.03390081018633877,
"median": 4.9636405106599994,
"user": 1.7278894399999998,
"system": 1.93752754,
"min": 4.940607721659999,
"max": 5.03561122766,
"times": [
4.940607721659999,
4.96103010766,
4.9554962506599995,
4.96625091366,
4.960428032659999,
5.00792859366,
4.94968760866,
5.02812947066,
5.03561122766,
4.99232749966
]
},
{
"command": "pacquet@main",
"mean": 4.97688632666,
"stddev": 0.038478475824973525,
"median": 4.97453969566,
"user": 1.72373904,
"system": 1.9265377399999999,
"min": 4.91818239666,
"max": 5.04659271266,
"times": [
5.04659271266,
4.9687435966599995,
4.91818239666,
4.9241789236599995,
4.97175394866,
4.99812378566,
4.98412812566,
4.964361161659999,
4.97732544266,
5.01547317266
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6736641353599999,
"stddev": 0.02260708072467761,
"median": 0.66708160566,
"user": 0.32219993999999996,
"system": 1.2864787399999997,
"min": 0.65690852666,
"max": 0.73360288566,
"times": [
0.73360288566,
0.66658003666,
0.6677342026599999,
0.65690852666,
0.68648111266,
0.66365989166,
0.66758317466,
0.66351046466,
0.65835155766,
0.67222950066
]
},
{
"command": "pnpr@main",
"mean": 0.7183136438600001,
"stddev": 0.07713986928706175,
"median": 0.68968651066,
"user": 0.33201844,
"system": 1.3020250399999997,
"min": 0.67215782266,
"max": 0.92793518166,
"times": [
0.75300126266,
0.67917044166,
0.92793518166,
0.71038567566,
0.68303440266,
0.69543094466,
0.67215782266,
0.69140162566,
0.68797139566,
0.68264768566
]
}
]
} |
Add a --dry-run flag to the install command that runs dependency resolution and validates the lockfile without writing any changes to disk. This enables use in pre-commit hooks to catch outdated lockfiles and other common install errors.
640b3b0 to
d836086
Compare
|
Code review by qodo was updated up to the latest commit d836086 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #12270 +/- ##
=======================================
Coverage 88.00% 88.01%
=======================================
Files 308 308
Lines 41395 41401 +6
=======================================
+ Hits 36431 36438 +7
+ Misses 4964 4963 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… setups Address review feedback on the --dry-run option: - pacquet: skip persisting pnpm-lock.yaml in the frozen + lockfile-only branch (gate save_to_path on !frozen_lockfile), mirroring pnpm's !frozenLockfile gate, so a fresh dry run no longer touches the lockfile. - pacquet: bypass the optimistic up-to-date fast path for --dry-run so it takes the same full validation path as --frozen-lockfile --lockfile-only. - Reject --dry-run together with a configured pnpr server on both stacks: the pnpr path resolves through the server and writes the lockfile and store, which can't honor dry-run's no-write contract. - pnpm: remove the unused, misleading HeadlessOptions.dryRun plumbing (headlessInstall is never reached by --dry-run and still wrote to disk). - Document the lockfile-only frozen write suppression and add no-rewrite and conflict tests on both stacks. --- Written by an agent (Claude Code, claude-opus-4-8).
d836086 to
1de255d
Compare
|
Code review by qodo was updated up to the latest commit 1de255d |
|
No, this is not correct behavior for |
## Description Adds a `--dry-run` option to `pnpm install` with **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, no `node_modules`, no `.modules.yaml`, no workspace-state file) and **always exits 0**. ``` $ pnpm install --dry-run Dry run complete. A real install would make the following changes (nothing was written to disk): Importers . + is-negative 1.0.0 Packages + is-negative@1.0.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-run` as `--frozen-lockfile --lockfile-only` — i.e. a fail-on-drift *lockfile validator*. That collides with the well-established meaning of `--dry-run` across npm/yarn ("preview, never fail") and duplicated existing behaviour (`pnpm install --frozen-lockfile --lockfile-only` already does that). This PR implements the intuitive preview meaning instead. ### How it works (pnpm) - Reuses the existing `lockfileCheck` callback (resolve fully, skip the lockfile write, hand back the before/after wanted lockfile) plus `lockfileOnly` (skip `node_modules`, the workspace-state file, and metadata-cache writes). - The frozen/headless fast path is disabled whenever `lockfileCheck` is set, so a check-only install always resolves and never materialises anything. - The before/after lockfiles are diffed (reusing the dedupe diff engine, now exported as `calcDedupeCheckIssues`) and rendered into the report. - `--dry-run` with a configured pnpr server is rejected (that path resolves/links through the server). ### Pacquet Ported in the second commit — `pacquet install --dry-run` forces the fresh-resolve path, skips every write (a new `dry_run` flag on `InstallWithFreshLockfile` skips the `pnpm-lock.yaml` save), and a new `dry_run` module diffs the existing lockfile against the freshly-resolved one and prints the same report.
PR Summary by Qodo
feat(install): add --dry-run option to pnpm install
✨ Enhancement🧪 Tests⚙️ Configuration changes📝 Documentation🕐 20-40 MinutesWalkthroughs
User Description
Description
Adds a
--dry-runflag to the install command that runs dependency resolution and validates the lockfile without writing any changes to disk.This enables use in pre-commit hooks to catch outdated lockfiles and other common install errors.
resolves #7340
AI Description
Diagram
graph TD CLI[/"pnpm CLI\n--dry-run"/] --> Install["install.ts\nhandler()"] --> InstallDeps["installDeps.ts"] --> DepsInstaller["deps-installer\ninstall()"] --> Headless["deps-restorer\nheadlessInstall()"] --> Lockfile[("pnpm-lock.yaml\nvalidation")] Pacquet[/"pacquet CLI (Rust)\n--dry-run"/] --> DepsInstaller Config["Config.ts\ndryRun?: boolean"] -.-> Install subgraph Legend direction LR _cli[/"CLI"/] ~~~ _mod["Module"] ~~~ _db[("Lockfile")] endHigh-Level Assessment
The following are alternative approaches to this PR:
1. Dedicated verify-lockfile command
install --dry-runworkflow2. New resolver-only 'read-only' mode (no disk IO)
Recommendation: The chosen implementation (translate
--dry-runto--frozen-lockfile --lockfile-only) is the best tradeoff: minimal new logic, maximum reuse of existing correctness checks, and easy to mirror in pacquet. Alternatives either expand surface area (new command) or require much broader refactors (true IO-free mode) for limited additional value.File Changes
Enhancement (3)
Refactor (1)
Tests (4)
Documentation (1)
Other (2)
Summary by CodeRabbit
New Features
--dry-runinstall option that validates the lockfile without installing and exits non-zero if outdated (CLI flag supported).Bug Fixes
Tests
Documentation