i maintain a small cli tool called prj that scaffolds new project directories from templates. it started as a bash script, grew into a node package, and eventually became a bun binary. the last step made the biggest difference for the people who use it.
the problem with node clis
node-based clis require a runtime. that means either:
- the user installs node globally (a heavy dependency for a small tool)
- you bundle node with the package (wasteful and fragile)
- you use
npm -gand hope the user’s environment is compatible
none of these are good. the best user experience for a cli is a single binary that works on any linux machine regardless of what runtimes are installed. go and rust have this by default. typescript does not — until bun.
the solution
bun’s bun build --compile flag takes an entry point and produces a statically-linked binary. the binary contains the bun runtime, your source code compiled to machine code, and nothing else. the output is a single executable file with no dependencies:
bun build --compile --target=bun-linux-x64-modern ./src/cli.ts --outfile=prj
the --target flag specifies the platform and architecture. you can cross-compile for linux, macos, and windows from any host. the resulting binary is around 50mb — large for what it does, but the user never thinks about it beyond the download.
what changed
before the bun binary, npm install -g prj took seven seconds and required node 18 or later. after the bun binary, curl -L && chmod +x takes two seconds and works on any linux machine with a 2018+ cpu.
the bundle size went from 3mb (node_modules with dependencies) to 48mb (single binary). that is a net negative in absolute terms, but the user downloads it once and never thinks about it again. no version conflicts, no global package management, no nvm nonsense.
the tradeoffs
bun’s api surface for fs and path operations is nearly compatible with node, but not fully. i hit one compatibility issue: fs.cp with the recursive flag behaves differently on symlinks. the fix was a five-line polyfill.
the startup time is instant — bun compiles to native code, not bytecode. cold start on a standard aws instance is under 10ms. the previous node version took 120ms just to require the first dependency.
conclusion
if you write typescript cli tools and you want people to actually use them without asking questions about their node version, compile to a single binary. bun makes this trivial. the output is larger than a go binary, but the development experience — the ecosystem, the types, the familiarity — is worth the tradeoff.