Spike: bun as the package manager, to stop per-worktree node_modules duplication
Status: spike / draft — not a recommendation to merge. Nothing here changes how the repo installs today.package-lock.json remains the single source of
truth and npm ci remains the install path.
The problem this is a candidate answer to
One local machine currently carries 125 worktrees / 181.8 GB, of which 135.5 GB isnode_modules across 70 worktrees — on a volume with 47 GB
free. Every bootstrapped worktree pays a fresh ~2.0 GB because npm ci writes
new blocks for every file.
That is not an old-junk problem that cleanup fixes. Measured with a 7-day
dormancy filter, only one of those 70 worktrees is untouched — the pile is
generated faster than it can be swept.
Why npm cannot fix it
Verified, not assumed:npm --install-strategy=linkedexists (npm 11.12.1 supports it) but its store isnode_modules/.storeinside each project. It is an isolation feature for catching phantom dependencies; it shares nothing between checkouts.- npm’s cache is content-addressable but stores gzipped tarballs
(
~/.npm/_cacache/content-v2), so extraction must write new bytes. Confirmed on a real install: every file innode_modulesreportslinks=1. - The two upstream paths that would change this are not moving: nodejs/node#26489 (reflink support) is closed, and npm/rfcs#912 (“RFC: Content-Addressable Store”) was opened 2026-07-11 with zero comments.
Why bun rather than pnpm
Both solve the duplication. bun is proposed here for two repo-specific reasons:- bun’s default install backend on macOS is
clonefile(--backendacceptsclonefile(default),hardlink,symlink,copyfile). It produces real files. pnpm’s global virtual store is symlink-based, and.husky/pre-pushhard-fails onnode_modulesbeing a symlink — the guard exists because a later cleanup can delete the symlink target. - bun needs no configuration to get the win; pnpm needs
virtualStoreType: globalplus a store on the same filesystem.
pnpm leads the established cohort (
vitejs/vite itself, angular, supabase,
tailwindcss, shadcn-ui, n8n, immich). bun is the fastest riser among new
projects. WorldMonitor appears in the npm column of the first cohort.
What was measured on this repo
Two throwaway worktrees offorigin/main, since removed.
Disk, measured as real free-space delta rather than
du:
What this spike changes
Three things, each inert on a checkout with nobun.lock:
-
trustedDependenciesinpackage.json. bun blocks dependency lifecycle scripts by default; it blocked 4 here (browser-tabs-lock,core-js,es5-ext,protobufjs). The build and 447 sidecar tests passed anyway, butprotobufjsis listed explicitly because of the generated sebuf clients. npm ignores the field. -
A completed-install marker for bun. This is the load-bearing fix. bun
leaves no marker inside
node_modules— only.bin, which an interrupted install also creates — so.husky/pre-pushwould see nonode_modules/.package-lock.jsonand run a fullnpm cion every push, silently undoing the win.bun install --frozen-lockfileon an installed tree is not a workaround: it costs ~11s (measured twice), against a cached-green re-push of ~0.5s. Soscripts/bun-install.mjsstampsnode_modules/.wm-bun-installwith the sha256 of thebun.lockit installed from, and both the gate andshouldInstallDependencies()check it. That is stronger than npm’s marker, which proves completion but not freshness — a stale tree after a dependency bump is invisible to.package-lock.jsonand caught here. -
Lockfile policy.
bun.lockis gitignored.package-lock.jsonstays authoritative for the life of the spike; deleting those.gitignorelines is the deliberate act that flips it.
What is still unproven
- CI. Every workflow still runs
npm ci; nothing here touches them. pro-test/node_modulesis not installed by the postinstall chain (it belongs tobuild:pro), so the built-output tests were not exercised under bun.- Linux/CI filesystems have no APFS
clonefile. bun falls back tohardlinkthere, which shares blocks differently; unmeasured. - Desktop/Tauri and the Railway seeder images were not exercised.
How to reproduce
df free-space delta
against what du reports — du counts cloned blocks that cost nothing.