Skip to content

install: keep the packages bun.lock holds only through optional peers when an install re-resolves - #39002

Open
robobun wants to merge 1 commit into
mainfrom
farm/25e1db45/keep-loaded-optional-peer-entries
Open

install: keep the packages bun.lock holds only through optional peers when an install re-resolves#39002
robobun wants to merge 1 commit into
mainfrom
farm/25e1db45/keep-loaded-optional-peer-entries

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Lockfiles written by bun 1.3.x hold packages that only an optional peer slot points at (1.3.x never dropped a package once an optional peer was bound to it: opencode's encoding and @vitest/coverage-v8 trees, activepieces' dockerode tree, ...). Since install: drop packages held only by optional-peer resolution slots from bun.lock #35681 clean_with_logger drops such packages; install: keep optional-peer-held packages when the lockfile is frozen #38853 keeps them on a frozen install, so --frozen-lockfile passes, but every other install that re-resolves still prunes them (keep_optional_peer_targets = frozen_lockfile() || !summary.changes_resolutions(), src/install/lockfile.rs:973).
  • An install re-resolves on any package.json edit, and on untouched checkouts whose workspaces report a diff on every run (anomalyco/opencode@4643e65 and activepieces/activepieces at HEAD still print Workspace package "..." has ... updated 1 dependencies on main today, as they do on 1.3.14). On main, a plain bun install on a fresh clone of opencode prints Clean lockfile: 2711 packages -> 2697 packages, rewrites the committed bun.lock (-36 lines) and installs without those packages; activepieces goes 4069 -> 4063 (-22 lines, measured with install: bind an unsatisfiable peer to the copy bun.lock nests under its dependent #38992 applied, which fixes its other four lines). 1.3.14 re-saves both files byte-identically. So what gets installed from one lockfile depends on whether the install was frozen, and a bun add after upgrading carries an unrelated removal of every such package into the diff.

Fix

  • Lockfile::mark_loaded_packages, already called once right after load and before the differ, dedupe or an update re-point anything, also records held_at_load: the loaded packages some non-optional-peer dependency resolves to (one pass over the dependency rows into a bitset). In Package::clone an optional peer slot defers to Cloner::flush, and so can release its target, only for a target in that set or one appended during this resolve; a target the loaded file held through optional peers alone is cloned like a dependency. keep_optional_peer_targets goes away: frozen and plain installs apply the same rule.
  • The install: drop packages held only by optional-peer resolution slots from bun.lock #35681 behavior is unchanged: in bun remove x or the same edit by hand, x was held by a real dependency when the file was loaded, that dependency is gone after the diff, and the slot releases it. A fresh resolve never binds an optional peer to a package nothing else holds, so files written by this version are unaffected; bun dedupe keeps its model because a surviving version keeps every edge it had (comment in dedupe.rs updated). No snapshot changes (install: keep optional-peer-held packages when the lockfile is frozen #38853 already updated the arborist fixture this affects; edit-package-json--removed, where a real dependent leaves, still exits 1).
  • Verified:
    • test/cli/install/bun-lock.test.ts, "a re-resolving install keeps the packages an older lockfile holds through optional peers alone": a 1.3-shaped lockfile plus a package.json that gains a dependency the file already has. Rebuilt with main's src/install the plain install drops the no-deps entry; with this change it keeps it, installs it, and --frozen-lockfile accepts the saved file. install: keep optional-peer-held packages when the lockfile is frozen #38853's frozen test and the three install: drop packages held only by optional-peer resolution slots from bun.lock #35681 tests pass unchanged.
    • Debug build: every optional-peer, frozen and install: fix the package id boundary in Lockfile::eql #38870 test in bun-lock (14) and migration/migrate (125, snapshots untouched) pass. Full bun-lock and bun-dedupe runs on this box currently fail on ConnectionRefused from the test registry (load average above 200) on unrelated tests as well; the same logic on yesterday's base passed bun-lock, bun-dedupe, bun-remove, frozen-lockfile-pruned, bun-update, bun-update-transitive, bun-audit, bun-prune, catalogs, bun-workspaces, isolated-install, bun-install-registry and the migration suites in full.
  • Related: install: bind an unsatisfiable peer to the copy bun.lock nests under its dependent #38992 fixes the other shape in the same repos (required peers with a nested copy); independent of this change. install: let the hoister bind optional peers when loading bun.lock #37925 would stop binding optional peers at load, which is what makes these entries reachable at all, so it conflicts with keeping them.

Background


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

… when an install re-resolves

clean_with_logger let optional peer slots keep their targets alive only on a
frozen install or when package.json had no diff, so an install that did
re-resolve (any package.json change, or a diff a workspace reports on every
run) pruned the packages a 1.3.x lockfile holds that way and rewrote the file.
Record at load which packages a non-peer dependency resolves to; an optional
peer slot releases only those, and a package the loaded lockfile held through
optional peers alone is cloned like a dependency, whatever the install does.
@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: 23 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: 4113851d-6dd0-4175-99b9-4fdcef095ec4

📥 Commits

Reviewing files that changed from the base of the PR and between 732491c and a803736.

📒 Files selected for processing (5)
  • src/install/PackageManager/install_with_manager.rs
  • src/install/dedupe.rs
  • src/install/lockfile.rs
  • src/install/lockfile/Package.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
Updated 10:05 AM PT - Aug 15th, 2026

@robobun, your commit a803736 has some failures in Build #97933 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 39002

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

bun-39002 --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 didn't find any bugs. Because it changes core lockfile-rebuild semantics (what clean_with_logger keeps vs. drops) and the description notes it conflicts with the direction in #37925, a human look at the approach would still be worthwhile.

What was reviewed:

  • held_at_load bitset construction and its use in Package::clone — the out-of-bounds default (true) correctly defers session-appended packages, and the >= max_package_id guard above already handles invalid_package_id.
  • keep_optional_peer_targets fully removed (no remaining references); Cloner has a single construction site.
  • mark_loaded_packages's new fallible signature — sole caller propagates with ?; the fresh-lockfile / load-error paths reset the set via init_empty() before clean_with_logger reads it.
  • The new test triggers a re-resolve (root gains a-dep) against a 1.3-shaped lockfile and asserts the peer-only no-deps entry survives, is installed, and round-trips --frozen-lockfile.
Extended reasoning...

Overview

Replaces the session-level keep_optional_peer_targets boolean (previously frozen_lockfile() || !changes_resolutions()) with a per-package held_at_load bitset recorded at mark_loaded_packages time. In Package::clone, an optional-peer slot now defers to Cloner::flush only when its target either (a) had a non-optional-peer holder in the loaded lockfile or (b) was appended this session (out-of-bounds → true); a target the loaded file held through optional peers alone is cloned like a regular dependency, so it survives a re-resolving install unchanged. Touches lockfile.rs (new field, mark_loaded_packages now builds the bitset and returns Result<(), AllocError>), lockfile/Package.rs (the clone-time gate), install_with_manager.rs (propagate the ?), a comment in dedupe.rs, and adds one test to bun-lock.test.ts.

Security risks

None identified. This is internal lockfile-rebuild bookkeeping over data bun itself wrote; no new parsing of untrusted input, no path/URL handling, no privilege boundaries.

Level of scrutiny

High. This is the code path that decides which packages a bun install writes back to bun.lock and installs into node_modules, and it interacts with several recent changes (#35681, #37426, #38853). The description also flags that #37925 takes a conflicting direction (unbinding optional peers at load), so which approach lands is a maintainer call. The logic itself checks out — the is_set_allow_out_of_bound(.., true) semantics correctly encode "session-appended reads as held" per the field comment, and the pre-existing *old_resolution >= max_package_id guard covers invalid_package_id before the bitset lookup.

Other factors

The three #35681 tests and the #38853 frozen test are unchanged in the file and the description states they still pass, so the "remove drops it / non-peer keeps it" invariants are preserved by the new gate (a removed direct dep was held-at-load, so its peer slot still defers). The author notes the full bun-lock/bun-dedupe suites hit unrelated ConnectionRefused failures on this box, so CI is the confirmation that no snapshot drifted. Given the semantic scope and the open design question against #37925, deferring rather than approving.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

  • Reproduced on main with anomalyco/opencode@4643e65 and activepieces/activepieces (HEAD): a plain bun install on the untouched checkout prunes the optional-peer-held packages (Clean lockfile: 2711 -> 2697 on opencode) and rewrites the committed bun.lock; with this branch both files re-save without those removals and --frozen-lockfile still passes.
  • CI (build 97933): 177 jobs passed, no test failures; the build is marked failed only because its two :darwin: 14 aarch64 - test-bun jobs expired waiting for an agent (that lane has a multi-hour backlog today and is expiring jobs across many builds). Not retriggering into that queue; the diff has nothing platform-specific.

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.

2 participants