install: keep a peer nothing in bun.lock satisfies where the file records it - #38892
install: keep a peer nothing in bun.lock satisfies where the file records it#38892robobun wants to merge 3 commits into
Conversation
|
Warning Review limit reached
Next review available in: 22 minutes Limit details: You’ve used all 5 included reviews currently available under your plan. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: fixed, awaiting review. Reproduced on main two ways: the new tests ( Update: #38992, which fixed the same case by binding the |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it reorders peer-edge binding in the lockfile loader and the description flags a coordination point with #38767's save-side scan, a human look would still be worthwhile.
Checked that the reorder is behavior-preserving for edges the version scan already skipped (optional peers, * peers, non-peers) — each still reaches the same first probe. Verified get_in_own_node's length-overflow None falls through to the same handling as before. Confirmed the test fixtures (strict-peer-dep, no-deps@1.0.0/1.0.1, one-dep, peer-deps-fixed, one-fixed-dep) exist in the verdaccio registry.
Extended reasoning...
Overview
This PR changes parse_into_binary_lockfile in src/install/lockfile/bun.lock.rs so that when binding a peer dependency edge, the loader first checks for an entry in the dependent's own node (<pkg_path>/<name>), then runs the version scan, then walks the printed tree. Previously the version scan ran first, which could bind the edge to a different package than the one the lockfile explicitly recorded, causing that entry to be dropped on the next save. The change is applied uniformly across the three binding loops (root, workspace, package) and adds a small PkgMap::get_in_own_node helper for the package loop. Six new tests in bun-lock.test.ts cover the four dependent shapes plus both linker modes.
Security risks
None. This is a change to the order in which existing lockfile entries are consulted when reconstructing dependency-edge bindings. No new inputs are trusted, no new syscalls or allocations are introduced, and the bounds check in get_in_own_node mirrors the one already present in find_resolution_impl.
Level of scrutiny
High. The lockfile loader determines what gets installed on disk, and peer-dependency binding is one of the subtler parts of it — the PR description itself traces how #32182, #38767, #38837 and #38853 all touch adjacent semantics. I traced each of the three loops before and after to confirm that non-peer, optional-peer, *-peer and overridden-peer edges resolve identically (each already reached the own-node probe first via the walk or via deferred_peer_range returning None), and that the only edges whose binding changes are required non-* peers that have an entry in their dependent's node — exactly the class the PR targets.
Other factors
The tests are well-constructed: hand-written lockfiles for each of the three loop shapes plus both branches of the version scan (nothing-satisfies fallback and higher-in-range-elsewhere), with a fixed-point re-save assertion and installed-tree checks for both linkers. The description is unusually thorough about mechanism and about which existing suites were re-run. What tips this to defer rather than approve is the author's own note that #38767's save-side rescan would fight this loader on the same shape and "whichever lands second" needs adjustment — that is a coordination decision for a maintainer, not something automated review should sign off on alone.
|
(Superseded after the rework: the interaction with #38767 described in the first revision no longer exists; see #38767 (comment) and the "Interaction with the open save-side work" bullet in the PR body.) |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate of #38837; the two are independent. (Edited after this PR was reworked. The original version of this comment said the branch bound every edge with a The current revision removes the helper's fallback for peers whose range nothing in the file satisfies. #38837 exempts bundled peer edges from the scan, and its test is an in-range shape (a bundled |
c561641 to
a20f2c0
Compare
…ords it resolve_peer_dep_version_based bound a required peer edge whose range no version in the lockfile satisfies to the highest version present. The fresh resolver makes that pick once, against the versions present at the time, and later installs keep the binding, so by the time a lockfile is loaded the version the edge is on is recorded only by the tree: printed next to the dependent when it conflicts with the version hoisted above, otherwise deduped onto that version. Re-picking the highest version of the final set moved the edge whenever another out-of-range version was present; the recorded copy lost its last edge and the next save dropped it, or a new nested copy appeared. The pnpm migration, which binds peers through the same helper, lost pnpm's recorded resolution the same way. Return None when nothing satisfies the range, so the loader falls back to the printed tree and the migration to pnpm's recorded version, as both already do for the other edges the helper declines to bind.
e01eef8 to
fbb385d
Compare
|
Reworked after self-review; the PR body now describes the current shape. Summary of what changed and why:
|
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/install/lockfile/bun.lock.rs:3426-3429— The PR title/description describe a broader fix than the code implements: they reference a newPkgMap::get_in_own_nodethat reorders the three binding loops to probe the dependent's own node first, claim to fix the in-range case (higher satisfying version elsewhere rebinds the edge), list a test for that shape, and claim #38837's test passes on this branch — butget_in_own_nodedoes not exist, the loops at :3091/:3173/:3244 are unchanged, and the only code change is dropping thecandidates.first()fallback soresolve_peer_dep_version_basedreturnsNonewhen nothing satisfies. Either the own-node-first commit was dropped from the branch, or the description is stale from an earlier iteration; if the latter, please narrow the title/description and drop the #38837-subsumption claim (and the follow-up comment restating it) so a reviewer doesn't close that PR or believe the in-range case is covered.Extended reasoning...
What the description says vs. what the diff does
The Fix section of the PR body states:
Each binding loop now looks the edge up in the dependent's own node first (the root's bare
<name>entry,<workspace>/<name>, and<pkg path>/<name>via the newPkgMap::get_in_own_node), then runs the version scan, then walks the printed tree.and
Bundled peer edges always have their own entry, so this also covers the shape #38837 fixes … its test passes on this branch
and lists among the verified test shapes:
a package whose entry holds a lower in-range version than one elsewhere in the file
None of that is in the diff:
grep get_in_own_node src/returns nothing — the method the description names does not exist.- The three binding loops in
parse_into_binary_lockfileatbun.lock.rs:3091,:3173,:3244are byte-identical to main: each still callsresolve_peer_dep_version_basedfirst, then falls through topkg_map.get/find_resolution. No own-node probe precedes the scan. git diff HEAD~1 --statonbun.lock.rsshows +16/-19 — that is the doc-comment rewrite plus removing thecandidates.first()fallback block. There is no loop reordering and no newPkgMapmethod.- All four new test shapes in
bun-lock.test.tsusepeerDependencies: { "no-deps": "^2.0.0" }with onlyno-deps@1.0.0/1.0.1/1.1.0present — every one exercises the nothing-satisfies case. The described in-range test (entry holds a lower satisfying version while a higher satisfying version exists elsewhere) is absent.
What the code actually fixes
The narrower change — deleting the
candidates.first()fallback soresolve_peer_dep_version_basedreturnsNonewhen no candidate satisfies the range — fixes exactly the nothing-satisfies case: the caller now falls through to the path walk, whose first probe is the<dependent>/<name>entry, so the edge stays on the version the file records. The commit title ("keep a peer nothing in bun.lock satisfies where the file records it"), the new doc-comment paragraph, thedescribetitle, and all six new tests match this narrower scope precisely, so the code and tests are internally consistent.What is not fixed but the description says is
The in-range case. When the
<dependent>/<peer>entry holds a version that satisfies the range but a higher satisfying version exists elsewhere in the file, the scan loop at:3420-3426still returnsSome(higher)before any own-node lookup runs, so the edge is still rebound off its entry. The description's Problem section explicitly names this case ("in the in-range case whenever a higher satisfying version exists elsewhere in the file") and the Fix section claims to address it via own-node-first ordering — but that ordering is not in the code.#38837 subsumption. #38837 exempts bundled peers from the scan. This branch does not: for a bundled peer whose range something in the file does satisfy, the scan still returns that version and the own-node entry is never consulted. So #38837's test would only pass here if its bundled peer's range is unsatisfied by everything in the file — the general bundled-peer case is not covered. The description (and the author's follow-up reply to the duplicate-PR bot) both claim "#38837's test passes here as well" and "this branch binds every edge with an own-node entry to it before the scan", which is not what the code does.
Step-by-step: why this matters
- Bot comments "This PR may be a duplicate of #38837"; author replies that this branch subsumes #38837's load-side fix.
- A reviewer reads the description + reply, sees "own-node-first" and "#38837's test passes here", and marks #38837 as superseded on the load side.
- This PR merges with only the nothing-satisfies fix.
- A bundled peer whose range is satisfied by something in the file, or any peer with an own-node entry holding a lower in-range version, is still rebound on load — but the merged PR's description says otherwise, so the next report of that shape gets triaged as "already fixed in #38892".
Which is it — dropped code or stale description?
The evidence points to a stale description: the commit title, doc-comment,
describetitle, and every test all consistently scope to nothing-satisfies-only. If code had been dropped you'd expect the in-range test the description lists to be present (and failing). But only the author can say for sure.Suggested action
Either:
- (a) push the missing
PkgMap::get_in_own_node+ loop-reordering commit and the in-range test shape the description lists, or - (b) narrow the PR title to match the commit title, cut the own-node-first paragraph, the in-range test-shape claim, and the #38837-subsumption claim from the description, and edit the reply to the duplicate-PR bot so nobody closes #38837 on the strength of it.
CLAUDE.md rule 11: "NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user."
|
The review above compared the reworked code against the description of the first revision; it is right that the two did not match at that moment. Current state: the title and body were rewritten for the reworked code (the body's "Earlier revision" section records what was dropped and why), and the earlier reply to the duplicate-PR bot has been edited: this PR does not cover #38837's case (its test is an in-range shape, which the scan here still rebinds), so #38837 stays independent and should not be closed on the strength of this one. The in-range own-entry case is intentionally out of scope here, as the body now says. The code is as the review describes: the only |
There was a problem hiding this comment.
I reviewed this PR and found no bugs. The source change is small and well-reasoned (removing the nothing-satisfies fallback so callers read the printed tree instead), but it changes lockfile-load semantics for unsatisfied peers, carries an acknowledged fresh-install-vs-reload trade-off, and coordinates with three other open PRs (#38767, #38768, #38837) — a human should confirm the design direction.
What was reviewed:
- Traced all four callers of
resolve_peer_dep_version_based(three loader loops, pnpm migration) — each already falls back to the recorded tree/snapshot onNone, so no caller is left unhandled. - Checked that the removed block was the only consumer of
candidates.first(); the satisfying-scan loop above it is unchanged, so in-range edges keep their #32182 binding. - New tests cover the four loader shapes, both linkers under
--frozen-lockfile, re-save fixed-point, and the pnpm migration path; assertions are specific (entry substrings, installed versions on disk) and use the local Verdaccio registry.
Extended reasoning...
Overview
The PR removes 12 lines from resolve_peer_dep_version_based in src/install/lockfile/bun.lock.rs: the fallback that bound an unsatisfied required peer to the highest-ordered candidate of the same tag. With it gone the helper returns None when no candidate satisfies the range, and every caller falls through to its existing path — the three loader loops walk the printed tree (<dependent>/<name> first), and the pnpm migration reads the snapshot suffix. The doc comment is shortened to match. Seven tests are added across bun-lock.test.ts (six) and pnpm-lock-v9.test.ts (one).
Security risks
None. This is package-manager lockfile-loading logic operating on already-parsed structures; no untrusted input parsing, auth, crypto, or filesystem-path handling is touched.
Level of scrutiny
High. bun.lock loading determines what bun install places on disk for every user; a wrong binding here silently installs a different version than the lockfile records. The change also intentionally accepts a divergence between a fresh resolve and a subsequent reload in one shape (root pins an out-of-range version while a higher out-of-range version exists elsewhere), trading it against the previous behavior where every shape with a printed record drifted. That is a defensible trade but a design call, not a mechanical fix.
Other factors
- The PR is tightly coupled to three open PRs: #38767 re-runs this helper inside the hoister on every tree build, #38768 re-implements the fallback for a subset and (per the body) should drop it too, and #38837 is a sibling covering bundled peers. The author verified composition with #38767 by applying its
src/on top, but the sequencing of these merges is a maintainer decision. - The comment-cop bot flagged the function's doc comment four times; the author responded that the current three-line return contract replaced a seven-line fallback description and the rationale lives in the PR body. That is a judgment call for a human.
- Test coverage is thorough: the four
it.eachshapes exercise the root loop, workspace loop, and package loop; the two linker tests verify the on-disk tree matches the recorded binding under--frozen-lockfile; the re-save fixed-point check catches drift in either direction; the pnpm test covers the fourth caller. All are hermetic (Verdaccio, hand-written lockfiles) and assert specific entry strings and installedpackage.jsonversions. - The extensive suite runs listed in the PR body (bun-lock, isolated-install, bun-install-registry, migration suites, etc.) give reasonable confidence no existing behavior regressed, but the acknowledged trade-off case is not covered by an automated test.
|
On the one open point, the trade-off shape not having a test: the shape only differs in what the isolated linker links the dependent against on the first reload after a fresh resolve (the hoisted layout is identical either way, since the root's pin wins the slot regardless of the binding), and which of the two bindings is preferable there is the design question flagged for maintainers in the body. It is documented rather than pinned on purpose; if the direction is confirmed I am happy to add an isolated-linker test that asserts the reload binds to the root's pin and that later reloads are a fixed point. |
… re-saved lockfile in the unsatisfied-peer tests
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes lockfile-load peer-binding semantics, carries a documented trade-off the author has flagged for maintainer input, and interacts with three other open PRs (#38767, #38768, #38837) whose overall direction should be confirmed together, a human look would still be worthwhile.
What was reviewed:
- Traced all four callers of
resolve_peer_dep_version_based(three loader loops inbun.lock.rs, three sites viaresolve_peer_like_bun_lockinpnpm.rs) — each already falls back to the printed tree / pnpm snapshot onNone, andmay_stay_unresolvedcovers peers so an unbound edge does not fail the load. - Checked that the removed fallback's tag-match guard (Npm/Git/Github) has no remaining consumer; the iterator rewrite of the satisfying scan is behavior-preserving.
- New tests cover all three loader loops, both linkers under
--frozen-lockfile, the fixed-point property, and the pnpm migration path; thestrict-peer-depfixture exists in the local registry.
Extended reasoning...
Overview
The src/ change is small: resolve_peer_dep_version_based in src/install/lockfile/bun.lock.rs drops its "nothing satisfies the range" fallback (which returned candidates.first() when its resolution tag matched the dependency's version tag) and now returns None in that case, so callers fall back to the tree recorded in the lockfile / pnpm snapshot. The explicit loop over candidates is also rewritten as .iter().copied().find(..) with .get().is_some_and(..), which is behavior-identical to the old bounds-checked loop. The doc comment is trimmed to describe the new contract. Seven new tests cover four lockfile shapes (nested next to a package, deduped onto the hoisted copy, next to a workspace, root's own peer), both linkers, and the pnpm-v9 migration path.
Security risks
None identified. The change only affects which already-recorded lockfile entry a peer edge is bound to on load; it does not touch integrity verification, registry auth, tarball fetching, or any untrusted-input parsing surface. Returning None instead of a package id makes the helper strictly more conservative.
Level of scrutiny
High. This is core bun install lockfile-load semantics: the binding chosen here decides what the hoisted linker installs under a dependent and how the isolated linker keys its store entries. A wrong binding silently changes the runtime dependency graph. The PR body itself documents one shape (root pins an out-of-range version while a higher out-of-range version exists elsewhere) where a fresh resolve and a reload now disagree on the first cycle, and the author has explicitly left whether that trade is acceptable — and whether to pin it with a test — as a maintainer call.
Other factors
- The author verified the tests fail on main without the
src/change and pass with it, and validated against activepieces/activepieces. - All four callers were checked: on
Nonethey fall through to the printed-tree lookup (root entry /<workspace>/<name>then root / path walk starting at<dependent>/<name>) or pnpm'simporter_versions, and every path is guarded bymay_stay_unresolved(peers qualify) or an explicitis_peer()continue, so an unbound edge never fails the load. - The change is entangled with #38767 (which the body says re-runs this helper in the hoister), #38768 (which the body says re-implements the removed fallback and should drop it too), and #38837 (bundled peers, independent). The author cross-tested with #38767's
src/applied on top, but confirming the direction across all four is a maintainer decision. - The comment-cop inline comments are all resolved; the remaining doc comment on the function is a three-line return contract, not a workaround justification.
…from the tree A version satisfying the range entering the file rebinds the edge; a package printed at two paths reads the record under its last path; a peer on the root's own out-of-range copy binds to that copy (pm why); the pnpm migration leaves a peer pnpm left unmet unbound instead of nesting the highest version under the dependent.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The src/ change is small and the callers' None fallbacks check out, but because it changes how unsatisfied peer edges bind on load (with an acknowledged trade-off shape flagged for a maintainer call) and needs to be sequenced against #38767/#38768/#38837, a human look is still worthwhile.
What was reviewed:
- Confirmed all four callers of
resolve_peer_dep_version_based(three loader loops +resolve_peer_like_bun_lockin pnpm.rs) already handleNonevia the tree/snapshot fallback the PR body describes. - Checked the rewritten
findscan is behavior-identical to the old satisfying-candidate loop; only the post-loopcandidates.first()fallback is dropped. - Tests cover the caller matrix (root/workspace/package loops, hoisted+isolated linkers, pnpm migration) and pin the fixed-point +
--frozen-lockfileacceptance.
Extended reasoning...
Overview
The PR removes the "nothing satisfies" fallback from resolve_peer_dep_version_based in src/install/lockfile/bun.lock.rs (net -12 lines). When no version in the lockfile satisfies a required peer's range, the helper now returns None instead of picking candidates.first(), letting each caller fall back to the record it already uses for optional/*/overridden peers: the printed tree in the three loader loops, and pnpm's snapshot suffix in the migration. Nine new tests across bun-lock.test.ts and pnpm-lock-v9.test.ts cover the affected shapes, both linkers, and fixed-point re-saves.
Security risks
None. This is peer-dependency edge binding during lockfile load; no untrusted parsing, no network, no filesystem writes beyond what bun install already does. Tests use the local Verdaccio registry and hand-written lockfiles.
Level of scrutiny
High. This is core bun install resolution logic that decides which package version a peer edge binds to, which directly affects what lands in node_modules and what --frozen-lockfile accepts. The line-count change is small, but the semantics are subtle: the PR body itself documents a trade-off shape (root pins an out-of-range version with a higher out-of-range version nested elsewhere) where the new behavior differs from a fresh resolve on the first reload, and the author explicitly left that direction open for maintainer confirmation rather than pinning it with a test.
Other factors
- The PR was already reworked once after self-review; the current shape is the second design.
- It interacts with three other open PRs (#38767 rebinds through this helper in the hoister, #38768 reimplements the same fallback, #38837 handles bundled peers) and consolidates a fourth (#38992). A maintainer should confirm the sequencing and that #38768 will drop its copy of the fallback as the body suggests.
- Test coverage is thorough and follows the file's conventions (
makeInstallRunner,--lockfile-onlyfixed-point checks, both linkers). Thestrict-peer-depfixture and integrity constants added to the pnpm test look consistent with neighbors. - CI build #98557 is referenced but its result isn't in the timeline; the author reports the install suites pass locally with debug-build timeout noise only.
Problem
bun.locksatisfies does not stay on the version the file records. activepieces/activepieces at HEAD hasreact-json-view(peersreact/react-domat^17.0.0 || ^16.3.0 || ^15.5.4) withreact-json-view/react-dom= 18.3.1 andreact-json-view/react= 18.3.1 printed next to it, andflux/react= 18.3.1 likewise, while the root holds 19.2.5. On an untouched checkout the 1.4 canary'sbun install --lockfile-onlybinds those edges to 19.2.5 and removesflux/react,react-json-view/react,react-json-view/react-domandreact-json-view/react-dom/schedulerfrom the file; 1.3.14 leaves them alone.--frozen-lockfilethe hoisted linker no longer installs the nested copy (the dependent resolves the root copy at runtime) and the isolated linker links the dependent against the other version, re-keying its store entry. The drift also runs the other way: when the peer was deduped onto the copy hoisted above it and a higher out-of-range version is nested elsewhere, a no-op re-save adds a<dependent>/<peer>entry for that higher version (second test shape below).resolve_peer_dep_version_based(src/install/lockfile/bun.lock.rs), which since install: fix stale global-store links on disable and peer edge drift across bun.lock loads #32182 binds required ranged peers on load and since install: pnpm parity — dedupe, prune, pm licenses, audit fix, add --filter/--catalog, nested overrides, transitive update, and workspace fixes #38333 in the pnpm migration, falls back to the highest version present when nothing satisfies the range (candidates.first(), mirroring the resolver's "incorrect peer dependency" branch). The resolver makes that pick once, against the versions present at that moment, and later installs keep the binding (Package::clone), so the version the edge is actually on is recorded only by the tree; re-picking the highest version of the final set moves the edge whenever another out-of-range version exists. The recorded target then has no edge left, the clean drops it, and the next save prints a different tree than the one loaded. The pnpm migration loses pnpm's recorded resolution the same way.Fix
Nonewhen nothing satisfies the range. Every caller already has the record for that case and already uses it for the other edges the helper declines (optional,*, overridden): the three loader loops fall back to the printed tree (the root entry,<workspace>/<name>then the root, and the path walk, whose first probe is<dependent>/<name>), and the pnpm migration falls back to the version pnpm recorded in the snapshot.isolated-install.test.tstests pass; none of them has an unsatisfied range), and a satisfying version entering the file later still rebinds the edge (pinned by the "rebinds" test). The resolver's own highest-version pick for a fresh resolve is intentionally kept; only the re-derivation on load goes. Since install: stop looping on a peer dependency no published version satisfies #38851 an edge the fallback cannot bind stays unbound rather than failing the load.package.jsonitself pins an out-of-range version and a higher out-of-range version is nested elsewhere. Nothing is printed for the edge (the hoister dedupes a peer onto a root dependency regardless of range), the fresh resolve binds the higher version, this PR binds the root's copy. Both linkers install the root's copy for either binding (the hoister by that same rule, the isolated linker because its ancestor walk takes a parent's dependency of that name regardless of range), so the file, the hoisted layout and the store key (+7347ae2d86f1441afor the test shape, checked with both builds) are identical either way; the binding shows inpm whyand in trees built without the root copy (--productionwhen the pin is a devDependency,--filter). Pinned by thepm whytest.*peers included) and is tracked separately; the order that works is pinned by the "two paths" shape.*peers already are, and the first load of the migrated file binds it to the root copy, which is what the package sees in pnpm's layout too (second pnpm test). The isolated linker keys the migration's own install without that peer and re-keys the entry on the next install; unmet*peers already do that on main, tracked separately.src/on top of this branch: the tests here, install: bind peer edges in the hoister, the way loading bun.lock binds them #38767's own tests and the activepieces re-save all agree with this branch alone). install: rebind ranged peers whose target the saved tree drops #38768'sresolve_peer_dep_version_based_amongre-implements the fallback for edges whose target was dropped from the tree and should drop it for the same reason. install: re-resolve a peer row instead of binding it to a stale bun.lock leftover #38901'sexisting_peer_targetdescribes the loader as binding to the highest candidate and should say the tree is read instead once this lands. install: keep bundled peer edges on their own bun.lock entry when loading #38837 (bundled peers) is independent.<dependent>/<peer>entry before it, which covers the first test shape and none of the others: built with itssrc/, the "hoisted above" shape gainsstrict-peer-dep/no-deps= 1.1.0, the root's-own-peer shape rebinds to 1.0.1 and dropsno-deps@1.0.0, and the pnpm migration still binds 1.0.1 (its root loop and the migration passed nothing to look up). Its own test passes on this branch; the one assertion it had that the tests here lacked,--frozen-lockfileaccepting the re-saved file, is now the last step of every re-save test.test/cli/install/bun-lock.test.ts,describe("loading bun.lock keeps a peer nothing in the file satisfies where the file records it"), nine tests on hand-written lockfiles: five re-save shapes (the copy printed next to a package; deduped onto the copy hoisted above it with a higher version nested elsewhere; printed next to a workspace; the root's own peer; a package printed at two paths), each of which must print the expected entries, be a fixed point and pass--frozen-lockfile; the rebind-once-something-satisfies boundary; the root-pin shape viapm why; and--frozen-lockfileplacement of the first shape under the hoisted and the isolated linker. Built with main's helper (732491c) eight of the nine fail (the boundary test passes on both by design): entries removed or rewritten to the higher version, the second shape gainsstrict-peer-dep/no-deps= 1.1.0, the two-paths record is dropped,pm whylists the peer under 1.0.1, the hoisted copy is missing, the isolated entry links 1.0.1.test/cli/install/migration/pnpm-lock-v9.test.ts, two tests under "peer dependencies": the recorded suffix is kept (strict-peer-dep/no-deps= 1.0.0, installed there by--frozen-lockfile; main binds 1.0.1 and drops it) and an unmet peer gets no nested copy (main addsstrict-peer-dep/no-deps= 1.1.0).bun install --lockfile-onlykeeps the four entries above; the only remaining diff is thedockerodecluster, held solely by an optional peer ofsandbox-agent, which is a separate case.src/change before the test additions: isolated-install (66), bun-install-registry (243), the pnpm and npm migration suites (all snapshots unchanged), hoist, bun-workspaces, catalogs, overrides, nested-overrides, frozen-lockfile-*, lockfile-only, lockfile-version-2, bun-lockb, migrate-bun-lockb-v2, isolated-relink, config-version, test-dev-peer-dependency-priority, yarn-lock-migration, bun-add, bun-remove, bun-update, bun-update-transitive, bun-prune, bun-pm-why, bun-dedupe. The only failures were 5-second per-test timeouts under the debug build when many files ran at once (pnpm-migration-complete, yarn-cli-repo, a few nested-overrides and bun-prune cases); each passes run on its own with unchanged snapshots.cargo clippyon the crate passes; the first revision'sforscan trippedmanual_find, which is why the scan is written asfind.Background
bun.lockstores packages keyed by their path in a hoistednode_moduleslayout ("a","a/b") and has no field for what an edge resolved to. On load a regular edge is bound by probing<dependent path>/<name>and then each enclosing path up to the root.Tree::hoist_dependencydedupes the edge onto it and prints nothing for the edge, which is why install: fix stale global-store links on disable and peer edge drift across bun.lock loads #32182 binds such edges by version on load (the path walk would rebind them to the hoisted copy while the writer had bound the highest satisfying version, re-keying isolated store entries, which are namedname@version+<hash of peer bindings>). When the version above is out of range and not a root dependency, the hoister places the edge's own target in the dependent's node instead, which is what produces a<dependent>/<peer>entry.incorrect peer dependency. A required peer edge keeps its target alive through the clean, so a version nothing else depends on can stay in the file next to its dependent for as long as the edge points at it; the shapes in the tests are what such a file looks like after the dependency that originally brought that version in has left the project.Tree::hoist_dependencydedupes a peer onto a copy held as a direct dependency of the root even when it is out of range, and the isolated linker resolves a peer by walking the dependent's ancestors for a regular dependency of that name, taking the first one regardless of range; the edge's own binding only matters to either linker when no such copy is above the dependent. A package that is nested (because the root holds another version of it) under two parents is printed once per parent, and the loader binds its edges once per printed row.Earlier revision of this PR
The first revision kept the fallback and instead made the three loader loops bind any peer edge to the entry printed in its dependent's own node before running the scan, which also covered entries holding an in-range version lower than one elsewhere in the file. Review pointed out that #38767 rebinds every edge through the helper inside the hoister, so a loader-only rule is overridden during the same load, and that the field case (and every shape with a printed record) goes through the helper's nothing-satisfies fallback, which all callers share. This revision fixes that fallback instead; the in-range case is left to the hoister-side binding #38767 is establishing.
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-lock.test.ts