feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397)#419
Conversation
The v9 lockfile spec carries an `optional: true` field on each snapshot ([upstream type](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/lockfile/types/src/index.ts#L34-L39)), folded by pnpm's resolver from the importer graph. pacquet's reader previously dropped it. Adding it as `pub optional: bool` on `SnapshotEntry` with `#[serde(default, skip_serializing_if = "is_false")]` so the wire shape matches upstream: absent ↔ false, and `false` is never serialized. The flag is the input that the next commit's optional- aware build-failure handling reads — pacquet trusts the precomputed flag rather than re-deriving it, matching upstream's `lockfileToDepGraph` at [`deps/graph-builder/src/lockfileToDepGraph.ts:315`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/graph-builder/src/lockfileToDepGraph.ts#L315). Tests round-trip an `optional: true` snapshot through deserialize → serialize, and confirm the field defaults false / omits when false.
New `LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog)`
variant + `SkippedOptionalPackage` + `SkippedOptionalReason` enum.
Wire shape mirrors pnpm's `SkippedOptionalDependencyMessage` at
`pnpm/core-loggers/src/skippedOptionalDependencyLogger.ts` (top-
level `details?`, `package: { id, name, version }`, `prefix`,
`reason`); the `parents` field upstream marks as TODO is left off
here too. `SkippedOptionalReason` carries all four upstream reasons
even though pacquet only emits `BuildFailure` today — keeps the
enum closed and means future emit sites for unsupported-engine /
-platform / resolution-failure don't have to widen the type.
Tests cover the wire-shape round-trip, the `details: None`
omit-when-absent path, and the snake_case discriminant for every
reason variant — matching the conventions of the other reporter
event tests.
Ports `building/during-install/src/index.ts:218-240` from pnpm
v11 (`b4f8f47ac2`). When `run_postinstall_hooks` returns an error
for a snapshot whose `optional` flag is set, `BuildModules.run`
now emits the failure through the `pnpm:skipped-optional-dependency`
channel (reason `build_failure`, `package.id` = the dep's pkg dir,
matching upstream's `depNode.dir`) and continues with the rest of
the install. Non-optional failures still propagate as
`BuildModulesError::LifecycleScript` so the install aborts on a
required dep's failure.
Two tests cover the new branches: a `cfg(unix)`-gated test on a
`SnapshotEntry { optional: true, .. }` with a failing postinstall
asserts the install succeeds and the `Reporter` captures a
`SkippedOptionalDependency` event with `reason: BuildFailure` and
the right `name` / `version` / `details`; a symmetric test on a
non-optional snapshot asserts `BuildModules.run` returns
`Err(BuildModulesError::LifecycleScript(_))`.
`SnapshotEntry { optional: false }` is now also set explicitly in
`build_snapshot.rs` (the synthetic-snapshot builder) and the
`build_sequence` tests so the new field is unambiguous at every
construction site.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (6)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
🧰 Additional context used📓 Path-based instructions (1)**/*.rs📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (2)📚 Learning: 2026-05-07T23:19:08.272ZApplied to files:
📚 Learning: 2026-05-01T10:01:33.766ZApplied to files:
🔇 Additional comments (2)
📝 WalkthroughWalkthroughAdds an ChangesOptional Dependency Lifecycle
Registry Mock Wrapper & Wiring
Sequence DiagramsequenceDiagram
participant BM as BuildModules::run
participant PH as run_postinstall_hooks
participant Reporter as Reporter
participant Err as Caller/Error
BM->>BM: read `optional` from snapshot
BM->>PH: run_postinstall_hooks(opts)
alt postinstall succeeds
PH-->>BM: Ok
else postinstall fails
PH-->>BM: Err
alt opts.optional == true
BM->>Reporter: emit pnpm:skipped-optional-dependency (BuildFailure)
BM-->>Err: continue (no error)
else opts.optional == false
BM-->>Err: return BuildModulesError::LifecycleScript
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
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 docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #419 +/- ##
==========================================
+ Coverage 84.70% 85.21% +0.50%
==========================================
Files 78 79 +1
Lines 5415 5439 +24
==========================================
+ Hits 4587 4635 +48
+ Misses 828 804 -24 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
crates/reporter/src/tests.rs (1)
617-637: 💤 Low valueConsider adding diagnostic logging closer to the assertion.
The
dbg!call is several lines before the assertion that checksdetailsabsence. Adding a focused log immediately before line 636 would improve test debuggability per CODE_STYLE_GUIDE.Suggested addition
let json: Value = envelope .pipe_ref(serde_json::to_string) .expect("serialize envelope") .pipe_as_ref(serde_json::from_str) .expect("parse JSON"); + eprintln!("json.get(\"details\") = {:?}", json.get("details")); assert!(json.get("details").is_none(), "details must be omitted when absent, got {json:?}",);🤖 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 `@crates/reporter/src/tests.rs` around lines 617 - 637, In the test skipped_optional_omits_absent_details, add a focused diagnostic print immediately before the assert that checks json.get("details").is_none() so failures show the exact JSON under test; e.g., log the `json` Value or `json.get("details")` right before the assertion (where `envelope` is serialized into `json`) to improve debuggability of the failing assertion in this test.crates/lockfile/src/snapshot_entry/tests.rs (2)
27-38: ⚡ Quick winAdd diagnostic logging before assertions.
Same issue as the previous test—add explicit logging before
assert!calls per CODE_STYLE_GUIDE. Based on learnings, add logging (e.g.,eprintln!/eprintln!(...)) so that useful diagnostic values are printed when a test fails, unless the assertion isassert_eq!(where the differing values are already included).Suggested logging additions
#[test] fn optional_defaults_false_and_omits_when_false() { let yaml = text_block! { "dependencies:" " bar: 1.0.0" }; let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse"); + eprintln!("entry.optional = {}", entry.optional); assert!(!entry.optional, "default must be false when absent"); let out = serialize_yaml::to_string(&entry).expect("serialize"); + eprintln!("serialized output:\n{}", out); assert!(!out.contains("optional"), "false must not be serialized:\n{out}"); }🤖 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 `@crates/lockfile/src/snapshot_entry/tests.rs` around lines 27 - 38, Add diagnostic logging in the test optional_defaults_false_and_omits_when_false before each assert: print the parsed SnapshotEntry optional flag (e.g., eprintln!("entry.optional = {:?}", entry.optional)) before the assert!(!entry.optional...) and print the serialized YAML string (e.g., eprintln!("serialized out = {}", out)) before the assert!(!out.contains("optional")...) so failing assertions emit useful diagnostics; keep logs simple and use eprintln! to match the other tests' style.
10-22: ⚡ Quick winAdd diagnostic logging before assertions.
Per the CODE_STYLE_GUIDE learning, tests should include
eprintln!ordbg!to log actual values beforeassert!calls (not just in the assertion message) so failures can be diagnosed without rerunning. Based on learnings, add logging (e.g.,eprintln!/eprintln!(...)) so that useful diagnostic values are printed when a test fails, unless the assertion isassert_eq!(where the differing values are already included).Suggested logging additions
#[test] fn optional_true_round_trips() { let yaml = text_block! { "dependencies:" " foo: 1.2.3" "optional: true" }; let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse"); + eprintln!("entry.optional = {}", entry.optional); assert!(entry.optional, "deserialize must capture optional: true"); let out = serialize_yaml::to_string(&entry).expect("serialize"); + eprintln!("serialized output:\n{}", out); assert!(out.contains("optional: true"), "serialize must round-trip optional: true:\n{out}"); }🤖 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 `@crates/lockfile/src/snapshot_entry/tests.rs` around lines 10 - 22, The test optional_true_round_trips lacks diagnostic logging before assertions; add eprintln! (or dbg!) calls to print the deserialized SnapshotEntry state and the serialized output so failures are easier to diagnose: log entry.optional (or the entire entry) right after let entry: SnapshotEntry = ... and log the serialized out right after serialize_yaml::to_string(&entry).expect(...), keeping the existing asserts unchanged.
🤖 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.
Nitpick comments:
In `@crates/lockfile/src/snapshot_entry/tests.rs`:
- Around line 27-38: Add diagnostic logging in the test
optional_defaults_false_and_omits_when_false before each assert: print the
parsed SnapshotEntry optional flag (e.g., eprintln!("entry.optional = {:?}",
entry.optional)) before the assert!(!entry.optional...) and print the serialized
YAML string (e.g., eprintln!("serialized out = {}", out)) before the
assert!(!out.contains("optional")...) so failing assertions emit useful
diagnostics; keep logs simple and use eprintln! to match the other tests' style.
- Around line 10-22: The test optional_true_round_trips lacks diagnostic logging
before assertions; add eprintln! (or dbg!) calls to print the deserialized
SnapshotEntry state and the serialized output so failures are easier to
diagnose: log entry.optional (or the entire entry) right after let entry:
SnapshotEntry = ... and log the serialized out right after
serialize_yaml::to_string(&entry).expect(...), keeping the existing asserts
unchanged.
In `@crates/reporter/src/tests.rs`:
- Around line 617-637: In the test skipped_optional_omits_absent_details, add a
focused diagnostic print immediately before the assert that checks
json.get("details").is_none() so failures show the exact JSON under test; e.g.,
log the `json` Value or `json.get("details")` right before the assertion (where
`envelope` is serialized into `json`) to improve debuggability of the failing
assertion in this test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f4a7a9f1-7e4e-4d48-8833-de27c0bd2c65
📒 Files selected for processing (8)
crates/lockfile/src/snapshot_entry.rscrates/lockfile/src/snapshot_entry/tests.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rscrates/package-manager/src/build_sequence/tests.rscrates/package-manager/src/build_snapshot.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: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plainStringor&strto preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation viaTryFrom<String>and/orFromStrwithout providing an infallible public constructor
If upstream never validates a branded string type, expose an infallibleFrom<String>constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bareasassertion, expose afrom_str_uncheckedconstructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use#[serde(try_from = "String")]for deserialization to go through the validator and#[serde(into = "String")]for serialization
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls on branded types rather than handwritingimplblocks
Model string-literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers
Template literal types (like${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide inCODE_STYLE_GUIDE.mdcovering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Warnings are errors incargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....
Files:
crates/package-manager/src/build_snapshot.rscrates/package-manager/src/build_sequence/tests.rscrates/lockfile/src/snapshot_entry/tests.rscrates/lockfile/src/snapshot_entry.rscrates/reporter/src/lib.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rscrates/reporter/src/tests.rs
🧠 Learnings (3)
📚 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_snapshot.rscrates/package-manager/src/build_sequence/tests.rscrates/lockfile/src/snapshot_entry/tests.rscrates/lockfile/src/snapshot_entry.rscrates/reporter/src/lib.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.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 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_sequence/tests.rscrates/lockfile/src/snapshot_entry/tests.rscrates/package-manager/src/build_modules/tests.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 (11)
crates/lockfile/src/snapshot_entry.rs (1)
5-49: LGTM!The
optionalfield addition is well-structured:
- Comprehensive documentation linking to upstream behavior
- Correct serde configuration for default + skip-when-false semantics
- Simple, focused helper function
The implementation correctly mirrors pnpm's v9 lockfile spec where
optionalis precomputed by the resolver and written to the snapshot.crates/package-manager/src/build_snapshot.rs (1)
110-110: LGTM!Explicitly setting
optional: falseensures the snapshot construction always populates the field, which aligns with the serdeskip_serializing_if = "is_false"behavior (the field will be omitted from YAML output).crates/package-manager/src/build_sequence/tests.rs (1)
37-37: LGTM!The test helper now constructs
SnapshotEntrywith the newoptionalfield, maintaining consistency with the updated struct definition and production code inbuild_snapshot.rs.crates/reporter/src/lib.rs (2)
153-164: LGTM!The
SkippedOptionalDependencyevent variant is correctly integrated:
- Proper serde rename to
pnpm:skipped-optional-dependencyfor wire compatibility- Comprehensive documentation linking to upstream emit site
- Clear scope note that only
build_failureis emitted today
533-573: LGTM!The new reporter types correctly mirror pnpm's wire format:
SkippedOptionalDependencyLogwith optionaldetailsfield properly configuredSkippedOptionalPackagematches upstream's non-resolution-failure shapeSkippedOptionalReasonenum with snake_case serialization for pnpm compatibility- All four upstream reasons declared for forward compatibility
The serde configuration ensures
@pnpm/cli.default-reporterwill parse these events correctly.crates/reporter/src/tests.rs (2)
580-613: LGTM!The test comprehensively verifies the
pnpm:skipped-optional-dependencywire format:
- Channel name, level, and reason fields
- Nested
packagestructuredetailsfield presence- Uses
dbg!(&json)for diagnostic logging
641-653: LGTM!The parameterized test correctly verifies all four
SkippedOptionalReasonvariants serialize to the snake_case strings pnpm's reporter expects.crates/package-manager/src/build_modules.rs (1)
223-270: LGTM!The optional-dependency build-failure handling correctly mirrors pnpm's behavior:
- Reads the precomputed
optionalflag from the v9 lockfile snapshot- On postinstall failure, branches to either:
- Emit
pnpm:skipped-optional-dependencywithBuildFailurereason and continue (optional)- Return
BuildModulesError::LifecycleScriptand abort (non-optional)- Populates the event with package identification (
id,name,version), errordetails, andprefixThe use of
pkg_dir.to_string_lossy()for theidfield matches upstream'sdepNode.dirand is appropriate for paths that need serialization.crates/package-manager/src/build_modules/tests.rs (3)
264-326: LGTM!This test correctly verifies that an optional dependency's postinstall failure is swallowed and logged as
pnpm:skipped-optional-dependencywith:
- Reason
BuildFailure- Populated
detailsfield- Correct package
nameandversionThe Unix gating is appropriate given the spawn/shell semantics differences, and the use of
dangerouslyAllowAllBuildscorrectly exercises the build-failure path rather than the policy gate.
328-357: LGTM!The symmetric test correctly verifies that a non-optional dependency's postinstall failure propagates as
BuildModulesError::LifecycleScript, matching upstream'sthrow errpath.
359-377: LGTM!The
create_failing_pkghelper cleanly mirrorscreate_buildable_pkgwith a failing"exit 1"postinstall script, allowing the tests to exercise the error path.
There was a problem hiding this comment.
Pull request overview
This PR aligns pacquet’s install/build behavior and reporter wire format with pnpm v11 for optional dependencies whose lifecycle scripts fail, by continuing the install and emitting a pnpm:skipped-optional-dependency event instead of aborting.
Changes:
- Surface
snapshots[<key>].optionalin the lockfile model (SnapshotEntry.optional) with correct defaulting/serialization behavior. - Add reporter support for
pnpm:skipped-optional-dependency, includingSkippedOptionalReasonserialization and wire-shape tests. - Update
BuildModulesto swallow lifecycle failures for snapshot-marked optional deps and emit the skipped-optional event; add tests for optional vs non-optional failure behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/reporter/src/lib.rs | Adds the new pnpm:skipped-optional-dependency log event types and reason enum. |
| crates/reporter/src/tests.rs | Adds wire-shape/serialization tests for the new skipped-optional event and reasons. |
| crates/lockfile/src/snapshot_entry.rs | Adds SnapshotEntry.optional with serde defaulting and omission when false. |
| crates/lockfile/src/snapshot_entry/tests.rs | Adds YAML round-trip tests for optional: true and omission when absent/false. |
| crates/package-manager/src/build_modules.rs | Swallows optional-dep lifecycle failures and emits pnpm:skipped-optional-dependency. |
| crates/package-manager/src/build_modules/tests.rs | Adds tests ensuring optional failures are swallowed+reported and non-optional failures propagate. |
| crates/package-manager/src/build_snapshot.rs | Initializes the new SnapshotEntry.optional field when constructing snapshots. |
| crates/package-manager/src/build_sequence/tests.rs | Updates helper snapshot construction to include optional: false. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.4848017133200004,
"stddev": 0.06907687884485912,
"median": 2.49404528482,
"user": 2.4575992399999995,
"system": 3.4635440799999997,
"min": 2.3779451208199998,
"max": 2.59274864282,
"times": [
2.52154781982,
2.53616557782,
2.38677455782,
2.45490747982,
2.52926637982,
2.45237257782,
2.52974622682,
2.46654274982,
2.59274864282,
2.3779451208199998
]
},
{
"command": "pacquet@main",
"mean": 2.4743788276200003,
"stddev": 0.06559854009645909,
"median": 2.46812412682,
"user": 2.49985134,
"system": 3.46317088,
"min": 2.37267978782,
"max": 2.58598184482,
"times": [
2.42213938682,
2.48337624182,
2.45250379282,
2.58598184482,
2.45287201182,
2.41281283782,
2.5543671248199997,
2.37267978782,
2.49144847482,
2.51560677282
]
},
{
"command": "pnpm",
"mean": 6.33137536802,
"stddev": 0.08417253664739424,
"median": 6.33468592332,
"user": 9.43948484,
"system": 4.53789378,
"min": 6.16596284482,
"max": 6.43819772382,
"times": [
6.43335162182,
6.37493580882,
6.33111235482,
6.33268822382,
6.26962364182,
6.38023295682,
6.33668362282,
6.25096488082,
6.43819772382,
6.16596284482
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6829253511600001,
"stddev": 0.05891428408173842,
"median": 0.65879246386,
"user": 0.35924528,
"system": 1.4768329599999999,
"min": 0.6515822248600001,
"max": 0.84446150486,
"times": [
0.84446150486,
0.6793471848600001,
0.65364347986,
0.67624851486,
0.6539902898600001,
0.7001044728600001,
0.6611675988600001,
0.6515822248600001,
0.65641732886,
0.6522909118600001
]
},
{
"command": "pacquet@main",
"mean": 0.68398827296,
"stddev": 0.04499486798505863,
"median": 0.6625674183600001,
"user": 0.36068068,
"system": 1.4753041599999999,
"min": 0.6451331988600001,
"max": 0.7645867838600001,
"times": [
0.7645867838600001,
0.65420370386,
0.7538578468600001,
0.6630001968600001,
0.65054664386,
0.6621346398600001,
0.6451331988600001,
0.71962859586,
0.6513259038600001,
0.6754652158600001
]
},
{
"command": "pnpm",
"mean": 2.6641386669600005,
"stddev": 0.08230622631575653,
"median": 2.6558912998600004,
"user": 3.22523238,
"system": 2.21655336,
"min": 2.53712671886,
"max": 2.77983913686,
"times": [
2.58576947486,
2.53712671886,
2.67391816186,
2.6378644378600002,
2.73355683186,
2.63639855086,
2.5826190128600004,
2.76567928686,
2.7086150568600003,
2.77983913686
]
}
]
} |
…#397) Per review feedback. The two BuildModules tests for the optional- build-failure path used synthetic identifiers (`flaky@1.0.0`, `required@1.0.0`) and a stripped-down `exit 1` script body. They now use the upstream fixture identity verbatim — `@pnpm.e2e/failing-postinstall@1.0.0` from `/Volumes/src/pnpm/registry-mock/packages/failing-postinstall/` (pinned at the same commit pnpm v11's `b4f8f47ac2` consumes) — and the same `postinstall` body (`echo hello && echo world && exit 1`). Mirrors the test cases at `installing/deps-installer/test/install/optionalDependencies.ts:563` and `:574`. The full e2e port (resolve + extract through the live registry-mock then run scripts) is deferred: pacquet's pinned `@pnpm/registry-mock@^3.16.0` predates the `failing-postinstall` fixture (introduced in v5.x). Upgrading that dep is its own follow-up. Renames: - `optional_dep_build_failure_is_swallowed_and_reported` → `do_not_fail_on_optional_dep_with_failing_postinstall` - `non_optional_dep_build_failure_propagates` → `fail_when_failing_postinstall_is_required` - `create_failing_pkg` → `create_failing_postinstall_fixture`
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/package-manager/src/build_modules/tests.rs (1)
320-335: ⚡ Quick winStrengthen the optional-failure assertions to catch silent regressions.
This test currently logs
ignoredbut does not assert it; it also accepts emptydetailsas long as it isSome. Tightening both checks will make the behavior contract explicit.Proposed test hardening
- dbg!(&ignored); + assert!( + ignored.is_empty(), + "optional build failure should not add ignored scripts: {ignored:?}", + ); @@ - assert!(skipped_event.details.is_some(), "details must carry the error toString"); + let details = skipped_event.details.as_deref().unwrap_or(""); + assert!( + !details.trim().is_empty(), + "details must carry non-empty lifecycle error context: {skipped_event:?}", + );🤖 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 `@crates/package-manager/src/build_modules/tests.rs` around lines 320 - 335, The test currently prints `ignored` and only checks `details.is_some()`, which allows silent regressions; update the assertions to (1) assert that the `ignored` collection (variable `ignored`) contains the expected entry(s) for the failing-postinstall package (i.e., not empty and/or contains the package name), and (2) assert that `skipped_event.details` is a non-empty string (not just Some) so it actually carries the error text; locate the test where `ignored` is created and the `skipped_event` is extracted from `EVENTS` (matching `LogEvent::SkippedOptionalDependency`) and replace the weak checks with concrete assertions that `ignored` includes the failing package and `skipped_event.details.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false)` is true while keeping the existing checks for `SkippedOptionalReason::BuildFailure` and package name/version.
🤖 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.
Nitpick comments:
In `@crates/package-manager/src/build_modules/tests.rs`:
- Around line 320-335: The test currently prints `ignored` and only checks
`details.is_some()`, which allows silent regressions; update the assertions to
(1) assert that the `ignored` collection (variable `ignored`) contains the
expected entry(s) for the failing-postinstall package (i.e., not empty and/or
contains the package name), and (2) assert that `skipped_event.details` is a
non-empty string (not just Some) so it actually carries the error text; locate
the test where `ignored` is created and the `skipped_event` is extracted from
`EVENTS` (matching `LogEvent::SkippedOptionalDependency`) and replace the weak
checks with concrete assertions that `ignored` includes the failing package and
`skipped_event.details.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false)`
is true while keeping the existing checks for
`SkippedOptionalReason::BuildFailure` and package name/version.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2d5ab006-4fb0-43e2-9b59-17d7bef94812
📒 Files selected for processing (1)
crates/package-manager/src/build_modules/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: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (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: Log emissions are part of 'match pnpm'—when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plainStringor&strto preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation viaTryFrom<String>and/orFromStrwithout providing an infallible public constructor
If upstream never validates a branded string type, expose an infallibleFrom<String>constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bareasassertion, expose afrom_str_uncheckedconstructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use#[serde(try_from = "String")]for deserialization to go through the validator and#[serde(into = "String")]for serialization
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls on branded types rather than handwritingimplblocks
Model string-literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers
Template literal types (like${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide inCODE_STYLE_GUIDE.mdcovering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Warnings are errors incargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....
Files:
crates/package-manager/src/build_modules/tests.rs
🧠 Learnings (2)
📚 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.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.rs
🔇 Additional comments (1)
crates/package-manager/src/build_modules/tests.rs (1)
350-378: Nice branch coverage for required dependency failures.This cleanly pins the
optional: falsepath toBuildModulesError::LifecycleScript, which complements the optional-skip test well.
Bumps the mock from `^3.16.0` (resolves to 3.50.0) to `^6.0.0`.
v3 predates the `@pnpm.e2e/failing-postinstall` and
`@pnpm.e2e/has-failing-postinstall-dep` fixtures (added in v5.x);
v6 ships them along with ~250 other `@pnpm.e2e/*` fixtures used by
upstream pnpm's tests, which unlocks faithful test parity for
ports against pnpm v11.
Three follow-on adjustments needed for v6 to actually launch and
for existing tests to keep passing:
- **Launch via the programmatic API.** v6's default CLI export
invokes the bundled verdaccio 5.33 directly with the host
Node, but verdaccio 5.33 rejects its own auto-generated
64-character storage secret on Node 22+
(`Invalid storage secret key length, must be 32 characters
long but is 64`). Pnpm's own jest `globalSetup`
(`pnpm/__utils__/jest-config/with-registry/globalSetup.js`)
works around this by calling the programmatic
`start({ useNodeVersion: '20.16.0', listen, … })` so verdaccio
runs under a Node version where the 32-char enforcement
isn't there. Added a tiny wrapper `tasks/registry-mock/launch.mjs`
that mirrors that shape; the Rust launcher
(`node_registry_mock.rs`) now returns a pre-populated
`Command` of `node <launch.mjs>` instead of pointing at the
bare `registry-mock` CLI binary in `node_modules/.bin`.
- **`nodeLinker: hoisted` in `pnpm-workspace.yaml`.** Verdaccio's
CJS deps (notably `@verdaccio/signature` requiring
`@verdaccio/config`) don't resolve cleanly under pnpm's strict
isolation when running from the nested `node_modules/.bin`. A
flat node_modules makes the resolution predictable.
- **Snapshot refresh for three CLI install tests** —
`should_install_dependencies` / `should_install_index_files` /
`should_install_exec_files`. v6's `@pnpm.e2e/hello-world-js-bin`
and `…-js-bin-parent` fixtures now ship MIT LICENSE files and
slightly different `package.json` content, so the recursive
on-disk-listing snapshots had to be regenerated. Reviewed and
accepted — the changes are consistent with "fixture content
was refreshed upstream," nothing structural.
… install test (#397) Adds an integration test that drives `Install::run` end-to-end through the live mocked registry (now serving the `@pnpm.e2e/failing-postinstall` and `@pnpm.e2e/has-failing-postinstall-dep` fixtures after the previous commit's `@pnpm/registry-mock` upgrade). Ports `'do not fail on an optional dependency that has a non-optional dependency with a failing postinstall script'` at `installing/deps-installer/test/install/optionalDependencies.ts:563` in pnpm v11 (`b4f8f47ac2`). The test resolves `@pnpm.e2e/has-failing-postinstall-dep@1.0.0` as an `optionalDependencies` entry, runs install, and asserts both the wrapper and the transitive `@pnpm.e2e/failing-postinstall` land in the virtual store without the install aborting. Pacquet's `frozen_lockfile=false` path doesn't run scripts (script execution is `BuildModules`'s job in the frozen path), so this test pins the fetch + extract behavior on the optional edge. The unit-level `BuildModules` test for the same fixture (`crate::build_modules::tests::do_not_fail_on_optional_dep_with_failing_postinstall`) covers the script-failure swallow.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/package-manager/src/install/tests.rs (1)
708-724: ⚡ Quick winAdd diagnostic logs around
assert!checks in the new test.Lines 708-724 use
assert!checks without printing the evaluated runtime state, which makes CI failures harder to triage.Suggested patch
- assert!( - is_symlink_or_junction( - &project_root.join("node_modules/@pnpm.e2e/has-failing-postinstall-dep"), - ) - .unwrap(), - "wrapper symlink missing", - ); + let wrapper_link = project_root.join("node_modules/@pnpm.e2e/has-failing-postinstall-dep"); + eprintln!( + "wrapper_link={wrapper_link:?} symlink_or_junction={:?}", + is_symlink_or_junction(&wrapper_link) + ); + assert!( + is_symlink_or_junction(&wrapper_link).unwrap(), + "wrapper symlink missing", + ); - assert!( - project_root - .join("node_modules/.pacquet/@pnpm.e2e+has-failing-postinstall-dep@1.0.0") - .is_dir(), - "wrapper virtual-store dir missing", - ); + let wrapper_store_dir = + project_root.join("node_modules/.pacquet/@pnpm.e2e+has-failing-postinstall-dep@1.0.0"); + eprintln!( + "wrapper_store_dir={wrapper_store_dir:?} is_dir={}", + wrapper_store_dir.is_dir() + ); + assert!(wrapper_store_dir.is_dir(), "wrapper virtual-store dir missing"); - assert!( - project_root.join("node_modules/.pacquet/@pnpm.e2e+failing-postinstall@1.0.0").is_dir(), - "transitive `failing-postinstall` must be extracted to the virtual store", - ); + let transitive_store_dir = + project_root.join("node_modules/.pacquet/@pnpm.e2e+failing-postinstall@1.0.0"); + eprintln!( + "transitive_store_dir={transitive_store_dir:?} is_dir={}", + transitive_store_dir.is_dir() + ); + assert!( + transitive_store_dir.is_dir(), + "transitive `failing-postinstall` must be extracted to the virtual store", + );Based on learnings: “In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g.,
eprintln!) forassert!/assert_ne!-style assertions so failures are diagnosable.”🤖 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 `@crates/package-manager/src/install/tests.rs` around lines 708 - 724, The three assert! checks around is_symlink_or_junction(...) and the project_root.join(...) path existence checks lack diagnostic output; before each assert!, print the evaluated runtime state (e.g., the boolean result of is_symlink_or_junction, the full path strings, and whether .is_dir() returned true) using eprintln! so CI failures show actual values; specifically, for the is_symlink_or_junction call store its Result/boolean and eprintln! the path and result before asserting, and for the two .is_dir() checks eprintln! the joined path and the .is_dir() boolean before the assert!.
🤖 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.
Nitpick comments:
In `@crates/package-manager/src/install/tests.rs`:
- Around line 708-724: The three assert! checks around
is_symlink_or_junction(...) and the project_root.join(...) path existence checks
lack diagnostic output; before each assert!, print the evaluated runtime state
(e.g., the boolean result of is_symlink_or_junction, the full path strings, and
whether .is_dir() returned true) using eprintln! so CI failures show actual
values; specifically, for the is_symlink_or_junction call store its
Result/boolean and eprintln! the path and result before asserting, and for the
two .is_dir() checks eprintln! the joined path and the .is_dir() boolean before
the assert!.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 17408aa9-5b8f-4f6f-9d22-093317f8a0df
⛔ Files ignored due to path filters (4)
crates/cli/tests/snapshots/install__should_install_dependencies.snapis excluded by!**/*.snapcrates/cli/tests/snapshots/install__should_install_exec_files.snapis excluded by!**/*.snapcrates/cli/tests/snapshots/install__should_install_index_files.snapis excluded by!**/*.snaptasks/registry-mock/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
crates/package-manager/src/install/tests.rstasks/registry-mock/launch.mjstasks/registry-mock/package.jsontasks/registry-mock/pnpm-workspace.yamltasks/registry-mock/src/mock_instance.rstasks/registry-mock/src/node_registry_mock.rs
✅ Files skipped from review due to trivial changes (3)
- tasks/registry-mock/package.json
- tasks/registry-mock/pnpm-workspace.yaml
- tasks/registry-mock/launch.mjs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Agent
- GitHub Check: Code Coverage
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plainStringor&strto preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation viaTryFrom<String>and/orFromStrwithout providing an infallible public constructor
If upstream never validates a branded string type, expose an infallibleFrom<String>constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bareasassertion, expose afrom_str_uncheckedconstructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use#[serde(try_from = "String")]for deserialization to go through the validator and#[serde(into = "String")]for serialization
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls on branded types rather than handwritingimplblocks
Model string-literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers
Template literal types (like${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide inCODE_STYLE_GUIDE.mdcovering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Warnings are errors incargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....
Files:
crates/package-manager/src/install/tests.rstasks/registry-mock/src/node_registry_mock.rstasks/registry-mock/src/mock_instance.rs
🧠 Learnings (2)
📚 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/install/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/install/tests.rstasks/registry-mock/src/node_registry_mock.rstasks/registry-mock/src/mock_instance.rs
🔇 Additional comments (2)
tasks/registry-mock/src/node_registry_mock.rs (1)
18-24: Clean launch-command refactor.Using a preconfigured
Commandfornode <launch.mjs>keeps the call sites simple and avoids PATH-based binary lookup complexity.tasks/registry-mock/src/mock_instance.rs (1)
81-102: Prepare/spawn wiring looks solid.The updated prepare and spawn flows align well with the new
node_registry_mock()command API and keep process setup readable.
Dylint blocker + seven Copilot threads: - `crates/reporter/src/tests.rs:636` — `assert!` collapsed to a single-line trailing-comma form by `cargo fmt`, which dylint's `perfectionist::macro-trailing-comma` rejects. Remove the trailing comma. (Same conflict we hit on #418.) - `pnpm:lifecycle` events now carry the dep's `optional` flag. Previously `LifecycleMessage::Script` / `Exit` were hard-coded to `optional: false`, so an optional dep's lifecycle records diverged from upstream's `lifecycleLogger.debug({ optional, … })` payloads at `pnpm/exec/lifecycle/src/runLifecycleHook.ts:102` and `:165`. Added `RunPostinstallHooks.optional`, threaded through to both emit sites, and added a `cfg(unix)` test asserting the flag survives the spawn-and-capture round-trip. `BuildModules` reads the value from `SnapshotEntry.optional` (already in the lockfile, now in the wire too). - Tightened the `SkippedOptionalDependencyLog` doc comment to explain that `ResolutionFailure` has a distinct `package` shape upstream (`bareSpecifier`, no `id`) and will need a sibling struct / untagged enum when an emit site lands — not just a new `reason` value. Refactoring deferred until then per YAGNI. - `crates/lockfile/src/snapshot_entry/tests.rs` — `!out.contains("optional")` would falsely fire on a future fixture containing `optionalDependencies:`. Tightened to check the exact key spelling (`optional: ` or `optional:\n`). - `crates/package-manager/src/build_modules/tests.rs` — stale comment pointed at `crate::executor::lifecycle::tests` which is not a valid path from this crate. Now references `pacquet_executor::select_shell` and the executor crate's `shell::tests` module. - `tasks/registry-mock/launch.mjs` — signal-forwarding logic installed `process.on('SIGINT'|'SIGTERM'|'SIGHUP', …)` handlers that overrode Node's default termination behavior, while the child-exit handler tried to re-raise via `process.kill(process.pid, signal)` — that hits our own listener in a loop. Switched to forwarding signals to the child only, and exiting with the shell-convention `128 + signo` when the child reports it was killed by a signal. - `tasks/registry-mock/launch.mjs` — stale path `crates/tasks/registry-mock/...` → `tasks/registry-mock/...`. - `tasks/registry-mock/pnpm-workspace.yaml` — comment said "Rust calls the CLI binary" but the Rust launcher now spawns `node launch.mjs`. Updated.
The `install_optional_failing_postinstall_dep_via_registry_mock_succeeds` test came in via the merge of `main` (commit 98c9886, PR #419 landed after this branch forked) and still constructed the config with `Npmrc::new()`. The rest of `pacquet-config` is `Config` now, so CI was failing to compile `pacquet-package-manager` (lib test) on both windows-latest and macos-latest. --- Written by an agent (Claude Code, claude-opus-4-7).
Summary
Closes item #6 of #397. Optional dependencies whose
postinstallhooks fail no longer abort the install — pacquet now reports the
failure through the
pnpm:skipped-optional-dependencychannel(reason
build_failure) and continues, matchingpnpm v11's behavior at
building/during-install/src/index.ts:218-240.Three commits:
feat(lockfile): surface snapshot-level optional flag—pacquet's lockfile reader previously dropped the
snapshots[<key>].optionalfield. Adding it aspub optional: boolon
SnapshotEntrywith#[serde(default, skip_serializing_if = "is_false")]so absent ↔ false and
falsenever serializes. The flag ispre-computed by pnpm's resolver at install time (ALL-paths-
optional fold), so pacquet trusts the precomputed value rather
than re-deriving it — matching
lockfileToDepGraphat deps/graph-builder/src/lockfileToDepGraph.ts:315.feat(reporter): add pnpm:skipped-optional-dependency event—new
LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog)variant +
SkippedOptionalPackage+SkippedOptionalReasonenum.Wire shape mirrors pnpm's
SkippedOptionalDependencyMessage.All four upstream reasons (
BuildFailure,UnsupportedEngine,UnsupportedPlatform,ResolutionFailure) are declared eventhough only
BuildFailureis emitted today — keeps the enumclosed so the other emit sites can land without widening it.
feat(package-manager): swallow optional-dep build failures—BuildModules.runreadssnapshot.optional. Whenrun_postinstall_hooksreturns anErr, the dep's optionalflag decides: optional ⇒ emit a
pnpm:skipped-optional-dependencyevent (
reason: build_failure,package.id= the dep's pkg dirto match upstream's
depNode.dir) and continue; non-optional ⇒propagate as
BuildModulesError::LifecycleScriptso the installaborts. Two
cfg(unix)-gated tests cover both branches.Test plan
just ready(478 tests + clippy clean)taplo format --check(clean —just readydoesn't run this, CI does)Out of scope
The
parentsfield upstream marks as TODO atbuilding/during-install/src/index.ts:227is also omitted here. The other threeSkippedOptionalReasonvariants are declared in the enum but not yet emitted —UnsupportedEngine/UnsupportedPlatformcome from pacquet's not-yet-built resolution-time platform-check phase, andResolutionFailurefrom the registry side.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Bug Fixes
Logging
Tests
Tools