Skip to content

feat(pacquet): honor preferFrozenLockfile in the install dispatch#11824

Merged
zkochan merged 1 commit into
mainfrom
fix/11815
May 21, 2026
Merged

feat(pacquet): honor preferFrozenLockfile in the install dispatch#11824
zkochan merged 1 commit into
mainfrom
fix/11815

Conversation

@zkochan

@zkochan zkochan commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

The install dispatch now has four ordered states:

  1. `--frozen-lockfile` flag → frozen path (lockfile required, freshness check fatal).
  2. No flag + lockfile present + effective `preferFrozenLockfile == true` + freshness check passes → frozen path (same code as state 1).
  3. No flag + lockfile present + opt-out or stale → fresh-resolve, seeded from the existing lockfile's snapshots so unrelated pins survive the rewrite. Mirrors upstream's `update: false` resolver mode.
  4. No lockfile → fresh-resolve with no seed (the existing feat(pacquet): write pnpm-lock.yaml and <vsd>/lock.yaml on fresh install #11816 behaviour).

The freshness-check logic that used to live inline in the frozen branch is now `check_lockfile_freshness`, a shared helper consumed by both state 1 (where any `Err` is fatal) and state 2 (where a stale-lockfile `Err` falls through to fresh-resolve and an invalid `pnpm.overrides` stays fatal).

Adds `--prefer-frozen-lockfile` / `--no-prefer-frozen-lockfile` CLI flags mirroring pnpm so users can override per invocation. `pacquet add` explicitly opts out of the fast path since the manifest is necessarily stale by the time the install dispatch runs.

Test plan

  • Three new dispatch-state tests in `pacquet/crates/package-manager/src/install/tests.rs`:
    • `prefer_frozen_lockfile_takes_frozen_path_when_lockfile_is_fresh` — state 2.
    • `no_prefer_frozen_lockfile_flag_forces_fresh_resolve` — state 3 with the CLI opt-out.
    • `stale_lockfile_under_no_flag_falls_through_to_fresh_resolve` — state 3 with manifest drift.
  • 298 package-manager tests pass (was 295 + 3 new).
  • 94 cli integration tests pass.
  • `just lint` clean.
  • `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --document-private-items` clean.
  • `just fmt` clean.

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

Summary by CodeRabbit

  • New Features

    • Added --prefer-frozen-lockfile and --no-prefer-frozen-lockfile CLI flags for granular control over lockfile handling during installation.
  • Behavior Changes

    • pacquet add now always re-resolves dependencies after modifying the manifest, ensuring updated dependency trees.

Review Change Stack

`pacquet install` (no flag) didn't consult `preferFrozenLockfile`. A
fresh lockfile got re-resolved from the registry instead of taking the
cheap frozen path, and a stale lockfile was silently overwritten
without seeding the resolver from the existing pins. Closes #11815.

The install dispatch now has four ordered states:

1. `--frozen-lockfile` flag → frozen path (lockfile required, freshness
   check fatal).
2. No flag + lockfile present + effective `preferFrozenLockfile == true`
   + freshness check passes → frozen path (same code as state 1).
3. No flag + lockfile present + opt-out or stale → fresh-resolve, seeded
   from the existing lockfile's snapshots so unrelated pins survive the
   rewrite (mirrors upstream's `update: false` resolver mode).
4. No lockfile → fresh-resolve with no seed.

`check_lockfile_freshness` is the shared helper: it runs
`pnpm.overrides` parsing, `check_lockfile_settings`, the overrides-aware
manifest re-apply, and `satisfies_package_manifest`. State 1 surfaces
its `Err` as `InstallError`; state 2 treats a stale-lockfile `Err` as
fall-through and surfaces `InvalidOverrides` as fatal.

CLI exposes `--prefer-frozen-lockfile` / `--no-prefer-frozen-lockfile`
mirroring pnpm so users can override per invocation; `pacquet add` opts
out of the fast path explicitly since the manifest is necessarily
stale by the time the install dispatch runs.
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ce6cb8c7-b98d-4d81-a92d-6443c3f3141b

📥 Commits

Reviewing files that changed from the base of the PR and between 5881b57 and ed04b88.

📒 Files selected for processing (5)
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/add.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

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

Files:

  • pacquet/crates/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.rs
🧠 Learnings (2)
📚 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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.rs
🔇 Additional comments (14)
pacquet/crates/cli/src/cli_args/install.rs (1)

83-97: LGTM!

Also applies to: 179-180, 188-199, 238-238

pacquet/crates/package-manager/src/install.rs (7)

13-13: LGTM!

Also applies to: 68-75, 283-283, 290-294


429-510: LGTM!


517-518: LGTM!


593-616: LGTM!


644-653: LGTM!


789-916: LGTM!


918-951: LGTM!

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs (1)

117-125: LGTM!

Also applies to: 250-250, 384-398

pacquet/crates/package-manager/src/add.rs (1)

97-105: LGTM!

pacquet/crates/package-manager/src/install/tests.rs (4)

61-3999: LGTM!


4002-4068: LGTM!


4070-4141: LGTM!


4143-4205: LGTM!


📝 Walkthrough

Walkthrough

This PR implements the preferFrozenLockfile dispatch mechanism, wiring config-driven lockfile freshness checks into install branching. Users gain CLI flags to override per-invocation, dispatch consults an extracted freshness helper to decide frozen vs. fresh paths, stale-lockfile rewrites seed preferred versions from existing snapshots, and pacquet add forces fresh-resolution despite config defaults.

Changes

Prefer Frozen Lockfile Dispatch

Layer / File(s) Summary
CLI flags definition and wiring
pacquet/crates/cli/src/cli_args/install.rs
Two new mutually-conflicting clap flags prefer-frozen-lockfile and no-prefer-frozen-lockfile are added to InstallArgs, mapped to Option<bool> via flag-to-option conversion logic, and threaded into Install construction alongside existing args.
Install dispatch refactoring with freshness extraction
pacquet/crates/package-manager/src/install.rs
Install struct gains prefer_frozen_lockfile: Option<bool> field; dispatch logic refactored to compute take_frozen_path decision covering explicit frozen and auto-frozen fast paths; new check_lockfile_freshness helper centralizes frozen-path gates (overrides parsing with catalog support, settings drift check, manifest specifier validation); FreshnessCheckError enum distinguishes fatal vs. fall-through cases; validation for unsupported combinations (nodeLinker: hoisted, skip_runtimes) moved after dispatch.
Fresh-path lockfile seeding for preferred versions
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
New wanted_lockfile: Option<&Lockfile> field on InstallWithFreshLockfile supplies optional existing lockfile; resolver seeding changed to derive preferred versions from both manifest and wanted lockfile snapshot pins when present, preserving pinned versions during stale-lockfile rewrite.
Add command prefer-frozen override
pacquet/crates/package-manager/src/add.rs
Add::run forces prefer_frozen_lockfile: Some(false) in Install dispatch to ensure manifest mutation always triggers fresh-resolve rather than taking auto-frozen fast path.
Test updates and integration coverage
pacquet/crates/package-manager/src/install/tests.rs
All existing Install { ... } literals updated with prefer_frozen_lockfile: None; new dispatch-state integration tests verify frozen path is taken when lockfile is fresh and prefer_frozen_lockfile defaults to true, fresh-resolve is forced when no-prefer-frozen-lockfile is set, and stale lockfile under same flag falls through to fresh-resolve without surfacing OutdatedLockfile.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI as InstallArgs
  participant Install as Install::run
  participant Dispatch as Dispatch Logic
  participant Freshness as check_lockfile_freshness
  participant FrozenPath as Frozen Path
  participant FreshPath as Fresh Path (InstallWithFreshLockfile)
  
  User->>CLI: --prefer-frozen-lockfile<br/>(or --no-prefer-frozen-lockfile)
  CLI->>Install: prefer_frozen_lockfile: Some(true/false)
  Install->>Dispatch: effective prefer_frozen_lockfile
  Dispatch->>Dispatch: Check --frozen-lockfile flag?
  alt Frozen flag
    Dispatch->>FrozenPath: take_frozen_path = true
  else No frozen flag
    Dispatch->>Dispatch: Lockfile exists & prefer_frozen == true?
    alt Yes
      Dispatch->>Freshness: check_lockfile_freshness()
      Freshness->>Freshness: Parse overrides, check settings, validate manifest
      alt Fresh
        Freshness-->>Dispatch: Ok(fresh)
        Dispatch->>FrozenPath: take_frozen_path = true
      else Stale
        Freshness-->>Dispatch: Err(stale)
        Dispatch->>FreshPath: take_frozen_path = false
      end
    else No
      Dispatch->>FreshPath: take_frozen_path = false
    end
  end
  FrozenPath->>FrozenPath: Use lockfile snapshot, skip resolve
  FreshPath->>FreshPath: Seed resolver from wanted_lockfile snapshots
  FreshPath->>FreshPath: Re-resolve, write new lockfile
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #11815: This PR directly implements the proposed four-dispatch-state restructure, freshness-check extraction, CLI flag wiring, and stale-lockfile seeding described in the issue.
  • #11813: Changes modify the same install dispatch and fresh-lockfile path (InstallWithFreshLockfile) to integrate writable-lockfile behavior and preferred-version seeding.

Possibly related PRs

  • pnpm/pnpm#11820: Both modify frozen-lockfile freshness logic to correctly parse pnpm.overrides with catalog: resolution (main PR via centralized check_lockfile_freshness helper).
  • pnpm/pnpm#11811: Both thread freshness-gate bypass (ignore_manifest_check / satisfies_package_manifest) through the same frozen-lockfile logic.
  • pnpm/pnpm#11665: Both modify Install::run's frozen/fresh lockfile handling decision and freshness checks in the same install.rs code path.

Poem

🐰 A frozen lockfile swift and true,
Now pacquet knows when old is new.
Prefer fresh? Or cache the past?
With flags to choose, your builds are fast.
When manifests shift and pins align,
The seeded path preserves every line. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: enabling the preferFrozenLockfile configuration in the install dispatch logic, which is the core focus of all changes across multiple files.
Linked Issues check ✅ Passed The PR successfully implements all key requirements from issue #11815: four-state dispatch logic, freshness-check helper, CLI flags, pacquet add opt-out, and seed-from-lockfile for fresh-resolve fallback.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing preferFrozenLockfile dispatch, freshness checking, and CLI flag exposure; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      8.0±0.53ms   539.2 KB/sec    1.07      8.6±0.61ms   505.3 KB/sec

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.70968% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.60%. Comparing base (5881b57) to head (ed04b88).

Files with missing lines Patch % Lines
pacquet/crates/package-manager/src/install.rs 88.57% 12 Missing ⚠️
pacquet/crates/cli/src/cli_args/install.rs 75.00% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11824   +/-   ##
=======================================
  Coverage   87.60%   87.60%           
=======================================
  Files         203      203           
  Lines       24045    24107   +62     
=======================================
+ Hits        21064    21119   +55     
- Misses       2981     2988    +7     

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

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

@zkochan zkochan marked this pull request as ready for review May 21, 2026 15:47
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.523 ± 0.104 2.397 2.717 1.03 ± 0.06
pacquet@main 2.451 ± 0.105 2.335 2.711 1.00
pnpm 4.879 ± 0.036 4.815 4.935 1.99 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.52260250688,
      "stddev": 0.10374096533189638,
      "median": 2.49749569828,
      "user": 2.80368318,
      "system": 3.74716192,
      "min": 2.39673823978,
      "max": 2.71720754178,
      "times": [
        2.54044391578,
        2.47735488478,
        2.39673823978,
        2.67642858078,
        2.71720754178,
        2.50134114378,
        2.41791122478,
        2.45743117178,
        2.54751811278,
        2.4936502527799997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.4513383196799996,
      "stddev": 0.10513865240383856,
      "median": 2.4343276097799995,
      "user": 2.7402421800000005,
      "system": 3.7538510200000004,
      "min": 2.3350944397799998,
      "max": 2.71140797378,
      "times": [
        2.71140797378,
        2.50910967178,
        2.4453351517799997,
        2.46835764678,
        2.3350944397799998,
        2.46328952878,
        2.3871402107799997,
        2.40599537778,
        2.4233200677799998,
        2.3643331277799997
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.87861636898,
      "stddev": 0.035560054968969,
      "median": 4.87947326128,
      "user": 8.19449378,
      "system": 4.25917022,
      "min": 4.81522215978,
      "max": 4.9350320537800005,
      "times": [
        4.87344566478,
        4.9350320537800005,
        4.87816645778,
        4.880780064780001,
        4.883478241780001,
        4.81522215978,
        4.925416039780001,
        4.89059614978,
        4.86570077978,
        4.838326077780001
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 718.6 ± 30.1 687.8 798.0 1.00
pacquet@main 732.0 ± 52.1 683.2 809.9 1.02 ± 0.08
pnpm 2582.2 ± 76.9 2503.3 2721.0 3.59 ± 0.18
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.71864294902,
      "stddev": 0.03010169907342208,
      "median": 0.71116578422,
      "user": 0.3912095,
      "system": 1.57182122,
      "min": 0.68780510622,
      "max": 0.79799073722,
      "times": [
        0.79799073722,
        0.71248762122,
        0.71600449422,
        0.7082039262200001,
        0.71842113222,
        0.73086126422,
        0.70220072922,
        0.68780510622,
        0.70261053222,
        0.70984394722
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7320113763200001,
      "stddev": 0.05205645456424582,
      "median": 0.70732179372,
      "user": 0.39211339999999995,
      "system": 1.58093702,
      "min": 0.6831785502200001,
      "max": 0.80994234722,
      "times": [
        0.80857645322,
        0.6831785502200001,
        0.79105821022,
        0.71930105822,
        0.69143028522,
        0.6953425292200001,
        0.80994234722,
        0.73940894622,
        0.69141138422,
        0.69046399922
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.58224073032,
      "stddev": 0.07687252401927189,
      "median": 2.5732623357200004,
      "user": 3.2175531,
      "system": 2.26185902,
      "min": 2.5033213842200004,
      "max": 2.7209701952200005,
      "times": [
        2.6105127042200005,
        2.5060410722200004,
        2.5115350692200002,
        2.6954470622200004,
        2.5898031252200004,
        2.5330612712200002,
        2.59499387322,
        2.5033213842200004,
        2.7209701952200005,
        2.5567215462200004
      ]
    }
  ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pacquet: implement preferFrozenLockfile dispatch + stale-lockfile rewrite

2 participants