Skip to content

install: stop looping on a peer dependency no published version satisfies - #38851

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/419162cf/unmet-peer-no-matching-version
Aug 15, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/419162cf/unmet-peer-no-matching-version

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A package declares a required peerDependencies entry whose package exists in the registry but whose range no published version satisfies, and nothing else in the tree provides it. Registry: a@2.1.0 with peerDependencies: { b: "^1.0.1" }, only b@2.0.1 published; project depends on a. (Real-world shape, #26046: rolldown-plugin-solid-oxc's peer solid-jsx-oxc@*, whose only versions are prereleases.)

  • bun install exits 0 with no diagnostic and leaves the peer edge unresolved.
  • The bun.lock it writes does not load back: bun install --frozen-lockfile / bun ci / bun pm ls fail with error: Failed to resolve peer dependency 'b' for package 'a', InvalidLockfile: failed to parse lockfile, then lockfile had changes, but lockfile is frozen. A peer declared by the root or a workspace package.json fails the same way (Failed to resolve root peer dependency 'b').
  • The next plain bun install (which ignores the unparseable lockfile and re-resolves), or any install without a lockfile, never returns while b's manifest is still fresh in the manifest cache: 100% CPU in wait_for_resolution -> process_peer_dependency_list -> enqueue_dependency_with_main -> get_or_put_resolved_package.
  • Cause of the loop: in get_or_put_resolved_package (src/install/PackageManager/PackageManagerEnqueue.rs, FindVersionError::NotFound path) a peer returned Ok(None) even during the peer pass (install_peer). The caller, enqueue_dependency_with_main_and_success_fn, takes Ok(None) to mean the manifest is not loaded; in the peer pass it drops the manifest's dedupe entry, loads the still-fresh manifest from the cache and continues into the same lookup, which returns Ok(None) again. With a cold cache the same Ok(None) pushes a callback onto a manifest task that has already completed, which is why the first install is silent.
  • Cause of the unloadable lockfile: the three dependency loops in parse_into_binary_lockfile (src/install/lockfile/bun.lock.rs) only tolerate an unbound edge that carries the OPTIONAL bit, while the resolver can leave a required peer unbound and the writer records such an edge without a package entry.
  • Not new to the Rust port: 1.3.14 spins on the same repro with either linker.

Fix

  • get_or_put_resolved_package defers with Ok(None) only when !install_peer, the condition get_or_put_resolved_package_with_find_result already uses for the same deferral. In the peer pass it returns NoMatchingVersion / DistTagNotFound like a regular dependency, so the loop ends.
  • The caller turns those two errors into a warning for peers (warn: No version matching "^1.0.1" found for peer dependency "b" (but package exists)) and leaves the edge unresolved. Failing the install instead would change established behavior: verify_resolutions deliberately accepts unresolved peers, test/cli/install/bun-install.test.ts ("should handle installing the same peerDependency with the same version") pins exit 0 for a root peer with no matching version, and catalogs.test.ts documents the same policy. A peer whose package 404s still errors as before.
  • The bun.lock parser accepts an unbound peer edge the way it accepts an unbound optional edge (may_stay_unresolved, at the root, workspace and package sites). Nothing else is needed: the hoisted tree, the isolated linker, Lockfile::eql and verify_resolutions already skip unresolved peers, and the writer already emits the declaration. The loaded state equals the freshly resolved state, so --frozen-lockfile passes and a reload writes identical bytes, including for root and workspace peers. Recording such peers as optionalPeers instead (what the lockb and pnpm migrations do) would not be stable for those: Package::Diff compares behavior bits, so package.json's plain peer edge would be re-resolved on every install and Lockfile::eql would then see a different tree.
  • Overlaps with the parser part of install: resolve peers provided by file: dependencies #33156 (its third item is this lockfile symptom); that PR is about file: peers and does not touch the loop or the warning.
  • Verified with test/cli/install/bun-lock.test.ts, "peer no published version satisfies", for both linkers: the install warns, peer-target is not installed, the lockfile matches an inline snapshot, --frozen-lockfile passes and leaves it unchanged, and a second resolve with both manifests cached (the test registry sees zero requests) finishes and writes the same bytes; plus a root + workspace variant. Without the fix each step fails on its own: no warning; the InvalidLockfile errors above; the re-resolve is killed by the spawn timeout (exit 143).
  • Also ran bun-install-registry (242 pass), bun-lock, bun-install, isolated-install, catalogs, nested-overrides, frozen-lockfile-pruned, bun-lockb and migration/ locally. The only failures need public network access (bitbucket/gitlab/badssl), which this environment does not have.

Fixes #26046

Background

  • Peer resolution has two passes. While regular edges resolve, every peer edge is parked in peer_dependencies; wait_for_resolution then drains that queue with install_peer = true. In that pass a peer is first bound to a same-named package already in the tree (a satisfying one, or any one with the "incorrect peer dependency" warning) and otherwise installed from the registry. Ok(None) from get_or_put_resolved_package means "park it" in the first pass and "manifest not loaded yet" in the second, so in the second pass only a genuinely unloaded manifest may produce it.
  • The manifest cache keeps registry responses on disk together with their max-age; inside that window bun resolves from the cache without creating a network task, so the whole peer pass runs synchronously. That is why the loop needs a warm cache and why the test registry sends cache-control: max-age=300, as registry.npmjs.org does.
  • bun.lock stores each package's declared dependency groups plus the resolved packages keyed by tree path. On load every declared edge is bound to a package entry; an edge that cannot be bound is a parse error unless it is allowed to stay unbound, which until now meant the OPTIONAL bit (optional dependencies and peerDependenciesMeta optional peers).

no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-lock.test.ts

…fies

In the peer pass, get_or_put_resolved_package returned Ok(None) for a peer
whose manifest has no version in range. The caller reads Ok(None) as "manifest
not loaded", reloads it from the manifest cache and retries, so an install with
the manifest cached never returned; with a cold cache the edge was silently
left unresolved instead.

Defer with Ok(None) only outside the peer pass (as the find_result variant
already does) and warn for the peer in the pass itself, leaving the edge
unresolved like other unmet peers. bun.lock parsing now accepts an unresolved
peer edge the same way it accepts an unresolved optional edge, so the lockfile
written after such an install loads again and --frozen-lockfile passes.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 19 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: 7d8214b2-72dc-410d-89c8-8edadf6376d8

📥 Commits

Reviewing files that changed from the base of the PR and between 39fb3c1 and 5e8017e.

📒 Files selected for processing (4)
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/PackageManager/PackageManagerResolution.rs
  • src/install/lockfile/bun.lock.rs
  • test/cli/install/bun-lock.test.ts

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed, waiting on CI.

Reproduced on main (1.4.0 canary) and on 1.3.14 with a two-package local registry (a@2.1.0 with peer b@^1.0.1, only b@2.0.1 published), hoisted and isolated linkers: first install silent, --frozen-lockfile rejects the written bun.lock, re-resolving with the manifests cached never returns. Same lockfile symptom as #26046.

With this branch all three go away on both linkers; the new tests in test/cli/install/bun-lock.test.ts fail on main at each step (no warning, InvalidLockfile, re-resolve killed by timeout) and pass here.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. install: resolve peers provided by file: dependencies #33156 - Also references error: Error loading lockfile: InvalidPackageInfo #26046 and makes the same three-site change in parse_into_binary_lockfile (replacing dep.behavior.contains(Behavior::OPTIONAL) with a helper that also tolerates unresolved peer edges), plus near-identical tests for writing a loadable lockfile when a peerDependency has no matching version.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate, though the overlap is real and is called out in the description. #33156 is about peers provided by file: packages; as a side item it makes the same parser change (unresolved peer edges are tolerated when loading bun.lock), which fixes the lockfile half of #26046. It does not change the FindVersionError::NotFound path in get_or_put_resolved_package (its enqueue changes are the find_peer_provider binding and the folder arm), so with that PR alone an install whose peer has no matching version still resolves forever once the manifest is cached, and still reports nothing when it is not. This PR fixes that loop and adds the warning; the parser change here is three one-line sites plus a helper, so whichever lands second rebases trivially.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 14th, 2026

❌ @robobun, your commit 5e8017e has some failures in Build #97275 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38851

That installs a local version of the PR into your bun-38851 executable, so you can run:

bun-38851 --bun

@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 the automated bug hunt found nothing. The fix looks correct and thoroughly tested, but since it changes peer-resolution semantics and widens what bun.lock accepts on load, a human look at the policy choice (warn + leave unresolved vs. error) would still be worthwhile.

What was reviewed:

  • The new && !install_peer guard matches the existing deferral at get_or_put_resolved_package_with_find_result (line 2141) — consistent.
  • warn_unmet_peer_dependency is only reached for required peers with no fail_fn, so bun add/update paths that pass a fail_fn are unaffected.
  • may_stay_unresolved widens lockfile acceptance to any PEER edge; verify_resolutions and the tree builders already skip unresolved peers, so the parser is being brought into agreement with what the writer already emits.
  • Tests cover both linkers, the cached-manifest loop, and the --frozen-lockfile round-trip for registry, root, and workspace peers.
Extended reasoning...

Overview

Four files: PackageManagerEnqueue.rs (the loop fix + new warning helper), PackageManagerResolution.rs (comment-only), bun.lock.rs (three call sites now accept unbound peer edges via may_stay_unresolved), and ~180 lines of new tests in bun-lock.test.ts.

Security risks

None identified. The change loosens what bun.lock accepts on load (any PEER-flagged edge may lack a package entry, not just OPTIONAL ones), but this only affects whether the parser rejects the file — it does not cause anything extra to be fetched or installed. The unresolved edge stays unresolved after load, as it does after a fresh resolve.

Level of scrutiny

Medium-high. The install resolver is a dense, stateful subsystem where Ok(None) carries different meanings across passes, and the fix hinges on that distinction. The core one-line change (&& !install_peer) is well-justified by the existing precedent at line 2141 in get_or_put_resolved_package_with_find_result, and the PR description traces the loop mechanism precisely. Still, this is a behavioral/policy change to how required-but-unsatisfiable peers are handled (warn vs. error), and it changes lockfile-load acceptance. That is the kind of decision a maintainer should sign off on rather than an automated approval.

Other factors

The PR description is unusually thorough — it explains why the alternative of recording the peer as optionalPeers would not be stable under Package::Diff, why erroring would break existing pinned tests, and lists the full local test matrix run. Tests are strong: inline snapshots of the lockfile, request-count assertions proving the cache path is exercised, a spawn timeout that would catch the loop regressing, and both-linker coverage. No CODEOWNERS apply to the touched paths. Deferring only because the semantics change is not mechanical.

The CI runner sets BUN_INSTALL_CACHE_DIR per test file, which takes precedence
over the bunfig cache setting, so the concurrent hoisted and isolated variants
shared one cache and the second one never requested the tarball.
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated
Comment thread src/install/PackageManager/PackageManagerResolution.rs Outdated
Comment thread src/install/lockfile/bun.lock.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 PR and didn't find any bugs. The fix is well-reasoned and the tests are thorough, but it changes install semantics in a way that warrants a human look — the lockfile parser now tolerates any unresolved peer edge (not just optional ones), and the peer-pass error path now surfaces a new user-facing warning.

What was reviewed

  • Traced the !install_peer guard in get_or_put_resolved_package — it mirrors the existing deferral at line 2138 in get_or_put_resolved_package_with_find_result, and the fall-through correctly returns NoMatchingVersion/DistTagNotFound so the caller stops retrying.
  • Checked Behavior::is_required() — it returns true for both required and optional peers (!OPTIONAL || PEER), so optional peers with no matching version will also hit warn_unmet_peer_dependency; that reads as intentional but is a broader surface than the PR title implies.
  • Verified may_stay_unresolved covers exactly the three call sites and that intersects(OPTIONAL | PEER) is the right predicate for the writer's output.
  • Tests: hermetic in-process registry with cache-control: max-age=300, per-project cache dir, both linkers, root/workspace variant, and a re-resolve step that would have looped without the fix.
Extended reasoning...

Overview

This PR fixes an infinite loop and an unloadable bun.lock when a required peerDependencies entry names a package that exists in the registry but whose range no published version satisfies (issue #26046). Three source changes:

  1. PackageManagerEnqueue.rs — get_or_put_resolved_package now only defers with Ok(None) on FindVersionError::NotFound when !install_peer; in the peer pass it returns the error instead, ending the retry loop. The caller adds a peer-specific arm that warns instead of erroring for NoMatchingVersion/DistTagNotFound, plus a small #[cold] helper for the warning.
  2. bun.lock.rs — three sites in parse_into_binary_lockfile replace dep.behavior.contains(Behavior::OPTIONAL) with a shared may_stay_unresolved(dep) that also accepts unbound PEER edges.
  3. PackageManagerResolution.rs — comment-only, replacing a TODO that said unresolved required peers should error with a pointer to the new warning.

Tests add a two-package in-process registry (hoisted + isolated linkers) that pins the warning text, an inline lockfile snapshot, a --frozen-lockfile round-trip, and a re-resolve with a warm manifest cache and zero registry requests.

Security risks

None identified. This is package-manager resolution and lockfile-parsing logic; no untrusted-input parsing, auth, or filesystem-path handling changes. The lockfile parser is loosened (unbound required-peer edges are now accepted), but the writer already emits lockfiles in that shape, and the edge stays unresolved rather than resolving to anything attacker-controlled.

Level of scrutiny

High. bun install is production-critical and this touches core peer-resolution flow, the meaning of Ok(None) from get_or_put_resolved_package in the peer pass, and lockfile-parsing tolerance. The change is small in line count but semantically significant: it introduces a new user-facing warning, loosens what the lockfile parser accepts, and interacts with the manifest cache. It also overlaps with open PR #33156 on the parser change, so a maintainer should decide sequencing.

Other factors

  • The !install_peer guard directly mirrors the pre-existing deferral in get_or_put_resolved_package_with_find_result (line 2138), which supports the claim that the two functions were meant to agree here.
  • Behavior::is_required() is !is_optional() = !OPTIONAL || PEER, so optional peers also enter the new warning branch. That is probably fine (it is a warning, not an error) but is broader than the description states.
  • The tests are hermetic and follow harness conventions (tempDir, port: 0, per-project BUN_INSTALL_CACHE_DIR, spawn timeout as a hang guard, test.concurrent, both linkers via describe.each).
  • The comment-cop bot flagged four long comments; the author cut them in 5e8017e and those threads are resolved.
  • CI (#97275) was still building at the last timeline update; no green result is recorded yet.

Given the semantic reach into install behavior and the overlap with #33156, this should get a human maintainer's eyes before merging.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

One correction to the review above: optional peers cannot reach the new warning. enqueue_dependency_with_main_and_success_fn returns before resolving anything when behavior.is_optional_peer() (the first statement of the function), and both get_or_put_resolved_package call sites are inside it, so the is_peer() arm in the error handling and warn_unmet_peer_dependency only ever see required peers. Optional peers are still bound by the tree builder or left unresolved silently, exactly as before.

@Jarred-Sumner
Jarred-Sumner merged commit d1d256c into main Aug 15, 2026
8 of 9 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/419162cf/unmet-peer-no-matching-version branch August 15, 2026 07:25
robobun added a commit that referenced this pull request Aug 15, 2026
…g duplicate package paths

Squash of the reviewed commits, rebased on main. The bun.lock parser part
of the original change landed separately in #38851 and is no longer in
this diff; see the PR description for the full problem, cause and fix
write-up.
robobun added a commit that referenced this pull request Aug 15, 2026
…g duplicate package paths

Squash of the reviewed commits, rebased on main. The bun.lock parser part
of the original change landed separately in #38851 and is no longer in
this diff; see the PR description for the full problem, cause and fix
write-up.
robobun added a commit that referenced this pull request Aug 22, 2026
…g duplicate package paths

Squash of the reviewed commits, rebased on main. The bun.lock parser part
of the original change landed separately in #38851 and is no longer in
this diff. A self-contained workspace (#40014) is a hoisting barrier, so a
package in its dependency closure does not take a root folder as its peer
provider. See the PR description for the full write-up.
robobun added a commit that referenced this pull request Aug 22, 2026
…g duplicate package paths

Squash of the reviewed commits, rebased on main. The bun.lock parser part
of the original change landed separately in #38851 and is no longer in
this diff. A self-contained workspace (#40014) is a hoisting barrier, so a
package in its dependency closure does not take a root folder as its peer
provider. Peers a root folder could satisfy are decided only once the
dependency graph is complete. See the PR description for the full
write-up.
@robobun

robobun commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

The flaky "declared by a registry package" case this PR added is fixed in #40377 (a temporary file name collision in the manifest cache on macOS and Windows, plus the warm-cache setup from #39190).

Jarred-Sumner pushed a commit that referenced this pull request Aug 25, 2026
### Problem
- `test/cli/install/bun-lock.test.ts` > "peer no published version
satisfies" > "declared by a registry package" (from #38851) fails on
main about every fourth build (build 104990).
`expect(registry.requests).toEqual([])` at bun-lock.test.ts:1778
receives `["/peer-target"]`: the manifest cache has no usable entry for
`peer-target`. It has two causes. #40073 covers the first, the
`save_async` exit race. This PR fixes the second.
- `Serializer::save` (src/install/npm.rs:1352) named the temporary file
`<name hash>.npm-<milliseconds>` in the temporary directory every bun
process shares. The four concurrent peer tests all save `peer-target`,
and two saves in one millisecond opened one file. Windows, release
build, four installs saving one package at once: 77 of 80 lost their
entry. Linux uses `O_TMPFILE` today; #38916 starts using the name on
Linux too, so this fix has to land first.

### Fix
- `Serializer::save` names the file with `FileSystem::tmpname`, as
`TarballStream` already does. Correct because the name no longer depends
on anything two processes share.
- Two tests in `bun-install-registry.test.ts`: four synchronized
installs keep their own valid entries, and an install still caches its
manifest when directories occupy the old temporary names. The second
fails 3 of 3 on Windows without the fix and passes 5 of 5 with it. The
test registry serves the tarball only after the cache entry exists, so
neither test depends on the exit race.
- Verified: `bun bd test test/cli/install/bun-install-registry.test.ts`
(250 pass) on Linux, both new tests on Windows.

### Background
- Manifest cache: each fetched manifest is serialized to `<cache
dir>/<name hash>-<registry hash>.npm`. Within the response's `max-age`,
a later install resolves from that file and makes no request.
- `write_file` opens the temporary file with `O_CREAT | O_TRUNC`,
writes, then renames it into the cache. Two processes on one name write
one file. On macOS the second rename fails and the first cache holds the
second's bytes, which `load_by_file` rejects. On Windows `rename_at_w`
moves by handle, so the second rename moves the file out of the first
cache again.
- The exit race: `save_async` writes the entry from a thread pool task
that `bun install` does not wait for, by design (#37203). The test
change in #39190 works around it for the bun-lock test. The harness flag
in #40073 removes it from every install test.

<details><summary>Notes</summary>

Probe (Windows, 16 vCPUs): one registry and one project per install,
`dependencies: { "peer-target": "2.0.1" }`, each registry holds its
manifest response until all four have been asked, then all respond at
once. After exit, the `.npm` entry is read and checked for the install's
own registry origin.

| binary | temp dir | rounds | ok | missing | wrong registry |
| --- | --- | --- | --- | --- | --- |
| 1.4.1-canary.1 (release, unfixed) | shared `%TEMP%` | 20 | 3 | 60 | 17
|
| 1.4.1-canary.1 (release, unfixed) | shared, responses not synchronized
| 40 | 135 | 17 | 8 |
| 1.4.1-canary.1 (release, unfixed) | one per install | 40 | 160 | 0 | 0
|
| debug, unfixed | shared | 20 | 74 | 3 | 3 |
| debug, this branch | shared | 20 | 80 | 0 | 0 |

The unfixed debug build loses far fewer entries than the release build
because its slower parse spreads the four saves over several
milliseconds. That is also why the concurrent test detects the bug
almost always on release lanes and only sometimes on a debug build. The
directory test fails deterministically on a debug build (3 of 3 and, in
an earlier shape of the test, 5 of 5 on Windows).

Windows mechanism in detail: `open_file_at_windows` maps `O_CREAT |
O_TRUNC` to `FILE_OVERWRITE_IF` with `FILE_SHARE_READ | WRITE | DELETE`,
so every process opens and truncates the same file. `rename_at_w`
(src/sys/windows/mod.rs:1933) opens the source by path and then moves it
by handle. When two processes have opened the path before either moves
it, both moves succeed: the second one relocates the file out of the
first process's cache. That process sees a successful save and has no
entry, which is the "no entry, no error" case in the probe.

`FileSystem::tmpname` (src/resolver/lib.rs:198) formats `.<random ^
nanoseconds>-<counter>.<ext>`. The temporary directory probe in
`PackageManagerDirectories.rs` and `TarballStream::open_destination` use
it the same way.

Test registry and the exit race: both new tests read the cache after the
install exits. To keep them independent of the `save_async` exit race,
the registry answers the tarball request only once a `.npm` entry exists
in the project's cache (polling with a 5 second deadline, after which it
answers anyway so a lost write ends as a failed assertion instead of a
hang). The tarball is requested after the manifest is parsed, so the
install cannot finish before its entry is on disk.

Sequencing: #38916 changes the Linux `O_TMPFILE` path to link the file
into the temporary directory under `tmp_path` before renaming it over an
existing entry, which makes the name load-bearing on Linux. This PR
should land before it. #39190 (the bun-lock warm-up loop) and #40073
(harness flag) handle the exit race; an earlier shape of this PR carried
#39190's commit, which the self-review flagged as a duplicate of an open
PR, so it was dropped.

Gate: the fixed code path is not reachable on Linux today (`O_TMPFILE`),
so the new tests pass on Linux with and without the fix. The fail-before
proof is the Windows run above.

Also run: `cargo clippy -p bun_install`, clean.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/cli/install/bun-install-registry.test.ts,
test/cli/install/bun-lock.test.ts

<!-- robobun:evidence:end -->
robobun added a commit that referenced this pull request Aug 27, 2026
Squash of the reviewed commits, rebased on main. Parts of the original
change landed separately while it was in review and are no longer in
this diff: the bun.lock parser change (#38851) and the tree dedupe of a
peer bound to a folder package (#40564). A self-contained workspace
(#40014) is a hoisting barrier, so a package in its dependency closure
does not take a root folder as its peer provider. Peers a root folder
could satisfy are decided only once the dependency graph is complete.
See the PR description for the full write-up.
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.

error: Error loading lockfile: InvalidPackageInfo

2 participants