Repository navigation
install: bound isolated store entry names; tarball URL credentials; file: tarballs relative to their folder package - #38867
Conversation
With the isolated linker, a store entry is named <name>@<resolution>, and for folder, tarball, git and github dependencies the resolution embeds the whole path or URL. On Windows the entry's absolute path then passes MAX_PATH, which CreateProcess rejects as the cwd of the package's lifecycle scripts (bun install fails with "Failed to run script ... due to error ENOENT"), and a resolution longer than NAME_MAX cannot be created on any platform. Write at most 80 bytes of the resolution: a longer one is cut to 63 bytes and suffixed with + and the 16 hex digit wyhash of the full text. Every consumer of the name (project store, global store, dependency symlinks, entry hash, bun pm licenses, bun pm prune) formats it through StoreKeyFormatter, so they all agree on the cut name. Names of 80 bytes or less are unchanged.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughChangesThe isolated-install store now bounds long resolution names to 80 bytes, preserves UTF-8 boundaries, and appends a hash. Store-key paths and installation tests cover long URLs, peer hashes, symlinks, and lifecycle scripts. Documentation describes the format and Windows limitation. Isolated-install store names
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Status: reproduced, fix and tests pushed, CI green on 474da9d, #37470 closed in favor of this PR; needs a maintainer to decide on the shape and the constant.
|
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it introduces a design decision — a fixed 80-byte cap with a hash-suffix scheme for isolated store directory names — that changes persistent on-disk layout, is documented as a public contract, and supersedes an alternative approach open in #37470, a human sign-off on the chosen shape and constant would still be worthwhile.
What was reviewed:
- Verified
Wyhash::update/final_is chunk-invariant (src/wyhash/lib.rstest_iterative_chunked_matches_oneshot), so hashing viaResolutionSink::write_strmatchesBun.hash(text)regardless of how the formatter splits its writes. - Traced the
write_strtruncation:buf[..sink.len]in the ≤80 branch is never mid-chunk, and the cut branch backscutto a char boundary beforestr_utf8, so nofmt::Erroron valid input. - Checked that
entryStoreNameandgitExecutableused by the new tests already exist in the file, and that the tests follow harness conventions (Buffer.alloc,port: 0, boundary at exactly 80/81 bytes).
Extended reasoning...
Overview
The PR bounds the resolution portion of isolated-linker store entry names at 80 bytes: longer resolutions are truncated to 63 bytes (backed to a UTF-8 char boundary) and suffixed with +<16-hex wyhash>. It touches src/install/isolated_install/Store.rs (the StoreKeyFormatter Display impl plus a new ResolutionSink/write_resolution helper), adds a long store entry names test suite to test/cli/install/isolated-install.test.ts, and documents the naming rule in docs/pm/isolated-installs.mdx.
Security risks
None identified. The change is confined to formatting a directory name derived from the lockfile's resolution string; no new untrusted input is parsed, and the hash is non-cryptographic wyhash used purely for disambiguation between long resolutions of the same package. It does not affect integrity checks, tarball extraction, or path validation.
Level of scrutiny
High. This alters the on-disk naming convention of the isolated store, which is persistent state consumed by bun install (re-detection of existing entries), bun pm prune (split_store_key), bun pm licenses (BunStore::lookup), and the global virtual store. The 80-byte constant is a tunable with a documented derivation, and the PR explicitly overlaps with the alternative in #37470 (200-byte cap on the whole key). A maintainer should confirm the chosen constant and scheme before it becomes a documented public contract.
Other factors
I confirmed the streaming-hash correctness concern is not an issue: bun_wyhash::Wyhash buffers to 48-byte rounds and its unit tests (test_iterative_chunked_matches_oneshot, test_iterative_maintains_last_sixteen) prove init(0).update(a).update(b).final_() == hash(0, a++b), so the per-write_str hasher.update calls in ResolutionSink produce the same value as Bun.hash(full_text) regardless of how fmt::write chunks the formatter output. I also checked that the ≤80-byte fast path can never observe a truncated chunk in buf (truncation implies sink.len > 80), and that the char-boundary loop guarantees str_utf8 succeeds on the cut prefix. The new tests cover the exact boundary, hash disambiguation of a shared prefix, multi-byte cut, peer-hash suffix ordering, global store link naming, and the reported git lifecycle-script case; entryStoreName and gitExecutable are pre-existing helpers in the file. Given the design decision and the overlap with #37470, deferring rather than approving.
|
Updated 11:05 PM PT - Aug 14th, 2026
❌ @robobun, your commit cd82495 has some failures in 🧪 To try this PR locally: bunx bun-pr 38867That installs a local version of the PR into your bun-38867 --bun |
|
Updated 3:14 PM PT - Aug 17th, 2026
✅ @Jarred-Sumner, your commit 9df4f83cfa973d1f5d41c1d0805c856a2e91a559 passed in 🧪 To try this PR locally: bunx bun-pr 38867That installs a local version of the PR into your bun-38867 --bun |
There was a problem hiding this comment.
Thanks for addressing the gitEnv nit — the git test now isolates bun's internal clone too. I re-reviewed and found no bugs. Because this changes the on-disk store entry naming scheme (a design decision with a tunable constant) and is positioned as superseding #37470's alternative approach, a maintainer should sign off on the 80-byte bound and the hash-suffix shape.
What was reviewed
write_resolution/ResolutionSink: buffer fill can't split a char in the ≤80 path (all bytes fit); the >80 path backs up to a UTF-8 boundary andcutcan't underflow (is_on_char_boundaryis true at 0).- Confirmed all store-name consumers route through
StoreKeyFormatterper the description;prune.rssplits at@so the cut-inside-resolution shape doesn't break it. - Tests cover the exact-limit / one-past / shared-prefix / multi-byte-boundary / peer-hash / global-store / git-lifecycle cases and compute expected names via
Bun.hash.
Extended reasoning...
Overview
The PR bounds the resolution portion of isolated-install store entry names to 80 bytes: resolutions ≤80 bytes are written verbatim (so registry versions and github+owner+repo+<sha> are unchanged); longer ones are cut to ≤63 bytes at a UTF-8 char boundary and suffixed with +<16-hex wyhash>. This fixes Windows CreateProcessW MAX_PATH failures for lifecycle-script cwds and cross-platform ENAMETOOLONG for very long tarball/folder/git resolutions. Changes: src/install/isolated_install/Store.rs (new ResolutionSink fmt::Write sink + write_resolution, wired into StoreKeyFormatter's Folder and catch-all arms), a 6-test describe block in test/cli/install/isolated-install.test.ts, and a paragraph in docs/pm/isolated-installs.mdx.
Security risks
None identified. The change shortens filesystem path components; no new external input is parsed. The wyhash is used for disambiguation, not security. Truncation preserves valid UTF-8 (verified is_on_char_boundary and str_utf8 in src/bun_core/string/immutable.rs), so no invalid-UTF-8 path is written.
Level of scrutiny
Medium-high. The mechanism is small and self-contained (one fmt::Write sink and one helper), but it changes the on-disk naming scheme for a subset of store entries — a format decision that every consumer of store entry names (installer paths, global-store links, bun pm prune, bun pm licenses, entry-hash seeding) depends on. The description enumerates those consumers and argues each still works, and names ≤80 bytes are byte-identical so existing installs aren't re-linked. That reasoning looks correct, but the choice of 80 and the hash-suffix shape are tunable design calls (the description compares against pnpm's 120-byte whole-name default and against #37470's 200-byte whole-name approach). Per the review guidelines, design decisions of this kind warrant a maintainer's sign-off rather than auto-approval.
Other factors
- My earlier nit (pass
gitEnvtorunBunInstallso bun's internal git clone is isolated from runner config) was addressed in 39bc380. - The comment-cop bot has repeatedly flagged the doc comment on
MAX_RESOLUTION_LEN; after the two shortening commits it's now a 5-line doc comment explaining a magic number and its Windows constraint, which reads as necessary context rather than a workaround justification — likely a heuristic false positive, but a maintainer can judge. - Test coverage is thorough: exact-limit vs one-past, two long resolutions sharing a prefix (hash keeps them distinct), peer-hash suffix after the cut name, multi-byte char at the cut point, tarball >NAME_MAX with global store enabled, and the reported git+file:// lifecycle-script case. Tests derive expected names via
Bun.hash(matchingWyhash::init(0)). - This PR explicitly supersedes #37470 if it lands and interacts with #36973 and #38810 — coordination another reason for a human look.
…t declares them A local tarball dependency declared in the package.json of a file: folder dependency was read relative to the project directory instead of the folder, so the install failed with ENOENT, or installed a same-named tarball from the project directory when one existed. Only workspace declarers had their directory joined in. enqueue_local_tarball now asks local_tarball_base_dir for the base: the directory of the declaring workspace or file: folder package when that package's own specifier is the tarball path, and the top-level dir otherwise. The second case covers paths substituted from the root package.json (overrides, resolutions, catalogs), which were previously read relative to the workspace when applied to a workspace dependency. Both the resolve pass and the install from bun.lock go through the same function, so they read the same file.
…ation A dependency declared as http://user:pass@host/pkg.tgz (or a registry dist.tarball URL of that shape) was requested without any Authorization header. NetworkTask::for_tarball now splits the userinfo off the request URL and sends it as Authorization: Basic base64(user:pass), the header npm sends for such URLs. Credentials configured for the registry scope still take precedence when they apply to the tarball. The request URL no longer carries the userinfo, so the HTTP client's same-origin check keeps the header across a redirect within the host and error output prints the URL without the credentials.
There was a problem hiding this comment.
I reviewed the expanded PR (now bundling the tarball-URL credential and folder-relative file: tarball fixes on top of the original store-name bound) and didn't find bugs. A maintainer should still look at this one: the author has flagged the 80-byte MAX_RESOLUTION_LEN constant and the cut-vs-threshold shape as needing a maintainer decision, and the new for_tarball credential path (userinfo → Authorization: Basic, precedence vs. registry credentials, redirect handling) is security-sensitive.
What was reviewed:
write_resolution/ResolutionSink: UTF-8 boundary backup, the ≤80 verbatim / >80 hash-suffix split, and that every store-name consumer goes throughStoreKeyFormatter.split_url_userinfo/basic_authorization_from_userinfo: authority-only@detection (scoped-path@excluded), empty-userinfo handling, and the count/append two-passHeaderBuilderstaying balanced.local_tarball_base_dir: thedeclared_by_parentguard so overrides/catalogs stay root-relative, and thatget_parent_pkg_of_dependencyreturningNonefalls back to the top-level dir.
Also: the comment-cop bot's four open flags on NetworkTask.rs look like the same false positive that was already answered for Store.rs (why-comments on constants/behavior, not workaround justifications).
Extended reasoning...
Overview
This PR bundles three independent install fixes across 10 files: (1) bounding the resolution part of isolated store entry names to 80 bytes with a wyhash suffix (Store.rs, docs, ~300 lines of new tests in isolated-install.test.ts), (2) sending tarball-URL userinfo as Authorization: Basic and requesting the URL without it (NetworkTask.rs, ~220 lines of new tests in bun-install.test.ts), and (3) resolving file: tarball paths relative to the file: folder package that declares them (PackageManagerEnqueue.rs, new get_parent_pkg_of_dependency in lockfile.rs, tests in bun-install.test.ts and bun-workspaces.test.ts). Since my last comment (2026-08-15, a nit on gitEnv that was applied), fixes 2 and 3 were merged in from #39025 and #39017, so the PR is substantially larger than when I last looked at it.
Security risks
Fix 2 is security-relevant: it derives an Authorization header from user-controlled URL userinfo, decides precedence vs. configured registry credentials, and depends on the HTTP client's origin comparison for whether the header follows redirects. The description documents the npm-compat rationale and the tests cover same-host redirect (keeps header), cross-host redirect (drops header), scoped-path @ not treated as userinfo, and registry-credential precedence. I didn't find a bypass, but this is exactly the class of code (auth, credentials on the wire) the review guidelines say a human should sign off on. Fix 1 changes on-disk directory names on upgrade (git entries over 80 bytes get re-created), which is a one-time compat effect the description calls out. Fix 3 changes which file a relative path resolves to; the declared_by_parent guard keeps overrides/catalogs root-relative and both are covered by decoy-tarball tests.
Level of scrutiny
High. The author's own status comment says "needs a maintainer to decide on the shape and the constant" for the 80-byte bound, and a follow-up comment lays out two open trade-offs (unbounded name part; most git+https entries over 80 bytes get renamed on upgrade). That is an explicit design decision left for a maintainer. The credential handling is new auth-header logic. The new get_parent_pkg_of_dependency is an O(packages) scan per local-tarball enqueue — likely fine given local tarballs are rare, but it is new lockfile surface. Three independent fixes in one PR also means a maintainer should confirm they want them landed together.
Other factors
CI is green on the latest commit (Build #98588). All three fixes have thorough tests (boundary at exactly 80 bytes, one past, shared-prefix collision, multi-byte cut, deep directory, global store, git postinstall; a 12-case credential table including redirects and precedence; decoy tarballs at the wrong-base path for both linkers, plus override/catalog guards). My earlier gitEnv nit was applied. The four unresolved comment-cop flags on NetworkTask.rs are the same automated heuristic that fired on Store.rs and was answered there; the flagged comments are why-comments (npm-compat citation, redirect-origin rationale, precedence rule), not workaround justifications.
|
is it expected that username and password is written to package.json and lockfile? |
|
Yes, intentional, and it is not new behavior from this PR: the URL is the dependency spec itself, so current bun already writes it as given to package.json and bun.lock (just reproduced with 1.4.0-canary against a local tarball server). This PR only changes what goes on the wire (the credentials move from the request URL to an Authorization header) and documents the persistence in add.mdx because it looked worth stating. npm does the same: with npm 11.16 against the same server, Stripping them from the lockfile would also break reinstalls: unlike registry credentials, which live in .npmrc, there is nowhere else to recover them from, so a |
…33106) ### Problem - A root `file:` package with relative `file:` dependencies of its own fails to resolve: `error: Could not find package.json for "file:../packages/bun-types" dependency "@types/bun"`. From a lockfile: `error: refusing to install dependency shared-lib with unsafe folder path "../packages/shared-lib"`. - The escape check from #31417 (`PackageManagerEnqueue.rs`, `PackageInstaller.rs`) rejects any transitive folder path that leaves the project root, whoever declared it. Worked in 1.3.14, broke in 1.4.0. Fixes #40314. ### Fix - Both sites call `Lockfile::is_trusted_folder_dependency(id)`: the declaring package is the root, a workspace, or a `Folder` package the root or a workspace depends on directly, or a plain override names the dependency (#32452, unchanged). The `package-lock.json` migration (`npm_lock.rs`) applies the same rule. - A `file:` path in one of those package.jsons is user authored, like a root `file:` dependency, which may already point anywhere. Registry, git, and tarball packages stay constrained, as does a folder one of them ships. - The anchor is checked, not assumed: a migrated or hand-written lockfile can carry dependencies for a folder a registry package ships. - Verified: `bun-install.test.ts` (3 tests fail on main), `migration/migrate.test.ts` (1 fails on main), and the other install suites (Notes). ### Background - A `file:` dependency becomes a `Folder` package. Its lockfile path is root relative, so an outside package starts with `../`. - `bin_target_escapes_package_dir` detects a path that leaves its base directory. #31417 applied it to transitive folder paths so a registry package cannot link local directories. - Overrides come from the root package.json only. A flat rule makes every edge of that name root authored (#32452 whitelists it). A nested rule covers one edge, so `contains_name` ignores it. <details><summary>Notes</summary> This repo's own `test/` directory hits the bug on `bun update` (`bun-plugin-svelte` is a `file:` dependency outside the project and declares `"@types/bun": "../bun-types"`). Minimal repro: a root package.json with `{ "dependencies": { "plugin": "file:../packages/plugin" } }` where `packages/plugin/package.json` declares `"shared-lib": "../shared-lib"`. The two sites that picked `local_package_features` with the same three-tag match (`Tree.rs`, `PackageManagerResolution.rs`) now call `is_local_package()` too. The overlong-path check and the #32452 carve-out are unchanged. Rebase on current main (requested in the PR comments): main already has `Lockfile::get_parent_pkg_of_dependency` (#38867, returns `Option<PackageID>`), so this branch uses it instead of its own copy. Visibility follows main (`pub(crate)`). One test on main conflicts in behavior, not in text. #38333 added `rejects a nested "<field>" rule pointing at a file: path outside the project`. Its parent is `pkg-a: file:./pkg-a`, a local folder package, so this change exempts the path and the nested rule installs. The test is re-anchored on a registry parent (`baz` from the dummy registry declaring `shared: 1.0.0`), which the name-based check still rejects, with the same two error lines. A second test asserts the local-parent case installs, from package.json and from the lockfile. The same re-anchoring was already done for the two tests from #31417/#32452 that pinned the old behavior with a local folder parent. Test changes in `bun-install.test.ts`: - New: `installs transitive file: dependencies of a local file: package that point outside the project`. Runs `bun install` (fresh), `bun update`, and `bun install` from the saved lockfile with an empty `node_modules`. Asserts both transitive packages are linked under the declaring package. - New: `applies a root "<field>" file: path to a registry package's dependency` (the #32452 carve-out, with a remote parent, at both sites). - New: `installs a nested "<field>" rule pointing at a file: path outside the project for a local file: package's dependency`. - The `different name` reject test now points at an existing folder with a package.json (absolute path), so a bypassed check would link `loot` and fail the test. - Re-anchored on a registry parent: `does not install a registry package's transitive file: dependency that points outside its package`, `still rejects a registry package's transitive file: dependency that escapes when a different name is in "<field>"`, `rejects a nested "<field>" rule pointing at a file: path outside the project for a registry package's dependency`. Trust anchor: `is_dependency_of_local_package` accepts a `Folder` parent only when `is_workspace_declared_package(parent)` finds it in the root or a workspace resolution list. Without that, a lockfile with `evil (registry) -> inner (Folder, node_modules/evil/inner) -> loot (file:/outside)` was installed, because the migration writes dependencies for `inner` while bun's resolver never would. `refuses to install an escaping file: dependency that a registry package's own folder declares in the lockfile` (hand-written `bun.lock`) and `a file: dependency declared by a registry package's own folder is still skipped` (migration) cover both layers. Migration: in `npm_lock.rs` the gate at `link_package` only exempted `Root | Workspace` parents. A blanket `Folder` exemption would also migrate `o -> p: ""` in the `external-link--root` fixture (a registry spec that npm resolved to a bare folder beside `o`, which does not exist as a package), so the exemption is limited to a `Folder` parent with a `file:` spec. That is the case this PR is about: the path is written in that package.json. The existing out-of-tree migration tests are unchanged. Fail-before was checked with main's `src/install/` swapped in: the three positive tests fail with the errors above, and everything else in the file has the same result as on main (the remaining failures there are the network-dependent bitbucket/gitlab/external URL tests). Suites run with the fix: `bun-install`, `overrides`, `nested-overrides`, `bun-workspaces`, `bun-update`, `bun-add`, `bun-dedupe`, `isolated-install`. #33159 (merged) materializes transitive `file:` dependencies of local `file:` packages at install time. This PR removes the resolve-time and install-time rejection of their paths, and the positive test asserts the combined result. </details>
### 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 …
This PR bundles three independent install fixes (each was reviewed on its own PR; folded here so they land together):
file:tarballs relative to thefile:folder package that declares them (from install: read file: tarballs relative to the file: folder package that declares them #39017)Rebased on main after #39014: the isolated-store test file now computes expected git entry names through the same resolution cut (
storeEntryName), since agit+http+++host+port+repo.git+<url hash>+<sha>resolution passes 80 bytes.1. install: bound the resolution part of isolated store entry names
Problem
linker = "isolated", a trusted git dependency with any lifecycle script makesbun installfail on Windows witherror: Failed to run script prepare due to error ENOENT(exit 1). The same project installs fine with the hoisted linker. This is the Windows failure of the isolated case in install: install a git dependency's devDependencies before running its prepare scripts #38810's test, which that PR currently skips on Windows.<name>@<resolution>(StoreKeyFormatter,src/install/isolated_install/Store.rs). For git, github, tarball and folder dependencies the resolution is the whole URL or path with separators turned into+(src/install/repository.rsStorePathFormatter,src/install/resolution.rsStorePathFormatter), plus the commit for git: a git dependency checked out from a temp directory gets an entry likedep@git+file++++C++Users+AZUREU~1+AppData+Local+Temp+lc-repro+dep-repo+3c6955c70dbe1ff186a126c93c5e77c3ae7bd6b4(111 bytes here, 170+ in install: install a git dependency's devDependencies before running its prepare scripts #38810's test), and the name grows with the repository path.<project>\node_modules\.bun\<entry>\node_modules\<name>, is the cwd of the package's lifecycle scripts (Scripts::get_list->lifecycle_script_runner.rsSpawnOptions.cwd). bun's own file operations use long-path-capable NT paths, so the entry is created and linked, butCreateProcessWdoes not accept a current directory longer thanMAX_PATH, and the spawn (libuv'suv_spawnon Windows) reports that as ENOENT. Measured on Windows x64 with the released1.4.0-canary.1: a package directory of 238 characters runs the script, one of 268 characters fails as above.NAME_MAX: a tarball URL or folder path of 250+ bytes makes the install fail withENAMETOOLONG: File name too long: failed to link package(the case of install: keep isolated store entry names within NAME_MAX #37470).Fix
StoreKeyFormatternow writes the resolution throughwrite_resolution: a resolution of at most 80 bytes (MAX_RESOLUTION_LEN) is written unchanged; a longer one is written as its first 63 bytes (backed up to a UTF-8 character boundary) followed by+and the 16 hex digit wyhash of the full text (seed 0, soBun.hash(text)reproduces it), which is 80 bytes again. Thename@prefix and the+<peer hash>suffix are unchanged. A cut git entry looks likedep@git+file++++C++Users+AZUREU~1+AppData+Local+Temp+lc-repro-aaaaa+ef7ad81a431ad661(the 268 character case from the measurement above; its package directory is now 206 characters).Displayimpl: the project store directory and the dependency symlinks into it (Installer.rsappend_store_path/append_store_node_modules_path/link_to_hidden_node_modules), the lifecycle script cwd (sameappend_store_path), the global store directory name and the entry hash seeded from the name (Installer.rsappend_global_store_entry_path,isolated_install.rsentry hash),bun pm prune's set of expected directories (prune.rspush_store_entry_names) andbun pm licenses's lookup (pm_licenses_command.rsBunStore::lookup, viafmt_store_key). They all see the same cut name; nothing persists or parses the old one (the lockfile stores resolutions, and an existing entry is detected by re-deriving its name).Wyhash's streaming form is chunk-invariant (test_iterative_chunked_matches_oneshotinsrc/wyhash/lib.rs), which is what lets the tests compute the expected names withBun.hash.prune.rs(split_store_key,store_has_entries,store_link_target, which split the name at@) andpm licenses(which matches<key>or<key>+<peer hash>) keep working without changes.github:shorthands (github+owner+repo+<sha>is about 60 to 75 bytes) are unaffected on upgrade. Most full git URLs are over 80 bytes (git+https+++github.com+is 23 bytes and the commit adds 41), so those entries are re-created once under the cut name on the first install after upgrading, and the old directories stay untilbun pm prune, as for any re-resolved entry. The cut keeps the URL itself readable:git-pkg@git+file++++tmp+licenses-git-repo_eXITMt+491eb29762d8d19b570b43+f87e35b31f207dd4.<project> + 34 + 2 * <name> + <resolution>(+17 with peers) characters, so with 80 it fitsMAX_PATHwhenever<project> + 2 * <name>is at most 145 (128 with peers); install: install a git dependency's devDependencies before running its prepare scripts #38810's case (61 character temp dir, 25 character name) ends up at about 225 instead of about 290. It also makes the entry directory name fitNAME_MAXtogether with every suffix the installers append (+<peer hash>,-<entry hash>,.tmp-<hex>) for names up to 119 bytes, and with the peer suffix alone for names up to 157 bytes. For comparison, pnpm'svirtual-store-dir-max-lengthdefaults to 120 bytes for the whole directory name; 80 bytes of resolution plus a typical name lands in the same range.name@resolution, for theNAME_MAXcase only) is closed in favor of this PR: that limit does not help theMAX_PATHcase (the failing names above are 130 to 180 bytes), and a cut throughname@would no longer work withbun pm prune/pm licenses(see theprune.rsbullet above). Its repro installs with this branch and its deep-directory case is carried over below. What it covered and this PR does not is the name part: a name over 119 bytes (157 in the project store) can still produce an entry overNAME_MAXonce its resolution is cut; if that should be covered too, it would be the same sink applied to the name separately, keeping the@. install: document and pin the isolated linker's store layout #36973 (documenting the layout) would need a sentence about this rule; this PR adds a short one todocs/pm/isolated-installs.mdx. install: install a git dependency's devDependencies before running its prepare scripts #38810's Windows skip of its isolated case can be removed once both land. install: leave URL credentials out of isolated store entry names #39014 changes the same text (it removes URL credentials, and the hash is of the text as formatted), so landing it in the same release as this avoids renaming those entries twice.long store entry namesintest/cli/install/isolated-install.test.ts:Bun.hash; two resolutions of one package that only differ after the cut point get separate entries that each resolve to their own package; a second install finds the same entries+<peer hash>appended after the cut name and links the peera formatting trait implementation returned an error; the test only asserts the shape because the spelling of non-ASCII bytes in these names is install: preserve non-ASCII UTF-8 bytes in isolated store path names #32304's subject).bun/node_modulesfallback link point at the cut names, and a second install finds the same entriesinstall.globalStoreenabled: the local entry has the cut name, links tolinks/<cut name>-<entry hash>, and the package imports at runtimegit+file://dependency whose repository directory name alone is 60 characters runs its postinstall script with the isolated linker; the entry has the cut nameStore.rschange the first three tests and the git one fail on the entry name (on Windows the git one fails witherror: Failed to run script postinstall due to error ENOENT, the reported message) and the deep-directory and tarball ones exit 1 withENAMETOOLONG; all of them pass withbun bd teston Linux (the five original ones also on a local Windows x64 build; the deep-directory one on the Windows lanes in CI). install: keep isolated store entry names within NAME_MAX #37470's repro from its description (./x/../x130 tarball spec) installs with this branch with an 84 byte entry.isolated-install.test.ts,bun-pm-licenses.test.ts(itsgit dependency is listed (isolated)case now goes through a cut name on every platform, since the repo lives in a temp dir),bun-prune.test.ts,public-hoist-pattern.test.ts,isolated-relink.test.ts,bun-install-native-binlink.test.ts,config-version.test.ts, and the PowerShell reproduction from the report on Windows x64 (package directory of 268 characters before, 206 after, script runs).Background
node_modules/.bun/<entry>/node_modules/<name>and everything else symlinks to it.<entry>is<name>@<resolution>, plus+<16 hex>when the package was installed for a specific set of peer dependencies. Withinstall.globalStore,node_modules/.bun/<entry>is itself a symlink to<cache>/links/<entry>-<16 hex entry hash>.MAX_PATH: Windows' 260 character limit for paths passed to Win32 APIs. Applications can opt out of it for file operations (bun does, by using\\?\/ NT paths), but the current directory handed toCreateProcessis still limited to it, and libuv maps the resulting Win32 error to ENOENT.NAME_MAX: the 255 byte limit on one path component on Linux and macOS (255 UTF-16 units on NTFS); exceeding it fails withENAMETOOLONGregardless of how long the whole path is.Windows measurement with the released build (1.4.0-canary.1)
Git repo and project created under
%TEMP%withlinker = "isolated",trustedDependencies: ["dep"]and"prepare": "echo prepared > prepared.txt"; the temp directory name was lengthened to move the package directory across the limit.Same second layout with this PR's build:
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install.test.ts test/cli/install/isolated-install.test.ts
2. install: send credentials embedded in a tarball URL as Basic authorization (from #39025)
Problem
"no-deps": "http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz", is downloaded without anAuthorizationheader. A server that needs the credentials answers 401 and the install fails witherror: GET http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz - 401(bun 1.4.0 andmain, hoisted and isolated linker). npm 11 installs the same package.json and sendsAuthorization: Basic Y2Fyb2w6czNjcmV0(base64("carol:s3cret")).http://token@127.0.0.1:PORT/x.tgz) does not reach the server at all: the request goes to the hostnametoken@127.0.0.1.NetworkTask::for_tarball(src/install/NetworkTask.rs) only ever attaches the registry scope's configured token or_auth, and only for npm packages whose tarball is on the registry's origin. Nothing reads the URL's userinfo, and the HTTP client does not either (it only turns the userinfo of a proxy URL intoProxy-Authorization). The misrouted username-only form comes frombun_url::URL::parse, which takestoken@127.0.0.1for the hostname when the userinfo has no:and a port follows (tracked separately; this change no longer depends on how that parser splits the authority).Fix
for_tarballsplits the userinfo off the request URL before anything else looks at it (split_url_userinfo: the authority runs from://to the first/,?or#, the userinfo is everything in it up to the last@, so the@of/@scope/pkg/-/pkg.tgzis not one) and sends it asAuthorization: Basic base64(userinfo)(basic_authorization_from_userinfo, which appends:when the userinfo has no password). The request URL is the URL without the userinfo._auth), they are still sent and the URL's are not. This is npm's order as well: npm-registry-fetch sets the header from the config, and node only derivesAuthorizationfrom the URL'sauthwhen the request has no such header.user:pass,user(sent asuser:),:passand a percent-encoded password (sent undecoded); transcript below. It is also how bun already treats credentials written into a registry URL (install: send credentials embedded in --registry and registry env var URLs #38796 stores the username and password bytes as given). pnpm was not available offline. Two spellings are deliberately not npm's: a second:inside the password is sent as written where npm percent-encodes it, and:token@in a tarball URL is Basic like npm rather than the Bearer that bun's registry URLs make of it; both are called out in the test table and in the code comment.bun_url::URL::originincludes the userinfo and the HTTP client compares origins to decide whetherAuthorizationfollows a redirect, so with the userinfo left in, a redirect to the same host would drop the header (verified: with only the header added, the redirect test below fails withauthorization: nullon the second hop). It also fixes the username-only form without touchingbun_url, and theGET <url> - 401line now prints the URL without the credentials. The cross-host rule is unchanged: the HTTP client strips the header on a redirect to another origin, same as for the registry token (test included).package.json, the lockfile, the task id and the cache key still use the URL as written. Documented indocs/pm/cli/add.mdx.test/cli/install/bun-install.test.ts,describe("credentials embedded in a tarball URL"): 12 tests, 10 of which fail on the unfixed build (the two guards, scoped path and registry credentials taking precedence, pass on both). The rest of the file is unchanged: the remaining failures locally are the tests that need the public internet, identical with the unmodified binary.cargo check -p bun_install,cargo clippy -p bun_install, rustfmt and prettier are clean.Background
package.jsonentry whose version is anhttp(s)://URL ending in.tgz/.tar.gz/.tar. Bun downloads it directly; no registry manifest is involved, so the registry's configured credentials never applied to it (Authorization::NoAuthorizationat the call sites inPackageManagerEnqueue.rs). Registry packages reach the samefor_tarballthrough their manifest'sdist.tarballURL withAllowAuthorization, which is the case where the registry scope's credentials can apply.user:password@part of a URL's authority (RFC 3986 section 3.2.1). HTTP never puts it on the wire; clients that honor it (npm through minipass-fetch, curl, browsers) convert it intoAuthorization: Basic base64(user:password).NetworkTask::for_tarballbuilds one HTTP request per tarball download:url_buf(the request URL) andheader_buf(the headers, built withHeaderBuilderin two passes,countthenappend, because the buffer is allocated exactly once in between). Retries reuse the same request, so the header is sent on every attempt.bun_url::URL::parseis the allocation-free splitter the HTTP client andfor_tarballuse; it is not a WHATWG parser. Itsoriginis a prefix of the input string, which is why it still contains the userinfo.npm 11.16 against a local server logging the Authorization header (same tarball, same package.json shape)
bun 1.4.0-canary.1 (eabb96d) for the first row: the server logs
auth=nulland bun printserror: GET http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz - 401.3. install: read
file:tarballs relative to thefile:folder package that declares them (from #39017)Problem
file:folder dependency whose own package.json declares a local tarball fails to install (released 1.4.0 canary and main, both linkers): project package.json{"dependencies":{"lib":"file:./vendor/lib"}},vendor/lib/package.jsondeclaring"tool": "file:./tool.tgz",vendor/lib/tool.tgzpresent:<project>/tool.tgz. If that file happens to exist it is installed astoolinstead of the folder's copy, with no error. The directory form of the same declaration ("tool": "file:./tool") is already resolved relative tovendor/lib.enqueue_local_tarball(src/install/PackageManager/PackageManagerEnqueue.rs) picks the directory a local tarball path is relative to, and the only declarer it looked at was a workspace (get_workspace_pkg_if_workspace_dep). Every other declarer, including afile:folder package, fell through to the top-level dir.overrides/resolutionsentry or catalog entry pointing a dependency atfile:./x.tgzwas read relative to the workspace when the dependency it applied to was declared by a workspace member, sooverrides: { bar: "file:./bar.tgz" }withbar.tgzin the project root failed with the same ENOENT as soon as a workspace depended onbar. This is Feature: Workspaceoverrideswithfile:paths should resolve relative to workspace root's package.json #25835 (overrides) and Catalog entries withfile:relative paths fail when referenced from workspace packages #25752 (catalogs); both reproduce as reported on the released build and install with this change. The directory form of an override is already resolved relative to the project (Folderarm ofget_or_put_resolved_package).Fixes #25835
Fixes #25752
Fix
enqueue_local_tarballnow takes the base directory fromlocal_tarball_base_dir: the directory of the declaring package when it is a workspace or afile:folder package and that package's own specifier is the tarball path being read; the top-level dir in every other case.file:and what bun already does for workspace declarers and for the directory form. Afile:folder package is read from the project like a workspace is, and itsResolution::Folderpayload is its directory relative to the top-level dir (folder_resolver.rs,NewResolver { folder_path: rel }), the same shape as a workspace'sResolution::Workspacepayload, so both are joined the same way. The only otherResolution::Folderpackages are the stubs created forfile:directories declared by something other than the root or a workspace (Folderarm ofget_or_put_resolved_package); those carry no dependency list, so they are never the declarer of an edge.overrides,resolutionsand catalogs are only parsed from the root package.json (Package.rs,FEATURES.is_main), and applying one leaves the declaring package's stored edge untouched (the replacement is local toenqueue_dependency_with_main_and_success_fn). So when the stored edge's specifier is not the path being read, the root wrote the path and the top-level dir is the only directory it can mean. Without this condition, root overrides applied to a folder-declared dependency, which work today only because of the bug, would start being read from the folder.version_was_replaced:enqueue_local_tarballis also reached fromenqueue_tarball_for_readingwhen a project with abun.lockis installed into an empty cache. The lockfile row keeps the path as declared ("tool": ["bar@./tool.tgz", ...]under"lib": [..., { "dependencies": { "tool": "file:./tool.tgz" } }]), and the edge's declarer and specifier are available there too, so both passes compute the same directory. Lockfile format is unchanged and existing lockfiles keep working; the path in the row is joined onto a different directory only for declarers that previously failed or installed the wrong file.Lockfile::get_parent_pkg_of_dependencyis the same helper that PR adds.bar@./tool.tgz), so two project packages declaring the same relative path to two different files still share one row and one read, the limitation workspaces already have. A package.json inside a folder dependency that worked around this bug by writing a project-relative path (file:./vendor/lib/tool.tgz) will now need the path relative to itself, which is what npm requires for it as well.test/cli/install/bun-install.test.ts,describe("file: tarball declared by a file: folder dependency"): the folder's tarball is installed with the hoisted and with the isolated linker, and a root override supplying the path is read from the project. Each test plants a different tarball at the other candidate path and runs a fresh install followed by--frozen-lockfileinto an emptied cache, so both the resolve pass and the install frombun.lockhave to read the right file.test/cli/install/bun-workspaces.test.ts,relative tarballs > from a root override / catalog entry applied to a workspace dependency, covers Feature: Workspaceoverrideswithfile:paths should resolve relative to workspace root's package.json #25835 and Catalog entries withfile:relative paths fail when referenced from workspace packages #25752 the same way. The four folder/workspace tests install the wrong tarball without thesrc/change (checked against a debug build of main and against the released build); the override-on-folder test passes before and after and pins that case.bun-workspaces.test.ts(74 pass),overrides.test.ts+nested-overrides.test.ts(151 pass),bun-lock.test.ts(40 pass),bun-install.test.ts -t "tarball|tgz|file:|folder|override|resolutions"(51 pass;should treat non-GitHub http(s) URLs as tarballsfails identically on the released build, it needs network),isolated-install.test.ts,bun-add.test.tsandbun-install-registry.test.tsfiltered to tarball/file tests (all pass).cargo clippy -p bun_installis clean.Background
file:dependency on a directory is aTag::Folderdependency; one on a.tgzis aTag::Tarballdependency with a local URI. The latter resolves toResolution::LocalTarball(<path as written>); the path string is both the task id for reading it and the package's identity inbun.lock.file:folder dependencies from disk (Package::parse), and stores for each of the latter two aResolution::Workspace/Resolution::Folderwhose payload is the package directory relative to the top-level dir. Packages from the registry, git or a tarball get their dependency lists from the manifest or the extracted archive instead and have no directory in the project while they resolve.enqueue_local_tarballis called from two places: theTarballarm of the resolve pass (no lockfile row yet, the tarball is read to learn the package's name and dependencies) andenqueue_tarball_for_readingduring install (a row exists inbun.lock, but the extracted package is missing from the cache). It computes the on-disk path on the main thread and hands it to a thread pool task, which only reads the file.Closes #39025
Closes #39017