Skip to content

install: keep a peer nothing in bun.lock satisfies where the file records it - #38892

Closed
robobun wants to merge 3 commits into
mainfrom
farm/eb3d1dd1/bun-lock-peer-own-entry
Closed

robobun wants to merge 3 commits into
mainfrom
farm/eb3d1dd1/bun-lock-peer-own-entry

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A required peer whose range no version in bun.lock satisfies does not stay on the version the file records. activepieces/activepieces at HEAD has react-json-view (peers react / react-dom at ^17.0.0 || ^16.3.0 || ^15.5.4) with react-json-view/react-dom = 18.3.1 and react-json-view/react = 18.3.1 printed next to it, and flux/react = 18.3.1 likewise, while the root holds 19.2.5. On an untouched checkout the 1.4 canary's bun install --lockfile-only binds those edges to 19.2.5 and removes flux/react, react-json-view/react, react-json-view/react-dom and react-json-view/react-dom/scheduler from the file; 1.3.14 leaves them alone.
  • The installed tree changes the same way when the file is not rewritten: with --frozen-lockfile the 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).
  • Cause: 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

  • The helper returns None when 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.
  • Why this is right: the satisfying scan has a criterion (highest version matching the declared range) that can be recomputed from any package set, which is what makes the binding reproducible. An unsatisfied edge has none; the resolver's pick is arbitrary and recorded only in the tree, so the tree is the thing to read. Every edge this changes ends up bound exactly where 1.3's loader bound it; edges with a satisfying candidate are not touched, so install: fix stale global-store links on disable and peer edge drift across bun.lock loads #32182's store-key stability is unchanged (its isolated-install.test.ts tests 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.
  • Shape that binds differently from a fresh resolve: the root package.json itself 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 (+7347ae2d86f1441a for the test shape, checked with both builds) are identical either way; the binding shows in pm why and in trees built without the root copy (--production when the pin is a devDependency, --filter). Pinned by the pm why test.
  • Shape this improves without fully fixing: a dependent printed at two or more paths. The loader binds a package's edges once per printed path and the last path wins, so the recorded copy is read back when it sits under the last-sorted path, while a walk from another path can reach a different copy (the root's) and the next save then rewrites the entry to that copy, once. Main drops such a record in every order; when nothing is recorded (the root-pin shape above with two paths) main is exact and this PR can rewrite once depending on the order. The per-path overwrite is pre-existing loader behavior for every edge the walk binds (optional and * peers included) and is tracked separately; the order that works is pinned by the "two paths" shape.
  • pnpm migration: for a peer pnpm recorded a resolution for, the snapshot suffix now wins (first pnpm test). For a peer pnpm left unmet (no suffix), main bound the highest version present, nesting a copy under the dependent that pnpm's tree did not have whenever that version was not the root's; it is now left unbound, like unmet * 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.
  • Interaction with the open peer work: install: bind peer edges in the hoister, the way loading bun.lock binds them #38767 rebinds every peer edge through this same helper inside the hoister, on every tree build including the one at the end of a load; with the fallback in place it would move these edges at save time too, with this change it leaves them where the loader put them (verified by applying install: bind peer edges in the hoister, the way loading bun.lock binds them #38767's 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's resolve_peer_dep_version_based_among re-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's existing_peer_target describes 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.
  • install: bind an unsatisfiable peer to the copy bun.lock nests under its dependent #38992 fixed the same activepieces case in the same function and is consolidated into this PR. It kept the fallback and looked up only the <dependent>/<peer> entry before it, which covers the first test shape and none of the others: built with its src/, the "hoisted above" shape gains strict-peer-dep/no-deps = 1.1.0, the root's-own-peer shape rebinds to 1.0.1 and drops no-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-lockfile accepting the re-saved file, is now the last step of every re-save test.
  • Verified:
    • 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 via pm why; and --frozen-lockfile placement 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 gains strict-peer-dep/no-deps = 1.1.0, the two-paths record is dropped, pm why lists 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 adds strict-peer-dep/no-deps = 1.1.0).
    • activepieces (workspace manifests regenerated from the lockfile): with this build bun install --lockfile-only keeps the four entries above; the only remaining diff is the dockerode cluster, held solely by an optional peer of sandbox-agent, which is a separate case.
    • Suites run with this build: bun-lock (49), pnpm-lock-v9 (83), and with the same 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 clippy on the crate passes; the first revision's for scan tripped manual_find, which is why the scan is written as find.

Background

  • bun.lock stores packages keyed by their path in a hoisted node_modules layout ("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.
  • A peer edge is satisfied by whatever is installed next to or above the dependent. When the version hoisted above satisfies the range, Tree::hoist_dependency dedupes 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 named name@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.
  • When no version in the tree satisfies a peer, the resolver binds the edge to the highest version present at that moment and warns 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.
  • Two rules the trade-off bullets lean on: Tree::hoist_dependency dedupes 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

@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: 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.
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: 5e81b08b-5eeb-4b32-952f-d5ea11e6d472

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6398 and e2d1e20.

📒 Files selected for processing (3)
  • src/install/lockfile/bun.lock.rs
  • test/cli/install/bun-lock.test.ts
  • test/cli/install/migration/pnpm-lock-v9.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 11:58 AM PT - Aug 15th, 2026

@robobun, your commit 2ff1795 is building: #98557

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fixed, awaiting review.

Reproduced on main two ways: the new tests (test/cli/install/bun-lock.test.ts, test/cli/install/migration/pnpm-lock-v9.test.ts) fail on a build of main without the src/ change and pass with it, and activepieces/activepieces at HEAD, where bun install --lockfile-only on the 1.4 canary removes flux/react, react-json-view/react, react-json-view/react-dom and react-json-view/react-dom/scheduler; with this branch the re-save keeps all four. Fix: #38892 (the helper no longer falls back to the highest version when nothing satisfies the range; see the PR body).

Update: #38992, which fixed the same case by binding the <dependent>/<peer> entry ahead of the fallback, is closed in favor of this PR; its test passes here and its frozen-install assertion is folded into the re-save tests. After a self-review of the consolidated change, e2d1e20 adds tests pinning the boundaries described in the body (a satisfying version arriving later rebinds, a package printed at two paths, the root-pin shape via pm why, and the pnpm migration's unmet-peer arm), and the body's trade-off bullets were rewritten to match what both builds actually do in those shapes. src/ is unchanged apart from 2ff1795 writing the scan as find for the clippy job.

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

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

(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.)

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. install: keep bundled peer edges on their own bun.lock entry when loading #38837 - Fixes the same load-time defect in parse_into_binary_lockfile, where the resolve_peer_dep_version_based scan rebinds a peer edge off the <dependent>/<peer> entry printed for it; install: keep bundled peer edges on their own bun.lock entry when loading #38837 handles the bundled-peer subcase that this PR subsumes.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

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 <dependent>/<peer> entry before the version scan and therefore also passed #38837's test; that was true of the first revision only.)

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 no-deps@1.0.0 entry with a root no-deps@1.1.0 that satisfies ^1.0.0), which the scan here still rebinds, so #38837 is still needed for that case and this PR does not cover it. Conversely #38837 does nothing for the unbundled entries this PR is about (activepieces' react-json-view/react-dom and friends carry no bundled marker). The diffs do not overlap textually.

Comment thread src/install/lockfile/bun.lock.rs Resolved
Comment thread src/install/lockfile/bun.lock.rs Resolved
Comment thread src/install/lockfile/bun.lock.rs Resolved
…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.
@robobun
robobun force-pushed the farm/eb3d1dd1/bun-lock-peer-own-entry branch from e01eef8 to fbb385d Compare August 15, 2026 12:10
Comment thread src/install/lockfile/bun.lock.rs Resolved
@robobun robobun changed the title install: keep a peer on the bun.lock entry printed next to its dependent install: keep a peer nothing in bun.lock satisfies where the file records it Aug 15, 2026
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Reworked after self-review; the PR body now describes the current shape. Summary of what changed and why:

  • The first revision kept resolve_peer_dep_version_based's nothing-satisfies fallback and made the three loader loops prefer a <dependent>/<peer> entry before calling it. Two problems with that: install: bind peer edges in the hoister, the way loading bun.lock binds them #38767 (in its current form) re-runs the helper for every peer edge inside the hoister, including the lockfile.resolve() at the end of a load, so a loader-only rule is overwritten during the same load and all of the tests here would fail once it lands; and the field case, along with every other shape that has a printed record, goes through the helper's fallback, which the loader loops, the pnpm migration and install: bind peer edges in the hoister, the way loading bun.lock binds them #38767 all share.
  • This revision removes that fallback instead (net -12 lines in src/): when nothing satisfies the range the helper returns None, and each caller keeps the record it already falls back to for optional / * / overridden peers (the printed tree on load, pnpm's snapshot suffix in the migration). The in-range case the first revision also covered (an entry holding a lower satisfying version than one elsewhere in the file) is deliberately left to the hoister-side binding install: bind peer edges in the hoister, the way loading bun.lock binds them #38767 is establishing, since that is a question of which binding is wanted rather than of reading the file.
  • Checked the interaction directly by applying install: bind peer edges in the hoister, the way loading bun.lock binds them #38767's src/ on top of this branch: the seven tests here pass, install: bind peer edges in the hoister, the way loading bun.lock binds them #38767's own tests pass, and the activepieces re-save is identical to this branch alone (the four react entries stay). The only textual overlap between the two is one doc paragraph.
  • Tests now cover the two loader outcomes for such an edge (recorded next to the dependent, or deduped onto the copy above, where the old fallback added a new nested entry on a no-op re-save), the workspace and root loops, both linkers under --frozen-lockfile, and the pnpm migration path, which is the other caller whose behavior changes. All seven fail on main built without the src/ change.
  • The remaining comment-cop flags are on the function's three-line return contract, which replaced a seven-line description of the fallback; the rationale lives in the PR body and commit message rather than in the source.

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

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 new PkgMap::get_in_own_node that 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 — but get_in_own_node does not exist, the loops at :3091/:3173/:3244 are unchanged, and the only code change is dropping the candidates.first() fallback so resolve_peer_dep_version_based returns None when 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 new PkgMap::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_lockfile at bun.lock.rs:3091, :3173, :3244 are byte-identical to main: each still calls resolve_peer_dep_version_based first, then falls through to pkg_map.get / find_resolution. No own-node probe precedes the scan.
    • git diff HEAD~1 --stat on bun.lock.rs shows +16/-19 — that is the doc-comment rewrite plus removing the candidates.first() fallback block. There is no loop reordering and no new PkgMap method.
    • All four new test shapes in bun-lock.test.ts use peerDependencies: { "no-deps": "^2.0.0" } with only no-deps@1.0.0/1.0.1/1.1.0 present — 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 so resolve_peer_dep_version_based returns None when 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, the describe title, 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-3426 still returns Some(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

    1. Bot comments "This PR may be a duplicate of #38837"; author replies that this branch subsumes #38837's load-side fix.
    2. 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.
    3. This PR merges with only the nothing-satisfies fix.
    4. 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, describe title, 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."

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

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 src/ change is that resolve_peer_dep_version_based returns None when nothing satisfies the range.

@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 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 on None, 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.each shapes 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 installed package.json versions.
  • 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.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

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

@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 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 in bun.lock.rs, three sites via resolve_peer_like_bun_lock in pnpm.rs) — each already falls back to the printed tree / pnpm snapshot on None, and may_stay_unresolved covers 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; the strict-peer-dep fixture 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 None they fall through to the printed-tree lookup (root entry / <workspace>/<name> then root / path walk starting at <dependent>/<name>) or pnpm's importer_versions, and every path is guarded by may_stay_unresolved (peers qualify) or an explicit is_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.

@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 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_lock in pnpm.rs) already handle None via the tree/snapshot fallback the PR body describes.
  • Checked the rewritten find scan is behavior-identical to the old satisfying-candidate loop; only the post-loop candidates.first() fallback is dropped.
  • Tests cover the caller matrix (root/workspace/package loops, hoisted+isolated linkers, pnpm migration) and pin the fixed-point + --frozen-lockfile acceptance.
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-only fixed-point checks, both linkers). The strict-peer-dep fixture 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.

Jarred-Sumner added a commit that referenced this pull request Oct 14, 2026
### What does this PR do?

It consolidates 597 open package manager pull requests into one reviewed
change for Bun 1.5. Each was ported, taken in part, or left out with a
reason. This is the complete change log: `X -> Y` = symptom on main ->
behaviour now; a line without `->` names a symptom that is gone; dep =
dependency.

| absorbed | in part | not taken | obsolete | total | issues closed |
|---|---|---|---|---|---|
| 464 | 40 | 74 | 19 | 597 | 133 |

699 files, +150,684 / -85,553 (src 221 files +41,383 / -46,459, net
-5,076).

#### Behaviour changes someone will notice

1. Unknown long flags are errors in every package manager command and
bunx (main: `bun install --network-timeout 100000` adds the package
`100000`). Drop the flag; bunx: tool flags go after the tool's name. In
`BUN_OPTIONS` they are skipped.
2. `bun ci`/`--frozen-lockfile` fail (`note: dependencies in
package.json changed since bun.lock was saved`) when a dep of the root
or a workspace was added, removed, re-grouped or re-ranged. Equivalent
spellings, `file:` directory rows (warning),
`trustedDependencies`/`patchedDependencies` edits pass. Run `bun
install`, commit (#41931; refs #22689).
3. `file:` folders are hard-linked real files (hoisted: was a symlink
per file, their deps did not resolve). The folder's own `node_modules`
is not taken, except `bundleDependencies`. Linked, and trusted scripts
run, once, not on every install. Declare or bundle what the folder
needs; `link:` / a workspace for a live link; `--force` reruns scripts
(#40628, #37127; fixes #13379).
4. Hoisted: what the previous `bun.lock` placed and the new one does not
leaves `node_modules`, with what only it needed and its bins; not under
`--filter`, not what no lockfile placed (`bun prune`) (#29794, #44406).
5. One-time re-downloads: registry metadata (cache format bump), `file:`
/ URL tarballs, packages of registries on a port, path or http; `bunx`
starts a new cache.
6. Retries wait: a registry that refuses connections costs about 9 s
before the install fails (main: at once).
`BUN_CONFIG_HTTP_RETRY_BACKOFF=<ms>` sets the first wait.
7. Dep names over 255 bytes per component are refused (main: a
4,100-byte name in a manifest aborts), as are the names under Security.
8. Workspace's symlinked `node_modules` is replaced by a real directory,
like links at `@scope`, `.bin`, `.bun/**` (main wrote and deleted
through them). Only the root `node_modules` link is followed: link the
root, not members (#42039).
9. `"trustedDependencies": []` kept the default trusted list off only on
the install that created `bun.lock` -> always, also in a checkout
without the workspace that declares it; written to `bun.lock` (one line;
Bun 1.4 honours it, drops it at its next save) (#44765).
10. `!dir/**` in `workspaces` also removes `dir`, as npm; negated
entries work in every spelling (`!./pkg`, `!pkg/`) and position, add no
members; `bun run --filter`/`--workspaces` follow them (#43401, #43400;
fixes #43399).
11. `bun pm ls` lists what is on disk, exits 1 without `node_modules`;
isolated: a dep of the root only if its link exists (#38923; fixes
#38909).
12. Linker sticks: while `node_modules/.bun` exists, a command that
names no linker keeps the isolated layout (main: lays hoisted over it).
Back: `--linker=hoisted` or delete `node_modules`.
13. `NODE_ENV=development`/`test`: `bun install` reads the `.env` files
`bun run` reads, not the production ones.
14. `--provenance`, `--provenance-file`, `publishConfig.provenance`,
`NPM_CONFIG_PROVENANCE`, `provenance=true` in `.npmrc` are errors (main:
published without it). Unset them or publish with npm (#30522; refs
#18611).
15. `packageExtensions` adds a key to `bun.lock`, a trailing section to
`bun.lockb`, only when an extension added something; older versions load
both.
16. Fresh resolves pick differently (locked versions stay): see
Resolver; Overrides. glibc hosts stop installing `-musl` variants
(`--libc='*'` for both).
17. New `warn:` lines: incorrect peers, deprecated versions, skipped
required deps, stale / misplaced `patchedDependencies`, unreadable
config files, retries, unusable hashes.
18. Dep's script finds its own `.bin`, then the inherited `PATH`, then
shared `.bin`s, so an untrusted package's `node` bin cannot run inside a
trusted script. Cost: a global `node-gyp` wins over the project's, for
dep scripts only (#44488).
19. If `bun install` itself is killed while a trusted dep's script runs,
the next install calls the package installed (registry packages: as
main; `file:` folders, local tarballs: the rerun on every install hid
it). `bun install --force` (#42001).

#### Decisions that need the maintainer's confirmation

- Frozen installs start failing (2). Of 241 `bun.lockb` files from this
repo's release tags, 6 that pass on main fail here (4 real drift).
- A frozen install with only a foreign lockfile writes nothing: no
`bun.lock`, no package.json edit, one note (#38804). Reverses #38333.
- Changes 1, 9 (teams on 1.4 and 1.5 see the `[]` line come and go), 10,
11, 12 (removing `linker = "isolated"` no longer switches a tree), 14,
18.
- The `packageExtensions` lockfile key (15): without it an edited or
removed extension cannot be told from the package's own deps. #43104 /
#43209 are out for needing a key.
- Scripts that run now and did not: #41803 (its opposite #41866 is out),
#41845, #39443, #43913, #42001, #38780, `bunx --trust`. No longer: root
scripts under `--dry-run`; under frozen, scripts of an installed package
trusted after the lockfile was saved (main: every frozen install; now a
plain install once, frozen never, silently).
- No state file, so a killed install goes unnoticed (19). The complete
fix is one bit or list per project, read once per install.
- Tarball cache entries named by integrity; full manifests by package
name for `libc`; #38813; +1 `renameat` per linked package (#38783); a
token survives only an http -> https redirect on one hostname (#38001);
#39603 flips a row of #38867.
- Hoisted thread pool on macOS: on; CI only, no stress run.
- Open: mask URLs in source excerpts under a diagnostic (not done);
should `bun pm pack` follow a symlinked `node_modules` (it does,
#41925).

#### New features and flags

- Yarn Berry migration (on main since): here package.json is written
just before `bun.lock` and only if that is saved; `link:` stays `link:`;
`libc` is honoured (#40057)
- `link:./dir`, `link:../x`, absolute paths: symlink to that directory,
relative to the declaring package.json, as Yarn / pnpm; `link:foo` stays
a `bun link` name (#40025; fixes #4719, #5045)
- `packageExtensions` in the root package.json adds missing deps / peers
to registry packages, can mark a peer optional (#40053)
- `bun pm cache pack <file>`/`unpack <file>`: the cache entries a
lockfile needs as one file (CI); unpack adds what is missing, refuses a
damaged pack whole (#40012)
- `bun pm cache prune [--max-age <days>] [--dry-run]`: drops cache
entries older than 30 days, not the global store (#42592)
- `bun pm fetch`: downloads all deps (pinned git too) into the cache; no
`node_modules`, scripts or lockfile write (#29509)
- `bun publish --recursive`/`--filter <pattern>`: packs the selected
workspaces, publishes deps first, skips private and already published
ones; the first failure ends the run (#41292)
- `bun update --depth 0`: direct deps only, transitive ones stay locked
(#41720; fixes #41625)
- `bun prune --check`: exit 1 when anything would be removed (#39216)
- `minimumReleaseAgeExcludes` takes `name@version`; new
`--minimum-release-age-excludes a,b@1.2.3`; `bun audit fix` honours the
list (#37909, #42803; fixes #28967, #42799)
- `minimumReleaseAge` accepts `90m`, `12 hours`, `3d`, `2 weeks`;
`nan`/`inf` are config errors; `0` no longer fetches full manifests
(#30529)
- `libc`: on Linux only the glibc or musl variant of an optional native
package is installed; recorded in `bun.lock`; `--libc` overrides
(#38797; fixes #16282)
- `bunx --cwd <dir>` (#28669; fixes #28668)
- `bunx --env-file <file>` (main: `unrecognised dependency format:
@.env`); not `bun --env-file F x pkg` (#35599; fixes #12430, #17759)
- `bunx --minimum-release-age`, part of its cache key (#30751; fixes
#30748)
- `--env-file`, `--no-env-file`, bunfig `env = false` for all package
manager commands (main: `--env-file ci.env` added the package `ci.env`)
(#36672; fixes #12011, #31450)
- `npm-shrinkwrap.json` is migrated like package-lock.json (#38861)
- Deprecated versions are reported: one line per direct dep, one for the
rest, all with `--verbose` (#35457; fixes #6883)
- `bun add -d | --optional | --peer <pkg>` moves a package another group
lists; `--save-prod` moves it back (#36284; fixes #4852, #5714)
- `bun audit fix` reports and rewrites the `overrides` rule that holds a
vulnerable version (#38785)
- `bun pm pkg set`/`delete`: arrays (append, index), bracketed names
(`exports[./package.json]`) (#38025; fixes #22035)
- `--dry-run` for `bun link`, `unlink`, `pm version`, `pm cache rm`, `pm
trust`, `pm pkg`, `pm migrate` (main: they wrote) (#41690)
- `bun publish` honours `publishConfig.registry` and
`publishConfig["@scope:registry"]` (#38322; fixes #18670)
- Auto-install reads registries and credentials from `.npmrc` (main:
`@corp/x` came from the default registry) (#44754)
- `NPM_CONFIG_USERCONFIG` names the user `.npmrc`, as
`actions/setup-node` sets it (#38047)
- `Cache-Control: no-cache`, `no-store`, `max-age` under 300 s on
registry metadata (#41644)
- Workspace can depend on the root package with `workspace:*`, also for
pack / publish (#36594; fixes #2773, #5176)
- `"gypfile": false` turns off the implicit `node-gyp rebuild`; a
trusted tarball, git, `file:` package with `binding.gyp` and no scripts
is built like a registry one (#43913; fixes #43903)
- Scripts get `npm_package_name`/`version`/`json` of their own package,
`npm_config_local_prefix`, `npm_package_config_*`,
`npm_lifecycle_event`, `npm_lifecycle_script`; inherited `npm_package_*`
are dropped (#36690, #38110; fixes #12752)
- Hoisted linker links packages on up to 8 threads, whatever
`node_modules` holds (nothing, a part, other versions, `--force`) and
after a download; not on Windows; `file:`/`link:` / workspace / trusted
deps, bins and scripts stay on the main thread; everything in today's
order, scripts too; a cold install is no faster (#29496)

#### Security

- Registry URL that is not http(s) (`htps://`, no scheme): `pm view`,
`whoami`, `publish`, `audit`, `pm diff` sent the token there over plain
TCP; a bad `BUN_CONFIG_REGISTRY` fell back to another registry with the
token -> error, no request, on every route (#41662, #41773)
- Token, `_auth`, user or password with CR, LF, NUL could inject
headers, as could `--otp`, `--user-agent` -> refused (#37458, #41661)
- `token = "$NPM_TOKEN"` with the variable unset sent the literal text
-> warning, nothing sent (#41049)
- Passwords and tokens in registry, tarball, git URLs were printed in
errors, warnings, summaries, `--verbose`, `pm ls`/`why`/`view`/`diff`,
`audit`, migrations -> masked (#38977, #38997, #38817)
- Control characters from manifests, lockfiles, registry answers reached
the terminal (`pm view`, `licenses`, `untrusted`, `audit`, resolutions,
bin names, errors) -> escaped (#38536, #38631, #39358, #38525)
- Dep names `.bin`, `node_modules`, a bare `@scope`, with control
characters, empty or scope-only were installed or requested -> refused;
skipped with a warning in a third party's manifest (#44439, #38615,
#43331, #41735)
- Registry, git or tarball package could `link:` a directory, use a
local `.tgz` of the project or a `file:` path that leaves it -> refused
at resolve and install time (#39023, #38986, #43101)
- Dep's own `npm:` alias could replace a same-named direct dep of the
root; an alias in a refused or stale lockfile kept redirecting -> the
root's dep wins; loaders register no aliases (#44412, #44294)
- Frozen install asked the registry for a range that `bun.lock` links to
a workspace missing on disk (dependency confusion) -> fails without a
request (#43471)
- Global store: a trusted package's script could modify shared deps; `pm
trust` ran scripts in the store, saw nothing under isolated -> its
closure is project-local (#40239, #35106)
- Failing script of an optional workspace / `link:` dep deleted that
folder -> only what the linker placed goes (#43226)
- `bun prune`, `update`, `dedupe` removed packages behind a symlinked
workspace `node_modules` -> prune refuses (exit 1), the others skip it
(#44484; fixes #44669)
- `bun add -g`, `link`, `unlink` replaced or deleted foreign files in
the global bin directory -> only what Bun made (#39745)
- `bun pm cache rm` could delete the project, home or Bun's directory
(an empty `BUN_INSTALL_CACHE_DIR` meant the project) -> refused (#39750)
- Project's bunfig.toml could point `globalDir`/`globalBinDir` anywhere
-> ignored with a warning (#41775)
- `bunx` installed into the shared temp directory, where another user's
`node_modules` / package.json was loaded -> `<cache>/.bunx-<uid>`, 0700
(#44393, #31447)
- Applying a patch followed symlinks and wrote through hard links ->
refused (#32677)
- Tarball could make Bun allocate the disk or memory its headers claim;
sparse members keep their holes (#36542)
- `minimumReleaseAge`: an exact version passed when only the abbreviated
manifest was cached; so did a publish time that is not a date (#38659,
#43307)
- `BUN_CONFIG_NO_VERIFY=0` turned verification off, `=1` did not -> off
only for `1` or `true` (#38310)
- Manifest hash Bun cannot use skipped verification silently -> warning
(#38484)
- `bun audit` took a malformed registry answer for a clean audit ->
failed audit; a scoped registry is named and skipped (#38557)
- Pack: keys of a bundled dep that are not package names pulled in
outside files; `**/a/b` patterns, a BOM, trailing spaces disabled ignore
rules; a symlinked README was sent (#44902, #41531, #41871)
- `bun publish` published with `"private": "true"` or `1` -> any truthy
value is private (#41664)
- Auto-install read a package.json version from the wrong buffer, an
alias could redirect other packages (#42839)

#### Install core

- Killed or failed install left a half-linked package that later
installs accepted -> linked in a staging directory, renamed into place
(#38783; fixes #43256)
- Switching hoisted <-> isolated left a mixed tree -> whole tree
replaced, workspaces' `node_modules` too (#38779)
- Each linker switch kept a copy of the old tree in
`node_modules/.old_modules-<hex>` -> deleted after the install; `bun
prune` collects old ones
- `--dry-run` ran root lifecycle scripts and wrote `yarn.lock` (#38830)
- bunfig `[install] dryRun = true` was ignored -> honoured, also by
publish, `pm pack`, prune, dedupe, `audit fix` (#34979)
- Empty package.json (root or member) deleted the lockfile, exit 0 ->
error, lockfile kept (#39666)
- Download that failed for an optional dep was skipped (exit 0) although
another dep or a non-optional peer needs it -> `error: failed to
download <name>@<version>`, exit 1 (#43197, #43219; fixes #43214)
- `--offline` failed on what is below an optional dep it had skipped ->
only misses still reachable are errors (#43185)
- `--offline` exited 0 without a required dep of an optional package
installed in one place, missing in another
- Required direct dep left out for `os`/`cpu` was silent -> warning;
`bun add` reports it as skipped (#41956)
- `--production`/`--omit=dev` failed on a `workspace:`, `link:`, `file:`
devDependency whose target is missing -> left out (#36704; fixes #14134,
#15102)
- `--production` installed, and ran scripts of, a devDependency that is
also an optional peer; `bun prune --production` removes it (#42040)
- Commands that only read package.json opened it for writing -> work on
a read-only file (#38745)
- `$TMPDIR` on another file system than the cache kept a probe file per
install (#38859)
- `bun add`, `remove`, `update`, `patch` rewrote a package.json in which
nothing changed
- `Infinity`/`NaN` were written into package.json -> `null` (#41743)
- Relative `--cwd` was applied twice by `bun update`, `link`, `unlink`,
`why` (#31154; fixes #31152)
- Warnings from native code appeared at exit -> flushed when printed,
also from Workers (#43004; fixes #42989)
- `bun --bun run` worked only for the first user of a shared machine ->
`node` shim directory per user (#35565; fixes #7504)

#### Resolver and version picking

- Range picked a deprecated version (`@types/*` stubs under `*`) ->
prefers one that is not, like npm; a locked version stays until `bun
update` (#36630; fixes #7571, #25314)
- `bun add pkg` installed a deprecated `latest` -> highest release that
is not deprecated; `pkg@latest` still follows the tag (#40317; fixes
#40310)
- Missing or dangling `dist-tags.latest` failed add, install, `update
--latest`, hid the package from `outdated`/`update -i` -> highest
release, with a warning (#36671; fixes #12041)
- `*`, `npm:pkg`, `npm:pkg@*` skipped a prerelease `latest` -> follow
the tag, like npm (#39774)
- Wide range (`*`, `>=1`) nested the newest version -> shares a resolved
version that satisfies it; `bun update` no longer re-adds what `bun
dedupe` removed (#38770)
- `1.0.0 || ^2.0.0`, `=2.5.0 <2` resolved as their first exact version
(#30076)
- `>=0.5 0` was read as alternatives -> intersection, like npm (#32993)
- `^`, `~`, x-ranges admitted prereleases of their upper bound
(`pkg@>=5.0.0-alpha.0 <8` overrode `^4`) -> end at `<X.Y.Z-0` (#43102;
fixes #41371)
- Exact prerelease met a range that does not admit it (`overrides` keys)
(#43150; fixes #43138)
- `>1` meant `>1.0.0` -> `>=2.0.0`; `>1.2` -> `>=1.3.0`, like
node-semver (#43253)
- `<x`, `<*`, `>x`, `>*` matched everything -> nothing; `^x`, `~x` match
any version (#43258)
- Version number above 2^64-1 was read as 0 -> invalid (#42458)
- `Bun.semver.satisfies()` was true for `""`, `1.2.3.4`, `1.2.3 junk`
(#32998)
- `satisfies("1.0.0+build-1", "^1.0.0")` was false; `< =1` was misread
- `pkg@2x`, `1beta`, `1.2x` were read as ranges -> dist-tags (#35259;
fixes #19065)
- Words starting with `ssh`/`http` were read as URLs -> dist-tags
(#37133)
- Leading whitespace in a version literal (tag, folder, tarball, `npm:`,
`catalog:`) broke the install, lost a pin in `bun add --catalog`, or
panicked (#38331, #38309)
- Version published after the manifest was cached failed for up to 5
minutes -> manifest fetched again once (#39790)
- Root dep edited to another package (`npm:` alias) under a range the
old version fits kept the old package (#41774)
- Plain dep got a same-named `npm:` alias's package outside its range
(#43267)
- New dep that follows an `npm:` alias asked for a garbled name (`GET
/1.0.0no-deps - 404`) (#43515; fixes #23264)
- `file:` and `link:` on one directory became one package (#38114)
- `file:` folder in both `dependencies` and `devDependencies` was
duplicated (#41833)
- Editing package.json re-downloaded GitHub / tarball deps, re-fetched
git ones that `bun.lock` resolves (#37142)
- Registry package that bundles a `file:../x` dep failed to install
(#43125)
- Bundled dep's dep took a slot the bundling package resolves through
(#38848; fixes #29263)
- `bundleDependencies` were kept from one dependency group of a manifest
only (#38857)
- Tarball, folder or git package whose own `name` is invalid was
installed under it -> refused (#38633)
- Folder, tarball, git packages without a `name` got broken lockfile
rows -> named after their source (#38681; fixes #17060)
- Dangling `node_modules` symlink was a resolver error -> missing
module; isolated: a failed optional dep goes with the links to it
(#38756)

#### Hoisted linker

- overlayfs: a replaced package of a lower layer kept its old files
beside the new ones, exit 0
- Install over an existing `node_modules` hung when the OS refused the
pool's first thread
- `bun prune` left the `.old-<random>` of a killed install
- macOS: clonefile kept a file already at the destination -> replaced (a
nested package could hold two versions' files)
- Package was hoisted next to an out-of-range version of its peer
(#36300; fixes #20376)
- Dep nested under a package with `bundledDependencies` resolved the
bundled version (#43377; fixes #43351)
- After a version bump the old version of a transitive dep stayed at the
root (#41053)
- Required peer that a migrated lockfile left unbound stayed unbound
(#39343)
- Tree saved after a ranged peer gained a higher candidate differed from
the next install's (#38767)
- `file:` override on a registry package's or transitive dep was not
linked or resolved from the wrong directory -> from the project root,
linked with its deps (#38994, #44339; fixes #44299)
- One `file:` folder that cannot be hard-linked (EXDEV) switched all
later packages to copyfile (#40663)
- Failed optional dep left its `.bin` links (#44931)

#### Isolated linker, global store

- `node_modules` or entry that is a mount point (Docker volume) is kept
and reset in place
- `bun add -g` never linked bins into the global bin directory (#30451;
fixes #28597, #30450)
- Optional dep whose tarball cannot be downloaded or is corrupt failed
the install -> skipped with a warning, also `--offline` (#43229; fixes
#43212)
- `node_modules/.bun/node_modules` changed between installs of one
lockfile with `npm:` aliases -> links named by the dep, one layout
(#40356; fixes #40355)
- Packages that are peers of each other were installed twice (#29345;
fixes #29343)
- Missing `link:` target was accepted -> install fails, as hoisted
(#38045)
- Registry package's own `file:./vendor/x` dep failed -> linked from
inside the package (#38856)
- Workspace's scripts ran before the root's deps and bins were there ->
after everything else; a bin such a script writes is linked for every
dependent (#44868)
- Empty directory, or one without package.json, in a link slot stayed
(#41453)
- Store names of non-ASCII paths were garbled (#40719)
- Package used under its name and an alias got bins for one dependent
only (#44446)
- `--omit=peer` dropped a peer an ancestor provides; `bun prune
--omit=peer` agrees (#41416)
- Files deleted from a `file:` folder stayed in its copy (#37138)
- Links of a renamed or removed workspace stayed in `node_modules` (both
linkers) (#38511)
- `bun prune` left dangling links to removed optional / peer deps in
store entries (#42625)
- Switching the global install's linker removed `bun link` registrations
(#43582)
- Global store: a package whose peer is typed by the project's
`@types/*` was shared, TypeScript missed the types -> project-local
(#36796; fixes #36795)
- Ranged peer whose auto-installed version drops out of the saved tree
was linked to nothing (#38768)

#### Bins

- Hoisted: with `--silent` a failed bin link exited 0 and saved the
lockfile -> 1, nothing saved (#44997, #44958)
- Every install recreated `.bin` links, failed with `EEXIST` on a
read-only up-to-date tree -> a right link is left alone (per bin: chmod
1 -> 0, unlink 1 -> 0, symlink 2 -> 1) (#36701, #20067; fixes #14736)
- Several packages with one bin name: an arbitrary one won -> direct
deps first, then registry / git / tarball before workspace, `file:`,
`link:` packages, then by name (#31416, #41867)
- Isolated made bin targets 0777 -> `0777 & ~umask` (#41509)
- One bin that fails to link stopped the rest (#38972)
- `bin` entry whose value is not a string dropped the package's other
commands (#43159; fixes #43148)
- File system that refuses symlinks (EACCES / EPERM) failed the install
-> one warning, no `.bin` links (#40161)
- Bare `bun update -g`/`bun ci -g` linked no bins; a reinstalled global
package gets its command back (#43384)
- `bun pm migrate`/`pm trust` wrote uninitialised bin bytes into
`bun.lockb` (#37581)

#### bun.lock, bun.lockb

- Corrupt or crafted `bun.lockb` crashed or hung install, `pm ls`,
`why`, `outdated` -> validated at load, refused with a warning; files by
Bun 0.x are normalised and keep loading (#32753, #39720)
- Two strings whose hashes collide became one in the lockfile (#41642)
- Root `file:` or registry dep named like a workspace made Bun write a
lockfile it refuses (`Duplicate package path`) -> the member beats
`file:`; a registry range it does not satisfy gets the root slot
(#37245, #37248)
- Root alias of the registry package named like a workspace dropped the
workspace (#43567)
- `file:` paths from the lockfile were resolved from the wrong directory
after `bun update` or an override removal (#38850)
- Re-resolving install dropped packages `bun.lock` holds only through
optional peers (#39002)
- Package printed at several paths got its edges from the wrong row
(#39347)
- Re-saving moved a bundled peer off its entry's version, and a peer
nothing satisfies (#38837, #38892)
- `bun add` installed nothing when `bun.lock` repeats a dependency line
(#43271)
- `--lockfile-only` saved after an error; `--silent` failed on a
`bun.lock` it ignores (#44364)
- `bun.lock`, `bun.lockb`, `yarn.lock` ignored the umask (`yarn.lock`
0666) (#41808)
- `bun <path>.lockb` printed the directory's lockfile, read `.env`,
needed package.json -> prints the given file (git textconv works)
(#44870)
- `git+git://` resolutions did not load back from `bun.lock` (#38798)
- `bun pm migrate`/`pm trust` wrote another
`configVersion`/`patchedDependencies` than install; yarn entries without
`#sha1` (#41908; fixes #38193)
- Absolute or over-long workspace path in `bun.lock` / pnpm-lock.yaml
crashed or counted as pruned (#43405)
- `bun install --yarn` rewrote an unchanged `yarn.lock` every time

#### --frozen-lockfile, bun ci

- `bun ci` failed in a workspace when nothing changed (member with a
script and a `latest` row); one member's edit moved pins elsewhere ->
members are compared row by row, keep their pins (#44919; fixes #26070)
- Version the lockfile pins was blocked by `minimumReleaseAge` (#40032;
fixes #40031)
- After a migration with a root postinstall, or `pm migrate` to
`bun.lockb`, the next frozen install failed on the meta hash (#44501)
- With both `overrides` and `resolutions` only `overrides` applied ->
both; a lockfile without those rules fails frozen, 1.4's too (#38811)
- Direct dep pinned outside its unchanged range (one-sided merge) was
installed, frozen passed -> re-resolved; frozen fails (#43375; fixes
#43795)

#### Migrations (npm, pnpm, Yarn v1)

- Yarn Berry: a range `yarn.lock` has no key for is the workspace that
satisfies it; Yarn's own `packageExtensions`; `resolutions` keyed on an
alias; a name in several groups of a workspace; `catalog:` in an
extension; `%xx` in a folder name; clearer refusals (#45052)
- Yarn v1, npm: a dep the lockfile has no entry for is reported (#45052)
- npm: a scope registry with a path and no trailing slash gave
`InvalidNPMLockfile` (#38698)
- npm: a package's own `file:` directory was resolved from the project;
one that climbs out is skipped (#38990)
- npm: a `file:` directory copied by `--install-links` failed with
EISDIR (#43103)
- pnpm: `pnpm-workspace.yaml` was read only with a migratable lockfile;
`bun patch --commit` then segfaulted (#38754)
- pnpm: the `pnpm` block was removed from package.json -> kept, fields
copied (#38775)
- pnpm: `onlyBuiltDependencies` was lost -> `trustedDependencies`;
isolated reports blocked scripts (#38780)
- pnpm: importer rows differed from a package.json parse, the next
install rewrote `bun.lock` (#38791)
- pnpm: only used catalog entries were migrated (#38829)
- pnpm: non-string keys / values in `pnpm-workspace.yaml` crashed ->
scalars copied as written (`react: 18` -> `"18"`) (#44470)
- Read-only commands and failed installs wrote a migrated lockfile and
package.json edits -> only a command that saves a lockfile (#38804)
- Yarn v1: workspace packages were dropped when the root has no deps
(#38878)
- Yarn v1: `npm:` alias packages were named after the tarball URL
(#38948)
- Yarn v1: registry entries without `resolved` failed (#38985)
- Yarn v1: an entry without a buildable resolution aborted the migration
-> skipped with a warning (#39001)

#### Workspaces, catalogs

- Member without a `name` failed the install -> skipped (#38781; fixes
#6317)
- Dist-tag (`latest`, `""`) on an unpublished sibling got a 404 -> links
the workspace (#35468; fixes #4830)
- Empty `workspace:` spec linked the project into itself -> error; a
glob like `../*` made the root its own member (#41653)
- Workspace globs skipped symlinked member directories (#41567)
- Range linked to a workspace stayed linked after the root dropped it
(#43468)
- Workspaces of a package.json in `/` (or `C:\`) were not found (#43404)
- `workspaces` glob that fails named no path (#43517)
- `bun update <name>` rewrote `workspace:^`, `file:` and other
non-registry entries (#38847)
- `bun add member@workspace:^` lost the spelling (#38866)
- `bun update` printed no row, or another catalog's version, for a moved
catalog entry (#38763)
- `bun update -i` updated one of several catalogs defining a package ->
a row per catalog (#36446)
- Failed `catalog:` dep always suggested `bun add --catalog` -> only
where that works (#44638)
- `npm run <script> -w <ws>`/`--workspaces` inside a script recursed
forever -> runs the workspace's script (#33800; fixes #22719, #24838,
#33796, #41543)

#### Overrides, peers

- Auto-installed peer took the newest version of one asker -> newest
that everyone asking accepts (#36646; fixes #8292, #15711)
- `*` peers resolved by manifest arrival order -> after their siblings;
unprovided peers in a fixed order (#37713)
- `incorrect peer dependency` was checked against any copy in the tree,
hoisted only, every install -> the copy each dependent resolves, both
linkers, only when something was resolved (#41936; fixes #7403)
- Peer that a `file:`, `link:`, tarball or git dep provides was looked
up in the registry (#33156)
- Peer dependency on `bun` downloaded the `bun` npm package (80-170 MB)
-> optional (#39757)
- `minimumReleaseAge` blocking a peer or optional pin failed the install
-> warning (#38995)
- `bun update name@version` bound a peer row to a stale lockfile
leftover (#38901)
- Override replaced the link to a member of that name (#37271)
- Changing `overrides` / catalogs re-resolved rows a package no longer
owns (#38849)
- `bun update <name>` left rows of a `$name` override stale, frozen
passed (#43981; fixes #43975)
- `bun update --latest` moved versions an override selects; `-r` wrote
`"latest"` into package.json (#44485)
- `overrides`/`resolutions` that are not objects were ignored -> error
(#40729)
- Keys `foo@catalog:`, `my-foo@npm:foo[@^1]` (Yarn) were ignored with a
warning

#### Registries, credentials, .npmrc, bunfig

- `.npmrc` credentials follow npm's key walk: `//host/` covers paths
below it, `_auth` sent as written, a line reaches registries named by
`--registry` / env / `$VAR`, a tarball on another https host gets that
host's line; a sibling or look-alike path inherits nothing; `:443`/`:80`
keys keep working (#40423; fixes #28233, #30311, #30513, #40549)
- Registry URL without a trailing slash lost its last path segment
(#36294; fixes #5368, #14017, #16185, #20373)
- `user:pass@` in a registry URL from an env var was dropped (#38834)
- `user@`/`user:@` sent nothing -> Basic auth with an empty password
(#39063)
- URL credentials were sent percent-encoded -> decoded (`p%40ss` sends
`p@ss`) (#39603)
- `.npmrc`: `;`/`#` comments were part of the value; `ignore-scripts=1`,
`=True`, empty did nothing -> as npm (#41916)
- `.npmrc` keys and values kept Unicode whitespace (#41714)
- `--ca`/`--cafile` were shadowed by a config file's `cafile` -> one
setting, a later layer replaces both (#41898)
- `-g` commands ignored `globalDir` from `--config` and `~/.bunfig.toml`
(#35454; fixes #4553; refs #12886)
- `~` in `cache.dir`, `globalDir`, `globalBinDir`, `cafile` was a
literal directory (#36667; fixes #10152, #26715)
- `[install.cache] disable = true` still cached manifests (#36219)
- Unreadable `.npmrc` / bunfig.toml was skipped silently -> warning
(#41693)
- `bun publish`, `pm whoami`, `audit`, `pm view`, `pm diff` ignored
`http_proxy` and `NODE_TLS_REJECT_UNAUTHORIZED=0` (#38075, #38093)
- `bun pm whoami` sent a stub CI name -> the real vendor
(`ci/github-actions`) (#40805)

#### Network, retries

- Retries came at once, one network error halved concurrency -> 250 ms
doubling to 10 s with jitter, `Retry-After`, 429 / 408 retried,
certificate errors not (#40009)
- Timed-out request was retried silently -> a warning per retry (#35168)
- `BUN_CONFIG_MAX_HTTP_REQUESTS` was ignored (#38744)
- Help and docs said `--network-concurrency` defaults to 48 -> 64
(#38759)

#### Cache, tarballs, extraction

- Process could exit before the manifests it fetched were cached:
`--offline` right after an install could fail
- `file:` / URL tarballs were cached by path or URL, two projects or
branches got each other's package -> by the integrity in `bun.lock`
(#39016)
- `file:` / URL tarball deps were reinstalled on every run (hoisted) ->
verified by a `.bun-tag` (#35576; fixes #8260)
- `--force`, `bun update`, re-adding the same path / URL kept old bytes
or the old hash -> direct tarball deps are fetched again (#31868; fixes
#31864)
- Re-extraction leaked the displaced cache folder; extraction
directories stayed in `$TMPDIR` (#38702, #33979; fixes #33977; refs
#28062)
- Two registries on one host (port, path, http) shared cache entries
(#41636)
- Concurrent installs sharing a cache deleted each other's entries
(ENOENT without `RENAME_EXCHANGE`, NFS) -> first writer wins (#36229;
fixes #36227)
- Gzip trailer was not verified (since 1.3.14) -> a damaged tarball
fails; also `Bun.Archive` (#41503)
- Body that is not gzip installed as an empty package; a tarball without
package.json stayed in the cache
- Query string / fragment of a tarball URL ended up in the extraction
folder name (#39011)
- `github:` tarballs extracted symlink entries -> skipped, like npm
(#31481)
- Rewriting a cached manifest unlinked it first -> atomic replace
(#38916)
- `--no-verify` cached unchecked bytes under the lockfile's hash, for
all projects -> own key; isolated kept such a copy

#### git, GitHub, folder deps

- Cached git dep never saw tags / branches pushed later; `bun update`
follows moved refs (#35566; fixes #11548, #13769, #15336, #18947)
- `git+ssh://git@host:2222/..` lost its port in the ssh fallback
(#36932; fixes #36931)
- `deploy@host:org/repo.git` became `git@deploy@host` (#35026)
- `bun add name@git+ssh://host/repo.git` lost the alias for hosts other
than GitHub (#36687; fixes #13891)
- `github:o/r#feat%2Fx` silently installed the default branch; an
escaped owner / repo is an error (#43172)
- Optional git dep that cannot be cloned or lacks the ref failed the
install -> skipped with a warning, also when locked (#43076, #43352;
fixes #43350)
- Several `github:` / git refs on one commit, or a `file:` folder
declared by several packages, failed with EEXIST or wrote a lockfile Bun
refuses -> one package (#42640, #42545)
- Concurrent cold installs replaced each other's git cache folder
(#42656)
- `GIT_DIR` (git hooks) made install and `bun create` work on another
repository -> 13 git variables are not passed on (#41715)
- `file:` folders declared by git / tarball packages were resolved from
the project (#38816)
- Bare word or `git@host/path` was handed to `git clone` as a local
path; a URL could be read as an option
- `bun link -g`/`unlink -g` failed; `bun link -g <name>` removed the
registration -> `-g` is ignored (#33142; fixes #21397, #33141)

#### Lifecycle scripts, trust

- Hoisted: beyond `--concurrent-scripts`, the next script waited for all
running ones to end -> for one
- Failed or unstartable script left other packages in place whose
scripts never ran, and never would -> removed
- `trustedDependencies` entry added to a member after the first install
did nothing -> its blocked scripts run once (#41803)
- Isolated: a package trusted after it was installed never ran its
scripts (#41845)
- Package installed through an `npm:` alias could not be trusted
consistently -> by its real name in package.json, `--trust`, `pm trust`,
`bun.lock` (#39443)
- `bun pm untrusted`/`trust`/`ls --trusted` followed a stale lockfile,
knew nothing with `bun.lockb` -> read package.json (#41933, #34327)
- `pm untrusted`/`trust` listed and reran workspace members (#41513)
- Failing script of a package both optional and required deleted the
package, exit 0 -> install fails (#43218; fixes #43211)
- `--concurrent-scripts` did not apply to the isolated linker (#42445;
fixes #42421)
- bunfig `concurrentScripts` beat `--concurrent-scripts` (#42422)
- Node-gyp shim was unreachable when the temp directory falls back to
the cache (#38098; fixes #38079)
- `--help` said dep scripts never run -> `--ignore-scripts` also skips
trusted ones (#38893)
- `bun pm -g untrusted` suggested `bun pm trust` -> `bun pm -g trust`
(#44663)
- After a failed script, a half-deleted git / tarball / patched package
counted as installed

#### Security scanner, audit

- Scanner saw registry packages only -> every resolved dep (git, GitHub,
tarball, `file:`, `link:`), URLs without userinfo / query; a fatal
advisory stops them before install and scripts (#37590)
- `--production`/`--omit=dev` failed when the scanner is a devDependency
(#31031; fixes #31028)
- Scanner process loaded `.env` itself -> gets the install's
environment, follows `--env-file`, `--no-env-file`, `env = false`
(#44845)
- `bun audit fix` could downgrade into an older major (#39317)
- `bun audit --help` lacked `--production`, `--omit`, `--dry-run`,
`--cwd` (#38836)

#### add, remove, update, outdated, link

- Hoisted: `bun remove foo` deleted the `node_modules/foo` another
workspace has for `"foo": "npm:bar"`
- `link`, `install <pkg>`, `install --analyze`, `update -i`, `remove`
exited 0 after `Failed to install N packages` -> 1 (#44989)
- `bun add ../path` from below package.json, `bun install -g .` resolved
from the wrong directory -> the cwd (#43348, #36623; fixes #5682)
- `bun add <folder | tarball | git url>` of a declared package wrote it
twice (#43380; fixes #8031)
- First `bun add -g` used a package.json above the global directory
(`~/package.json`) (#30659; fixes #40683)
- `bun add --dry-run` in an empty directory created package.json
(#36694; fixes #13244)
- `bun remove` reordered package.json keys -> order, tabs, final newline
kept (#35586; fixes #7215, #9351)
- `bun remove -g` left dangling links in the global `node_modules/.bin`,
deleted foreign dangling links in the bin directory (#43403)
- `bun update <name>` blanked an optional / peer range no version
satisfies (#43195)
- `bun update` that only rewrites package.json said `(no changes)`
(#38918; fixes #38908)
- `bun update -i` repeated its prompt on mouse clicks and in short
terminals (#39683)
- `update -i`/`outdated` tables were sized in bytes -> display width
(CJK, emoji), cells escaped (#43333, #43236)
- `outdated`/`update -i` used the lockfile's ranges -> package.json's
(#42005)
- `bun outdated a b` needed all patterns to match -> union; `!pattern`
removes (#43309)
- `outdated`/`why` knew an aliased dep by one name -> either, both
printed (#43338)
- Coloured version diff dropped the build tag, left a tag that only
grows (`rc.1` -> `rc.10`) dim; long prerelease tags were garbled
(#38688, #38649)
- `minimumReleaseAge` error swapped range and name (#37895)
- `bun link`/`unlink --help` described a wrong `--save` default (#38915)

#### bun pm, why, info

- `bun pm pkg get` of several values altered one with a `}` in it, left
a key unescaped (#45061)
- `bun pm cache rm --cache-dir <dir>` cleared the global cache ->
`<dir>`; project config still cannot redirect it
- `bun pm bin -g`/`pm ls -g` failed before any global install -> work,
write nothing (#36622; fixes #6382, #7501, #13019)
- `bun pm cache rm` says when the global store goes with the cache; docs
name `--force` for stale manifests (#43667)
- `bun pm version` saw no repository in worktrees / submodules (`.git`
is a file) (#41754)
- `bun pm version prerelease` overflowed above 2^32; versions npm calls
invalid were accepted (#42459)
- `bun pm view pkg@tag`/`pm diff` used `latest` for an unknown dist-tag
-> error listing the tags (#42063)
- `bun pm`, `pm --help`, an unknown subcommand printed three help texts,
unindented, without `pm diff` and `ls -g` -> one (#39487, #43314,
#43098; fixes #43310)
- `bun pm bin -g` said `not in $PATH` for an entry with a trailing slash
(#28773; refs #28771)
- `bun why` printed a package's dependents at every occurrence
(exponential) -> once, repeats `*deduped` (#37922)
- `bun why name@version` failed for exact / scoped names, ignored a
workspace's version (#43284, #43290; fixes #43288)
- `bun why`: no-match error on stdout -> stderr (#43287; fixes #43285)
- `bun why`: branch left open after the last visible workspace dependent
(#43292; fixes #43291)
- `bun why`: a cycle back to the queried package is `*circular` at its
first repeat (#43300; fixes #43295)
- `bun why`: a root package.json without `name` was missing as a
dependent (#43277)
- `bun why`: a peer dependent was shown under the wrong copy (#41941)

#### pack, publish

- `workspace:`/`catalog:` versions came from `bun.lock` (stale, or
failed without one) -> the package.json files; no lockfile is read
(#38813; fixes #20477, #20829)
- `workspace:` aliases and directories were published as written ->
`npm:` aliases; the workspace's version (#38788, #38835)
- `!pattern` in `.npmignore` could not re-include files below an
excluded directory (#36699; fixes #15602)
- Relative `--destination`, `--filename`, `bun publish ./x.tgz` were
resolved from the package root -> the cwd (#38739, #38704)
- Failed pack left a partial tarball, `publish --dry-run` deleted an
existing one, a FIFO `--filename` hung -> built in memory, written once
complete (#38721)
- File names that are not UTF-8 were mangled, two became one entry
(#38715)
- File systems whose readdir reports `DT_UNKNOWN` lost entries (#38716)
- `bin` paths naming no file, `..`, a trailing slash in
`directories.bin` broke pack; an empty `bin` hid `directories.bin`
(#38720)
- File under several `bin` names was packed several times (#41291)
- Directory, bin or bundled dep that is there but unreadable was left
out silently -> exit 1 (#44400, #44873)
- `bun publish` accepted a `version` that is not semver -> refused after
the scripts; `v1.0.0` goes out as `1.0.0` (#43142)

#### bun patch

- `--commit` from a workspace member flipped the root package.json's
final newline to the member's
- Patch adding a file in a new directory failed with `EACCES` for
non-root users (#33573; fixes #13330, #33571, #38569)
- Hunks were applied by line number, wrote wrong text when the file had
moved; `-U0` hunks landed a line off -> placed by context like `git
apply`; `hunk #N does not apply` fails the install (#41733, #41695;
fixes #38879)
- Old-style diffs whose hunks delete lines starting with `-- a/` were
misparsed (#34870)
- Failed copy, missing cache entry, missing git or failed `git diff`
left a partial or no `node_modules/<pkg>` -> staged copy, everything put
back (#43188, #43160; fixes #43164)
- `bun patch` dropped the package's bundled and nested deps, `--commit`
recorded them as deleted (#43178)
- `bun patch` of a bundled dep deleted the bundled copy -> refused
(#43199)
- `bun patch` of a `file:` folder, `link:` or workspace package damaged
`node_modules` -> refused (#43223; fixes #43200)
- `bun patch <path>` replaced a folder with another version -> refused
when the version is not locked (#43367; fixes #43353)
- `npm:` alias could not be named by its package; a second `bun patch
<alias>` lost the first patch (#43382)
- Isolated: `bun patch <transitive>` made a stray `node_modules/<name>`
-> edits the store entry (#43383)
- `bun patch` replaced workspace links and symlinks above the package,
could write into the global store -> links stay; anything under the
cache is refused (#43406)
- `--commit` on a patched package wrote `.bun-tag` files and cache paths
into the patch, read `/etc/gitconfig` (#44349; fixes #19327, #44320)
- `--commit`: a new binary file became empty, a changed one was skipped,
exit 0 -> commit fails
- Member's `patchedDependencies` were ignored silently -> warning
(#39869)
- Isolated: `bun patch` replaced `node_modules/<pkg>` by a directory
that could not load its deps -> edits a copy in the store entry, the
link stays; global store: `--commit` restores the link (#43365)

#### bunx

- `bunx pkg@latest` relinked everything each run, a day-old tree was
refreshed every run -> once; served with a warning when the registry
gives no answer (#41226; fixes #23597, #41211)
- Several `bunx <pkg>` at once corrupted the shared install -> file lock
(#37851; fixes #37830)
- `directories.bin` was resolved from the wrong directory (#39096)

#### bun create

- Windows: a GitHub template silently overwrote files (its conflict list
was always empty) and wrote through junctions -> listed; `--force`
replaces the link
- `bun create <local template> <dir>` deleted an existing directory ->
merges, lists conflicts (links in the destination too), overwrites only
with `--force` (#36351; fixes #27948, #36341)
- `git init` / commit raced tasks and install -> finished first; the
first commit holds exactly the template (#41342; fixes #36953)
- `bun-create` tasks ran without a shell, not at all without deps,
failures ignored -> run like package.json scripts, with
`npm_package_name`; a failing task stops with its exit code (#41598,
#38955)
- Failed nested `bun install` was ignored -> exits with its code
(#38485)
- Absolute template path crashed -> local folder; `.`, `..`, an empty
name are refused (#41770)
- `bun create ./Component.tsx` overwrote your stylesheet,
`styles/globals.css`, `lib/utils.ts` -> kept and listed; `--force`
overwrites (#44668)
- Template's package.json indentation was lost (#36965; fixes #36964)
- Missing-export hint printed `{{` `}}` (#37847)

#### Auto-install

- Timers and I/O callbacks ran inside `require()`, `import()`,
`Bun.resolveSync()` while a package was fetched -> the thread waits; git
deps are no longer cloned at run time (#36159; fixes #43842)
- Second version (`pkg@2` after `pkg@1`) with the manifest in the disk
cache failed with `Unexpected` (#36659; fixes #10411)
- Failed lookup failed until the process exited -> a 404 stands as long
as a cached manifest (5 min, or its `Cache-Control`); any other failure
is asked again once the event loop has gone around (#42883)
- Failed download limited `fetch()` to 4 concurrent requests for the
rest of the process (#42945)
- `--prefer-offline` still asked the registry -> cached manifest
(#36776)
- Resolve errors did not say why (#38180)
- bunfig `install.prefer` worked for `bun run <file>` only -> also `bun
<file>`, `bun test`
- Dep whose name or version has a JSON escape fell back to `latest`

#### Completions, installer

- Second completions line was added to a `.zshrc` that already sources
`~/.bun/_bun` (#35561; fixes #10897)
- fish completions went to `/etc` when `~/.config/fish/completions` is
missing (#41659)
- install.ps1: the `another bun.exe is already in PATH` check never
fired (#38567)

#### Crashes, panics, hangs

- Hang until out of memory on a dependency cycle -> `dependency cycle
cannot be laid out in node_modules`, exit 1; a bundled back-edge
installs (#38976)
- Hang: a dangling `node_modules/@scope` symlink (hoisted)
- Hang: `--concurrent-scripts=0` -> the default (#42379)
- Hang: isolated, a tarball URL that cannot be requested (`ftp://`)
(#39672)
- Hang: isolated, cold cache, a git dep that is scp-form, a peer, from
`overrides` or spelled differently in `bun.lock`; after a failed clone /
checkout (#44448, #37156, #43075)
- Panic: paths that do not fit the path buffer in both linkers' walks,
`link:` targets, long linked names, `<alias>/package.json` ->
`ENAMETOOLONG`/`Invalid dependency name` (#37424, #37400, #38369,
#38571, #38575)
- Panic: over-long `--cafile`, `--cwd`, `add --filter`;
`$BUN_INSTALL`/`$HOME` with `-g`; bunx bin names; pnpm `link:`/`file:`
paths; progress bar names (#41702, #38413, #37394, #43537, #38558)
- Abort of all package manager commands, `bun run`, `bunx` in a cwd
within 13 bytes of `PATH_MAX` -> `ENAMETOOLONG`, or the file counts as
unreadable (#42502)
- Panic: `outdated`, `update`, `update -i` on a `bun.lock` without
`packages` (#41647)
- Segfault: package.json nested ~8,400 levels; a deep YAML alias chain
in pnpm / Berry files
- Panic: all `bunx` install flags at once
- Use-after-free in `bun add --trust` and at the end of an isolated
install (#31245, #39640)
- `Bad system call` on Android / Termux (`openat2`, `fchmodat2` under
seccomp) (#39084)

#### Windows

- Bins whose shebang uses `env -S` did not run (#31391; refs #22422,
#27924, #31387)
- `.bunx` shims broke when the package is on another drive (#38018)
- `bun remove` left `.bunx`/`.exe` shims (#35585; refs #11970)
- `bun install` failed in the root of a drive (#43437; refs #29273)
- `EINVAL: Failed to replace old lockfile` on exFAT / FAT32 (#33029;
refs #10169)
- Concurrently published cache entry was deleted; renames failed while a
virus scanner holds a new file -> retried for 5 s
(`BUN_INSTALL_WINDOWS_RENAME_RETRY_MS`) (#33884, #35568; refs #11250,
#28062)
- `bunx.exe` was written when the hard link fails -> `bunx.cmd` (#35686)

#### Performance

User-space instructions of `bun install` (`perf stat`, median of 3,
millions), generated project of N packages, loopback registry; warm =
nothing to do, relink = cache + lockfile, no `node_modules`.

|  | main N=400 | branch N=400 | main N=1600 | branch N=1600 |
|---|---|---|---|---|
| hoisted warm | 22.5 | 20.3 | 146.5 | 80.1 |
| hoisted relink | 24.5 | 21.6 | 150.3 | 83.3 |
| hoisted cold | 121.0 | 122.2 | 511.2 | 474.6 |

System calls per package (`strace -c`, slope between 100 and 300
packages of 31 files). Per run: a few more `lstat` (change 8), one read
per `file:` directory dep.

|  | main | this branch |
|---|---|---|
| hoisted cold | 215.96 | 211.95 |
| hoisted relink | 72.00 | 70.00 |
| hoisted warm | 4.00 | 4.00 |

Quadratic scans in the `bun.lock` workspace parse, `yarn.lock` printer
(#34537) and Yarn v1 migration (#37244) are gone; no allocator heap per
cached package.json (#44610).

|  | main | this branch |
|---|---|---|
| `bun pm hash`, 32,000 workspaces (ms) | 6,914 | 236 |
| `bun pm migrate`, Yarn v1, 20,000 entries (ms) | 24,210 | 270 |
| peak RSS of `bun install`, 2,100 workspaces (KB) | 90,840 | 34,728 |
| hoisted, 1,000 packages x 30 files, cache, no `node_modules`, 8 cores,
wall (ms) | 381 | 120 |
| same, 4 cores | 383 | 174 |
| same, 2 cores | 222 | 162 |
| same, `node_modules` lacks half, 8 cores | 140 | 54 |
| same, 4 cores | 174 | 65 |
| same, 2 cores | 161 | 107 |
| same, no cache, no `node_modules`, 8 cores | 363 | 394 |
| same, 4 cores | 418 | 401 |
| same, 2 cores | 477 | 541 |

#### Internal, no user-visible effect

- Earlier fold of 171 PRs is the first layer; 12 of them were taken out
again (#39403)
- One implementation each: trusted set, required-dep predicate,
failed-optional discard, bin linking, package.json walk, `--cwd`, cache
publish, specifier -> version (was 4), lockfile writer, registry request
- `pack()` consumes its context; one `binding.gyp` predicate; Yarn v1
migrator 2,032 -> 888 lines, no `unsafe` (#37670, #43912, #37539,
#38303, #37865)
- Debug builds: opt-in stack dump, `node` shim directory kept, manifest
cross-check deleted (#37335, #37384, #43170)
- Binary size pass (outlined helpers, shared flag tables, index sorts):
+464 KB -> see Performance; dead auto-install disk scan removed.
`unsafe` in src/install: 585 -> 540

#### Test-only PRs

- verdaccio replaced by a registry on `Bun.serve` (on main since):
#44055
- No public network: #35035 #35377 #35384 #36717 #36718 #36721 #36889
#38185 #38467 #39022 #39655 #40087 #40538 #40956 #42800 #42802 #43289
(fixes #43286)
- Concurrent, a cache per test: #38464 #38504 #40095 #40635 #39646
#40198 #40450 #39956 #39740 #41110
- Exact output / lockfile / layout (nested-overrides, bun-add-filter,
lockfile-sync, frozen-lockfile-pruned, pm-licenses, isolated-relink):
#39648 #39746 #40452 #40454 #40456 #41118
- Flake causes removed (stdin / pty races, racing ranges, cold manifest
cache, un-awaited matchers): #34175 #41401 #41591 #38858 #39190 #38627
#37333 #33279
- Faster (streaming-extract, git-deps, npm-arborist): #35915 #40447
#40977
- New guards (`..` tarball entries, Bun's own `bun.lock` passes frozen,
hoisted layouts, what a linker verifies): #37431 #42384 #43878 #41864
- Harness (`isRoot`, peak RSS, `pm trust` CPU test on Windows): #39232
#43192 #35626 (refs #11252)

#### Taken in part (what is in. Not: what is not)

- #19637: opt-in `bunx --trust` (runs the scripts of the package and its
whole closure). Not: trust by default
- #29794: change 4. Not: removing a stale copy no lockfile placed (`bun
prune`)
- #30522: change 14. Not: signing; #15601 open (refs #18611)
- #31327: a junk integrity in a v2+ `bun.lock` is an error. Not: pinning
a computed sha512; refusing an empty integrity under frozen
- #32022: 10 helpers redone on current code, 26 of 28 pinning tests.
Not: non-install parts
- #35253: `bun create` without a git identity stages the files, says the
commit was skipped. Not: a made-up author
- #35807: `npm_config_ignore_scripts=true` turns scripts off. Not:
`false`/`0` turning them on over a config file
- #36387: false `Workspace name already exists` on a hash collision.
Not: the same for `trustedDependencies` names (one stays blocked) (refs
#36386)
- #36973: docs of the store layout as it is. Not: a stability promise
- #38001: a token survives an http -> https redirect on one hostname.
Not: another port (still stripped) (refs #15516)
- #38832: its forced-arrival-order tests, expecting one copy (#38770).
Not: its two-copies policy
- #39505: cold generics -> runtime flags in summary printer, `pack`,
lockfile read. Not: typed arrays, coverage
- #39886: an override on a peer was lost behind a same-named root `npm:`
alias. Not: a peer without an override shares the alias (`react` ->
`npm:@preact/compat`)
- #40053: features, change 15. Not: `[install.packageExtensions]` in
bunfig; `pnpm.packageExtensions` without a migration; `catalog:` values
(ignored, warning)
- #40110: warning for a `patchedDependencies` key that matches no
installed version. Not: failing frozen
- #41724: moot for tarballs (named by integrity). Not: new names for
bare git clones
- #41759: a package.json in a sticky shared directory above the cwd
(`/tmp`) is ignored, with a warning. Not: the owner rule
- #41863: `pm pkg set --json` with a value that is not JSON is an error,
nothing is written. Not: strict JSON (`0x10`, `.5` are normalised)
- #41921: Linux: a script no longer outlives a killed install. Not:
signal forwarding; macOS
- #41925: symlinked bins / `directories.bin` are neither packed nor
listed. Not: race-proof opener, Windows, a symlinked `node_modules`
- #41931: change 2. Not: failing on trusted / patched edits or
equivalent spellings (refs #22689)
- #42001: after a failed script the next install reruns it and what it
blocked. Not: change 18
- #42030: a plain install re-reads a `file:` directory's package.json.
Not: failing frozen (warns)
- #42039: change 8; `bun prune` refuses a linked `.bun`. Not: the root
link (on purpose); a package's own directory on a warm run
- #42052: `bun pm view`/`info`: no project needed, several fields, npm
paths (`maintainers[0].name`), semver-sorted `versions`, `bin:`,
`DEPRECATED`. Not: new `--json` errors (refs #20673)
- #42139: npmrc.test.ts: isolation per case, concurrent. Not: exact pins
- #42400: `pm diff` compares a folder of this project as pack publishes
it. Not: folders of other projects
- #42597: nothing of the project's `.env` reaches install's `git`. Not:
proxy / TLS variables from `.env`
- #43067: the 255-byte name bound, the install cases. Not: the repo-wide
sweep
- #43375: which package owns an override row; direct deps outside their
range. Not: transitive rows
- #43556: `bun patch` never writes into the global store. Not: a
project-local copy that installs keep
- #43581: a registry package's peer / optional edge to a workspace
missing on disk is dropped. Not: the pre-flight report
- #43793: the walker looks at link entries; the `@scope` hang. Not:
materialising symlinked cache entries; #43788 unverified
- #44058: Windows: `node` shim next to bun.exe when `%TEMP%` is on
another drive (never run). Not: a missing `%TEMP%` (warning) (refs
#44033)
- #44406: change 4. Not: a copy no loaded lockfile names
- #44488: a dep's script finds its own `.bin`, then `PATH`, then shared
`.bin`: another package's bin cannot shadow `node`/`sh`. Not: bins per
script owner
- #44641: its install test and docs hunk. Not: 13 HTTP / fetch test
files
- #44793: Windows: the launcher of a `.cmd`/`.bat` bin (any case)
refuses cmd.exe special characters. Not: `bun run <bin>.cmd`
- #44798: a `node_modules` a git dep commits is dropped unless it
bundles (then without links). Not: bundled closure only
- #44847: a workspace's `node_modules` is reset when only the root's was
deleted. Not: moving stale directories aside; a marker

#### Not taken

- #20178: the isolated linker covers it; #6850 open
- #28515: prints the repo URL, no changelog
- #29512: `bun pm sbom`: own PR
- #30289: a `bun link` name would override every project
- #30340: checks after the fetch, fails open
- #30582: config surface without demand
- #30863 #40976 #43643: not install: install.sh, `bun upgrade`
- #31585 #41750 #43144 #43242 #44509: not install: `Bun.Archive`
- #31979 #37596 #37646 #37678 #39165 #40312: refactor nothing needs
- #33113: needs a Windows measurement
- #33972: masks a flake; cause gone with #37713
- #35105: distrusts the cache directory
- #35371: borrow refactor, no longer applies
- #35571: re-lays existing monorepos; #9838 open
- #35602: false positives, harmful advice
- #36081: needs the public registry
- #36464: stale; weakens lockfile parsing
- #36476: the next step after this PR
- #36556: reverses an earlier hardening
- #36557: its code is deleted (#44393)
- #36587: restores a dead warning
- #36631: contradicts a landed test; #7869 open
- #36682: the typo already gets a clear error
- #36688: a landed test pins the opposite; #13509 open
- #36696: own feature (config root for bunx / create); #13247, #5361
open
- #36888 #39741 #39742 #39957 #40639: test rewrite without measurable
gain
- #37143: cause removed by #44919
- #37551: part of #40284
- #37644 #38365 #39206 #39936 #40855 #44031 #44425: not install
(transpiler, `bun run`, `Bun.write`, pipe reader, repo-wide)
- #38741: lockfile v2 has shipped
- #38777: follows links out; main case gone with change 3
- #38810: turned down earlier; #10297 open
- #38814: draft; rows grow per path
- #39217: little demand
- #39499: codemod for an unlanded change
- #39724: refuses installable tarballs; see #41503
- #40045: 16 public JSON formats: own PR
- #40073: test flag that hides a race
- #40277: second store layout
- #40284: stacked on branches that have not landed (needs new task,
request and archive types in six other crates); follows separately
- #40300: Windows bunx.exe identity; #40298 open
- #40717: opposite of #36690
- #40753: loosens default-trust hardening
- #41764: subset of #43322
- #41866: opposite of #41803
- #43104: needs a new `bun.lock` key
- #43209: needs #43104's key
- #43322: breaking; realpath per workspace
- #44117: stacked on #44058
- #44621 #44809 #44813: draft
- #44622: changes patch hash and cache names; #25932 open

Obsolete (on main via the PR in brackets, or moot): #18582 (the Zig
build is gone) #35542 (#38333) #35603 (#41854) #36352 (verdaccio is
gone) #36670 (#38269) #36977 (#40010; `--tarball-dir`, #7956 stay open)
#37126 (#40734) #37137 (#38269) #37250 (#38333) #37616 (#40734) #37925
(#37426) #38648 (#40063) #38671 (#39229) #38687 (#40406) #38776 (#38796)
#38869 (#41302) #38891 (verdaccio is gone) #40766 (verdaccio is gone)
#41667 (#42881)

#### Fixed issues

Fixes #2773. Fixes #4553. Fixes #4719. Fixes #4830. Fixes #4852. Fixes
#5045. Fixes #5176. Fixes #5368. Fixes #5682. Fixes #5714. Fixes #6317.
Fixes #6382. Fixes #6883. Fixes #7215. Fixes #7403. Fixes #7501. Fixes
#7504. Fixes #7571. Fixes #8031. Fixes #8260. Fixes #8292. Fixes #9351.
Fixes #10152. Fixes #10411. Fixes #10897. Fixes #11548. Fixes #12011.
Fixes #12041. Fixes #12430. Fixes #12752. Fixes #13019. Fixes #13244.
Fixes #13330.
Fixes #13379. Fixes #13769. Fixes #13891. Fixes #14017. Fixes #14134.
Fixes #14736. Fixes #15102. Fixes #15336. Fixes #15602. Fixes #15711.
Fixes #16185.
Fixes #16282. Fixes #17060. Fixes #17759. Fixes #18670. Fixes #18947.
Fixes #19065. Fixes #19327. Fixes #20373. Fixes #20376. Fixes #20477.
Fixes #20829.
Fixes #21397. Fixes #22035. Fixes #22719. Fixes #23264. Fixes #23597.
Fixes #24838. Fixes #25314. Fixes #26070. Fixes #26715. Fixes #27948.
Fixes #28233.
Fixes #28597. Fixes #28668. Fixes #28967. Fixes #29263. Fixes #29343.
Fixes #30311. Fixes #30450. Fixes #30513. Fixes #30748. Fixes #31028.
Fixes #31152.
Fixes #31450. Fixes #31864. Fixes #33141. Fixes #33571. Fixes #33796.
Fixes #33977. Fixes #36227. Fixes #36341. Fixes #36795. Fixes #36931.
Fixes #36953.
Fixes #36964. Fixes #37830. Fixes #38079. Fixes #38193. Fixes #38569.
Fixes #38879. Fixes #38908. Fixes #38909. Fixes #40031. Fixes #40310.
Fixes #40355.
Fixes #40549. Fixes #40683. Fixes #41211. Fixes #41371. Fixes #41543.
Fixes #41625. Fixes #42421. Fixes #42799. Fixes #42989. Fixes #43138.
Fixes #43148.
Fixes #43164. Fixes #43200. Fixes #43211. Fixes #43212. Fixes #43214.
Fixes #43256. Fixes #43285. Fixes #43286. Fixes #43288. Fixes #43291.
Fixes #43295.
Fixes #43310. Fixes #43350. Fixes #43351. Fixes #43353. Fixes #43399.
Fixes #43795. Fixes #43842. Fixes #43903. Fixes #43975. Fixes #44299.
Fixes #44320.
Fixes #44669.

Expected fixed, first exercised by CI (Windows; close by hand): #28062
#11250 #11970 #29273 #10169 #22422 #27924 #31387 #11252 #28771 #44033
#12886

Covered by a PR taken in part (close by hand): #18611 #36386 #15516
#22689 #20673

#### Credits

- @Jarred-Sumner: #39403 (the earlier fold), #20067, #31245, #31416,
#33884, #37590, the design of #31447, part of #31327.
- @MarshallOfSound: #40057, #40025, #40053, #40012, #40009.
- @alii: #45052, #45051, #40423, #31481, #33279, helpers of #32022, half
of #39505, review findings in #35259 #35468 #35586 #38047.
- @dylan-conway: #19637 (opt-in), the order-dependent shapes of #36476
(tests here), the off-by-one fixes of #33113, the objection behind
#37138.
- @nikelborm: #41291.

### How did you verify your code works?

- Suite (offline, root): main's tests on main 146 files, 9,099 pass, 306
fail; this branch 174 files, 16,067 pass, 42 fail; 6,277 passing tests
are new, 268 that fail on main pass. Of the 42, 40 fail on main too; 1
needs Docker, 1 test deps.
- Absorbed = a test here covers the claim, passes here, fails on main.
Exceptions: test-only PRs, guards, performance PRs (measured); by
reading only: #39640 #37335 (no test possible), #34175 (Docker), #38858
#39190 #41401 #41591 (flakes), #40447 #39740, the refactors.
- Randomized differential: 600 generated projects (every dep kind, both
linkers, both lockfile formats) through installs, edits, prune, patch,
damage, kills; all output compared. First install succeeds: 545 -> 559.
Projects that violate an invariant:

|  | main | this branch |
|---|---|---|
| repeated install rewrites files | 265 | 10 |
| repeated install reports work | 101 | 2 |
| repeated install changes the tree | 22 | 3 |
| lockfile depends on manifest arrival order | 57 | 0 |
| exit code depends on the linker | 20 | 0 |
| writes outside the project | 21 | 0 |
| declared dep cannot be resolved | 13 | 1 |
| dep resolves to a wrong version | 4 | 0 |
| frozen / relink / cold cache fails on its own lockfile | 7 | 0 |
| package.json rewritten with the same bytes | 7 | 0 |
| crash or hang | 3 | 0 |
| dangling symlink | 2 | 0 |
| bin not executable | 2 | 2 |
| leftovers in the temp directory | 230 | 1 |
| tree or exit code after kill + reinstall differs (timing) | 3 | 4 |

- Credentials: 805 requests with main, this branch, npm 11.17.0: 534 as
main; 170 wider (npm too in 140, 30 deliberate); 82 narrower; 19 another
credential. 142 non-http(s) registry URLs x 16 routes: no request (main:
66).
- Secrets: a secret in every input of 698 projects x 6,490 commands, not
masked: data asked for on stdout, source excerpts under a diagnostic,
echoed arguments.
- Lockfiles: 90,000 mutated `bun.lockb` x 7 commands: no crash or hang.
- Version picking: npm-pick-manifest 11.0.3 over 200,000 generated
pairs: differs only for a dangling written `latest` and `^1.0.0-a.0`.
- Debug + ASAN: 9 files (1,253 tests); 200 rounds of #31245's race. Two
installs at once, 252 packages: tree right in 150 of 150 rounds, 2 of
300 exit 1 (main: 35 of 40 wrong, 69 of 80).
- Hoisted thread pool vs main thread, one binary: 704 generated
projects, 8,800 fuzz cases, CPU starvation x 4 backends, 432 injected
I/O errors, thread and fd limits, script order: equal.

Not verified:
- Windows, macOS: CI only. Android (#39084): a seccomp filter on Linux,
no device.
- By script, no test: `--trust` with an alias, an aborted upgrade leaves
no holes, two installs at once. overlayfs: a simulated `EXDEV`.
- Known limits: change 19.

Co-authored-by: nikelborm …
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

This is Claude (an AI assistant) writing on Jarred's behalf.

Included in #44968.

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.

3 participants