install: return immediately when nothing that determines node_modules has changed
What does this PR do?
Makes a repeat bun install with nothing to do close to free, without giving up correctness.
Today a no-op install still parses the lockfile, re-hoists, and walks node_modules opening and parsing every installed package.json to confirm name/version — O(packages) syscalls, which on large workspaces (and especially on Windows) is where almost all of the "no changes" time goes.
With this change, after a successful install bun records a fingerprint of the inputs that determine what ends up in node_modules, in <cache>/.install-state/<hash of project root>:
- content hashes of
bun.lock/bun.lockb, the root and every workspacepackage.json,./bunfig.toml,./.npmrc,~/.npmrc,~/.bunfig.toml,$XDG_CONFIG_HOME/{.bunfig.toml,.npmrc}, and every referenced patch file (absence is recorded too); - a hash of the install-relevant environment (
BUN_INSTALL*,BUN_CONFIG_*,NPM_CONFIG_*), the exact argv, and bun's version (only the hash is stored); lstatstamps of eachnode_modulesdirectory (root + workspaces), of its top-level entries and of each entry'spackage.json(through symlinks, so the isolated linker's store is covered);- stamps of the parent directories of the
workspacesglobs — present or absent, and for nested globs the existing levels below the literal prefix — so a workspace added on a branch switch is noticed; - for
file:/ local-tarball dependencies (projects usingbun link-ed packages always take the normal path), recursive stamps of the source (bounded; a project with enormous local sources just doesn't get a state file).
The next plain bun install re-hashes those few files, checks the stamps and, if everything matches, prints the usual Checked N installs across M packages (no changes) and returns. Anything else — bun add/remove/update, --force, --global, dry runs, positional filters, install.stateFile = false, or any changed input — takes the normal path. The state file is deleted before an install that does real work, so a crash mid-install can't leave a "clean" marker behind. As with other package managers' equivalent optimisation, root lifecycle scripts don't re-run on a no-op install.
The state lives in the cache dir (not node_modules) so directory listings are unchanged; a new readdirCacheSorted() test helper hides the bookkeeping dir from the few tests that snapshot the cache directory.
Docs: install.stateFile in docs/runtime/bunfig.mdx, a short "Repeat installs" note in docs/pm/cli/install.mdx.
How did you verify your code works?
- New
test/cli/install/bun-install-state.test.ts(dummy registry, bothhoistedandisolated): repeat install is a no-op with zero requests; then each of these invalidates it and is acted on — editing a workspacepackage.json, adding a workspace under the glob, removing a package dir fromnode_modules, deleting a file inside an installed package, running with different flags,--force,stateFile = false; plus afile:dependency whose untouched source is a no-op and whose edited source is re-installed. - Ran
bun-install,bun-add,bun-install-registry,bun-workspaces,isolated-install,bun-link,bun-lock,bun-remove,bun-update,bun-pmandbun-install-lifecycle-scriptssuites locally.
Supersedes #39901 (same change, moved from a fork branch to a branch in this repository; the review discussion so far is on that PR and has been addressed here).