install: fix regressions and older bugs found after the package manager consolidation
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
mainuses. Not a small revert. - Both linkers install through a
node_modulesthat is a symlink (the project's or a workspace's), remove nothing behind it in their cleanup passes, and warn;bun prunestops 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
.binnamed like a package's command stays, and the install fails withEEXIST. Reversible:is_behind_linkinbin.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_deleteinisolated_install.rs. - Hoisted layout: a package's location depends on
bun.lockonly, 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.lockgets three optional additions with nolockfileVersionbump:"unresolved","hasInstallScript"and"./<name>/<dep>"entries. Reversible: one writer and one parser block each inbun.lock.rs.- Every install checks
bun.lockagainst theoverridesthat name a version or a range: a locked version that a rule forbids is resolved again, and--frozen-lockfilefails. 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. overridesfor the name of a workspace apply to dependencies of the root and of other workspaces again, as in 1.4, whosebun.lockfailed--frozen-lockfileand was rewritten. This undoes #37271, which came with #44968.- Security scanner:
bun add <pkg>,bun install <pkg>andbun update <pkg>send every package when there is no lockfile, only another package manager's, or nonode_modulesfolder, otherwise what the command adds or moves. Reversible:|| !loaded_lockfileand|| !has_node_modulesinsecurity_scanner.rs, and thenarrowline in the runner. - An unreadable
bunfig.tomlor.npmrcfails closed: commands that install or publish stop (a directory there only warns). npm ignores an unreadable.npmrc, so a root-owned~/.npmrcfromsudo npm loginnow blocks them. Reversible: the two calls ofrefuse_unreadable_config. - An
XDG_CONFIG_HOMEthat is empty or not absolute is ignored, and$HOMEis used. Before,~/.bunfig.tomlwasn't read at all then. - Auto-install reads
.npmrcand 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 apackage.jsonon the way up to the workspace root, can't be read or parsed; a broken~/package.jsondoes that for everything below it. In a monorepo without a lockfile it reads every workspace'spackage.jsononce. - A workspace outside the project (
../x, an absolute path, a link that leaves the root's folder) is linked to and stays inbun.lock, but its dependencies aren't installed and its scripts not run, as in npm; anote:says so. Its ownbinis still made executable. Commands that install, typed in one that is a link, end with an error and change nothing; onmainthey installed there. This affects layouts like #25801 under the isolated linker, and #29598 asks for the opposite; both stay open. Reversible:Lockfile::is_member_outsidereturns 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 restoreis from 2.23;--end-of-options, whichmainalready passes for a#ref, from 2.24). - Every git command that Bun runs for a git dependency has hooks,
submodule.recurseand replace objects off, and a cached clone'sconfigis rewritten with Bun's own text before each use. gitlab:,bitbucket:,gist:andsourcehut:dependencies are written tobun.lockand printed asgit+ssh://git@gitlab.com/user/repo#<sha>(npm's form); they failed to install frombun.lockon 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 optionnpmthat only package tarballs set. Every other tar reader,Bun.Archiveincluded, reads as before. Reversible: remove it fromscripts/build/deps/libarchive.tsand drop the option. - The global store rebuilds its entries once after this upgrade;
bun pm cache rmfrees the old ones. Reversible: dropGLOBAL_STORE_LAYOUTfrom the entry hash. - Lifecycle scripts see
PATHentries of the formX/node_modules/.binby their real paths, so a dependency's script no longer finds the project's shared.binahead of the systemPATHthrough a link. Reversible:resolve_bin_dirs_in_path. --exact/exact = truealso pins a typed range, also with--catalogand for an alias:bun add -E react@18writes18.3.1, not18, as npm does. Reversible: one condition inPackageJSONEditor::edit.bun add pkg@2.0.0without 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.jsonentry from another host without anintegritykeeps 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 rmempties 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 unknownbun pm cachesub-commands are errors.bun info,bun pm view,whoami,diffandbun auditretry a failed request likebun 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 publishstill uploads once.bunx's daily refresh costs one manifest request per package.bunxinstalls into the cache directory that~/.bunfig.tomlor~/.npmrcnames (macOS, Linux); it ignored both.bun publishrefuses a prerelease version without--tagorpublishConfig.tag, like npm 11 (reversible: the secondifinrefuse_tag), and a tag that is a version or a range.bun publishreadspublishConfigagain afterprepublishOnly,prepackandprepare, like the name and version (flags still win). Of two equal keys inpackage.jsonthe last one counts, as inJSON.parse. A symlink at the name Bun gives the tarball is replaced, not written through.bun auditfails on an empty answer (empty body, 204, 304). One test from #44968 pinned the old result.- pnpm migration: the
patchedDependencieskey of a git-hosted package is rewritten inpackage.jsonto 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 versionfollows node-semver'sinc. Four existing tests pinned other values.- Piped
bun pm diffprintsdiff --gitfor files whose contents are in the patch (reversible:patch_nameinpm_diff_command.rs).--jsonhas two new keys for binary files,sha256Beforeandsha256After. warn: incorrect peer dependency ...is printed only with--verbose.- Isolated: a link that the user put in
node_modules/.bunin place of a package is replaced by every install;mainkept it. - With a
package-lock.jsonthat can't be used,bun ci,--frozen-lockfileandbun pm migratewarn and try ayarn.lock; they failed. - A dangling link in
.binstays 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$ scriptline ofbun runand 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 infmt.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.whichno longer takes a file without the execute bit for a command. Reversible: one#ifinc-bindings.cpp.
Regressions from #44968
Resolution and lockfile:
- A
package.jsonwith bothoverridesandresolutions: #44968 applied both, so the firstbun installafter the upgrade rewrotebun.lockand could move versions. As in every earlier release,resolutionsis read only when there is nooverrides, and abun.lockwritten by 1.4 installs unchanged, frozen or not. bun installrejected its own freshbun.lockwithdependency cycle cannot be laid out in node_moduleswhen an optional dependency has no published version that fits, or when a cycle is closed only through an optional peer.- A second
bun installadded a nested copy, and abun.lockentry 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.lockcould fail--frozen-lockfileright 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-lockfilefailed, andbun audit,bun whyandbun pm hashsaw it. (A peer that ends at a copy outside its range still leaves one.)--frozen-lockfileandbun cireported 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 updatewas held back by the peer range of a package that loads another copy.packageExtensions: a freshbun.lockfailed its own--frozen-lockfilewhen 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 writesworkspace:*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.jsonhas a free-textname("Project: X","foo@1") failed to install. It installs under the fallback name a nameless package gets, andbun.lockloads 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 installexits 0 again. An emptypackage.jsonnext to a lockfile is still an error.- Ranges such as
>=0.0.0-a *stopped matching prereleases of0.0.0. A comparator that means>=0.0.0admits them, as in npm.
Git dependencies:
bun installcould rungit fetch --pruneon 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 refwhen 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
#reffailed on every warm cache. A cached repository follows the remote's current default branch, also for#HEAD.
Linkers:
- With
node_modulesa symlink, an isolated install deleted everything in the folder behind the link. Hoisted did the same when that folder holds a.bunfolder, deleted a folder there named like a dependency that had leftpackage.json, and replaced a workspace's linkednode_modules(on a volume, a cache disk) with a real directory. - Hoisted: a
bun removeorbun installthat failed to savebun.lockorpackage.jsonstill deleted packages. - Hoisted:
bun add,bun installandbun removedeleted the copy of a dependency that a package's new version bundles, or ships undeclared in itsnode_modules. - Replacing a tree (switching linkers, or an isolated install after
rm -rf node_modulesat the root) deleted.cache,.viteand other tools' dot entries in everynode_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_MAXcrashed on the new staging directory, andbun pm untrusted/bun pm trustdidn't seelink:dependencies. - "Blocked N postinstalls" was no longer printed for Linux platform packages, because the registry's full metadata has
scriptsbut nohasInstallScript. Bun derives it fromscripts, as npm does. That also fixes the older gap underminimumReleaseAge.
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 whyno longer slow down with the square of the number of direct dependencies (an old cost). bun file.jsparsed everydependenciesentry of each package it loaded fromnode_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,removeandupdateaccept 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-devdrops the flag).--no-audit,--no-fund,--legacy-peer-depsand a few more are ignored. Other unknown flags stay errors.bun pm packheld 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 publishrefused a publish that turns provenance off (--no-provenance,NPM_CONFIG_PROVENANCE=0,publishConfig.provenance: null). Like npm, the command line beatspublishConfig, then the environment, then.npmrc.bun publish -rcrashed or published the wrongworkspace:version for a package that follows one with aprepack,prepareorprepublishOnlyscript. With npm's web login, Enter could open an earlier package's login page.bun pm pkg set --json x=010stored 8. Values that aren't JSON (010,0x10,1_000,.5,'text') are rejected, also inside arrays and objects.bun update --latestwith a name in two dependency lists rewrote one entry whilebun installused the other.bun update --latest <name>rewrites the entry thatbun installuses; the other stays as written.bun update --interactivecrashed in monorepos whose packages writecatalog:default(orcatalog:overcatalogs.default). It updates the catalog entry where it's defined.bun createinto an existing folder replaced your.gitignorewith the template'sgitignorefile. Yours stays unless you pass--force; a template's own.gitignoreis a listed conflict.- After a request had failed for good,
bun installstill 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@latestbroke the copy thatbunx toolruns, andbunx toolwaited for a runningbunx tool@latest. A failure before linking leaves the tree as it was. bun info pkgfailed on a registry document without alatesttag.bun info pkg@*names the versionbun addinstalls.- Migrating a
yarn.lockorpnpm-lock.yamlwaited 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.jsonwrites ranges asnpm:^1.0.0lost the versionsyarn.lockpins.bun installreadsnpm:<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:1andnpm:xstay aliases. bun installon ayarn.lockwith a git dependency outside GitHub failed withinvalid git commit "", and--frozen-lockfilestraight on ayarn.lockfailed when the root has a required peer that it doesn't list.- The warning about an unreadable
bunfig.tomlprinted up to three times. It prints once, and--silenthides it.bun a.jswithout a bunfig does one failedopeninstead of three. - Auto-install: a version written in an import (
import "foo@^2") was overridden by the range inpackage.json. bun patch --commitandbun installrejected patches that delete the last lines of a file with no trailing newline.bun patchand--commitfailed when the project's path contains", a tab or a backslash. A failed--commitwith the global store deleted the edited folder.- Undoing every edit and running
bun patch --commitno longer removed the patch. It removes thepatchedDependenciesentry, 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.lockkeys:"unresolved": [...], next tooptionalPeers: optional dependencies that couldn't be resolved, listed only when another package of that name is in the tree."hasInstallScript": truein a package's info object (npm's key). Both are inbun.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.lockdoesn'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 addorbun updatein another workspace could put a newer copy in front of it, outside that entry's range. bun installcould write abun.lockit 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 ofnode_modules.- A fresh
bun installwrites the samebun.lockwhichever 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
patchedDependenciesresolves 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
overridescheck under frozen printsnote: 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 fooandbun update --latestinstalled a version that a version-scoped override ("foo@>=2.0.0": "1.2.0") forbids. They write^2.0.0and install1.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 installandBun.semverread 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.jsonleaves itsnode_modules. - The command of a workspace that isn't built yet stays in
.binwhen another dependency leavespackage.json(#44968). --production,--omit=<type>,--filter,--osand--cpuused 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 thebun.lockkeys do: +0 % to +1.8 % folders on real lockfiles.- An install removes a folder from
node_modulesonly if thebun.lockit 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 pruneremoves that. - A warm install reads each installed
package.jsonwith 3 system calls instead of 4. - After only the project's
node_moduleswas deleted, a package that changed version was linked over the old folder in a workspace's ownnode_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 --productiondeleted 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.jsonremoved nothing fromnode_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 prunesees it. - A command whose file is written by the package's own install script (a workspace CLI built in
prepare) is linked by the samebun 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.jsonornode_modules/.binpath doesn't fit inPATH_MAXfails its lifecycle scripts withENAMETOOLONGinstead of crashing.
Isolated linker and global store
- A dependency removed from a
package.jsonstayed linked innode_modules, with its.binentries. It's unlinked, also when another package still needs it, inside a workspace (#26305), and in the hiddennode_modules/.bun/node_modules. Only that package's ownnode_modulesis touched, and only a link that leads where Bun had linked it: your own links,bun linkand 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_modulesno 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
hoistPatternadded to an installed project takes effect on the nextbun install: fallback links it doesn't match are removed. (publicHoistPatternstill 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 freshbun 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 everynode_modules/.bun/<pkg>link (4 system calls per package, now 1). Two installs at once no longer fail withEEXIST, and a running program no longer loses a package for a moment.bun install --forcereplaces a global store entry in one step on macOS and Linux, so other projects never see it missing.BUN_INSTALL_GLOBAL_STORE=false(alsono,offand 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.lockentry and gets its own dependencies installed, while the root'snode_modules/<name>holds what the root asked for. - A
bun.lockorbun.lockbfrom Bun 1.4 or older that leaves such a workspace out still passes--frozen-lockfileand isn't rewritten bybun 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 likebun add: siblingworkspace:andcatalog:dependencies resolve, and--saveupdates the workspace'spackage.jsonand the root'sbun.lock(#24190, #20015).bun add <name>inside a workspace, where<name>is a workspace that is also published, links the workspace and writesworkspace:*.bun add <name>@<tag>andbun update --latestfor 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 thenpm:form.bun add lib@npm:otherat the root, with a workspace namedlib, installsotherrather than linking the workspace.workspace:../lib,workspace:lib@^andworkspace: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 apackage.jsonno longer fails an install frombun.lock. - An install from a checked-in
bun.lockreports blocked scripts, because the lockfile records which packages have install scripts. bun pm trustrefuses to run with a frozen lockfile.- A fork, folder or workspace named
sharpruns its install scripts like any other package."ignoreScripts": []inpackage.jsonturns the built-in skip list off. bun pm pack,bun publishandbun pm versionrun their lifecycle scripts with the package's and its parents'node_modules/.bininPATH, so"prepack": "tsc"works. Withbun publish -reach package gets its own.- The
preversion,versionandpostversionscripts get$npm_old_version,$npm_new_version,$npm_command,$npm_package_json,$npm_package_nameand$npm_package_version. The new version is computed beforepreversionruns, 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 withignoreScripts = true,ignore-scripts=truein.npmrc(both read in the folder the command is typed in and in the home folder) orNPM_CONFIG_IGNORE_SCRIPTS.
bun patch
bun patchand--commitfetch the package again when it's installed but no longer in the cache (afterbun pm cache rm) instead of failing withENOENT, also for patched packages and git, GitHub and tarball dependencies.bun patch --commitchanges 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.lockand an older patch file stay as they were. With the global store,--commit --lockfile-onlydeleted the edited folder.- Isolated: an install with a patch that doesn't apply stops, as with the hoisted linker, and
bun add/bun updatewrite nothing. - The project's own
.gitattributes,.git/info/attributesor.git/configcould change or empty the patch. git runs without a repository. --commitfailed 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
patchedDependenciesis missing,bun installandbun patchfail and leavenode_modulesalone. The error namesbun patch --commit <pkg>, which writes the file again from the patched package innode_modules, and says how to drop the entry. --commiton 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 patchlost 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=/absinside the project is written relative to it. - When several versions match,
bun patchaccepts 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 patchdropped folders namedCMakeFilesor.gitfrom the copy.- A package's commands lost their executable bit between
bun patchand--commitwhen files are copied rather than hard-linked. - Global store: a package being patched keeps finding its own dependencies, and
bun pruneno longer deletes the folder thatbun patchprepared for a transitive dependency.
Tarballs, bun pm pack and bun publish
- Uncompressed
.tarpackages, and.tgzfiles served withContent-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 aContent-Length. Bun.Archive:extract(dir, { glob })andfiles()resolved with the entries read so far on a truncated or corrupted archive. They reject, likeextract(dir).bun pm packandbun publishleftbundledDependenciesout 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 ownfilesand.npmignore, and a broken sibling that nothing needs doesn't fail the pack.- A bundled name in
dependenciesthat isn't installed is an error, no longer left out. So is a ring of bundled versions that nonode_modulescan hold. - A
catalog:dependency whose catalog entry isworkspace:^(or anotherworkspace:form) was published as written. It's replaced with the workspace's version. An entry that is itself acatalog:reference fails the pack. bun pm packleft lockfile-named files out of a dependency of a bundled package whose name containsnode_modules.- A file that gets shorter while it's packed was padded with NUL bytes. The pack fails.
bun publish x.tgzlooksbinanddirectories.binup in the tarball, not in the current folder.- The registry's
npm-noticeheader is printed for every request ofbun publish. bun publishof2.0.0-beta.1without a tag made itlatest, the versionbun addinstalls. The refusal names--tag nextand--tag latest, and--dry-runrefuses too.bun add ./<path>.tgzwith a path longer thanPATH_MAXfails withENAMETOOLONGinstead of crashing.
Git dependencies
- Checkout: files are written straight from the cached bare repository (
git ls-tree, thengit restore --source=<sha> --worktreewithcore.symlinks=false). The second clone and its.gitfolder 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.sshCommandfrom any config file, plain includes,GIT_CONFIG_*,GIT_SSH,GIT_SSH_COMMANDandcore.askPassall count. A value behind a conditionalincludeIfis still overruled. git@host:user/repo.gitinstalled frombun.locktried 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 = truebroke 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
preparescript of a git dependency fails, a note says that Bun doesn't install itsdevDependenciesfirst.
Migrations (npm, yarn, pnpm)
- A
yarn.lockorpnpm-lock.yamlthat pins a GitHub dependency failed on the first install and lost the pin on the second. It installs the pinned commit and writes abun.lockthat loads. Migrated pnpm rows keep the full 40-character commit. - A
pnpm-lock.yamlwith atype: gitresolution installs the pinned commit, whichever wayrepo:is written. - pnpm: a patch keyed by a version range or a bare name was lost. It becomes one
patchedDependenciesentry for each version it patches. - yarn v1:
link:and bare path (./dir) entries failed with "failed to resolve". They're migrated, with theirbin, 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/repoentry is the archive thatyarn.locknames. - yarn v1: a monorepo where a registry package depends on a workspace failed with "failed to resolve".
- A root or workspace dependency whose
yarn.lockentry 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,osandcpufrom 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.lockorpnpm-lock.yamlnext 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.lockbincluded) writes them asoptionalPeersany more, so--frozen-lockfileaccepts thebun.lockthatbun installwrites.bun pm migrateleaves such a peer out; the nextbun installadds it. bun install --yarnwrote an npm alias under the package's own name. It uses the namepackage.jsongives it, as yarn v1 does, so an alias migrates back to the same package.
Auto-install
bun.lockwas never read withoutnode_modules(onlybun.lockbwas). Bun uses the versions pinned in either, and downloads them on first import.import "foo"also honorsdevDependenciesandpeerDependencies, and walks up to the firstpackage.jsonthat lists the name.overrides,resolutions,catalogsandpackageExtensionsof the project'spackage.jsonapply. An override counts when it names a version, range, tag,npm:alias orcatalog:; one that names a folder, tarball or git repository is passed over, as before.workspace:,file:andlink: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.jsonlists 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=forcerejected an import that names a version in a project with apackage.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 insiderequire()/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
bundledDependenciescan 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 atool@latestrun no longer puts it off. bunx pkg@3failed in a project that has another major ofpkginstalled. With a version, tag, URL or alias, nothing from the project is used.bunx @scope/pkgandbunx -p pkg cmdfind the bin through that package's ownpackage.json, in the project (no further up than its root) or inbunx's tree, and run that package's file, not another package's.binentry of that name (on Windows the shim is taken as is).bunx -p pkg cmdinstallspkgand runs any command, as npx does, withpkg's commands first inPATH.- A project's own tool run through
bunxno longer finds helper commands from an older copy thatbunxhad installed. bunx --no-install pkg@latestruns the copy that is on disk.bunx pkgwithout a version passes over a deprecatedlatest, asbun adddoes.- A
node_modules/.bin/<name>that exists but can't be executed iserror: <path> is not executable, exit 126, instead of a download. - A
bun installthat a package's script starts duringbunxis 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, sobun pm ls -glists it andbun remove -gremoves it.bun remove -g secondtook a bin name away fromfirstfor good ifsecondhad claimed it. A global package whose bin link is missing gets it back on the nextbun add -g,update -gorremove -g. Nothing already in the global bin folder is replaced.
bun update, bun outdated, bun audit, bun pm *
bun update --interactiverewrote every range as "first character + chosen version". Ranges such as>=1.0.0 <2,1.x,*,^1 || ^2orlateststay as written when they already allow the chosen version.bun updatewith--exactorinstall.exactno longer pins apeerDependenciesrange, or a catalog entry that acatalog:peer uses.bun update --latestwithout names leaves a peer range in apackage.jsonthat already allows the latest version (one in a catalog is still rewritten).bun outdatedhid dependencies that are ahead of thelatesttag (next/canarychannels,latestkept on an LTS major). It lists them whenbun updatewould move them. It also printed an optional peer twice.bun update --latestandbun update -idowngraded a dependency that is ahead oflatest, including one written as a dist-tag such as"canary". No key or flag in-ican pick an olderlatest, 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 nextbun audit, and the plugin's line is marked(peer).--dry-runsays the same as the real run, except when the narrow peer range is in one of the project's ownpackage.jsonfiles.bun auditreported "No vulnerabilities found" on an empty answer. It'serror: <registry> returned an empty audit response, exit 1, also with--jsonandfix(for a scoped registry, the existing warning).{}still means none found.bun pm version <increment>computes the same version asnpm version(1.0.0-rc.1+majoris1.0.0). It cleansv2.0.0to2.0.0, and rejects2,x, ranges, a bad--preid(also--preid 01, a number with a leading zero) and, withfrom-git, a tag that isn't a version.bun pm ls/bun listsupport--production,--omit,--depth=0and package names (bun pm ls lodash). This covers part of #13148.bun pm licensesskipped an installed package whose own version differs frombun.lock(a bundled copy, another spelling). It lists it under its own version.--dry-runprinted one line per dependency edge, skipped peers and ignored the platform. It lists what the install would put innode_modules, each package once (not yet narrowed by--filter).bun pm cache rmprints "Cleared" only on success.bun pm cache pack/unpackcrashed while reporting a file name that isn't UTF-8.bun infoandbun createcrashed on a name longer than a path.- A flag that belongs to another
bun pmsub-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.patchapplies withgit applyandpatch -p1when 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 keepdiff --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, andbun pm diff <pkg> 2.0.0starts from the installed version.bun pm diffcompares style sheets that containurl()by their canonical print too.- Commands that rewrite
package.jsonturned control characters such as\^[into\x1Bor\v, which npm and node can't parse.
Registry requests and the manifest cache
- The same commands and
bun publishdropped the registry token on a redirect from http to https on the same host and port. They keep it, asbun installdoes. - A redirected request ignored
NO_PROXYfor the new URL. Each hop picks its proxy by its own URL, as infetch(). - 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.tomlor.npmrcused to be a warning. It'serror: 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,globalDirorglobalBinDirinbunfig.tomlor.npmrcis relative to that file, wherever the command is typed. A relativecafileonly worked from the project root. - A relative
--config,--cache-diror--cafileis 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_BINandNPM_CONFIG_USERCONFIGaccept 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_REGISTRYor--registry. With credentials of its own it leaves the configured host for no other. --dry-run(anddryRun = true) was accepted and ignored bybun pm fetch,bun pm cache pack/unpack,bun patchand--commit.bun patch <pkg> --dry-runeven discarded edits. They print what they would do and leave the project alone.bun install --analyzeandbun add --analyzeskipped packages that are imported only by a subpath (lodash/debounce). An unresolved@/aliasimport 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) inbun 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).--jsonis untouched. bun installsilently skipped parts of a patch it can't apply: aBinary files ... differsection, a file copy, colored output, stray text. It still installs, and warns about each one. It fails on aGIT binary patchsection 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
patchedDependencieskey 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 patchprints 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 patchand--commitfail withENAMETOOLONGinstead of crashing when a path argument or--patches-diris longer thanPATH_MAX.- Diagnostics about the project's
.npmrcshow its absolute path, as forbunfig.tomland~/.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 patchfailed withEPERMwhile a program had a file of the package open (a file opened without sharing still fails, withEBUSYand the folder left whole); switching linkers with one file open stopped half way and left packages missing. XDG_CONFIG_HOMEorUSERPROFILEwithout 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_modulesthat 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 inc:\projforC:\proj, or through a junction. --patches-dirtakes/,\and.\, and always writes/topackage.jsonandbun.lock.- The
preversion/version/postversionscripts ofbun pm versionin a git repository ran with an empty environment. - A dependency's lifecycle script found the shared
node_modules\.binahead of the systemPATHwhenPATHspelled 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 installextracts 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 andfile: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 --commitnames new patch files without it. - A tarball URL with an invalid host is rejected with
InvalidURLbefore any connection is opened. - A git dependency's
package.jsonis always a regular file from its own repository. A cached checkout whosepackage.jsonis a link is checked out again when the dependency is resolved anew (nobun.lockentry,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/.bundoesn'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.lockkeeps the checksum of entries that have nointegrityline. - The pnpm migration keeps an empty
onlyBuiltDependenciesas"trustedDependencies": []inpackage.json. bun pm trusttakes only its positional arguments as package names. It andbun add --trustleave blocked what atrustedDependenciesentry cannot unblock forbun install: a tarball, git,file:orlink:dependency that neither the project nor a workspace declares, and a package that another one ships inside itself.bun pm untrustedmarks 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
--installflag. It counts when set in the global bunfig, or in abunfig.tomlbetween the working directory or the importing file and the project root. bun patch,bun patch --commit,bun link <pkg>andbun deduperun the configured security scanner before they install packages.- A relative
install.security.scannerpath in the global bunfig or in a--configfile 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.lockfrom the project of the importing file, whereverbunwas started; abun.lockin an unrelated parent folder is ignored. Apackage.jsonthat 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 packnever packs its own temporary files.bun pm diffrequests and checks downloads the waybun installdoes (the registry's integrity, andbun.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); onlybun pm diffsets it.bun pm difftreats fewer files as binary and prints sizes and a sha256 for the ones that are. Itspackage.jsonnotes compare whole values and say where the registry's metadata and the tarball disagree on a field thatbun installuses. It escapes control characters on a terminal withNO_COLORtoo.
Not in this PR
bun pm ls --jsonand--depth=Nabove 0 are rejected rather than supported (#8283 stays open).bun unlink <name>is still not implemented.- Plain
bun updatestill 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 updateretries 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_modulesfolder still ignorespackage.jsonandbun.lockfor 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.lockcan give different versions. - After
bun.lockchanges under an existingnode_modules(agit 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 onmain, 100 of 697 here. Therm bun.lockcase 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
.binand in the global bin folder. - Ctrl-C with a
SIGINTlistener 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 trustdoesn'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 patchfails for every package. A secondbun patch <pkg>discards uncommitted edits. - The
binof a tarball, GitHub or git dependency isn't linked after ayarn.lockorpnpm-lock.yamlmigration: 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
mainand 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 removeemptiespackage.jsonbefore it writes it, so a full disk at that moment leaves an empty file.- With the registry down,
bun installwaits 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
ENAMETOOLONGfrom 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 linkmaindoesn't, because it replaces the link. bun add p@2 --filter amoves a sharednode_modules/pwithout 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 patchfails withEBUSY, 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 ..\outsidefails, as before.
How did you verify your code works?
- State of the branch. The numbers are from CI's release builds of
cc9d7c2daand ofmainat the merge base. The head adds one fix, tobun 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 ofpackage.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 ofmain, no hang. 16,148 installs with one package answering 404: none ends with anotherbun.lockand 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 onmainfails here, on purpose: the error for a command typed in a linked workspace outside the project. The rest fail on both. - The whole diff against
main, one pass per kind of regression (deletes, credentials, loud to quiet, works to refused, lockfile compatibility, cost): about 95,000 differential cases, 364 real lockfiles and 9,571 real package folders found 22 regressions of this branch; each is fixed or listed above. - Changed existing tests. The 138 whose expectations the first push touches were audited line by line: none lost, four weakened ones restored. The fixed order changed when 31 tests release a held response, but no expectation.
- Cost, user-space instructions (millions, median of 3): 1,500 packages, 50 workspaces, bins, scripts, platform packages, overrides, a catalog, aliases; the hoisted project has peers too.
| Command | hoisted, main | hoisted, branch | isolated, main | isolated, branch |
|---|---|---|---|---|
| fresh, cold cache | 1501.3 | 1503.7 | 2441.0 | 2450.1 |
| warm, nothing to do | 185.8 | 164.1 | 175.9 | 176.2 |
--frozen-lockfile | 133.5 | 122.1 | 150.3 | 150.8 |
bun add | 182.3 | 160.5 | 171.1 | 171.7 |
no node_modules, cache warm | 198.6 | 179.1 | 1291.4 | 1304.1 |
no lockfile, --lockfile-only | 129.7 | 128.9 | 110.8 | 111.0 |
- Above 3 %:
bunxof a scoped or cached tool (+0.2 M, 35 system calls);bun pm untrusted(+5 %, one more hoist); an isolatedbun prunethat 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 pulledbun.lock: 17 end with a wrong version onmain, 21 here (the stale copy after a pull described above). Of planted folders that Bun didn't lay,mainloses 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 versionagainstsemver.inc(0 of 1,530,270 differ); 1,260 tarballs extract as before; older builds ofmainleave a lockfile with the new keys unchanged.