Skip to content

fix(resolver): skip optional deps whose version pick finds no match - #604

Merged
colinhacks merged 1 commit into
mainfrom
optional-dep-nomatch-580
Jul 29, 2026
Merged

fix(resolver): skip optional deps whose version pick finds no match#604
colinhacks merged 1 commit into
mainfrom
optional-dep-nomatch-580

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

rolldown declares 15 @rolldown/binding-* packages as optionalDependencies. The version pick runs before the os/cpu filter, so a binding the host will never install could fail the entire install when the registry did not carry it. An optional dependency should never be fatal — the fetch-failure and platform-mismatch paths already skip, but the version-pick path did not.

Optional deps whose range matches no version are now skipped and recorded, with a warning. The age-gate path stays fatal: a version withheld by minimumReleaseAge is a supply-chain signal the user opted into, not a package the registry lacks.

Verification

Differential against a registry with 1.0.3 removed from one wrong-platform binding, fresh cache per leg:

released 0.6.0 this branch
registry unmodified exit 0 exit 0
version removed ERR_NUB_NO_MATCHING_VERSION, exit 20 exit 0

require('rolldown') loads after the install, so the tree is usable rather than silently incomplete. npm and pnpm both install cleanly in the same scenario.

New tests cover both directions — an optional dep skips, a required dep still fails. The optional test was confirmed to fail without this change.

Closes #580

An optional dependency whose range matched no version aborted the whole
install instead of being skipped. The fetch-failure and platform-mismatch
paths already skip optional deps; the version-pick path did not.

This surfaces on packages shipping one optional binding per platform.
rolldown declares 15 `@rolldown/binding-*` optionals, and the version pick
runs before the os/cpu filter, so a binding the host will never install can
fail the install when the registry does not carry it — a mirror holding only
the bindings its own users have pulled, typically.

The age-gate path stays fatal: a version withheld by minimumReleaseAge is a
supply-chain signal the user opted into, not a package the registry lacks.

Refs #580
Copilot AI review requested due to automatic review settings July 29, 2026 06:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview, Comment Jul 29, 2026 6:09am

Request Review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blocking issues — two informational notes inline. The skip itself does byte-identical bookkeeping to the existing platform-mismatch path.

Reviewed changes — makes a NoMatch version pick non-fatal for optional dependencies in the vendored resolver, so a registry that carries a package but not the requested version can no longer abort an install over a dep the platform filter was about to drop anyway.

  • Guarded skip arm in the version-pick loop — a new PickResult::NoMatch if task.dep_type == DepType::Optional arm in driver.rs warns, records the skip for root deps, and returns Ok(()); the generic fatal NoMatch arm below it is unchanged, so required deps still hard-fail.
  • New warning codeWARN_AUBE_SKIPPED_OPTIONAL_NO_MATCHING_VERSION registered in warnings.rs under the resolver category with no exit code.
  • Two resolver tests — one asserting an optional dep with no matching version resolves cleanly, one asserting the same shape in dependencies still errors with Error::NoMatch.

I traced the early return against every structure the rest of process_task populates. It returns before the graph node, the parent dep wiring, the importer direct-dep list, visited, resolved_versions, and the transitive enqueue, so nothing downstream can reference the skipped name. A later task for the same package as a required dep still resolves normally, since resolved_versions is never populated and sibling dedupe can't reuse it. The age-gate arm staying fatal is a deliberate, documented divergence and I did not challenge it.

ℹ️ Nitpicks

  • vendor/aube/docs/error-codes.data.json is generated from the ALL registry and wasn't regenerated for the new warning, so the vendored tree's code table is now one entry behind. Nothing in nub's CI gates it.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment on lines +1025 to +1032
if task.is_root
&& let Some(spec) = task.original_specifier.as_ref()
{
self.skipped_optional_dependencies
.entry(task.importer.clone())
.or_default()
.insert(task.name.clone(), spec.clone());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recording this skip makes it sticky for root optional deps. check_drift exempts any name in skipped_optional_dependencies whose specifier still matches, so once the registry gap is recorded the dep is never retried on later installs even after the registry gains the version. That map is documented as capturing host-stable platform skips; registry availability is transient. Note the sibling fetch-failure skip records nothing — the comment above claims alignment with both paths, but only the platform one records.

Technical details
# Registry-availability skip is recorded in a map whose consumers assume host-stable skips

## Affected sites
- `vendor/aube/crates/aube-resolver/src/resolve/driver.rs:1025-1032` — new arm records into `skipped_optional_dependencies` for root deps.
- `vendor/aube/crates/aube-resolver/src/resolve/driver.rs:720-730` — the fetch-failure skip, the closest analogue (also a registry-availability failure), deliberately records nothing and only calls `note_root_done()`.
- `vendor/aube/crates/aube-lockfile/src/drift.rs:537-546` — the map is documented as "*runtime* platform skips"; the exemption is unconditional on cause.
- `vendor/aube/crates/aube-lockfile/src/drift.rs:1858-1878``fresh_when_optional_dep_was_recorded_as_skipped` confirms drift reports `Fresh`, so the install short-circuits.
- `vendor/aube/crates/aube-lockfile/src/pnpm/write.rs:362-379` — the entry is persisted to the lockfile.

## Symptom
1. `optionalDependencies: { "foo": "1.0.3" }`, mirror lacks `1.0.3`.
2. `nub install` skips, records, writes `skippedOptionalDependencies: { foo: 1.0.3 }`.
3. Mirror later syncs `1.0.3`.
4. `nub install` reports `Fresh` and never re-resolves; `foo` stays uninstalled until the specifier changes.

Only root optional deps are affected — the motivating `rolldown` bindings are transitive, so they record nothing and self-heal.

## Required outcome
- The reader can tell from the code why a transient registry gap is recorded in a map the drift check treats as permanent, or the recording is narrowed so the skip self-heals.

## Suggested approach
The tradeoff cuts both ways and the current choice is defensible — dropping the recording would make the next `nub install --frozen-lockfile` / `nub ci` fail with "manifest adds foo", which is exactly what the platform-skip recording exists to prevent. If recording stays, tightening the arm's comment to say it matches the platform path's bookkeeping (and why it diverges from the fetch-failure sibling that records nothing) would keep the next reader from concluding all three paths behave alike.

## Open questions for the human
- Is a permanently sticky skip acceptable for a cause that can resolve itself, or should a registry-gap skip be re-probed on the next non-frozen install?

.await
.expect("an optional dep with no matching version must not fail the resolve");
assert!(graph_has_package(&graph, "p-map", "7.0.4"));
assert!(!graph_has_package(&graph, "native-binding", "1.0.3"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither new test covers the recording half of the change. With an exact 1.0.3 range this assertion can't fail for any reason other than the resolve erroring, which the expect above already catches — asserting graph.skipped_optional_dependencies["."]["native-binding"] == "1.0.3" would cover the new lockfile-visible bookkeeping instead, matching how aube-lockfile/src/pnpm/tests.rs:3254 asserts the same map.

@colinhacks
colinhacks merged commit de83876 into main Jul 29, 2026
52 checks passed
@colinhacks

Copy link
Copy Markdown
Contributor Author

Shipped in v0.7.0: https://github.com/nubjs/nub/releases/tag/v0.7.0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Attempts to resolve @rolldown/binding-darwin-arm64@1.0.3 on Linux

2 participants