Skip to content

install: don't let overrides displace workspace member links - #37271

Closed
robobun wants to merge 9 commits into
mainfrom
farm/33104185/workspace-member-override-loop
Closed

robobun wants to merge 9 commits into
mainfrom
farm/33104185/workspace-member-override-loop

Conversation

@robobun

@robobun robobun commented Aug 9, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

# package.json
{ "name": "root", "workspaces": ["packages/*"],
  "dependencies": { "no-deps": "^1.0.0" },
  "overrides":    { "no-deps": "2.0.0" } }
# packages/m/package.json   { "name": "no-deps", "version": "1.0.0" }
$ bun install
error: Package "no-deps@2.0.0" has a dependency loop
  Resolution: "no-deps@workspace:packages/m"
  Dependency: "no-deps@^1.0.0"
error: An internal error occurred (DependencyLoop)

Every install fails the same way, including --frozen-lockfile; there is no working state. The same happens for every other dependency shape that links the member: an explicit "no-deps": "workspace:*", a wildcard range on a version-less member, and a "catalog:" range the member satisfies. The member-declared flavor (a sibling member depending on no-deps@^1.0.0) silently nests an overridden registry copy under the sibling instead of linking it.

Cause

Dependencies that accept a workspace member resolve to the member (the parse rewrite for satisfying ranges, the resolver's wildcard rule for version-less members, the catalog materialization feeding either), while the root keeps its injected workspace edge on the assumption that both edges resolve to the member. The override gate in enqueue_dependency_with_main_and_success_fn only exempts workspace-behavior edges (the injected ones), so the override rewrote the member-linking edge to a registry range. The root then carried two resolutions for one name, which the hoister reports as the misleading DependencyLoop internal error.

Fix

Overrides now skip any edge that, absent the override, links a workspace member of the current install (links_workspace_member in PackageManagerEnqueue.rs): workspace-tagged edges, npm edges whose range is a wildcard or satisfies the member's version under linkWorkspacePackages, and catalog: edges through the version the catalog materializes to. The loader's peer-binding mirror of the gate (resolve_peer_dep_version_based) gets the matching exemption for the edge flavor that can reach it.

Two presence subtleties shape the member check:

  • workspace_paths also holds names a workspace: dependency registered with no matching member, and an override is currently the only thing that makes such a dependency installable (it redirects it to the registry), so the check cannot key on that map.
  • The lockfile's package index still holds a member a previous install resolved after the member directory is deleted, so keying on it would make the stale link permanent: every install would fail with Workspace dependency "no-deps" not found with no way out short of deleting bun.lock. The check keys on the root package's injected workspace edges instead, which are rebuilt from package.json on every install, so removing the member hands the name back to the override (one re-save; --frozen-lockfile reports the usual "lockfile had changes" until regenerated).

Why this direction

npm rejects an override that conflicts with a workspace member outright (EOVERRIDE: Override for no-deps@^1.0.0 conflicts with direct dependency, verified with npm 11 against the same registry fixtures, and it errors even when only the workspace declares the name). Bun deliberately allows this configuration instead: #17417 established "overrides do not apply to workspaces" for the injected edges, and the real-world config behind it (ariakit overrides @ariakit/react to its own workspace folder) wants the member to stay linked. This PR completes that rule for the other edges that link a member. Overrides keep applying everywhere else with the member's name:

  • registry packages' dependencies on the name (they resolve from the registry, so the overridden copy installs nested when the member holds the root slot, same as before)
  • root/workspace ranges the member does not satisfy (they already resolve from the registry; the override picks the version)
  • workspace: dependencies with no matching member, and names whose member directory was removed (the registry rescue above)

Verification

Eleven tests in test/cli/install/bun-workspaces.test.ts. Seven fail on an unmodified build: the DependencyLoop shapes (satisfying range, workspace:*, version-less wildcard, catalog:), the sibling-member flavor that nested no-deps@2.0.0 under the sibling, and both member-removal migration tests (their first install already loops). The other four pin boundaries that must keep working: registry rescue for a missing member, non-satisfying range + override, non-satisfying workspace: range still rejected at parse, and peer dependencies binding to the member across a lockfile reload. The main test also covers lockfile stability, --frozen-lockfile reinstall, and the override still applying to a registry package's dependency on the member's name in the same install.

Locally green: bun-workspaces (74), overrides (7), catalogs (18), bun-install-registry (229 + 5 todo), isolated-install (62), bun-lock (17), bun-lockb (6), lockfile-only (2), lockfile-version-2 (10).

Lockfiles written by current bun for the member-declared shape (the only flavor that produced a lockfile at all) migrate on the next plain bun install: one re-save that links the member, stable from then on; checked by hand against a lockfile generated with 1.4.0-canary. A --frozen-lockfile install against such a stale lockfile reports the usual "lockfile had changes" error until it is regenerated.

Related but distinct: #37248 / #37249 / #37264 cover name collisions between npm deps and workspace members without overrides involved; this failure reproduces identically with those changes applied, and none of them touch the override gate.

An overrides entry for a name that is also a workspace member made every
install fail whenever a dependency linked the member (a satisfying npm
range or a workspace: range):

  error: Package "no-deps@2.0.0" has a dependency loop
  error: An internal error occurred (DependencyLoop)

Package.parse rewrites such edges into workspace links, but the override
gate in enqueue only exempted workspace-behavior edges, so the override
rewrote the linked edge back to a registry range and the root carried two
conflicting resolutions for the same name.

Overrides now skip any edge that links a present workspace member,
completing the rule from #17417 that overrides do not apply to
workspaces. They still apply to registry edges with the member's name, to
ranges that do not link the member, and to workspace: dependencies whose
member is missing.
@github-actions github-actions Bot added the claude label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 12 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2350e6c5-653c-4ee3-8887-38a4d49b87e1

📥 Commits

Reviewing files that changed from the base of the PR and between 501be58 and c13a49b.

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

Walkthrough

Changes

Workspace Override Resolution

Layer / File(s) Summary
Workspace package detection and override eligibility
src/install/lockfile.rs, src/install/PackageManager/PackageManagerEnqueue.rs, src/install/lockfile/bun.lock.rs
The resolver detects existing workspace packages across workspace, npm, wildcard, version, and catalog references. Existing workspace links bypass overrides. Missing or incompatible dependencies remain eligible for overrides.
Workspace override install coverage
test/cli/install/bun-workspaces.test.ts
Tests cover workspace links, sibling and peer dependencies, catalog references, missing members, incompatible ranges, member removal, repeated installs, and frozen-lockfile installs.

Possibly related PRs

  • oven-sh/bun#36360: Both changes modify dependency-resolution eligibility logic in PackageManagerEnqueue.rs.
  • oven-sh/bun#36476: Both changes modify workspace-aware override resolution and lockfile handling.
  • oven-sh/bun#37264: Both changes modify workspace dependency resolution and wildcard/range handling.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main change: preventing overrides from replacing workspace member links.
Description check ✅ Passed The description explains the problem, cause, fix, design rationale, test coverage, and verification results in sufficient detail.

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

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/lockfile.rs`:
- Around line 2629-2645: Update contains_workspace_package to verify that a
workspace resolution matching name_hash also satisfies the requested dependency
version before returning true; pass the dependency version and required string
buffer or reuse the existing workspace-candidate resolution logic. Ensure both
override call sites suppress overrides only for compatible workspace members,
while incompatible members remain overridable. Add an automated regression case
for workspace:^3.0.0 with no-deps@1.0.0 and an override to 2.0.0.

In `@test/cli/install/bun-workspaces.test.ts`:
- Around line 2087-2094: In both process-spawn blocks in
test/cli/install/bun-workspaces.test.ts at lines 2087-2094 and 2121-2128, drain
the configured stderr pipe by awaiting stderr.text() concurrently with exited,
then retain the existing exit-code assertion; apply the same change at both
affected sites.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e8946d6-a7c6-480c-bc17-fba352bef3bd

📥 Commits

Reviewing files that changed from the base of the PR and between d2f48c6 and 8599dc0.

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

Comment thread src/install/lockfile.rs Resolved
Comment thread test/cli/install/bun-workspaces.test.ts Resolved
Comment thread src/install/lockfile.rs Resolved
Comment thread src/install/lockfile/bun.lock.rs Resolved
Comment thread src/install/lockfile.rs Resolved

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/install/bun-workspaces.test.ts`:
- Around line 2141-2175: Add a concurrent install test near the existing
workspace override coverage that declares a peer dependency, provides a matching
workspace member, and includes an overrides entry for that package. Assert
installation succeeds with the dependency resolving to the workspace member and
verify no overridden registry copy is installed, using the existing setup,
spawn, and filesystem assertion patterns.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c4a3522-6653-443c-80e4-ab83b284c4e1

📥 Commits

Reviewing files that changed from the base of the PR and between 8599dc0 and 5c0d2aa.

📒 Files selected for processing (1)
  • test/cli/install/bun-workspaces.test.ts

Comment thread test/cli/install/bun-workspaces.test.ts Resolved

@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 bun install's override resolution semantics — a policy decision on how overrides interact with workspace member links (where npm instead errors with EOVERRIDE) — a human look from someone who owns the install subsystem would still be worthwhile.

What was reviewed:

  • The new contains_workspace_package helper: bounds-checks pkg_resolutions[id] and correctly distinguishes present members from workspace_paths entries with no member.
  • Both override gates (fresh enqueue and lockfile peer-binding mirror) apply the exemption identically; the Tag::Workspace guard means non-linking npm ranges (^3.0.0) still get overridden.
  • CodeRabbit's non-satisfying workspace:^3.0.0 concern — confirmed unreachable (parse rejects before the gate) and pinned by the added test.
  • Tests drain piped stderr concurrently, use the file's setupTest() semaphore, and cover the boundary cases (missing member, sibling dep, peer dep, frozen-lockfile reinstall).
Extended reasoning...

Overview

This PR changes 3 Rust files in src/install/ and adds 7 tests to test/cli/install/bun-workspaces.test.ts. It adds a ~15-line helper package_index::contains_workspace_package in lockfile.rs and adds one clause to the override-eligibility gate in both enqueue_dependency_with_main_and_success_fn (PackageManagerEnqueue.rs:692) and its lockfile-loader mirror resolve_peer_dep_version_based (bun.lock.rs:3086). The effect: a dependency whose version is already tagged Workspace and whose name resolves to a package with a workspace resolution is exempt from override rewriting, so the workspace member link is preserved instead of being rewritten to a registry range and triggering the misleading DependencyLoop error.

Security risks

None identified. This is dependency-graph resolution logic operating on already-parsed lockfile/package.json data; no new untrusted-input parsing, no filesystem writes outside the existing install path, no network changes.

Level of scrutiny

High. This is core bun install resolution logic that runs on every install for every user. The code change itself is small and mechanically consistent (same predicate applied at both gate sites, matching the existing exemption structure), but it encodes a semantic policy: overrides never apply to edges that link a present workspace member. The PR description notes npm rejects this configuration entirely with EOVERRIDE, and Bun is deliberately choosing a different behavior that extends #17417. That is a product decision a maintainer should confirm, not something I should approve unilaterally.

Other factors

  • All prior review threads (CodeRabbit, comment-cop) are resolved. The stderr-drain nit was fixed in 5c0d2aa; comments were trimmed in 186d430; the peer-dependency test was added in 9b2a80a; the non-satisfying-range concern was withdrawn after verification that Package::parse rejects it before the gate.
  • The helper's bounds check (id as usize) < pkg_resolutions.len() matches the pattern used elsewhere in the same file (e.g., the candidate loop just below in bun.lock.rs:3105).
  • Test coverage is thorough: 3 tests fail on unmodified builds (per PR description), 4 pin boundary behavior including lockfile stability and --frozen-lockfile reinstall. Tests follow the file's local conventions (setupTest semaphore, test.concurrent, stderr assertions before exit code).
  • No prior claude[bot] reviews on this PR; this is the first pass.

…rrent members

The exemption read the loaded lockfile's package index, so deleting a member
directory after an exempted install made every subsequent install fail with
"Workspace dependency not found": the stale lockfile still held the member's
workspace resolution. Key the exemption on the root package's injected
workspace edges instead; they are rebuilt from package.json on every install,
so removing the member hands the name back to the override (one re-save, and
frozen installs report "lockfile had changes").

Extend the exemption to the two member-linking flavors the workspace tag
check missed, which still failed with the same DependencyLoop: wildcard
ranges on version-less members (linked by the resolver's wildcard rule, not
the parse rewrite, so their edges stay npm-tagged) and catalog: dependencies
whose materialized range links the member.
Comment thread src/install/lockfile.rs Resolved
Comment thread src/install/lockfile.rs Resolved
@robobun

robobun commented Aug 9, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:42 AM PT - Aug 9th, 2026

❌ @robobun, your commit c13a49b has 1 failures in Build #91034 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37271

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

bun-37271 --bun

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/install/lockfile.rs`:
- Around line 860-868: Update is_workspace_member_name to first check whether
name_hash exists in workspace_paths, returning false immediately on a miss; only
perform the existing root dependency scan when the pre-filter matches,
preserving the current behavior for possible workspace members.

In `@test/cli/install/bun-workspaces.test.ts`:
- Around line 2407-2420: Strengthen the migration install test around the
`spawn` call by asserting that `stderrText` contains the lockfile-save message
and excludes workspace dependency errors, matching the sibling test’s meaningful
invariants. Add a second install for convergence and assert its output and
resulting package state as appropriate, ensuring every read of `stderrText`
contributes to a failing assertion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0319b2e3-c379-4b21-a40e-cbb0aed66e42

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0d2aa and 501be58.

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

Comment thread src/install/lockfile.rs Resolved
Comment thread test/cli/install/bun-workspaces.test.ts Resolved
@robobun

robobun commented Aug 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

This is ready for review at c13a49b (the last commit removes a provably inert loader-side clause flagged in review; the peer scan cannot match workspace-tagged edges, so the exemption there was dead code). Summary of the verification: eleven workspace-override tests in test/cli/install/bun-workspaces.test.ts, seven of which fail on an unmodified build (the DependencyLoop shapes for satisfying, workspace:*, version-less wildcard, and catalog: ranges, the sibling nesting, and both member-removal migrations), with the boundary cases pinned (registry rescue for a missing member, non-satisfying ranges, peer binding across a lockfile reload). The install suites (bun-workspaces, overrides, catalogs, bun-install-registry, isolated-install, and the lockfile suites) are green against this branch's debug build.

CI note: build 91034 on this head is 195/196 green; the one red is test/napi/napi.test.ts on the macOS 14 x64 lane, a pre-existing failure unrelated to this change (all install suites passed on every lane).

Comment thread src/install/lockfile/bun.lock.rs Resolved
…'s result

A workspace-tagged peer falls through resolve_peer_dep_version_based to None
with or without the exemption: satisfies_dependency_version and the kind
fallback only match the Npm/Git/Github tags, so the early return and the scan
produce the same binding. Remove the clause and the contains_workspace_package
helper it existed for, and note the invariant in the mirror comment.
Comment thread src/install/lockfile/bun.lock.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the automated bug-hunting pass found no issues; both of my earlier inline findings (operand ordering at the override gate, and the dead loader-side Workspace-tag exemption + contains_workspace_package) have been addressed in a87b19b and c13a49b. Because this is a semantic change to install resolution — extending #17417's "overrides don't apply to workspaces" policy to every member-linking edge, with a predicate that must stay in lockstep with three separate linking rules — a human look would still be worthwhile.

What was reviewed:

  • links_workspace_member: catalog dereference, Workspace/Npm arms, the link_workspace_packages gate, wildcard vs workspace_versions.satisfies, and that overrides.get short-circuits before the root-edge scan.
  • is_workspace_member_name keying on the root's injected workspace edges rather than workspace_paths/package_index, so member removal hands the name back to the override — both migration tests exercise this.
  • The bun.lock.rs hunk is comment-only; confirmed no behavioral change to resolve_peer_dep_version_based.
  • Test coverage: eleven cases spanning satisfying/workspace:*/version-less/catalog:/sibling/peer shapes plus the negative boundaries (missing member, non-satisfying range).
Extended reasoning...

Overview

The PR changes when overrides apply during bun install: an override entry for a name that is also a workspace member no longer rewrites edges that would otherwise link to that member. It adds a ~50-line predicate links_workspace_member in PackageManagerEnqueue.rs (gated behind overrides.get(name_hash) so it only runs when a matching override exists), a ~15-line is_workspace_member_name helper on Lockfile that scans the root package's injected workspace edges, a comment-only clarification in bun.lock.rs documenting why the loader-side peer mirror needs no matching clause, and eleven tests in bun-workspaces.test.ts.

Security risks

None identified. Inputs are the project's own package.json (workspaces, overrides, catalog) and the lockfile; there is no untrusted-boundary parsing, no new syscalls, and no change to registry fetching or extraction.

Level of scrutiny

High. This is core package-manager resolution — a wrong exemption either regresses the DependencyLoop fix or silently stops applying overrides that should still apply. The predicate mirrors three linking rules that live elsewhere (Package::parse's satisfying-range rewrite, get_or_put_resolved_package's wildcard rule, and catalog materialization), and the PR description notes two earlier revisions of this branch shipped bugs from keying on the wrong member set. It also encodes a policy choice: Bun keeps the member linked where npm would reject with EOVERRIDE. That is consistent with #17417, but a maintainer should confirm the extension is the intended direction.

Other factors

Both of my prior inline findings on this PR are resolved: the && operand order now puts overrides.get(name_hash) first (restoring the empty-overrides fast path), and the provably-inert loader-side Workspace-tag clause plus its contains_workspace_package helper were deleted in c13a49b. The test matrix is thorough (seven failure shapes on an unmodified build, four boundary pins, lockfile stability and --frozen-lockfile reinstall, member-removal migration and convergence), and the author reports the wider install suites green locally with one unrelated Windows-aarch64 flake in CI. No CODEOWNERS entry covers src/install/. Given the resolution-semantics scope and the embedded policy decision, deferring to a human reviewer rather than approving.

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.

Jarred-Sumner added a commit that referenced this pull request Oct 16, 2026
…er consolidation (#45307)

### What does this PR do?

Follow-up to #44968. It fixes the regressions that showed up after that
PR landed, plus older package manager bugs found while checking them,
and makes `bun.lock` independent of network timing. Almost every fix
comes with a test that fails on `main`. Some earlier changes caused
regressions of their own and were taken out again; see "Known to be
still open".

Fixes #24190
Fixes #20015
Fixes #45070
Fixes #29944

#### Behavior changes to sign off

One line each. Details are in the groups below. "Reversible" means the
change can be backed out on its own, in the place named.

- Dependencies are resolved in one fixed order, not as their manifests
arrive (see "Resolution and lockfile"). With every manifest cached, that
is the order `main` uses. Not a small revert.
- Both linkers install through a `node_modules` that is a symlink (the
project's or a workspace's), remove nothing behind it in their cleanup
passes, and warn; `bun prune` stops with an error. Hoisted followed such
links before #44968; isolated replaced the project's with a real folder
(npm replaces, pnpm follows). Not a small revert.
- Behind such a link, a file or folder in `.bin` named like a package's
command stays, and the install fails with `EEXIST`. Reversible:
`is_behind_link` in `bin.rs`.
- Switching linkers: an entry that can't be renamed aside stays whole,
with a warning, and the install goes on. It used to be deleted, or the
install stopped. Reversible: `move_or_delete` in `isolated_install.rs`.
- Hoisted layout: a package's location depends on `bun.lock` only, not
on `--production` / `--omit` / `--filter` / `--os` / `--cpu`. Older
versions worked the places out anew for such a run, so an install that
lays something over an existing tree also removes the folders at places
only the older layout fills. With those flags that costs one more hoist
(about 5 % of such a run); fresh, warm and nearly all plain installs pay
nothing. Not a small revert.
- `bun.lock` gets three optional additions with no `lockfileVersion`
bump: `"unresolved"`, `"hasInstallScript"` and `"./<name>/<dep>"`
entries. Reversible: one writer and one parser block each in
`bun.lock.rs`.
- Every install checks `bun.lock` against the `overrides` that name a
version or a range: a locked version that a rule forbids is resolved
again, and `--frozen-lockfile` fails. 29 lockfile shapes written fresh
by 1.4 pass unchanged; a 1.4 lockfile that disobeys its rules (half a
merge, a migration, a rule added later) is rewritten. Reversible:
`rows_outside_overrides`.
- `overrides` for the name of a workspace apply to dependencies of the
root and of other workspaces again, as in 1.4, whose `bun.lock` failed
`--frozen-lockfile` and was rewritten. This undoes #37271, which came
with #44968.
- Security scanner: `bun add <pkg>`, `bun install <pkg>` and `bun update
<pkg>` send every package when there is no lockfile, only another
package manager's, or no `node_modules` folder, otherwise what the
command adds or moves. Reversible: `|| !loaded_lockfile` and `||
!has_node_modules` in `security_scanner.rs`, and the `narrow` line in
the runner.
- An unreadable `bunfig.toml` or `.npmrc` fails closed: commands that
install or publish stop (a directory there only warns). npm ignores an
unreadable `.npmrc`, so a root-owned `~/.npmrc` from `sudo npm login`
now blocks them. Reversible: the two calls of
`refuse_unreadable_config`.
- An `XDG_CONFIG_HOME` that is empty or not absolute is ignored, and
`$HOME` is used. Before, `~/.bunfig.toml` wasn't read at all then.
- Auto-install reads `.npmrc` and bunfig's `[install]` in every folder
from the project root down to the importing file (the nearer wins). It
turns itself off when one of them, or a `package.json` on the way up to
the workspace root, can't be read or parsed; a broken `~/package.json`
does that for everything below it. In a monorepo without a lockfile it
reads every workspace's `package.json` once.
- A workspace outside the project (`../x`, an absolute path, a link that
leaves the root's folder) is linked to and stays in `bun.lock`, but its
dependencies aren't installed and its scripts not run, as in npm; a
`note:` says so. Its own `bin` is still made executable. Commands that
install, typed in one that is a link, end with an error and change
nothing; on `main` they installed there. This affects layouts like
#25801 under the isolated linker, and #29598 asks for the opposite; both
stay open. Reversible: `Lockfile::is_member_outside` returns false.
- An `npm:` alias in a dependency's manifest applies to that dependency
only; the project's own aliases are still followed everywhere.
- Git dependencies are checked out without symbolic links: a link to a
file in the repository becomes a copy, other links aren't installed. On
Linux, files appear where nothing was installed before; on macOS, links
used to be installed; on Windows, they were small text files.
- Git dependencies need git 2.24 or newer (`git restore` is from 2.23;
`--end-of-options`, which `main` already passes for a `#ref`, from
2.24).
- Every git command that Bun runs for a git dependency has hooks,
`submodule.recurse` and replace objects off, and a cached clone's
`config` is rewritten with Bun's own text before each use.
- `gitlab:`, `bitbucket:`, `gist:` and `sourcehut:` dependencies are
written to `bun.lock` and printed as
`git+ssh://git@gitlab.com/user/repo#<sha>` (npm's form); they failed to
install from `bun.lock` on a clean machine. Old entries still install
and aren't rewritten by a plain install.
- Isolated: a dependency on the project itself (`file:.`, `workspace:*`
on the root) is a live link to the project folder, not a hard-linked
copy of the repository; the old copy is removed on the next install.
- Vendored libarchive gets one more patch,
`patches/libarchive/tar-npm-compat.patch`: a reader option `npm` that
only package tarballs set. Every other tar reader, `Bun.Archive`
included, reads as before. Reversible: remove it from
`scripts/build/deps/libarchive.ts` and drop the option.
- The global store rebuilds its entries once after this upgrade; `bun pm
cache rm` frees the old ones. Reversible: drop `GLOBAL_STORE_LAYOUT`
from the entry hash.
- Lifecycle scripts see `PATH` entries of the form `X/node_modules/.bin`
by their real paths, so a dependency's script no longer finds the
project's shared `.bin` ahead of the system `PATH` through a link.
Reversible: `resolve_bin_dirs_in_path`.
- `--exact` / `exact = true` also pins a typed range, also with
`--catalog` and for an alias: `bun add -E react@18` writes `18.3.1`, not
`18`, as npm does. Reversible: one condition in
`PackageJSONEditor::edit`.
- `bun add pkg@2.0.0` without a group flag rewrites every entry of a
name that is listed in several dependency groups, unless the install
leaves one of those groups out (`--omit`, `--production`). It wrote an
entry that no install reads, so nothing changed.
- A `package-lock.json` entry from another host without an `integrity`
keeps its version and takes its tarball and integrity from the
configured registry, with a warning; the whole lockfile used to be
ignored. One older test pinned that.
- `bun pm cache rm` empties the cache directory and keeps it, so one
that is a symlink, a mount point or in a read-only parent goes on
working (5 expectations changed). `bun pm cache rm <word>`, which
cleared everything, and unknown `bun pm cache` sub-commands are errors.
- `bun info`, `bun pm view`, `whoami`, `diff` and `bun audit` retry a
failed request like `bun install`; one 503 or dropped connection used to
end them. With the registry unreachable they fail after about 8 s, not
at once; a timeout or a 501 isn't retried. `bun publish` still uploads
once.
- `bunx`'s daily refresh costs one manifest request per package. `bunx`
installs into the cache directory that `~/.bunfig.toml` or `~/.npmrc`
names (macOS, Linux); it ignored both.
- `bun publish` refuses a prerelease version without `--tag` or
`publishConfig.tag`, like npm 11 (reversible: the second `if` in
`refuse_tag`), and a tag that is a version or a range.
- `bun publish` reads `publishConfig` again after `prepublishOnly`,
`prepack` and `prepare`, like the name and version (flags still win). Of
two equal keys in `package.json` the last one counts, as in
`JSON.parse`. A symlink at the name Bun gives the tarball is replaced,
not written through.
- `bun audit` fails on an empty answer (empty body, 204, 304). One test
from #44968 pinned the old result.
- pnpm migration: the `patchedDependencies` key of a git-hosted package
is rewritten in `package.json` to Bun's own key
(`name@github:owner/repo#<sha>`); with the old key the next install
dropped the patch silently. One expectation of two older tests changed.
- `bun pm version` follows node-semver's `inc`. Four existing tests
pinned other values.
- Piped `bun pm diff` prints `diff --git` for files whose contents are
in the patch (reversible: `patch_name` in `pm_diff_command.rs`).
`--json` has two new keys for binary files, `sha256Before` and
`sha256After`.
- `warn: incorrect peer dependency ...` is printed only with
`--verbose`.
- Isolated: a link that the user put in `node_modules/.bun` in place of
a package is replaced by every install; `main` kept it.
- With a `package-lock.json` that can't be used, `bun ci`,
`--frozen-lockfile` and `bun pm migrate` warn and try a `yarn.lock`;
they failed.
- A dangling link in `.bin` stays while the package it leads into is
there: its build makes the file.
- Beyond the package manager: the arguments of `Output::err`, the path
in a system error, the `$ script` line of `bun run` and the log
printer's source excerpt (bundler, `bun lint`, `bun fmt`, `bun check`)
spell out control and bidirectional characters, and a path or line that
isn't UTF-8 prints U+FFFD. In an excerpt a NUL stays. Reversible: one
place each in `fmt.rs`, `output.rs`, `sys/lib.rs`, `ast/lib.rs`.
- HTTP client: a request can carry a flag that lets other requests start
first while every connection is taken. Only package tarball downloads
set it. Reversible: `yields_to_other_requests`.
- Runtime: on Alpine (musl), `Bun.which` no longer takes a file without
the execute bit for a command. Reversible: one `#if` in
`c-bindings.cpp`.

#### Regressions from #44968

Resolution and lockfile:

- A `package.json` with both `overrides` and `resolutions`: #44968
applied both, so the first `bun install` after the upgrade rewrote
`bun.lock` and could move versions. As in every earlier release,
`resolutions` is read only when there is no `overrides`, and a
`bun.lock` written by 1.4 installs unchanged, frozen or not.
- `bun install` rejected its own fresh `bun.lock` with `dependency cycle
cannot be laid out in node_modules` when an optional dependency has no
published version that fits, or when a cycle is closed only through an
optional peer.
- A second `bun install` added a nested copy, and a `bun.lock` entry for
it: of a package whose name an unresolvable optional dependency has, and
of an unmet peer whose dependent is installed in two places.
- A fresh `bun.lock` could fail `--frozen-lockfile` right away, or
change on the next install with nothing edited: with optional peers, and
with a workspace and the published package of its name both installed
and a peer dependency on that name.
- `bun.lockb`: after a peer moved to another version, the old one stayed
in the file with nothing depending on it. `--frozen-lockfile` failed,
and `bun audit`, `bun why` and `bun pm hash` saw it. (A peer that ends
at a copy outside its range still leaves one.)
- `--frozen-lockfile` and `bun ci` reported an out-of-date lockfile when
a workspace has a peer dependency on another workspace's name but gets
it from its own dependency on the published version.
- `bun update` was held back by the peer range of a package that loads
another copy.
- `packageExtensions`: a fresh `bun.lock` failed its own
`--frozen-lockfile` when a rule matched a version the lockfile doesn't
list. Removing a rule that had added two or more dependencies could
crash this and every later install, and what it had added stayed
loadable.
- `bun add <name>` at the root of a monorepo that has a workspace of
that name wrote the registry's latest version but installed the
workspace. It writes `workspace:*` again. A root dependency on a
dist-tag of such a name links the workspace from the first install.
- A tarball, folder or git dependency whose own `package.json` has a
free-text `name` (`"Project: X"`, `"foo@1"`) failed to install. It
installs under the fallback name a nameless package gets, and `bun.lock`
loads afterwards.
- A git, GitHub or tarball dependency that depends on a `file:` folder
inside itself was installed without that folder's dependencies. A folder
that isn't in the package is now an error. `workspace:`, `link:`,
`catalog:` and tarball-path entries of such a folder are left out: they
mean something in the author's project only.
- `touch package.json && bun install` exits 0 again. An empty
`package.json` next to a lockfile is still an error.
- Ranges such as `>=0.0.0-a *` stopped matching prereleases of `0.0.0`.
A comparator that means `>=0.0.0` admits them, as in npm.

Git dependencies:

- `bun install` could run `git fetch --prune` on your project's own
repository, and delete local branches and tags, when the cached copy of
a git dependency was no longer a repository and the cache sits inside
the project's work tree. Every git call now names its repository with
`--git-dir`.
- Several installs that share a cache failed with `cannot lock ref` when
a git dependency had new commits (9 of 20 runs). Such a fetch runs once
more.
- After upstream renamed its default branch, a git dependency without a
`#ref` failed on every warm cache. A cached repository follows the
remote's current default branch, also for `#HEAD`.

Linkers:

- With `node_modules` a symlink, an isolated install deleted everything
in the folder behind the link. Hoisted did the same when that folder
holds a `.bun` folder, deleted a folder there named like a dependency
that had left `package.json`, and replaced a workspace's linked
`node_modules` (on a volume, a cache disk) with a real directory.
- Hoisted: a `bun remove` or `bun install` that failed to save
`bun.lock` or `package.json` still deleted packages.
- Hoisted: `bun add`, `bun install` and `bun remove` deleted the copy of
a dependency that a package's new version bundles, or ships undeclared
in its `node_modules`.
- Replacing a tree (switching linkers, or an isolated install after `rm
-rf node_modules` at the root) deleted `.cache`, `.vite` and other
tools' dot entries in every `node_modules`. They stay. What belongs to a
package manager (`.pnpm*`, `.modules.yaml`, `.package-lock.json`,
`.yarn-integrity`, `.yarn-state.yml`) goes with the old tree.
- Isolated: a project path close to `PATH_MAX` crashed on the new
staging directory, and `bun pm untrusted` / `bun pm trust` didn't see
`link:` dependencies.
- "Blocked N postinstalls" was no longer printed for Linux platform
packages, because the registry's full metadata has `scripts` but no
`hasInstallScript`. Bun derives it from `scripts`, as npm does. That
also fixes the older gap under `minimumReleaseAge`.

Speed:

- Resolving got slower with many versions of one name (11x more
instructions with 320 versions each of 5 names) and many auto-installed
peers (4.8x with 2,400 plugins). Both loops no longer rescan everything
per step.
- Isolated installs and `bun why` no longer slow down with the square of
the number of direct dependencies (an old cost).
- `bun file.js` parsed every `dependencies` entry of each package it
loaded from `node_modules` (up to 2x the instructions and +50 MB with
2,000 packages, about +7 % on a realistic tree). A range is parsed only
when auto-install asks for that name.

Commands:

- `bun install`, `ci`, `add`, `remove` and `update` accept the common
npm, Yarn and pnpm flags again. `--save-dev`, `--save-exact`,
`--no-optional`, `--only=production`, `--immutable`, `--loglevel=...`
and others map to Bun's flags (`bun remove --save-dev` drops the flag).
`--no-audit`, `--no-fund`, `--legacy-peer-deps` and a few more are
ignored. Other unknown flags stay errors.
- `bun pm pack` held the whole tarball in memory (734 MB and 52 s for a
400 MB tarball). It streams to a temporary file, and the destination
changes only once the tarball is whole: a failed pack keeps an earlier
tarball.
- `bun publish` refused a publish that turns provenance off
(`--no-provenance`, `NPM_CONFIG_PROVENANCE=0`,
`publishConfig.provenance: null`). Like npm, the command line beats
`publishConfig`, then the environment, then `.npmrc`.
- `bun publish -r` crashed or published the wrong `workspace:` version
for a package that follows one with a `prepack`, `prepare` or
`prepublishOnly` script. With npm's web login, Enter could open an
earlier package's login page.
- `bun pm pkg set --json x=010` stored 8. Values that aren't JSON
(`010`, `0x10`, `1_000`, `.5`, `'text'`) are rejected, also inside
arrays and objects.
- `bun update --latest` with a name in two dependency lists rewrote one
entry while `bun install` used the other. `bun update --latest <name>`
rewrites the entry that `bun install` uses; the other stays as written.
- `bun update --interactive` crashed in monorepos whose packages write
`catalog:default` (or `catalog:` over `catalogs.default`). It updates
the catalog entry where it's defined.
- `bun create` into an existing folder replaced your `.gitignore` with
the template's `gitignore` file. Yours stays unless you pass `--force`;
a template's own `.gitignore` is a listed conflict.
- After a request had failed for good, `bun install` still asked the
registry for the peer dependencies, with a second round of retries. It
stops there. A 404 or "no matching version" below the first level still
asks for the peers.
- A failed `bunx tool@latest` broke the copy that `bunx tool` runs, and
`bunx tool` waited for a running `bunx tool@latest`. A failure before
linking leaves the tree as it was.
- `bun info pkg` failed on a registry document without a `latest` tag.
`bun info pkg@*` names the version `bun add` installs.
- Migrating a `yarn.lock` or `pnpm-lock.yaml` waited about 8 s when the
registry couldn't be reached. It asks once, and uses cached manifests of
any age.
- Yarn Berry projects whose `package.json` writes ranges as `npm:^1.0.0`
lost the versions `yarn.lock` pins. `bun install` reads `npm:<range>` as
a range of the dependency's own name when it has a range character or is
a complete version; `npm:bar`, `npm:latest`, `npm:1` and `npm:x` stay
aliases.
- `bun install` on a `yarn.lock` with a git dependency outside GitHub
failed with `invalid git commit ""`, and `--frozen-lockfile` straight on
a `yarn.lock` failed when the root has a required peer that it doesn't
list.
- The warning about an unreadable `bunfig.toml` printed up to three
times. It prints once, and `--silent` hides it. `bun a.js` without a
bunfig does one failed `open` instead of three.
- Auto-install: a version written in an import (`import "foo@^2"`) was
overridden by the range in `package.json`.
- `bun patch --commit` and `bun install` rejected patches that delete
the last lines of a file with no trailing newline.
- `bun patch` and `--commit` failed when the project's path contains
`"`, a tab or a backslash. A failed `--commit` with the global store
deleted the edited folder.
- Undoing every edit and running `bun patch --commit` no longer removed
the patch. It removes the `patchedDependencies` entry, reinstalls the
unpatched package and deletes the patch file if it's a regular file
inside the project and no other entry names it.

#### Resolution and lockfile

- New `bun.lock` keys:
- `"unresolved": [...]`, next to `optionalPeers`: optional dependencies
that couldn't be resolved, listed only when another package of that name
is in the tree.
- `"hasInstallScript": true` in a package's info object (npm's key).
Both are in `bun.d.ts`.
- `"./<name>/<dep>"` entries: the dependencies of a workspace whose name
the root gives to another package.
- Older versions load such lockfiles, pass `--frozen-lockfile` (2 of 900
dense random projects didn't) and drop the additions on their next save,
so a team on mixed versions sees them come and go. A plain install
doesn't rewrite a lockfile just to add the first two.
- A lockfile that lists a workspace next to a registry package of its
name is rejected by Bun <= 1.4 ("Duplicate package path": the lockfile
is ignored, frozen fails), as it already was when another workspace
depended on that workspace.
- An optional dependency that can't be resolved (no matching version, a
missing tarball, folder, git repository, alias or catalog entry) was
bound on the second or a clean frozen install to another package that
happens to have its name. It stays unbound, with both linkers. This
holds for lockfiles this version saves; an older one binds by name as
before until the entry is resolved again.
- A package installed in several places gets one stable binding for each
optional peer. Upgrades no longer leave a copy that `bun.lock` doesn't
list, and a committed lockfile that frozen kept rejecting passes.
- A required peer that the root's own dependency, or the package's own
dependency of that name, provides within its range counts as that
package for `bun update`, `outdated`, `prune`, `pm licenses`, `audit`,
`add --trust`, the security scanner, the install summary and
`--production` / `--omit` / `--filter`. They went by the highest version
that fits. Other providers are as before.
- A workspace or package that lists a name both as its own dependency
and as a peer always loads the version its own entry asks for. With the
hoisted linker, `bun add` or `bun update` in another workspace could put
a newer copy in front of it, outside that entry's range.
- `bun install` could write a `bun.lock` it couldn't read back (`Failed
to resolve prod dependency`, `InvalidLockfile`) when a package bundles a
package that another one names as an optional peer (in 1.4.3 too). Such
a peer, if nothing provides it, is no longer installed at the top of
`node_modules`.
- **A fresh `bun install` writes the same `bun.lock` whichever response
arrives first, with a cold or a warm cache.** Each dependency used to be
resolved when its manifest arrived, so network timing decided whether a
range shared a version.
- Dependencies are resolved in one fixed order, and only the first
unresolved one waits for its manifest.
- The ones behind it start their requests and downloads at once, and Bun
requests ahead the manifests that their most likely versions depend on.
That never touches the lockfile, and a request made on a wrong guess
can't fail the install.
- What a git, GitHub or tarball package depends on is only known after
its download, so these are added one at a time after the registry
packages, in the order of the dependencies that ask for them.
- While every connection is taken, manifests go before tarballs, and
peers don't wait for tarballs in flight.
  - No measurable cost: see the numbers at the end.
- A range that allows a version listed in `patchedDependencies` resolves
to that version.
- A 404 or "no matching version" about a version that deduplication
later drops no longer fails the install: it is a warning.
- The `overrides` check under frozen prints `note: bun.lock has b@1.2.9,
and overrides in package.json asks for 1.2.0`. It leaves alone bundled
dependencies, optional peers, and a peer or optional dependency that
shares its version with a dependency the rule doesn't apply to.
- An override added to an installed project reaches a package that is
only there as one package's peer dependency. If no version fits, that is
a warning, as on a fresh install.
- `bun add foo` and `bun update --latest` installed a version that a
version-scoped override (`"foo@>=2.0.0": "1.2.0"`) forbids. They write
`^2.0.0` and install `1.2.0`, like a clean install.
- An override set to `catalog:` whose package isn't in the catalog gets
a warning that points at the rule.
- `bun install` and `Bun.semver` read build metadata after a bare major
(`2+1`, `^2+x`) as part of that version, not as a second range that let
another major through. `Bun.semver.order("2+1", ...)` no longer throws.

#### Hoisted linker

- What a self-contained workspace drops from its `package.json` leaves
its `node_modules`.
- The command of a workspace that isn't built yet stays in `.bin` when
another dependency leaves `package.json` (#44968).
- `--production`, `--omit=<type>`, `--filter`, `--os` and `--cpu` used
to re-hoist, so a package could move between runs and an old nested copy
could shadow the right version. They now skip packages but never move
the rest. (#45070, #29944) Such a tree nests the way the `bun.lock` keys
do: +0 % to +1.8 % folders on real lockfiles.
- An install removes a folder from `node_modules` only if the `bun.lock`
it started from placed a package there, or Bun up to 1.4 would have for
the same lockfile and flags. A nested copy from before the upgrade used
to stay and go on being loaded after the next version bump. A folder
that a package ships in its tarball (the cache says; without the cache
entry the folder stays), that a script can have laid or that somebody
put there stays. What a flag merely leaves out stays too; `bun prune`
removes that.
- A warm install reads each installed `package.json` with 3 system calls
instead of 4.
- After only the project's `node_modules` was deleted, a package that
changed version was linked over the old folder in a workspace's own
`node_modules`, so files of the old version stayed (in 1.4.3 too). Such
a folder is treated as in a warm install.
- `bun prune --production` deleted nested packages below a package that
a full install had hoisted to a different folder than a production
install would.
- Removing the last dependency from `package.json` removed nothing from
`node_modules`.
- A workspace that switches a dependency from a registry package to the
workspace of that name kept loading the old nested copy. The install
removes it, and `bun prune` sees it.
- A command whose file is written by the package's own install script (a
workspace CLI built in `prepare`) is linked by the same `bun install`,
before the root's scripts run.
- Both linkers: symlinks inside a `file:` folder dependency were dropped
silently. Relative links that stay inside the folder are kept, links to
files elsewhere in the project (by real paths) are installed as files,
and anything else is skipped with a warning.
- A package whose `package.json` or `node_modules/.bin` path doesn't fit
in `PATH_MAX` fails its lifecycle scripts with `ENAMETOOLONG` instead of
crashing.

#### Isolated linker and global store

- A dependency removed from a `package.json` stayed linked in
`node_modules`, with its `.bin` entries. It's unlinked, also when
another package still needs it, inside a workspace (#26305), and in the
hidden `node_modules/.bun/node_modules`. Only that package's own
`node_modules` is touched, and only a link that leads where Bun had
linked it: your own links, `bun link` and other workspaces' links stay.
- A workspace whose folder, or a parent of it, is a symlink got dangling
dependency links. They're computed from the real folder (not on
Windows). Workspace links behind a linked root `node_modules` no longer
dangle either.
- A registry package that depends on, or peers on, a workspace that has
peer dependencies was stored under one name on the first install and
another on the second.
- A `hoistPattern` added to an installed project takes effect on the
next `bun install`: fallback links it doesn't match are removed.
(`publicHoistPattern` still only grows.)
- After `bun prune --production`, a command or a fallback link that a
removed package held goes to the package that stays, as in a fresh `bun
install --production`.
- `globalStore = true`: the first install against a warm store printed
nothing useful. It lists what it linked and which postinstall scripts
were blocked.
- `globalStore = true`: a no-op install deleted and re-created every
`node_modules/.bun/<pkg>` link (4 system calls per package, now 1). Two
installs at once no longer fail with `EEXIST`, and a running program no
longer loses a package for a moment.
- `bun install --force` replaces a global store entry in one step on
macOS and Linux, so other projects never see it missing.
- `BUN_INSTALL_GLOBAL_STORE=false` (also `no`, `off` and an empty value)
turned the store on.

#### Workspaces

- A workspace vanished from the install when the root depends on a
registry version (or any other package) of the same name. It keeps its
`bun.lock` entry and gets its own dependencies installed, while the
root's `node_modules/<name>` holds what the root asked for.
- A `bun.lock` or `bun.lockb` from Bun 1.4 or older that leaves such a
workspace out still passes `--frozen-lockfile` and isn't rewritten by
`bun install`. The workspace gets its entry the next time the
dependencies change, or with a fresh lockfile.
- `bun link <package>` inside a workspace treated the workspace as its
own project. It installs from the workspace root like `bun add`: sibling
`workspace:` and `catalog:` dependencies resolve, and `--save` updates
the workspace's `package.json` and the root's `bun.lock` (#24190,
#20015).
- `bun add <name>` inside a workspace, where `<name>` is a workspace
that is also published, links the workspace and writes `workspace:*`.
- `bun add <name>@<tag>` and `bun update --latest` for such a name left
a workspace entry bound to the registry package. They install and lock
what a clean install of the written range gives.
- `bun add alias@npm:<workspace>` wrote `"alias": "workspace:*"`, which
every later install rejected. It keeps the `npm:` form.
- `bun add lib@npm:other` at the root, with a workspace named `lib`,
installs `other` rather than linking the workspace.
- `workspace:../lib`, `workspace:lib@^` and `workspace:lib@~` rejected a
workspace with a prerelease version or no version.
- A `workspace:` range on the root package that the root's version
doesn't satisfy is an error, as for any other workspace.

#### Lifecycle scripts and trust

- Isolated: a trusted `link:` dependency runs its lifecycle scripts when
it's linked, as with the hoisted linker. An untrusted one is reported as
blocked.
- A trusted `link:` folder without a `package.json` no longer fails an
install from `bun.lock`.
- An install from a checked-in `bun.lock` reports blocked scripts,
because the lockfile records which packages have install scripts.
- `bun pm trust` refuses to run with a frozen lockfile.
- A fork, folder or workspace named `sharp` runs its install scripts
like any other package. `"ignoreScripts": []` in `package.json` turns
the built-in skip list off.
- `bun pm pack`, `bun publish` and `bun pm version` run their lifecycle
scripts with the package's and its parents' `node_modules/.bin` in
`PATH`, so `"prepack": "tsc"` works. With `bun publish -r` each package
gets its own.
- The `preversion`, `version` and `postversion` scripts get
`$npm_old_version`, `$npm_new_version`, `$npm_command`,
`$npm_package_json`, `$npm_package_name` and `$npm_package_version`. The
new version is computed before `preversion` runs, as in npm, so an
invalid or unchanged version fails first.
- `bun create <template>` has `--ignore-scripts`, and skips the
template's preinstall and postinstall tasks with `ignoreScripts = true`,
`ignore-scripts=true` in `.npmrc` (both read in the folder the command
is typed in and in the home folder) or `NPM_CONFIG_IGNORE_SCRIPTS`.

#### `bun patch`

- `bun patch` and `--commit` fetch the package again when it's installed
but no longer in the cache (after `bun pm cache rm`) instead of failing
with `ENOENT`, also for patched packages and git, GitHub and tarball
dependencies.
- `bun patch --commit` changes nothing until every check has passed. If
the new patch doesn't apply to a scratch copy, or the install it runs is
refused (the security scanner, `--frozen-lockfile`, a dependency that
doesn't resolve), the folder, `package.json`, `bun.lock` and an older
patch file stay as they were. With the global store, `--commit
--lockfile-only` deleted the edited folder.
- Isolated: an install with a patch that doesn't apply stops, as with
the hoisted linker, and `bun add` / `bun update` write nothing.
- The project's own `.gitattributes`, `.git/info/attributes` or
`.git/config` could change or empty the patch. git runs without a
repository.
- `--commit` failed when a patched file, the project folder or the cache
folder has a non-ASCII name, when git only printed a warning (more than
1,000 files deleted and 1,000 added), and for a git dependency whose
repository contains symlinks. It reported "No changes detected" when git
was killed.
- When a file in `patchedDependencies` is missing, `bun install` and
`bun patch` fail and leave `node_modules` alone. The error names `bun
patch --commit <pkg>`, which writes the file again from the patched
package in `node_modules`, and says how to drop the entry.
- `--commit` on a package that wasn't edited and has trusted install
scripts puts it back as installed and runs those scripts again.
- Where the package folder can't be renamed (a lower overlayfs layer in
Docker), `bun patch` lost nested packages and could leave a half-deleted
folder. Nothing is deleted before the copy is in place, and an error
puts everything back.
- Absolute patch paths work. `--patches-dir=/abs` inside the project is
written relative to it.
- When several versions match, `bun patch` accepts an entry of the list
exactly as it prints it.
- Appending lines to a file with no trailing newline lost the last line.
- A patch section that adds a binary file made an empty file. It makes
none, as its warning says.
- Files under a folder whose name ends in `" b"` were deleted, created
or chmod-ed at the wrong path.
- Applying a patch was quadratic: 40,000 changed lines in a 100,000-line
file took about 7.5 s, now about 10 ms.
- Swapped hunks without context that change the line count applied at
the wrong place. Hunks are applied in file order.
- `bun patch` dropped folders named `CMakeFiles` or `.git` from the
copy.
- A package's commands lost their executable bit between `bun patch` and
`--commit` when files are copied rather than hard-linked.
- Global store: a package being patched keeps finding its own
dependencies, and `bun prune` no longer deletes the folder that `bun
patch` prepared for a transitive dependency.

#### Tarballs, `bun pm pack` and `bun publish`

- Uncompressed `.tar` packages, and `.tgz` files served with
`Content-Encoding: gzip`, failed in the streaming extractor.
- A `github:` tarball whose first entry isn't its root folder was
accepted or rejected depending on whether the response had a
`Content-Length`.
- `Bun.Archive`: `extract(dir, { glob })` and `files()` resolved with
the entries read so far on a truncated or corrupted archive. They
reject, like `extract(dir)`.
- `bun pm pack` and `bun publish` left `bundledDependencies` out with
the isolated linker, in workspaces and for linked folders. They bundle
them and everything they depend on from any layout, looking no further
up than the project. Sibling workspaces are packed by their own `files`
and `.npmignore`, and a broken sibling that nothing needs doesn't fail
the pack.
- A bundled name in `dependencies` that isn't installed is an error, no
longer left out. So is a ring of bundled versions that no `node_modules`
can hold.
- A `catalog:` dependency whose catalog entry is `workspace:^` (or
another `workspace:` form) was published as written. It's replaced with
the workspace's version. An entry that is itself a `catalog:` reference
fails the pack.
- `bun pm pack` left lockfile-named files out of a dependency of a
bundled package whose name contains `node_modules`.
- A file that gets shorter while it's packed was padded with NUL bytes.
The pack fails.
- `bun publish x.tgz` looks `bin` and `directories.bin` up in the
tarball, not in the current folder.
- The registry's `npm-notice` header is printed for every request of
`bun publish`.
- `bun publish` of `2.0.0-beta.1` without a tag made it `latest`, the
version `bun add` installs. The refusal names `--tag next` and `--tag
latest`, and `--dry-run` refuses too.
- `bun add ./<path>.tgz` with a path longer than `PATH_MAX` fails with
`ENAMETOOLONG` instead of crashing.

#### Git dependencies

- Checkout: files are written straight from the cached bare repository
(`git ls-tree`, then `git restore --source=<sha> --worktree` with
`core.symlinks=false`). The second clone and its `.git` folder are gone:
about 20 % to 35 % less CPU per checkout on two repositories. An older
git gets a note that 2.24 is needed.
- Symbolic links: a link to a file inside the repository, also through
other links and linked folders, is installed as a copy with its
executable bit. Links to folders, links that leave the repository,
absolute, dangling and looping links are skipped, and no longer fail the
install. The rule is the same on every platform.
- An ssh git URL never fell back to ssh when the https attempt was
answered "not found". Bun tries every form of the URL whatever git said.
- A successful install no longer prints git's complaint about the failed
https attempt. A failed install shows every attempt.
- Bun overrode your own ssh command in cases its hand-written git config
parser missed. It asks git itself, once, before the first clone:
`core.sshCommand` from any config file, plain includes, `GIT_CONFIG_*`,
`GIT_SSH`, `GIT_SSH_COMMAND` and `core.askPass` all count. A value
behind a conditional `includeIf` is still overruled.
- `git@host:user/repo.git` installed from `bun.lock` tried a malformed
https URL. It tries the same URLs as a first install.
- A cached clone that git can't use (a stale lock file, a damaged ref
store) failed every install. It's cloned again. Clones made by older
versions keep working, also with `--offline`.
- Your own `submodule.recurse = true` broke a git dependency that has a
submodule.
- Isolated: a second install of a shorthand or scp-like git dependency
renamed its store entry.
- When the `prepare` script of a git dependency fails, a note says that
Bun doesn't install its `devDependencies` first.

#### Migrations (npm, yarn, pnpm)

- A `yarn.lock` or `pnpm-lock.yaml` that pins a GitHub dependency failed
on the first install and lost the pin on the second. It installs the
pinned commit and writes a `bun.lock` that loads. Migrated pnpm rows
keep the full 40-character commit.
- A `pnpm-lock.yaml` with a `type: git` resolution installs the pinned
commit, whichever way `repo:` is written.
- pnpm: a patch keyed by a version range or a bare name was lost. It
becomes one `patchedDependencies` entry for each version it patches.
- yarn v1: `link:` and bare path (`./dir`) entries failed with "failed
to resolve". They're migrated, with their `bin`, also for workspaces and
with `--frozen-lockfile`.
- yarn v1: a tarball or hosted-git entry was merged with the registry
package of the same name and version, so a fork was replaced by the
published package. They stay apart, and an `owner/repo` entry is the
archive that `yarn.lock` names.
- yarn v1: a monorepo where a registry package depends on a workspace
failed with "failed to resolve".
- A root or workspace dependency whose `yarn.lock` entry can't be used
is resolved instead of failing the install. A dependency of a dependency
without a usable entry still fails.
- A migrated entry takes `bin`, `os` and `cpu` from the registry's
metadata only when it's that registry's tarball: the same URL, the same
hash, or, when the hashes can't be compared, a URL with the registry's
layout for that name and version (a mirror).
- A lockfile that Bun can't migrate hid a `yarn.lock` or
`pnpm-lock.yaml` next to it. Bun warns (`...; trying yarn.lock`) and
tries the next one.
- pnpm and npm: the project's own required peers that the old lockfile
doesn't record are resolved. No migration (`bun.lockb` included) writes
them as `optionalPeers` any more, so `--frozen-lockfile` accepts the
`bun.lock` that `bun install` writes. `bun pm migrate` leaves such a
peer out; the next `bun install` adds it.
- `bun install --yarn` wrote an npm alias under the package's own name.
It uses the name `package.json` gives it, as yarn v1 does, so an alias
migrates back to the same package.

#### Auto-install

- `bun.lock` was never read without `node_modules` (only `bun.lockb`
was). Bun uses the versions pinned in either, and downloads them on
first import.
- `import "foo"` also honors `devDependencies` and `peerDependencies`,
and walks up to the first `package.json` that lists the name.
- `overrides`, `resolutions`, `catalogs` and `packageExtensions` of the
project's `package.json` apply. An override counts when it names a
version, range, tag, `npm:` alias or `catalog:`; one that names a
folder, tarball or git repository is passed over, as before.
- `workspace:`, `file:` and `link:` dependencies load from their folder
on disk, and tarball dependencies load through the cache.
- Peer dependencies of auto-installed packages failed with "Cannot find
module". They load, at the version the project's `package.json` lists if
nothing has loaded that name before. Optional peers are installed when
imported.
- A failed tarball download (5xx, 404, truncated file) was remembered
until restart. A later import tries again.
- `bun --install=force` rejected an import that names a version in a
project with a `package.json`.
- An import waiting on a registry that accepts the connection and never
answers failed after about half an hour. It fails after 30 s idle,
without retries.
- A cache or temp directory that can't be written (a read-only
container, no `HOME`) ended the process inside `require()` / `import()`.
The import throws "Cannot find package" with the reason as a note, and
packages already in the cache still load.
- A package that ships `bundledDependencies` can load its other
dependencies, and Bun no longer downloads the bundled ones.

#### `bunx` and global installs

- `bunx <tool>` with a day-old copy failed when the registry answered
5xx, 429 or 408, or when the tarball of a newer version couldn't be
fetched. It runs the copy it has, and asks a failing manifest once
instead of retrying for 8 s.
- The daily refresh never moved `pkg@<range>` or any dependency, because
the tree's own lockfile pinned them. It takes what was published since,
and a `tool@latest` run no longer puts it off.
- `bunx pkg@3` failed in a project that has another major of `pkg`
installed. With a version, tag, URL or alias, nothing from the project
is used.
- `bunx @scope/pkg` and `bunx -p pkg cmd` find the bin through that
package's own `package.json`, in the project (no further up than its
root) or in `bunx`'s tree, and run that package's file, not another
package's `.bin` entry of that name (on Windows the shim is taken as
is).
- `bunx -p pkg cmd` installs `pkg` and runs any command, as npx does,
with `pkg`'s commands first in `PATH`.
- A project's own tool run through `bunx` no longer finds helper
commands from an older copy that `bunx` had installed.
- `bunx --no-install pkg@latest` runs the copy that is on disk.
- `bunx pkg` without a version passes over a deprecated `latest`, as
`bun add` does.
- A `node_modules/.bin/<name>` that exists but can't be executed is
`error: <path> is not executable`, exit 126, instead of a download.
- A `bun install` that a package's script starts during `bunx` is an
ordinary install of its own project.
- `bun add -g` / `bun update -g`: when one bin can't be linked because
another file has its name, the command still fails, but the package is
saved, so `bun pm ls -g` lists it and `bun remove -g` removes it.
- `bun remove -g second` took a bin name away from `first` for good if
`second` had claimed it. A global package whose bin link is missing gets
it back on the next `bun add -g`, `update -g` or `remove -g`. Nothing
already in the global bin folder is replaced.

#### `bun update`, `bun outdated`, `bun audit`, `bun pm *`

- `bun update --interactive` rewrote every range as "first character +
chosen version". Ranges such as `>=1.0.0 <2`, `1.x`, `*`, `^1 || ^2` or
`latest` stay as written when they already allow the chosen version.
- `bun update` with `--exact` or `install.exact` no longer pins a
`peerDependencies` range, or a catalog entry that a `catalog:` peer
uses. `bun update --latest` without names leaves a peer range in a
`package.json` that already allows the latest version (one in a catalog
is still rewritten).
- `bun outdated` hid dependencies that are ahead of the `latest` tag
(`next` / `canary` channels, `latest` kept on an LTS major). It lists
them when `bun update` would move them. It also printed an optional peer
twice.
- `bun update --latest` and `bun update -i` downgraded a dependency that
is ahead of `latest`, including one written as a dist-tag such as
`"canary"`. No key or flag in `-i` can pick an older `latest`, and it no
longer lists packages whose only other version is one. An entry in a
catalog is still moved down.
- `bun audit fix`: after it upgrades a package past a plugin's peer
range, the "Fixed / remaining" counts and the exit code match the next
`bun audit`, and the plugin's line is marked `(peer)`. `--dry-run` says
the same as the real run, except when the narrow peer range is in one of
the project's own `package.json` files.
- `bun audit` reported "No vulnerabilities found" on an empty answer.
It's `error: <registry> returned an empty audit response`, exit 1, also
with `--json` and `fix` (for a scoped registry, the existing warning).
`{}` still means none found.
- `bun pm version <increment>` computes the same version as `npm
version` (`1.0.0-rc.1` + `major` is `1.0.0`). It cleans `v2.0.0` to
`2.0.0`, and rejects `2`, `x`, ranges, a bad `--preid` (also `--preid
01`, a number with a leading zero) and, with `from-git`, a tag that
isn't a version.
- `bun pm ls` / `bun list` support `--production`, `--omit`, `--depth=0`
and package names (`bun pm ls lodash`). This covers part of #13148.
- `bun pm licenses` skipped an installed package whose own version
differs from `bun.lock` (a bundled copy, another spelling). It lists it
under its own version.
- `--dry-run` printed one line per dependency edge, skipped peers and
ignored the platform. It lists what the install would put in
`node_modules`, each package once (not yet narrowed by `--filter`).
- `bun pm cache rm` prints "Cleared" only on success.
- `bun pm cache pack` / `unpack` crashed while reporting a file name
that isn't UTF-8. `bun info` and `bun create` crashed on a name longer
than a path.
- A flag that belongs to another `bun pm` sub-command (`bun pm ls
--json`) was silently ignored. It's an error that names the sub-commands
that take it.
- `bun pm diff a b > changes.patch` applies with `git apply` and `patch
-p1` when a version adds or removes an empty file, changes an executable
bit, or has file names with spaces (they're C-quoted). Binary files and
source maps keep `diff --bun`.
- `bun pm diff <pkg>` compared the first lockfile entry of that name. It
uses the version `<pkg>` means in the workspace you run it from, accepts
an alias, and `bun pm diff <pkg> 2.0.0` starts from the installed
version.
- `bun pm diff` compares style sheets that contain `url()` by their
canonical print too.
- Commands that rewrite `package.json` turned control characters such as
`\^[` into `\x1B` or `\v`, which npm and node can't parse.

#### Registry requests and the manifest cache

- The same commands and `bun publish` dropped the registry token on a
redirect from http to https on the same host and port. They keep it, as
`bun install` does.
- A redirected request ignored `NO_PROXY` for the new URL. Each hop
picks its proxy by its own URL, as in `fetch()`.
- A registry that redirects in a circle was retried and reported as "No
version matching". It's `TooManyRedirects`, without retries.
- A damaged manifest cache file could crash every install. The offsets
and lengths of what an install reads are checked, and a bad file is
fetched again.
- Bun deleted cached manifests that it couldn't read, also under
`--offline`. It leaves them, and the next fetch overwrites them.
- A manifest cached while the clock was ahead stayed "fresh" until that
time came. It's revalidated.

#### Configuration (`bunfig.toml`, `.npmrc`, flags)

- An unreadable `bunfig.toml` or `.npmrc` used to be a warning. It's
`error: This file cannot be read, and bun does not install packages
without its settings`, before any request. Commands that neither install
nor publish still warn and continue, and auto-install turns itself off.
- A relative `cafile`, `cache`, `globalDir` or `globalBinDir` in
`bunfig.toml` or `.npmrc` is relative to that file, wherever the command
is typed. A relative `cafile` only worked from the project root.
- A relative `--config`, `--cache-dir` or `--cafile` is relative to the
directory the command is typed in, not the project root.
- `BUN_INSTALL`, `XDG_CACHE_HOME`, `BUN_INSTALL_CACHE_DIR`,
`BUN_INSTALL_GLOBAL_DIR`, `BUN_INSTALL_BIN` and `NPM_CONFIG_USERCONFIG`
accept a leading `~/`. A relative value keeps its current base (the
project root for the cache).
- A scope whose registry URL is empty uses the default registry of the
run, including one set by `BUN_CONFIG_REGISTRY`, `NPM_CONFIG_REGISTRY`
or `--registry`. With credentials of its own it leaves the configured
host for no other.
- `--dry-run` (and `dryRun = true`) was accepted and ignored by `bun pm
fetch`, `bun pm cache pack` / `unpack`, `bun patch` and `--commit`. `bun
patch <pkg> --dry-run` even discarded edits. They print what they would
do and leave the project alone.
- `bun install --analyze` and `bun add --analyze` skipped packages that
are imported only by a subpath (`lodash/debounce`). An unresolved
`@/alias` import no longer aborts the run, and a file that imports the
package it belongs to no longer gets that name looked up or added.

#### Messages and diagnostics

- Names, versions, paths, URLs and script text from packages, lockfiles,
registries or the file system are printed with unprintable characters
spelled out (`\x1b`, `\r`, `\^[`, bidirectional marks) in `bun pm pack`,
`publish`, `audit fix`, `prune`, `pm whoami`, `run`, `create`, `pm
migrate`, in error messages and in source excerpts (a NUL in an excerpt
stays). `--json` is untouched.
- `bun install` silently skipped parts of a patch it can't apply: a
`Binary files ... differ` section, a file copy, colored output, stray
text. It still installs, and warns about each one. It fails on a `GIT
binary patch` section or a hunk without a file.
- Patch warnings and parse errors go through the normal logger, so they
show the patch file, line, column and source line.
- A `patchedDependencies` key without a version was silently not
applied. It gets a warning that points at the key.
- The "patch no longer applies" warning also covers tarball and git
dependencies, and a version that is a URL containing `@` no longer gets
a false one on every install.
- `bun patch` prints a note when the package has trusted install
scripts: they haven't run on the fresh copy, and run again at
`--commit`.
- Global store: `bun patch <transitive dependency>` prints that
dependents only see the changes after `--commit`.
- `bun patch` and `--commit` fail with `ENAMETOOLONG` instead of
crashing when a path argument or `--patches-dir` is longer than
`PATH_MAX`.
- Diagnostics about the project's `.npmrc` show its absolute path, as
for `bunfig.toml` and `~/.npmrc`.
- Git errors name the command that failed (`"git restore" for "x"
failed`) instead of "git checkout".

#### Windows

Found by @dylan-conway in review, by hand, on CI's Windows builds, and
tested again the same way on `ce845c1e2`: of 52 cases 35 go from fail to
pass, 15 controls pass on both, none goes from pass to fail.

- Regressions from #44968: a second isolated install crashed with a
stack overflow once a link's path reached 260 characters; `bun patch`
failed with `EPERM` while a program had a file of the package open (a
file opened without sharing still fails, with `EBUSY` and the folder
left whole); switching linkers with one file open stopped half way and
left packages missing.
- `XDG_CONFIG_HOME` or `USERPROFILE` without a drive letter, or on a
share that doesn't exist, stopped every install with "cannot be read".
An odd error counts only if the file is really there.
- git is given the platform's own name for the null device (`NUL`).
- Behind a `node_modules` that is a junction or a directory symlink, the
isolated linker writes absolute link targets, so the links resolve.
- A workspace that leaves the project through a junction counts as
outside the project, as on macOS and Linux.
- Links inside a `file:` folder were taken to "lead out of the project"
when the command was typed in `c:\proj` for `C:\proj`, or through a
junction.
- `--patches-dir` takes `/`, `\` and `.\`, and always writes `/` to
`package.json` and `bun.lock`.
- The `preversion` / `version` / `postversion` scripts of `bun pm
version` in a git repository ran with an empty environment.
- A dependency's lifecycle script found the shared `node_modules\.bin`
ahead of the system `PATH` when `PATH` spelled it in another letter
case.
- The hoisted linker installed a package through a junction at its
destination. The junction is removed first.
- `bun create <local template>` failed with "ENOENT: failed to copy
file" when the working directory, the template or the destination has a
non-ASCII character.

#### Hardening

- Auto-install started in a linked workspace takes the settings of the
workspace root and its own, and of no folder above the link's target.
- `bun install` extracts a few unusual tarball entries the way npm does,
or skips them as npm does, and takes two zero blocks for the end of a
package tarball (registry, URL and `file:` tarballs). It reads a tarball
to its end on every extraction path.
- Messages that show the URL of a git or tarball dependency, or of a
registry, no longer show any of the login part of the URL, also in what
git itself prints. `bun patch --commit` names new patch files without
it.
- A tarball URL with an invalid host is rejected with `InvalidURL`
before any connection is opened.
- A git dependency's `package.json` is always a regular file from its
own repository. A cached checkout whose `package.json` is a link is
checked out again when the dependency is resolved anew (no `bun.lock`
entry, `bun update`, `bun add`).
- A cached clone is used only if it's a plain bare repository without
links.
- The isolated linker reinstalls a package when its entry in
`node_modules/.bun` doesn't really live in the project's store (macOS,
Linux).
- With `globalStore = true`, packages that depend on each other are
shared only between projects that link them to the same versions.
- Migrating a `yarn.lock` keeps the checksum of entries that have no
`integrity` line.
- The pnpm migration keeps an empty `onlyBuiltDependencies` as
`"trustedDependencies": []` in `package.json`.
- `bun pm trust` takes only its positional arguments as package names.
It and `bun add --trust` leave blocked what a `trustedDependencies`
entry cannot unblock for `bun install`: a tarball, git, `file:` or
`link:` dependency that neither the project nor a workspace declares,
and a package that another one ships inside itself. `bun pm untrusted`
marks the packages that can't be trusted by name.
- A folder that a package ships inside itself never runs install scripts
of its own.
- With a security scanner configured, auto-install is off as documented,
whatever the cache state or `--install` flag. It counts when set in the
global bunfig, or in a `bunfig.toml` between the working directory or
the importing file and the project root.
- `bun patch`, `bun patch --commit`, `bun link <pkg>` and `bun dedupe`
run the configured security scanner before they install packages.
- A relative `install.security.scanner` path in the global bunfig or in
a `--config` file is relative to that file. File names with quotes work.
- Commands typed inside a workspace that is a symlink to a folder
elsewhere use the workspace root's settings and lockfile.
- Auto-install takes its settings and `bun.lock` from the project of the
importing file, wherever `bun` was started; a `bun.lock` in an unrelated
parent folder is ignored. A `package.json` that only marks the module
type (no name, workspaces or dependencies) is passed over when a real
project is above it; otherwise the topmost one is the root.
- `bun pm pack` never packs its own temporary files.
- `bun pm diff` requests and checks downloads the way `bun install` does
(the registry's integrity, and `bun.lock`'s when it's loaded).
- `bun pm diff`'s terminal view checks what it folds away as "formatting
only", and what it re-prints for minified files, against a full
re-parse, and shows the plain diff when the check doesn't hold. The
JavaScript parser gets one internal option for this (`keep_directives`);
only `bun pm diff` sets it.
- `bun pm diff` treats fewer files as binary and prints sizes and a
sha256 for the ones that are. Its `package.json` notes compare whole
values and say where the registry's metadata and the tarball disagree on
a field that `bun install` uses. It escapes control characters on a
terminal with `NO_COLOR` too.

#### Not in this PR

- `bun pm ls --json` and `--depth=N` above 0 are rejected rather than
supported (#8283 stays open). `bun unlink <name>` is still not
implemented.
- Plain `bun update` still doesn't move a peer-only entry within its
range (#13509).
- A package that a tarball declares as bundled but doesn't ship is still
not installed. A tarball that bundles an unpublished package still
fails. An override doesn't reach a bundled copy.
- An optional dependency that couldn't be resolved isn't retried by a
plain `bun install`; `bun update` retries most of them.
- Hoisted: the lifecycle scripts of a workspace whose name the root
gives to another package don't run.
- Auto-install in a project that has a `node_modules` folder still
ignores `package.json` and `bun.lock` for a package that is missing on
disk.

#### Known to be still open

Reopened because the fix was taken out of this PR again (as on `main`,
unless noted):

- A name in two dependency groups with different ranges: a fresh install
and one from `bun.lock` can give different versions.
- After `bun.lock` changes under an existing `node_modules` (a `git
pull`, `rm bun.lock`), a nested copy that the old lockfile didn't place
can stay and shadow the right version. Simulated pulls that ended wrong:
101 of 697 on `main`, 100 of 697 here. The `rm bun.lock` case is new
with #44968.
- An override named like a workspace displaces links to that workspace
again (#37271).
- Bin links that a package's new version no longer declares stay in
`.bin` and in the global bin folder.
- Ctrl-C with a `SIGINT` listener during auto-install's wait is handled
only when the wait ends (30 s idle at most); before #44968, at once.
- A package that is trusted already (through a `git pull`) but never ran
its scripts here: `bun pm trust` doesn't run them.
- A trusted `file:` folder that is edited in place isn't built again by
the next install (new with #44968).
- With a patch file missing, `bun patch` fails for every package. A
second `bun patch <pkg>` discards uncommitted edits.
- The `bin` of a tarball, GitHub or git dependency isn't linked after a
`yarn.lock` or `pnpm-lock.yaml` migration: neither file records it.
- A 404 below a version that couldn't be completed can make a fresh
install settle on a lower version that is already in the tree; the
warning names the request.
- An optional dependency whose own dependency can't be resolved fails
the whole install, as on `main` and in pnpm. npm leaves the optional
package out and asks again on the next install.

Regressions from #44968 that this PR doesn't fix:

- `bun remove` empties `package.json` before it writes it, so a full
disk at that moment leaves an empty file.
- With the registry down, `bun install` waits 7 to 9 s through its
retries without printing anything.
- The install cache of 1.4 isn't reused for a registry whose URL has a
path, a port or plain http, and every cached manifest is fetched again
once.
- A long package name from a registry other than npm's fails with
`ENAMETOOLONG` from 185 characters on (204 before).
- Two GitHub repositories can get the same cache folder name, and the
second one is then installed from the first one's folder.
- Three more are in the list above, marked "#44968".

Gaps in what this PR adds:

- Behind a linked `node_modules`, the hoisted linker replaces a folder
or file that has the name of a dependency, as 1.4.3 and pnpm do; behind
a workspace's link `main` doesn't, because it replaces the link.
- `bun add p@2 --filter a` moves a shared `node_modules/p` without
laying the nested copy for a workspace outside the filter, until the
next plain install.
- Hoisted, as on `main`: the install script of a deeply nested package
can start while a folder above it is still being replaced (`install
--force`, `add`, `update`), and fails because its own folder is gone.
- Windows: `bun patch` fails with `EBUSY`, the folder left whole, while
another process has a file of the package open without sharing deletes,
or has its working directory in the package (1.4.3 worked).
`--patches-dir ..\outside` fails, as before.

### How did you verify your code works?

- **State of the branch.** The numbers are from CI's release builds of
`cc9d7c2da` and of `main` at the merge base. The head adds one fix, to
`bun add --trust`, and a test.
- **Order of responses.** Random projects, each installed 24 times with
a warm and 24 times with a cold manifest cache, under random response
delays. Projects that got more than one `bun.lock`, `main` / branch: 6 /
0 of 950; 49 / 0 of 300 with many peers (8 + 8 runs); on the branch 0 of
1,400 with git, GitHub and tarball dependencies.
- **Other commands, and faults.** 21 commands (`add`, `remove`,
`update`, edits of `package.json`, ...) on random projects, cold against
warm: 1,260 cases, no difference. 7,640 pairs of a command and a
registry fault: the exit codes of `main`, no hang. 16,148 installs with
one package answering 404: none ends with another `bun.lock` and no
message. 6,168 bit flips in cached manifests: no crash.
- **Wall clock of a cold install,** 450 packages, delayed responses,
median of 9 runs, `main` / branch in ms: plain 1555 / 1505; three levels
of peers 1603 / 1585; with one slow host 1900 / 1914; three remote
tarballs that take 0.3 s 1588 / 1271, 1.5 s 1933 / 1893, 3 s 3426 /
3408.
- **A script per report.** 448 repro scripts: 61 of the first 417 pass
on `main`, 306 on the branch. One that passes on `main` fails here, on
purpose: the error for a command typed in a linked workspace outside the
project. The rest fail on both.
- **The whole diff against `main`, one pass per kind of regression**
(deletes, credentials, loud to quiet, works to refused, lockfile
compatibility, cost): about 95,000 differential cases, 364 real
lockfiles and 9,571 real package folders found 22 regressions of this
branch; each is fixed or listed above.
- **Changed existing tests.** The 138 whose expectations the first push
touches were audited line by line: none lost, four weakened ones
restored. The fixed order changed when 31 tests release a held response,
but no expectation.
- **Cost, user-space instructions** (millions, median of 3): 1,500
packages, 50 workspaces, bins, scripts, platform packages, overrides, a
catalog, aliases; the hoisted project has peers too.

| Command | hoisted, `main` | hoisted, branch | isolated, `main` |
isolated, branch |
| --- | --- | --- | --- | --- |
| fresh, cold cache | 1501.3 | 1503.7 | 2441.0 | 2450.1 |
| warm, nothing to do | 185.8 | 164.1 | 175.9 | 176.2 |
| `--frozen-lockfile` | 133.5 | 122.1 | 150.3 | 150.8 |
| `bun add` | 182.3 | 160.5 | 171.1 | 171.7 |
| no `node_modules`, cache warm | 198.6 | 179.1 | 1291.4 | 1304.1 |
| no lockfile, `--lockfile-only` | 129.7 | 128.9 | 110.8 | 111.0 |

- **Above 3 %:** `bunx` of a scoped or cached tool (+0.2 M, 35 system
calls); `bun pm untrusted` (+5 %, one more hoist); an isolated `bun
prune` that removes something (+4 %); auto-install in a workspace
without a lockfile (+13 %; double at 300 workspaces); and the hoist
named in the sign-off list. A warm hoisted install makes 8,326 system
calls, not 10,434.
- **Upgrades over an older tree.** 400 random hoisted projects laid by
`main`, taken over, then a pulled `bun.lock`: 17 end with a wrong
version on `main`, 21 here (the stale copy after a pull described
above). Of planted folders that Bun didn't lay, `main` loses 138, the
branch none.
- **On earlier builds:** semver against npm (59.9 M comparisons; 21 of
6,698 real ranges differ from npm's strict mode, none changes an
install); `bun pm version` against `semver.inc` (0 of 1,530,270 differ);
1,260 tarballs extract as before; older builds of `main` leave a
lockfile with the new keys unchanged.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Jarred-Sumner pushed a commit that referenced this pull request Oct 22, 2026
…te (#46230)

### Problem
- On main, `bun update` and `bun add` drop an override for the name of a
workspace package. If that package satisfies the range they write,
`bun.lock` binds it and not the version the rule chose. Exit 0, no
message. 1.4.3 is not affected.
- Cause: `Lockfile::bind_workspace_link`
(`src/install/lockfile.rs:2451`, from #45307) binds every rewritten
range that links a workspace package and does not ask the overrides.

### Fix
- `bind_workspace_link` binds only if `linked_workspace_path` links the
workspace package for the version the override gives.
`settle_without_manifest` decides the same way on a clean install.
- `sync_lockfile` binds after it copies the edited overrides. An
override value can be the range the command wrote (`"$name"`).
- Verified: `test/cli/install/bun-workspaces.test.ts` (48 of 54 new
cases fail on main) and 34 other install suites. Self-reviewed: 8
concerns raised, 7 addressed.

### Background
- A range that a workspace package satisfies links that package.
`Package::parse` stores the link in place of the range.
- An override replaces such a link on a clean install, as in 1.4. #45307
restored that and lists it as still open (#37271). This PR makes the
commands agree.
- Considered sending the rewritten rows through the resolver again. It
needs row ids that survive the clean before the override check.

### Downsides
- `edit_after_resolve_slow` grows by 2,009 bytes. A command runs 20 more
instructions per rewritten range and allocates one `Vec`. A plain
install is unchanged.
- Still wrong: two rules for one name, where the rule for the written
range names another package. The next `--frozen-lockfile` install fails,
as on 1.4.3.

<details><summary>Notes</summary>

#### Repro

Registry: the test registry of `test/cli/install` (`no-deps` has 1.0.0,
1.0.1, 1.1.0, 2.0.0).

- `package.json`:
`{"name":"root","workspaces":["packages/*"],"overrides":{"no-deps":"1.0.0"}}`
- `packages/m/package.json`: `{"name":"no-deps","version":"1.0.5"}`
- `packages/bar/package.json`:
`{"name":"bar","dependencies":{"no-deps":"^2.0.0"}}`
- `bun install`: bar gets the registry's `no-deps@1.0.0`.
- `cd packages/bar && bun update`: writes `"no-deps": "^1.0.0"`. On main
bar now gets the workspace package 1.0.5, and `bun.lock` has no
`bar/no-deps` entry. With this PR bar keeps `no-deps@1.0.0`.
- Remove `bun.lock` and `node_modules`, then `bun install`:
`no-deps@1.0.0` on both builds.

#### Which build fails the new cases

- The 48 cases are 4 rules (`"no-deps"` in `overrides`, `"bar>no-deps"`,
`"no-deps"` in `resolutions`, `"no-deps": "latest"`), 6 commands
(`update`, `update no-deps`, `update --latest`, `add no-deps` in the
workspace, `update -r` and `update --filter bar` in the root) and 2
linkers. Each case compares `bun.lock` with the one that a clean install
writes.
- They fail on a debug and on a release build of main (c1df64b). The
regression is not in a release: on 1.4.3-canary.1, 44 of the 48 pass.
The other 4 are `update --filter bar` with the hoisted linker.
- The 6 other new cases pass on main. Two pin a rule whose version the
workspace package satisfies. Four pin that the package links after a
command when the rule for the written range accepts it, or when the rule
is `"$no-deps"` and takes the written range. The first version of this
fix kept every dependency that had a rule, and failed those four.

#### Cells

- `bun add` and `bun update` in a workspace, 13 commands, 2 linkers, 3
combinations of member version, rule value and declared range, under a
rule without a selector: 78 cells. With this PR every cell has the
`bun.lock` of a clean install. On main 12 differ.
- The same 78 cells under a rule with a version selector
(`no-deps@^1.0.0`): equal to a clean install on both builds. A selector
does not match a range that links a workspace package, so this PR
changes nothing there.
- `--dev`, `--optional` and `--peer` forms of `bun add`, and `bun
update` with a peer range (18 cells): equal to a clean install. A peer
gets no copy, and it already was bound to the workspace package.

#### What a project pays that does not hit the bug

Release builds without LTO of main (c1df64b) and of this change on
that commit.

- Size (`llvm-size`): text 101,521,860, then 101,523,860. Data and bss
are equal. By symbol (`llvm-nm -S`): only
`package_json_write_back::edit_after_resolve_slow` changes, 15,505 to
17,514 bytes. `sync_lockfile` and `bind_workspace_link` are inlined
there.
- Instructions (`qemu-x86_64 -one-insn-per-tb -d exec,nochain -dfilter`
on the symbol ranges, two runs per build):
- `install --lockfile-only` on 6 fixtures (no rules, a rule for another
name, a rule with a selector and 8 dependents, a rule with a selector on
a member name, a rule without a selector on a member name, no
workspaces): `lookup_selecting`, `bind_row`, `parse_with_tag`,
`semver_query::parse_with` and `tag_workspace_links` run the same number
of instructions on both builds. `edit_after_resolve_slow` is not
entered.
- `bun update` that rewrites 3 ranges, none links a workspace package:
`edit_after_resolve_slow` 1,148, then 1,208.
- `bun update --latest` that writes a range that links a workspace
package, no rule for the name: `edit_after_resolve_slow` 1,016, then
1,072. `lookup_selecting` 87, then 116: one rule lookup for each
rewritten range that links.
- Lockfiles: `bun.lock` and `bun.lockb` are byte-identical between the
two builds on 6 of 6 fixtures, and each build installs the other's file
with `--frozen-lockfile`.
- `valgrind` and `bloaty` are not in the container. The allocation count
is from the code: one `Vec<DependencyID>` for a command that rewrites at
least one range.

#### The case that is still wrong

- `"overrides": {"no-deps": "npm:a-dep@1.0.1", "no-deps@^2": "1.0.0"}`,
workspace package 1.0.5, bar declares `^2.0.0`. `bun update` in bar
writes `^1.0.0`. A clean install then gives `a-dep@1.0.1`.
- main: the workspace package, and `--frozen-lockfile` passes. This PR
and 1.4.3-canary.1: `no-deps@1.0.0`, and `--frozen-lockfile` fails once.
A plain `bun install` corrects it.
- With two rules for `no-deps` itself (`"no-deps": "1.1.0"` beside
`"no-deps@^2": "1.0.0"`) this PR gives 1.1.0, as a clean install does.
main gives the workspace package.

#### Policy

- The guard applies the rule that a clean install applies today. If a
later change makes an override never replace a link to a workspace
package (#37271), the guard goes with it.

#### Self-review

Addressed:

- The guard decides by the version of the rule and not by its existence.
- The overrides are synced before the rows are bound.
- Parent-scoped rules, `resolutions`, `bun add`, `update -r` and `update
--filter` are tested.
- The failing baseline is named.
- Comments are one line.
- The test helper stops at the filesystem root.
- The change for rules with a version selector is not in this PR.

Not taken: one decider through the resolver (see Background).

#### Suites run on a debug build of this branch

`bun-workspaces` (339), `overrides`, `nested-overrides`, `catalogs`,
`bun-install`, `bun-install-registry`, `bun-lock`, `bun-lockb`,
`lockfile` checks under `frozen-lockfile-*` (4 files), `bun-update`,
`bun-update-lockfile-sync`, `bun-update-transitive`, `bun-add`,
`bun-add-filter`, `bun-add-catalog`, `bun-dedupe`, `bun-audit`,
`bun-remove`, `bun-prune`, `isolated-install`, `hoist`,
`bun-workspaces-self-contained`, and the 10 files of `migration/`. 0
failures with a per-test timeout of 180 s. The default 5 s times out
under the sanitizer build in this container.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants