Skip to content

install: link wildcard deps to prerelease workspace members in any position - #37264

Open
robobun wants to merge 5 commits into
mainfrom
farm/62df9d8d/workspace-star-prerelease
Open

install: link wildcard deps to prerelease workspace members in any position#37264
robobun wants to merge 5 commits into
mainfrom
farm/62df9d8d/workspace-star-prerelease

Conversation

@robobun

@robobun robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Repro

Same member, same range, opposite outcomes depending on which package declares the dependency. No registry involvement needed; pkg-x is not a registry package.

Shape A, the root declares it:

# package.json
{ "name": "sandbox", "workspaces": ["packages/*"],
  "dependencies": { "pkg-x": "*" } }
# packages/m/package.json   { "name": "pkg-x", "version": "1.0.0-beta.1" }
$ bun install
error: GET https://registry.npmjs.org/pkg-x - 404
error: pkg-x@* failed to resolve

Shape B, another member declares it:

# package.json
{ "name": "sandbox", "workspaces": ["packages/*"] }
# packages/m/package.json     { "name": "pkg-x", "version": "1.0.0-beta.1" }
# packages/bar/package.json   { "name": "bar", "version": "1.0.0",
                                "dependencies": { "pkg-x": "*" } }
$ bun install        # links the workspace member, works offline
$ bun install
Saved lockfile       # and re-saves the unchanged lockfile on every install

npm installs both shapes by linking the workspace member.

Cause

Two hand-copied versions of the same rule ("does this npm range accept this workspace member?") drifted:

  • Package::parse_dependency (Npm arm) decided link-vs-override with Group::satisfies(range, member_version) alone. * does not satisfy 1.0.0-beta.1 under npm semver rules (a prerelease only satisfies a range that names a prerelease of the same version), so a root-declared wildcard took the override branch: the member's workspace dependency is displaced and the name resolves from the registry.
  • get_or_put_resolved_package additionally accepts npm_group.is_star() for any present member (the Fix workspace packages not being found when they are moved #10899 rule), so a member-declared wildcard resolves to the workspace.

The disagreement also produced shape B's permanent re-save: resolution links the member and the bun.lock loader rewrites the loaded dependency to workspace-tagged (map_dep_to_pkg), but a fresh parse keeps it npm-tagged, so the loaded and re-parsed roots never compare equal and every install re-saves identical bytes (#37249 fixed the versionless flavor of this loop and listed this prerelease flavor as out of scope).

Fix

Extract the rule into one shared predicate, dependency::npm_range_accepts_workspace_member (satisfies, or wildcard on any present member), and call it from both sites. Resolution behavior is unchanged (the helper is the resolver's exact expression); parse gains the wildcard disjunct, which is the fix. The versionless case is unchanged: the Npm arm still requires a member version, and wildcard-on-versionless stays with the resolver per #10899.

A wildcard therefore links the member wherever it is declared, matching npm's resolution for this shape, and the only registry fallback left for a member name is a non-wildcard range the member's version does not satisfy, which is bun's existing, deliberate divergence from npm and is untouched here (npm links the member for any range; bun's override rule lets e.g. ^2.0.0 fetch a registry copy of a name that is also a member, and several existing tests depend on that).

No loader change is needed: parse now produces the same workspace-tagged dependency map_dep_to_pkg produces on load, so lockfiles round-trip byte-stable and shape B's re-save loop stops. Lockfiles written by current bun for shape B load without a diff.

Landing order: after #37248

Review of the first revision probed mixed declarations, where one package declares a range the member fails and another declares *, and found this PR must land after #37248 (same-name collision fixes). Details, all verified against a local registry:

#37249's loader gate mirrors the parse rule for versioned members and gains the same wildcard disjunct when both land; with the shared predicate that is a call-site change. The dist-tag flavor is #35468.

Verification

Three tests in test/cli/install/bun-workspaces.test.ts (member no-deps@3.0.0-beta.1 shadowing registry no-deps, latest 2.0.0):

  • root-declared *: resolves to no-deps@workspace:packages/no-deps, not registry 2.0.0; repeat install does not re-save; --frozen-lockfile passes
  • member-declared *: same resolution, and the second install no longer prints "Saved lockfile" (lockfile byte-identical)
  • boundary guard: root-declared ^1.0.0 against the prerelease member still installs registry 1.1.0 (passes on an unmodified build by design)

The first two fail on an unmodified build (registry 2.0.0 wins; "Saved lockfile" on the second install).

Locally green: bun-workspaces (66), bun-install-registry (229 + 5 todo), isolated-install (62), bun-lock (17), bun-lockb (6), lockfile-only, lockfile-version-2 (10), migration/migrate (21), migrate-bun-lockb-v2 (2), bun-add (54). Checked by hand: both shapes install offline with no registry contact, and a package-lock.json written by npm 11 migrates to the workspace link and converges with a stable lockfile in both shapes.

…sition

A '*' range on a name that is also a workspace member resolved
differently depending on who declared it: declared by another member it
linked the workspace, declared by the root it went to the registry,
because Package::parse_dependency decided link-vs-override with
Group::satisfies alone while get_or_put_resolved_package also accepts
is_star for members. Prerelease member versions fail satisfies under
npm semver rules, so the two predicates disagreed exactly there.

Mirror the resolver's rule in the parse predicate. This also stops the
per-install lockfile re-save for member-declared wildcards on prerelease
members: parse now produces the same workspace-tagged dependency the
bun.lock loader produces, so the loaded and fresh roots compare equal.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 506dc096-294a-4d8b-b4e3-0ff45ddf86da

📥 Commits

Reviewing files that changed from the base of the PR and between 0632b98 and d42ecd9.

📒 Files selected for processing (1)
  • src/install/dependency.rs

Walkthrough

The installer now links wildcard dependencies to prerelease workspace packages. Shared matching logic is used during package resolution and lockfile validation. Tests cover root and member dependencies, repeated installs, frozen lockfiles, and incompatible non-wildcard ranges.

Changes

Workspace prerelease matching

Layer / File(s) Summary
Wildcard workspace compatibility
src/install/dependency.rs, src/install/PackageManager/PackageManagerEnqueue.rs, src/install/lockfile/Package.rs
A shared predicate accepts semver-compatible workspace versions and wildcard ranges when a workspace path exists. Package resolution and lockfile validation use the predicate.
Install and lockfile validation
test/cli/install/bun-workspaces.test.ts
Tests verify prerelease workspace linking from root and member dependencies, repeated-install stability, frozen-lockfile support, and registry resolution for incompatible non-wildcard ranges.

Possibly related PRs

  • oven-sh/bun#37248: Both changes update workspace dependency acceptance in src/install/lockfile/Package.rs.
  • oven-sh/bun#37249: Both changes address wildcard workspace resolution and lockfile stability.
  • oven-sh/bun#36704: Both changes update workspace dependency resolution and workspace-install tests.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: linking wildcard dependencies to prerelease workspace members in any declaration position.
Description check ✅ Passed The description explains the problem, cause, fix, landing order, verification steps, and test results in sufficient detail.

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

Comment thread src/install/lockfile/Package.rs Outdated

@claude claude 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.

I reviewed this and didn't find any bugs. The one-line predicate change correctly mirrors get_or_put_resolved_package and the tests are thorough, but since this shifts install resolution behavior for prerelease workspace members and needs to be sequenced with #37248/#37249 on the same arm, a human look would still be worthwhile.

Checked: the is_star() disjunct matches PackageManagerEnqueue.rs:2298 exactly; link_workspace_packages = false still takes the override branch (condition short-circuits before satisfies); workspace_path.unwrap() remains sound since this arm is inside if let Some(workspace_version). Tests cover both declaration positions, lockfile round-trip stability, --frozen-lockfile, and the non-wildcard boundary.

Extended reasoning...

Overview

One-line logic change in src/install/lockfile/Package.rs (parse_dependency, Npm arm): the satisfies check gains || npm_version.is_star(), so a * range accepts a workspace member with a prerelease version at parse time. This aligns the parse-time link-vs-override decision with the resolve-time decision in get_or_put_resolved_package (PackageManagerEnqueue.rs:2292-2298), which already had workspace_path.is_some() && npm_group.is_star(). Three new tests in bun-workspaces.test.ts exercise root-declared and member-declared wildcards on a 3.0.0-beta.1 member shadowing a registry package, plus a boundary guard for ^1.0.0.

Security risks

None. No untrusted input parsing, no filesystem/network changes; this only adjusts which of two existing branches (workspace-link vs registry-override) an npm-tagged dependency takes when the member version is a prerelease and the range is *.

Level of scrutiny

Medium-high. The diff is tiny and mechanically correct — I verified the mirrored predicate at PackageManagerEnqueue.rs:2298 and confirmed the else (override) branch is unchanged for link_workspace_packages = false and for non-wildcard ranges. But this is package-manager resolution: it changes which package gets installed for a real user shape (root "pkg": "*" on a prerelease member previously hit the registry, now links the workspace). That is the correct npm-matching behavior and fixes both the 404 and the perpetual-resave loop, but it is a user-visible behavioral shift.

Other factors

  • The PR description explicitly flags coordination with open PRs #37248 (restructures this same arm) and #37249 (gates the loader rewrite on this predicate). Whichever lands second needs the shared hunk reconciled — a human should confirm the intended landing order.
  • An automated comment-cop flag on the new 4-line comment is unaddressed. The comment is arguably fine (it names the mirrored function, which is the non-obvious part), but it's an outstanding bot review.
  • Tests are strong: they use the Verdaccio harness, assert lockfile contents and byte-stability across repeat installs, exercise --frozen-lockfile, and include a negative boundary guard. The PR description reports the full install test matrix green locally.

Comment thread src/install/dependency.rs Outdated
… resolve

Extract npm_range_accepts_workspace_member so Package::parse_dependency
and get_or_put_resolved_package call one predicate instead of keeping two
hand-copied expressions in sync; the drift between them was this bug.
@robobun
robobun force-pushed the farm/62df9d8d/workspace-star-prerelease branch from 0632b98 to df33bcb Compare August 9, 2026 15:28
Comment thread src/install/dependency.rs

@claude claude 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.

I reviewed this PR and didn't find any bugs. Because it changes package-manager resolution behavior (wildcard-vs-registry for prerelease workspace members) and the description states it must land after #37248, a human look would still be worthwhile to confirm the landing order and the behavior change.

What was reviewed:

  • get_or_put_resolved_package call site — the extracted predicate is byte-equivalent to the old inline expression, so resolver behavior is unchanged.
  • Package::parse_dependency Npm arm — passing has_workspace_path: true is sound because the enclosing if let Some(workspace_version) block already unwraps workspace_path unconditionally.
  • New tests follow harness conventions (per-test setupTest, Verdaccio, drain-then-exit assertions) and include a boundary guard for the non-wildcard override path.
Extended reasoning...

Overview

The PR extracts a shared predicate dependency::npm_range_accepts_workspace_member (satisfies || (has_path && is_star())) and calls it from both get_or_put_resolved_package in PackageManagerEnqueue.rs and the Npm arm of Package::parse_dependency in lockfile/Package.rs. The resolver call site is a pure refactor; the parse site gains the wildcard disjunct, so a root-declared * on a prerelease-versioned workspace member now links the workspace instead of falling through to the registry-override branch. Three tests are added to bun-workspaces.test.ts.

Security risks

None identified. The change is a narrow adjustment to which of two existing branches (link-workspace vs override-with-registry) an npm-tagged dependency takes when its name matches a workspace member. No untrusted input handling, path resolution, or network code changes.

Level of scrutiny

High. bun install resolution semantics are user-visible and load-bearing; a subtle change here can silently alter what gets installed in real monorepos. The change itself is small and well-argued, but it is a deliberate behavior change (previously, a root * dep on a prerelease member fetched from the registry; now it links the workspace), not a mechanical fix. The PR also carries an explicit landing-order constraint ("must land after #37248") because mixed root/sibling range declarations can otherwise produce a lockfile re-save loop — that coordination is a maintainer decision.

Other factors

The extracted predicate is verified equivalent to the resolver's prior inline expression (workspace_version.is_some_and(|v| range.satisfies(v, ..)) || (has_workspace_path && range.is_star())). At the parse site, has_workspace_path is hard-coded true, which is safe because the surrounding block already unwrap()s workspace_path on the accept branch. The versionless-member case is untouched (parse's Npm arm still requires Some(workspace_version)). Tests cover both root- and member-declared shapes plus a negative guard, use the Verdaccio harness and test.concurrent, and assert lockfile stability across repeat installs and --frozen-lockfile. The comment-cop bot flags on the doc comment have been resolved by the author. Given the behavior change and the cross-PR landing dependency, deferring to a human reviewer.

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the install suites and all test lanes for this diff are green on build 90974 (194/196 jobs). The two red jobs are unrelated: the darwin 26 aarch64 lane failed during runner provisioning (SSH to the tart VM rejected before any test ran, marked pre-existing), and the debian x64-asan lane has a Blob/Store leak in test/js/web/workers/worker-terminate-funnels.test.ts, reported separately as a main break. No install test failed on any lane (bun-install-registry flaked once on windows aarch64 and passed on retry).

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

One more call site for npm_range_accepts_workspace_member once this lands: #38869 adds WorkspaceLinks::links in src/install/lockfile/bun.lock.rs, which decides on load whether a range entry bound to a workspace is loaded as a workspace dependency, and it mirrors the parser's current rule with the same satisfies call. Whichever of the two lands second should switch that call to the shared predicate, otherwise a * on a prerelease member would be linked by the parser but not by the loader and report an update on every install.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant