feat: fetch-failure swallow for optional snapshots + ResolutionFailure payload (#434 slice 4)#474
Conversation
…e payload (#434 slice 4) Slices 1–3 (#439, #456, #467) close the installability-driven skip path end to end. This slice adds the second skip pathway upstream handles in the frozen install: an `optional: true` snapshot whose tarball / metadata / extract step fails at install time must not abort the install. Mirrors pnpm/pnpm@94240bc046's silent catch sites at `deps/graph-builder/src/lockfileToDepGraph.ts:294-298` and `installing/deps-restorer/src/index.ts:912-921`: try { ... fetch ... } catch (err) { if (pkgSnapshot.optional) return; throw err; } The reporter side gains the `ResolutionFailure` sibling payload the doc comment at `crates/reporter/src/lib.rs:546-562` had been flagging. Defined for the resolver-side emit at `installing/deps-resolver/src/resolveDependencies.ts:1376-1383`; pacquet has no resolver so the variant is wire-shape-only until a resolver port lands. Models upstream's discriminated union at `core-loggers/src/skippedOptionalDependencyLogger.ts:10-31`. ## Changes - `SkippedOptionalPackage` becomes a `#[serde(untagged)]` enum with `Installed { id, name, version }` and `ResolutionFailure { name?, version?, bareSpecifier }` variants. Two new wire-shape tests pin the resolution-failure payload. - `SkippedSnapshots` gains a `fetch_failed` subset disjoint from the `installability` subset: - `contains` / `iter` return the union (downstream consumers should treat both as absent). - `iter_installability` returns only the persistent subset — `.modules.yaml.skipped` writes that, matching upstream's behavior of never updating `opts.skipped` at fetch-failure catch sites (a future install retries the URL). - `CreateVirtualStoreOutput` carries the per-install `fetch_failed: HashSet<PackageKey>`; the cold-batch dispatch in `CreateVirtualStore::run` swallows the per-snapshot error when `snapshot.optional` is true. A non-optional failure still propagates. - `InstallFrozenLockfile::run` folds the returned `fetch_failed` into its mutable `SkippedSnapshots`, so the downstream phases (`SymlinkDirectDependencies`, `LinkVirtualStoreBins`, `BuildModules`, hoist) skip the dropped snapshots through the same gate they already use for installability skips. ## Tests - `reporter::tests::skipped_optional_resolution_failure_event_matches_pnpm_wire_shape` pins the full payload (`bareSpecifier` camelCase, no `id`). - `reporter::tests::skipped_optional_resolution_failure_omits_absent_name_and_version` pins the optional-field shape. - `install::tests::frozen_install_silently_swallows_unreachable_optional_tarball` ports `installing/deps-restorer/test/index.ts:340` — unreachable optional tarball, install succeeds, slot not created, `.modules.yaml.skipped` left empty. Test-the-test verified by removing the `optional`-guard in the cold-batch match arm. - `install::tests::frozen_install_propagates_non_optional_fetch_failure` pins the polarity: same fixture, non-optional snapshot, install must error. Closes #471.
|
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:
📝 WalkthroughWalkthroughThis PR swallows fetch/extract failures for optional snapshots during CreateVirtualStore, surfaces those keys as a transient ChangesOptional Fetch Failures and SkippedOptionalPackage Redesign
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 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)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #474 +/- ##
==========================================
+ Coverage 89.18% 90.12% +0.93%
==========================================
Files 116 119 +3
Lines 10472 11134 +662
==========================================
+ Hits 9339 10034 +695
+ Misses 1133 1100 -33 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@crates/package-manager/src/create_virtual_store.rs`:
- Around line 657-674: The current match swallows every
InstallPackageBySnapshotError when snapshot.optional is true; change it to only
swallow the fetch/metadata/extract failure variants and propagate all other
InstallPackageBySnapshotError variants. Concretely, keep the tracing::warn!
branch but guard it with a pattern that checks snapshot.optional && matches the
error to the fetch/metadata/extract enum variants of
InstallPackageBySnapshotError (e.g. InstallPackageBySnapshotError::Fetch(..) |
InstallPackageBySnapshotError::Metadata(..) |
InstallPackageBySnapshotError::Extract(..)); for all other Err(err) cases return
Err(CreateVirtualStoreError::InstallPackageBySnapshot(err)) as before. Ensure
the tracing::warn! still logs snapshot_key and err when those specific variants
are swallowed.
In `@crates/reporter/src/tests.rs`:
- Around line 700-720: The test
skipped_optional_resolution_failure_omits_absent_name_and_version should print
the serialized JSON when an assertion fails; update the version absence check
(or add a debug print before asserts) so failures include the payload (e.g.,
change the second assertion to include the JSON in its message or call
eprintln!/dbg! on json) referencing the local variable json and the assert on
json["package"].get("version") so nextest shows the diagnostic output.
🪄 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: 716d534d-38e8-4f70-98c9-5d9abc31e61b
📒 Files selected for processing (10)
crates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rscrates/package-manager/src/create_virtual_store.rscrates/package-manager/src/install.rscrates/package-manager/src/install/tests.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/installability.rscrates/package-manager/src/installability/tests.rscrates/reporter/src/lib.rscrates/reporter/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). (6)
- GitHub Check: Agent
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/package-manager/src/build_modules/tests.rscrates/package-manager/src/installability/tests.rscrates/package-manager/src/build_modules.rscrates/reporter/src/lib.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/install.rscrates/package-manager/src/install/tests.rscrates/package-manager/src/create_virtual_store.rscrates/package-manager/src/installability.rscrates/reporter/src/tests.rs
🧠 Learnings (3)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/package-manager/src/build_modules/tests.rscrates/package-manager/src/installability/tests.rscrates/package-manager/src/install/tests.rscrates/reporter/src/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/package-manager/src/build_modules/tests.rscrates/package-manager/src/installability/tests.rscrates/package-manager/src/build_modules.rscrates/reporter/src/lib.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/install.rscrates/package-manager/src/install/tests.rscrates/package-manager/src/create_virtual_store.rscrates/package-manager/src/installability.rscrates/reporter/src/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In this repo’s Rust tests (run via `cargo nextest`), stdout/stderr from passing tests is captured and is only shown when a test fails. As a result, `eprintln!` (and similar stderr logging) in these tests should generally not be flagged as CI “noise” on the happy path; CI output should only appear for failing tests.
Applied to files:
crates/reporter/src/tests.rs
🔇 Additional comments (3)
crates/package-manager/src/install_frozen_lockfile.rs (1)
281-281: LGTM!Also applies to: 375-408
crates/package-manager/src/installability/tests.rs (1)
10-10: LGTM!Also applies to: 123-127
crates/package-manager/src/install/tests.rs (1)
1709-1879: LGTM!
There was a problem hiding this comment.
Pull request overview
Implements pnpm slice #434 (slice 4) behavior for frozen installs by treating fetch/extract failures of optional: true snapshots as non-fatal, and extends reporter wire compatibility to support the upstream resolution_failure skipped-optional payload shape.
Changes:
- Update
pnpm:skipped-optional-dependencypayload modeling to support both upstreampackageshapes via a#[serde(untagged)]enum (InstalledvsResolutionFailure) and add serialization tests for the new variant. - Split
SkippedSnapshotsinto persistentinstallabilityvs transientfetch_failed, ensuring only installability skips are persisted to.modules.yaml.skipped. - Swallow optional snapshot failures during
CreateVirtualStorecold-batch install and thread the resultingfetch_failedset throughInstallFrozenLockfileso downstream phases treat those snapshots as absent.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/reporter/src/lib.rs | Refactors skipped-optional package payload into an untagged enum to support resolution_failure wire shape. |
| crates/reporter/src/tests.rs | Updates existing skipped-optional tests and adds tests covering resolution_failure payload serialization. |
| crates/package-manager/src/installability.rs | Splits skipped snapshot tracking into persistent vs transient subsets and updates insert/iterate semantics. |
| crates/package-manager/src/installability/tests.rs | Adjusts assertions for the new SkippedOptionalPackage enum shape. |
| crates/package-manager/src/create_virtual_store.rs | Adds optional fetch/extract swallow path (cold batch) and returns a per-install fetch_failed set. |
| crates/package-manager/src/install_frozen_lockfile.rs | Folds fetch_failed into the live skipped set so downstream phases skip those snapshots. |
| crates/package-manager/src/install.rs | Persists only installability skips to .modules.yaml.skipped via iter_installability(). |
| crates/package-manager/src/install/tests.rs | Adds frozen-install integration tests for optional swallow vs non-optional failure propagation. |
| crates/package-manager/src/build_modules.rs | Updates skipped-optional emission to construct SkippedOptionalPackage::Installed. |
| crates/package-manager/src/build_modules/tests.rs | Updates assertions for the new skipped-optional package enum shape. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| match result { | ||
| Ok(()) => Ok(None), | ||
| Err(err) if snapshot.optional => { | ||
| // Silent swallow, matching upstream. `tracing::warn!` | ||
| // gives operator visibility without polluting |
There was a problem hiding this comment.
Resolved by the narrower swallow in 8c6db09. The warm batch only surfaces CreateVirtualDir errors (the tarball was already in CAFS, so the only thing left is the per-snapshot import/link), and the new is_fetch_side_failure helper excludes that variant from the swallow set. So both batches now consistently abort on local materialization errors — matching upstream's linkPkg path which sits outside the catch.
Written by an agent (Claude Code, claude-opus-4-7).
| tracing::warn!( | ||
| target: "pacquet::install", | ||
| snapshot = %snapshot_key, | ||
| error = %err, | ||
| "optional snapshot fetch/extract failed; dropping from install", |
There was a problem hiding this comment.
Resolved by the narrower swallow in 8c6db09. The CreateVirtualDir variant — the only one that runs after CAS import and could leave a partial slot dir — is no longer in the swallow set, so it propagates and aborts the install. The remaining swallowed variants (DownloadTarball, GitFetch) all fail before the per-snapshot slot dir is created (the slot is built by CreateVirtualDirBySnapshot which runs after a successful fetch), so there's nothing left to clean up.
Written by an agent (Claude Code, claude-opus-4-7).
| config.lockfile = false; | ||
| config.store_dir = store_dir.clone().into(); | ||
| config.modules_dir = modules_dir.clone(); | ||
| config.virtual_store_dir = virtual_store_dir.clone(); |
There was a problem hiding this comment.
Right — fixed in 8c6db09. Both frozen_install_silently_swallows_unreachable_optional_tarball and the sister frozen_install_propagates_non_optional_fetch_failure test now explicitly set config.enable_global_virtual_store = false, matching the convention from warm_reinstall_skips_snapshot_when_current_lockfile_matches.
Written by an agent (Claude Code, claude-opus-4-7).
| manifest.save().unwrap(); | ||
|
|
||
| let mut config = Config::new(); | ||
| config.lockfile = false; |
There was a problem hiding this comment.
Done in 8c6db09 — same comment as on the sister test above.
Written by an agent (Claude Code, claude-opus-4-7).
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.69242605686,
"stddev": 0.04421309524416897,
"median": 2.69639942186,
"user": 2.60316174,
"system": 3.761784759999999,
"min": 2.6277389853599997,
"max": 2.75775343436,
"times": [
2.73675734036,
2.6965023173600002,
2.75775343436,
2.64705354936,
2.6277389853599997,
2.7057123713599998,
2.69629652636,
2.74088281136,
2.64882295136,
2.66674028136
]
},
{
"command": "pacquet@main",
"mean": 2.70294241846,
"stddev": 0.06891256222439981,
"median": 2.70062786936,
"user": 2.5877858399999996,
"system": 3.7477670599999997,
"min": 2.59976533236,
"max": 2.79196796436,
"times": [
2.71071371236,
2.7866951693599997,
2.77411830536,
2.65772707436,
2.59976533236,
2.73120620536,
2.67438895836,
2.79196796436,
2.69054202636,
2.61229943636
]
},
{
"command": "pnpm",
"mean": 6.712586934159999,
"stddev": 0.24632152218047748,
"median": 6.81846635486,
"user": 10.002544539999999,
"system": 4.72426626,
"min": 6.34196919436,
"max": 7.03872955136,
"times": [
6.34196919436,
6.40478599536,
6.55390987836,
6.95814282736,
7.03872955136,
6.88357106636,
6.82132465136,
6.47563511636,
6.81560805836,
6.8321930023599995
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.80258750818,
"stddev": 0.0823209288781923,
"median": 0.77855658208,
"user": 0.3945681,
"system": 1.61587514,
"min": 0.7185039210799999,
"max": 0.96295146008,
"times": [
0.96295146008,
0.75089581708,
0.86075152908,
0.86780551008,
0.73936001708,
0.80621734708,
0.73324405808,
0.7185039210799999,
0.85976240708,
0.72638301508
]
},
{
"command": "pacquet@main",
"mean": 0.80897441478,
"stddev": 0.08099274468725157,
"median": 0.78552473908,
"user": 0.3854587,
"system": 1.63726744,
"min": 0.70676124208,
"max": 0.93926033808,
"times": [
0.93926033808,
0.79582459808,
0.84104761908,
0.76453746308,
0.91796897808,
0.77522488008,
0.74251042308,
0.70676124208,
0.87726242808,
0.72934617808
]
},
{
"command": "pnpm",
"mean": 2.7278477937800005,
"stddev": 0.11452067981143348,
"median": 2.7290162440800003,
"user": 3.2348706999999997,
"system": 2.28872974,
"min": 2.59470890408,
"max": 2.92952713308,
"times": [
2.8892663050800005,
2.92952713308,
2.60598325108,
2.6106354030800003,
2.7568137520800002,
2.59470890408,
2.7536175930800004,
2.7533392400800003,
2.6798931080800004,
2.7046932480800003
]
}
]
} |
…iants + GVS-off in tests Addresses PR #474 review comments: 1. **Swallow scoping** (CodeRabbit major): `InstallPackageBySnapshotError` carries both fetch-side variants (`DownloadTarball`, `GitFetch`) and post-fetch variants (`CreateVirtualDir`, plus the config-shape `MissingTarballIntegrity` / `UnsupportedResolution`). Upstream's catch at `deps/graph-builder/src/lockfileToDepGraph.ts:286-298` wraps `storeController.fetchPackage` only — the local materialization (`linkPkg`) sits outside. Restrict pacquet's swallow accordingly via `is_fetch_side_failure`, so a local link error on an optional snapshot still aborts the install. Side benefit: the warm-batch path (which only surfaces `CreateVirtualDir` errors) stays consistently abort-on-error without a parallel swallow site. 2. **Test diagnostic** (CodeRabbit minor): add `dbg!(&json)` and include `{json:?}` in the `version` assertion message on `skipped_optional_resolution_failure_omits_absent_name_and_version` so failures self-diagnose under nextest. 3. **GVS-off in tests** (Copilot): both new `frozen_install_{silently_swallows,propagates_non_optional}` tests now explicitly `config.enable_global_virtual_store = false`. The swallow test's path assertion targets the legacy `<virtual_store_dir>/<flat-name>` slot — with GVS on the slot lives elsewhere and the assertion would always pass regardless of the swallow firing. Same pattern as `warm_reinstall_skips_snapshot_when_current_lockfile_matches`.
| /// that picks the right shape depending on which variant the emit | ||
| /// site constructs. The pairing is not type-enforced against | ||
| /// `reason` (a `BuildFailure` reason with a | ||
| /// `ResolutionFailure` package is constructible in Rust), but every | ||
| /// emit site is centralized inside this crate, so the constructor | ||
| /// discipline lives there rather than in the type. |
There was a problem hiding this comment.
Good correction — the doc was misleading. Fixed in 6fdf5c5: the comment now names the actual emit sites (installability.rs, build_modules.rs, create_virtual_store.rs for slice 4 — all in pacquet-package-manager) and acknowledges the reason/package pairing is convention-driven rather than type-enforced.
Written by an agent (Claude Code, claude-opus-4-7).
…site locality Copilot review on PR #474 pointed out that the doc comment claimed emit sites are "centralized inside this crate" but the actual emitters live in `pacquet-package-manager` — `installability.rs` (slice 1), `build_modules.rs` (build-failure), and now `create_virtual_store.rs` (slice 4 fetch-failure swallow). Reword to name those sites explicitly and acknowledge the reason/package pairing is convention-driven, not type-enforced.
| /// sites live in `pacquet-package-manager` | ||
| /// (`installability.rs`, `build_modules.rs`, | ||
| /// `create_virtual_store.rs` for slice 4) and must keep the |
There was a problem hiding this comment.
Good catch — CreateVirtualStore's slice 4 path is intentionally silent on the wire (the swallow just drops the error; the per-snapshot pnpm:skipped-optional-dependency event upstream-equivalent fires from the resolver site, which pacquet doesn't have yet). Reworded in 89c27a0 to list only the actual constructor sites (installability.rs, build_modules.rs) and explicitly call out that the slice 4 path is silent.
Written by an agent (Claude Code, claude-opus-4-7).
…e list Copilot follow-up on the previous fix: `CreateVirtualStore`'s slice 4 fetch-failure path is silent on the reporter wire — it swallows the error without emitting `pnpm:skipped-optional-dependency` (matches upstream's `lockfileToDepGraph.ts:294-298` catch). So it's not a constructor site for this log type. Reword the doc comment to list only the actual emit sites (`installability.rs` and `build_modules.rs`) and explicitly note that the slice 4 path is silent.
…#485) Slice 5 of the [#434 umbrella](#434) (`Proper optionalDependencies support`). Slices 1–4 (#439, #456, #467, #474) handle installability + the fetch-failure swallow. This slice closes the user-facing `--no-optional` flag: the CLI flag already exists and threads through `Install::dependency_groups`, but only `SymlinkDirectDependencies` and the on-disk `included` field actually honored it. The rest of the install pipeline walked `optional_dependencies` unconditionally, so the optional subtree was still extracted, linked, and built. Mirrors upstream's depNode filter at [`installing/deps-installer/src/install/link.ts:109-111`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/src/install/link.ts#L109-L111): ```ts if (!opts.include.optionalDependencies) { depNodes = depNodes.filter(({ optional }) => !optional) } ``` ## Changes - **`SkippedSnapshots`**: gains a third disjoint subset `optional_excluded` alongside `installability` (slice 1) and `fetch_failed` (slice 4). The existing `contains` / `iter` already return the union, so every downstream consumer that filters by skip-set (`CreateVirtualStore`, `SymlinkDirectDependencies`, `BuildModules`, hoist, `link_bins`) now respects `--no-optional` through the same gate. `iter_installability` still returns only the persistent subset for `.modules.yaml.skipped` writes — `--no-optional` exclusions are transient (matching upstream's behavior of never updating `opts.skipped` at the filter site). - **`InstallFrozenLockfile::run`**: iterates the lockfile snapshots once and inserts every `snap.optional == true` entry into the new subset when the dispatch list doesn't contain `DependencyGroup::Optional`. The `SnapshotEntry::optional` flag is set by the resolver only when the snapshot is reachable **exclusively** through optional edges, so a snapshot reachable through any non-optional edge carries `optional: false` and survives the filter — covers [`optionalDependencies.ts:712`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/optionalDependencies.ts#L712) (`dependency that is both optional and non-optional is installed`).
Summary
Slice 4 of the #434 umbrella (
Proper optionalDependencies support). Slices 1–3 (#439, #456, #467) close the installability-driven skip path; this slice adds the second skip pathway upstream handles in the frozen install: anoptional: truesnapshot whose fetch / extract step fails at install time must not abort the install. Local-materialization errors (CreateVirtualDir) and config-shape errors still abort even for optional snapshots — matching upstream'slinkPkgpath which sits outside the catch.Mirrors the silent catch sites at
deps/graph-builder/src/lockfileToDepGraph.ts:294-298andinstalling/deps-restorer/src/index.ts:912-921:Changes
SkippedOptionalPackagebecomes a#[serde(untagged)]enum withInstalled { id, name, version }andResolutionFailure { name?, version?, bareSpecifier }variants. Models upstream's discriminated union atcore-loggers/src/skippedOptionalDependencyLogger.ts:10-31. The new variant is wire-shape-only in slice 4 — pacquet has no resolver yet, but a future resolver port at the upstream emit site (resolveDependencies.ts:1376-1383) lands without re-touching this type.SkippedSnapshots: gains afetch_failedsubset disjoint from the existinginstallabilitysubset.contains/iterreturn the union (downstream consumers treat both as absent);iter_installabilityreturns only the persistent subset that.modules.yaml.skippedrecords, matching upstream's behavior of never updatingopts.skippedat the fetch-failure catch sites.CreateVirtualStore: cold-batch dispatch swallows per-snapshot errors whensnapshot.optionalis true and the error is fetch-side. A newis_fetch_side_failurehelper restricts the swallow toInstallPackageBySnapshotError::DownloadTarballand::GitFetch— the same surface upstream wraps instoreController.fetchPackage. Local-materialization (CreateVirtualDir) and config-shape errors (MissingTarballIntegrity/UnsupportedResolution) propagate even for optional snapshots, matching upstream'slinkPkgpath which sits outside the catch. Side benefit: the warm-batch path (which only surfacesCreateVirtualDirerrors) stays consistently abort-on-error without a parallel swallow site. The per-installfetch_failed: HashSet<PackageKey>rides out onCreateVirtualStoreOutput.InstallFrozenLockfile::run: folds the returnedfetch_failedinto its mutableSkippedSnapshotsso downstream phases (SymlinkDirectDependencies,LinkVirtualStoreBins,BuildModules, hoist) treat the dropped snapshots as absent through the same gate they already use for installability skips.Test plan
reporter::tests::skipped_optional_resolution_failure_event_matches_pnpm_wire_shape— full payload (bareSpecifiercamelCase, noid).reporter::tests::skipped_optional_resolution_failure_omits_absent_name_and_version— optional-field shape.install::tests::frozen_install_silently_swallows_unreachable_optional_tarball— portsinstalling/deps-restorer/test/index.ts:340-360: unreachable optional tarball (http://127.0.0.1:1/...), install succeeds, slot not created,.modules.yaml.skippedleft empty. Test-the-test verified by inverting theoptionalguard in the cold-batch match arm. Runs withenable_global_virtual_store = falseso the slot-path assertion targets the legacy layout.install::tests::frozen_install_propagates_non_optional_fetch_failure— pins the polarity: same fixture, non-optional snapshot, install must error.just ready(typos + fmt + check + nextest + clippy).taplo format --check.just dylint(perfectionist).RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace.Out of scope
resolution_failureemit site — needs a resolver, tracked separately.--no-optional/--omit=optionalplumbing — umbrella slice 5.pnpm:skipped-optional-dependency.Closes #471.
Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Bug Fixes
Tests