install: fix regressions and older bugs found after the package manager consolidation

Merged
JA
25d ago
+36552-6425

What does this PR do?

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

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

Behavior changes to sign off

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

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

Regressions from #44968

Resolution and lockfile:

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

Git dependencies:

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

Linkers:

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

Speed:

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

Commands:

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

Resolution and lockfile

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

Hoisted linker

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

Isolated linker and global store

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

Workspaces

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

Lifecycle scripts and trust

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

bun patch

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

Tarballs, bun pm pack and bun publish

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

Git dependencies

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

Migrations (npm, yarn, pnpm)

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

Auto-install

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

bunx and global installs

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

bun update, bun outdated, bun audit, bun pm *

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

Registry requests and the manifest cache

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

Configuration (bunfig.toml, .npmrc, flags)

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

Messages and diagnostics

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

Windows

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

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

Hardening

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

Not in this PR

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

Known to be still open

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

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

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

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

Gaps in what this PR adds:

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

How did you verify your code works?

  • State of the branch. The numbers are from CI's release builds of cc9d7c2da and of main at the merge base. The head adds one fix, to bun add --trust, and a test.
  • Order of responses. Random projects, each installed 24 times with a warm and 24 times with a cold manifest cache, under random response delays. Projects that got more than one bun.lock, main / branch: 6 / 0 of 950; 49 / 0 of 300 with many peers (8 + 8 runs); on the branch 0 of 1,400 with git, GitHub and tarball dependencies.
  • Other commands, and faults. 21 commands (add, remove, update, edits of package.json, ...) on random projects, cold against warm: 1,260 cases, no difference. 7,640 pairs of a command and a registry fault: the exit codes of main, no hang. 16,148 installs with one package answering 404: none ends with another bun.lock and no message. 6,168 bit flips in cached manifests: no crash.
  • Wall clock of a cold install, 450 packages, delayed responses, median of 9 runs, main / branch in ms: plain 1555 / 1505; three levels of peers 1603 / 1585; with one slow host 1900 / 1914; three remote tarballs that take 0.3 s 1588 / 1271, 1.5 s 1933 / 1893, 3 s 3426 / 3408.
  • A script per report. 448 repro scripts: 61 of the first 417 pass on main, 306 on the branch. One that passes on main fails here, on purpose: the error for a command typed in a linked workspace outside the project. The rest fail on both.
  • The whole diff against main, one pass per kind of regression (deletes, credentials, loud to quiet, works to refused, lockfile compatibility, cost): about 95,000 differential cases, 364 real lockfiles and 9,571 real package folders found 22 regressions of this branch; each is fixed or listed above.
  • Changed existing tests. The 138 whose expectations the first push touches were audited line by line: none lost, four weakened ones restored. The fixed order changed when 31 tests release a held response, but no expectation.
  • Cost, user-space instructions (millions, median of 3): 1,500 packages, 50 workspaces, bins, scripts, platform packages, overrides, a catalog, aliases; the hoisted project has peers too.
Commandhoisted, mainhoisted, branchisolated, mainisolated, branch
fresh, cold cache1501.31503.72441.02450.1
warm, nothing to do185.8164.1175.9176.2
--frozen-lockfile133.5122.1150.3150.8
bun add182.3160.5171.1171.7
no node_modules, cache warm198.6179.11291.41304.1
no lockfile, --lockfile-only129.7128.9110.8111.0
  • Above 3 %: bunx of a scoped or cached tool (+0.2 M, 35 system calls); bun pm untrusted (+5 %, one more hoist); an isolated bun prune that removes something (+4 %); auto-install in a workspace without a lockfile (+13 %; double at 300 workspaces); and the hoist named in the sign-off list. A warm hoisted install makes 8,326 system calls, not 10,434.
  • Upgrades over an older tree. 400 random hoisted projects laid by main, taken over, then a pulled bun.lock: 17 end with a wrong version on main, 21 here (the stale copy after a pull described above). Of planted folders that Bun didn't lay, main loses 138, the branch none.
  • On earlier builds: semver against npm (59.9 M comparisons; 21 of 6,698 real ranges differ from npm's strict mode, none changes an install); bun pm version against semver.inc (0 of 1,530,270 differ); 1,260 tarballs extract as before; older builds of main leave a lockfile with the new keys unchanged.