Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.
This repository was archived by the owner on May 14, 2026. It is now read-only.

Proper optionalDependencies support — umbrella #434

@zkochan

Description

@zkochan

Proper optionalDependencies support — umbrella

Tracks everything pacquet needs to do to match pnpm's optional-dependencies behavior end to end. Pacquet already handles build-failure swallowing (#419) and parses optional: true on lockfile snapshots; everything else is missing or only partially wired.

Upstream references are pinned to pnpm/pnpm main at 94240bc046.

This is an umbrella, not a single PR. The slices in Workstream are independently shippable. Existing issue #266 covers the "no os/cpu/libc filter" slice and is folded in below as Slice 1.

Goals

  1. Correctness. A pacquet install on host X must not install, link, or build a package that pnpm would skip on host X — whether the skip reason is engine mismatch, platform mismatch, fetch failure, build failure, or --no-optional.
  2. Wire parity. Every pnpm:skipped-optional-dependency event pacquet emits has the same channel name, payload shape, and reason value @pnpm/cli.default-reporter already parses. Same goes for .modules.yaml.skipped and the wanted-vs-current lockfile split.
  3. pnpm-workspace.yaml/.npmrc parity. supportedArchitectures, --no-optional / --omit=optional, ignoredOptionalDependencies all behave identically.

Non-goals

  • No re-architecting of the install pipeline. Pacquet still consumes a lockfile rather than resolving from manifests, so the resolver-side hooks pnpm has (resolution_failure, wantedDependency.optional plumbed into ctx.resolve) only land if/when pacquet grows a resolver. The fetch-failure and build-failure swallow paths are the lockfile-driven analogues.
  • No changes to wanted-lockfile format. Pacquet keeps round-tripping every snapshot byte-for-byte; the platform-filtered view is only used to drive install decisions and to write the current lockfile.

Upstream surface

Every claim cites a permalink at 94240bc046.

1. Installability check (engine + platform)

2. supportedArchitectures

3. .modules.yaml.skipped

4. Fetch-failure swallow

5. optional: true propagation

6. --no-optional / --omit=optional

7. Build-failure swallow (already shipped — #419)

8. Resolver receives optional: true

9. pnpm:skipped-optional-dependency reason taxonomy

reason emit site
unsupported_engine https://github.com/pnpm/pnpm/blob/94240bc046/config/package-is-installable/src/index.ts#L49-L60
unsupported_platform https://github.com/pnpm/pnpm/blob/94240bc046/config/package-is-installable/src/index.ts#L57
resolution_failure https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-resolver/src/resolveDependencies.ts#L1376-L1383
build_failure https://github.com/pnpm/pnpm/blob/94240bc046/building/during-install/src/index.ts#L228-L237 and https://github.com/pnpm/pnpm/blob/94240bc046/building/after-install/src/index.ts#L426-L435

Payload union: https://github.com/pnpm/pnpm/blob/94240bc046/core/core-loggers/src/skippedOptionalDependencyLogger.ts#L10-L31. Note resolution_failure uses bareSpecifier instead of id.

10. Current vs wanted lockfile

11. ignoredOptionalDependencies

Separate from --no-optional. Workspace setting; implemented as a readPackage hook that mutates manifests in place, recorded in the lockfile, triggers needsFullResolution when changed.

Pacquet's current state

Concrete verdict per area, every "have" backed by file:line. (Audit done against optional-deps branch tip at the time of writing.)

# Surface Verdict Evidence
1 Installability check Missing No installable / check_platform / check_engine helper exists. PackageMetadata.cpu/.os/.libc/.engines are parsed (crates/lockfile/src/package_metadata.rs:17-23) but have zero consumers anywhere under crates/package-manager/.
2 supportedArchitectures config + CLI Missing No supported_architectures field on Config; no --cpu/--os/--libc flag definitions under crates/cli/src/cli_args/.
3 optional propagation through resolution Not applicable yet Pacquet has no resolver; the lockfile's pre-computed optional: true is the only entry point. PackageManifest::dependencies at crates/package-manifest/src/lib.rs:146-158 drops any optional bit at the importer iteration seam. Re-visit if/when a resolver lands.
4 optional: true parsed + consumed Partial Parsed at crates/lockfile/src/snapshot_entry.rs:43-44 with round-trip tests. Read at crates/package-manager/src/build_modules.rs:585. Not consumed by create_virtual_store.rs, deps_graph.rs, symlink_direct_dependencies.rs, link_bins.rs.
5 .modules.yaml.skipped Partial Field declared at crates/modules-yaml/src/lib.rs:196-197 with sort-on-write at :407. Never written non-empty (build_modules_manifest leaves it ..Default::default()crates/package-manager/src/install.rs:255-258 explicitly notes the gap). No read site anywhere.
6 Skip enforcement at install time Missing create_virtual_store.rs:270-281 walks every snapshot. deps_graph.rs:143-158 chains dependencies + optional_dependencies unconditionally. build_sequence.rs:91-118 iterates every group with no IncludedDependencies / Skipped filter.
7 Fetch-failure swallow for optional Missing No optional-aware error handling in crates/tarball/ or crates/registry/; create_virtual_store.rs has zero references to optional outside doc comments.
7b Build-failure swallow for optional Have crates/package-manager/src/build_modules.rs:628-651 — emits SkippedOptionalDependency { reason: BuildFailure, .. } and continues. Test: crates/package-manager/src/build_modules/tests.rs:459.
8 Current lockfile (node_modules/.pnpm/lock.yaml) Missing install.rs:125-135 hard-codes current_lockfile_exists: false with a TODO at :128. No filterLockfileByImportersAndEngine analog.
9 --no-optional / include.optionalDependencies Partial — only labels Modules.included written correctly from DependencyGroup set (install.rs:86-91). Direct-dep symlink emits respect the set (symlink_direct_dependencies.rs:91-101). Not plumbed into BuildModules, CreateVirtualStore, or build_sequence::collect_root_dep_paths. pacquet install --no-optional still builds and stores the optional subtree of the lockfile.
10 ignoredOptionalDependencies Missing No references in the workspace.

Reporter / wire shape

crates/reporter/src/lib.rs:

  • Channel name pnpm:skipped-optional-dependency at :163-164.
  • SkippedOptionalDependencyLog { level, details, package, prefix, reason } at :553-561; SkippedOptionalPackage { id, name, version } at :567-572.
  • SkippedOptionalReason enumerates all four upstream variants — BuildFailure, UnsupportedEngine, UnsupportedPlatform, ResolutionFailure — at :578-585.
  • Doc at :542-549 flags that ResolutionFailure carries a different payload shape upstream ({ name?, version?, bareSpecifier }, no id). Wiring it will need either a sibling payload struct or #[serde(untagged)].
  • Wire-shape tests at crates/reporter/src/tests.rs:586, :618, :642.

Tests already ported

  • crates/package-manager/src/build_modules/tests.rs:459do_not_fail_on_optional_dep_with_failing_postinstall (ports optionalDependencies.ts:563-572).
  • crates/package-manager/src/install/tests.rs:670 — registry-mock integration covering optional postinstall failure.
  • crates/executor/src/lifecycle/tests.rs:131lifecycle_events_carry_optional_flag.
  • crates/reporter/src/tests.rs:586, :618, :642 — reporter wire-shape coverage.

No known_failures modules under crates/package-manager/ or crates/cli/tests/ for engine/platform check, fetch-failure swallow, current-lockfile write, --no-optional filter, or ignoredOptionalDependencies.

Workstream

Slices are roughly ordered by what unlocks what. Each is independently shippable.

Slice 1 — Platform/engine installability + skip enforcement (covers #266)

Foundational. Without this every other slice's tests are easy to write but easy to get wrong, because the input is "what would pnpm install on host X" and there's no answer.

Scope:

  • New crate (or module in pacquet-config) for check_platform / check_engine / package_is_installable, matching the tri-state Some(true) | Some(false) | None semantics of the TS helper.
  • Thread package_is_installable calls through the install pipeline: create_virtual_store.rs, deps_graph.rs, build_sequence.rs, build_modules.rs, symlink_direct_dependencies.rs.
  • Emit pnpm:skipped-optional-dependency with reason: UnsupportedEngine / UnsupportedPlatform at the call sites; non-optional incompatible deps surface as ERR_PNPM_UNSUPPORTED_ENGINE / ERR_PNPM_UNSUPPORTED_PLATFORM via pacquet-diagnostics.
  • Build a Skipped: HashSet<PackageKey> set during virtual-store creation, plumb into the build phase so optional+skipped children are not walked.

Tests to port (from plans/TEST_PORTING.md):

  • optionalDependencies.ts:74 skip on OS mismatch (frozen reinstall).
  • optionalDependencies.ts:143 skip on Node version mismatch.
  • optionalDependencies.ts:283 optional sub-dep skipped.
  • optionalDependencies.ts:344 optional sub-dep of newly added optional is skipped.
  • optionalDependencies.ts:359 only the optional-only dep is skipped (optional/non-optional overlap).
  • optionalDependencies.ts:540 do not fail on unsupported dep of optional dep.
  • optionalDependencies.ts:552 fail on unsupported dep of non-optional dep.
  • optionalDependencies.ts:703 complex shared-optional dep scenario.

Slice 2 — supportedArchitectures config + --cpu / --os / --libc

Depends on Slice 1. The installability helper takes a "supported platforms" parameter; Slice 1 lands it computed from the host triple only, Slice 2 extends it from config + CLI.

Scope:

  • Config.supported_architectures: SupportedArchitectures deserialized from pnpm-workspace.yaml. Default: each list ['current'].
  • Three new CLI flags on install / add / update, multi-valued, override the config.
  • dedupe_current replaces 'current' with the host triple before passing the set down.
  • Honor 'any' and negation entries (!foo) to match checkPlatform.ts.

Tests to port:

  • optionalDependencies.ts:594 install optional dependency for the supported architecture set (parametric on nodeLinker).
  • optionalDependencies.ts:648 remove optional deps if supported architectures change.

Slice 3 — .modules.yaml.skipped write + read + headless re-check

Depends on Slice 1 (the Skipped set). Mostly plumbing.

Scope:

  • Populate Modules.skipped from the install-time Skipped set inside build_modules_manifest (install.rs:259).
  • Add a read path on subsequent installs: pacquet's frozen install loads .modules.yaml, seeds the install-time Skipped set from modules.skipped, then re-runs package_is_installable on every snapshot so the set is recomputed (covers "host arch changed since last install" — pnpm does this at lockfileToDepGraph.ts:206-215).
  • Sort-on-write already present.

Tests to port:

  • optionalDependencies.ts:74 (re-listed here for the survives-frozen-reinstall coverage).
  • optionalDependencies.ts:470 skip on OS mismatch when doing install on a subset of workspace projects.

Slice 4 — Fetch-failure swallow

Depends on the Skipped set existing (Slice 1). Independent of Slices 2–3 once that lands.

Scope:

  • Wrap each per-snapshot fetch / extract / link operation in create_virtual_store.rs and the tarball/store paths so an optional: true snapshot's failure is logged via pnpm:skipped-optional-dependency (reason: ResolutionFailure, payload shape { name?, version?, bareSpecifier } per core-loggers) and removed from the install set. A non-optional snapshot's failure still aborts.
  • Land the ResolutionFailure sibling payload in crates/reporter/src/lib.rs per the doc-comment at :542-549.

Tests to port:

  • deps-restorer/test/index.ts:340 skipping optional dependency if it cannot be fetched.

Slice 5 — --no-optional plumbing through build and store

Depends on nothing. Can ship before or in parallel with Slice 1.

Scope:

  • Thread IncludedDependencies into CreateVirtualStore and BuildModules.
  • Filter optional_dependencies out of build_sequence::collect_root_dep_paths and deps_graph::build_children when include.optional_dependencies is false.
  • Make symlink_direct_dependencies.rs (already wired) the reference for the others.

Tests to port:

  • optionalDependencies.ts:391 not installing optional deps when optional: false.
  • optionalDependencies.ts:419 optional dep has bigger priority than regular dep.
  • optionalDependencies.ts:436 only skip optional deps.
  • optionalDependencies.ts:712 dependency that is both optional and non-optional is installed, when optional deps should be skipped.
  • deps-restorer/test/index.ts:300 installing only optional deps.
  • deps-restorer/test/index.ts:323 not installing optional deps.

Slice 6 — Current-lockfile write (filterLockfileByImportersAndEngine analog)

Depends on Slices 1, 3, 5. Last because it consumes the union of the others.

Scope:

  • New module under crates/lockfile/ that takes (WantedLockfile, IncludedDependencies, Skipped, SupportedArchitectures) and produces a CurrentLockfile with the optional+skipped subtrees pruned. Mirrors the recursive walk at filterLockfileByImportersAndEngine.ts:120-202.
  • Write site under node_modules/.pacquet/lock.yaml (matching node_modules/.pnpm/lock.yaml), wired from both install and install_frozen_lockfile.
  • Flip current_lockfile_exists at install.rs:128.

Tests to port:

  • optionalDependencies.ts:213 optional sub-dep not removed from current lockfile when new dep added.
  • optionalDependencies.ts:618 remove optional deps that are not used.
  • optionalDependencies.ts:633 remove optional deps that are not used (hoisted linker).
  • optionalDependencies.ts:74 reverified that current lockfile matches wanted, with skipped tracked.

Slice 7 — ignoredOptionalDependencies

Depends on Slice 6 (because the setting goes into the current lockfile and getOutdatedLockfileSetting flags a change as needsFullResolution).

Scope:

  • Read ignoredOptionalDependencies: string[] from pnpm-workspace.yaml.
  • Apply at manifest read time: drop matching keys from optionalDependencies before any downstream consumer sees them.
  • Record on the current lockfile (and on the wanted lockfile on a non-frozen install, matching pnpm).
  • Flag a mismatch as needsFullResolution when the wanted lockfile's recorded set differs from the current config.

Tests to port:

  • (No optionalDependencies.ts tests target this directly; coverage lives elsewhere. Audit installing/deps-installer/test/ for ignoredOptionalDependencies and add to plans/TEST_PORTING.md.)

Cross-references


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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions