oven-sh/bun
Bundler
Active contributors: Jarred Sumner, Dylan Conway
bun build, Bun.build, bun build --compile (single-file executables), and the dev-server's bundling stage all flow through src/bundler/bundle_v2.zig — a 234,000-byte file that orchestrates parsing, linking, tree-shaking, and emitting JavaScript / CSS / HTML output.
Purpose
A multi-format bundler that:
- Bundles ESM, CommonJS, TypeScript, JSX, JSON, TOML, text, and binary inputs into JS/CSS chunks.
- Tree-shakes at the symbol level rather than the module level.
- Splits chunks for code-splitting and CSS extraction.
- Supports plugins via
src/bun.js/api/JSBundler.zig(onResolve/onLoad). - Targets
bun,browser, andnode. Emits CommonJS, ESM, or IIFE. - Compiles to a single executable (
bun build --compile) by appending bundled JS to a copy of thebunbinary; seesrc/StandaloneModuleGraph.zig. - Supports CSS bundling and CSS modules via
src/css/. - Emits HTML imports and full-stack apps via
src/HTMLScanner.zigand Bake.
Directory layout
src/
├── bundler/
│ ├── bundle_v2.zig # entry; builds the graph, drives passes
│ ├── BundleThread.zig # the dedicated bundler thread
│ ├── ParseTask.zig # one-file parse job (~60 KB)
│ ├── ServerComponentParseTask.zig # extras for React server components
│ ├── ThreadPool.zig # worker pool for parallel parsing
│ ├── LinkerGraph.zig # symbol + import graph
│ ├── LinkerContext.zig # the linker proper (~120 KB)
│ ├── linker_context/ # phase helpers
│ ├── Chunk.zig # output chunk model
│ ├── HTMLImportManifest.zig # HTML import bookkeeping
│ ├── PathToSourceIndexMap.zig
│ ├── Graph.zig # smaller subgraph types
│ ├── AstBuilder.zig # mini-AST helpers
│ ├── DeferredBatchTask.zig # async deferred plugin tasks
│ ├── IndexStringMap.zig
│ ├── barrel_imports.zig # barrel re-export elision
│ └── entry_points.zig
├── cli/build_command.zig # CLI wrapper for `bun build`
├── bun.js/api/JSBundler.zig # plugin host (onResolve, onLoad)
└── StandaloneModuleGraph.zig # `--compile` standalone executableKey abstractions
| Type | File | Purpose |
|---|---|---|
BundleV2 |
src/bundler/bundle_v2.zig |
Orchestrator. Holds the parse pool, the graph, the linker, and emits output files. |
LinkerGraph |
src/bundler/LinkerGraph.zig |
The cross-file symbol and import graph. Indexed by source index. |
LinkerContext |
src/bundler/LinkerContext.zig |
The linker. Walks the graph, marks live symbols, computes chunks, performs renaming, prints final output. |
ParseTask |
src/bundler/ParseTask.zig |
One source file's parse + transform job. Runs on the thread pool. |
Chunk |
src/bundler/Chunk.zig |
An output unit (entry chunk, async chunk, CSS chunk, HTML chunk). |
Plugin |
src/bun.js/api/JSBundler.zig |
A user-supplied JS plugin with onResolve and onLoad hooks. |
OutputFile |
src/OutputFile.zig |
One emitted artifact: bytes + path + sourcemap + asset metadata. |
Pipeline
graph TD
Entry[Entry points] -->|enqueue| Pool[ThreadPool]
Pool -->|parse| Parse[ParseTask: lex → parse → analyze imports]
Parse -->|new specifiers| Resolve[resolver: NodeJS / tsconfig / browser]
Resolve --> Pool
Parse --> Graph[LinkerGraph]
Graph --> Live[Tree shake: walk reachable symbols]
Live --> Chunks[Chunk planning: split, name, hash]
Chunks --> CSS[CSS extraction → css_parser.zig]
Chunks --> Print[js_printer with rename map]
Print --> Map[Sourcemap join]
Print --> Outputs[OutputFile.zig]
Outputs --> Disk[write or return JSValue]The parse phase is fully parallel — every file is a ParseTask on the bundler thread pool. The linker phase is serial because the graph is shared.
Tree shaking
Tree shaking happens at the symbol level. Each parsed file produces a list of Symbols and Refs (refer-to-by-source-index pairs). The linker walks reachable symbols starting from each entry-point's exports. A symbol is live if anything reachable references it.
This is more aggressive than module-level tree shaking: an unused export inside a module that has any used export still gets dropped. The bundler can also elide barrel re-exports (export * from "./foo") when only specific names are used; see src/bundler/barrel_imports.zig.
Loaders
A loader maps a file extension to a parsing strategy. Default loaders:
| Extension(s) | Loader |
|---|---|
.js, .cjs, .mjs |
JavaScript |
.ts, .cts, .mts |
TypeScript |
.tsx, .jsx |
TS+JSX / JS+JSX |
.json, .jsonc |
JSON (parsed at compile time → constant) |
.toml |
TOML |
.txt |
text (default export = string) |
.css |
CSS (src/css/) |
.html |
HTML entry (src/HTMLScanner.zig) |
.wasm |
WebAssembly |
.node |
N-API native addon |
.svg, .png, ... |
file (copy + URL) |
.sqlite |
embedded SQLite (bun:sqlite) |
.yaml / .yml |
YAML (via Bun.YAML) |
Custom loaders are registered through plugins (onLoad).
Plugins
Bun.plugin({ name, setup(build) { ... } }) registers a plugin. At bundle time, the plugin's onResolve callbacks run before the resolver, and onLoad callbacks run before the parser. The host is src/bun.js/api/JSBundler.zig.
Plugins can:
- Override resolution for matched specifiers.
- Return source bytes of an arbitrary loader type (
{ contents, loader: "ts" }). - Defer to "the next plugin" by returning
undefined.
The same plugin shape works at runtime (bun ./script.ts with a registered plugin) and at bundle time (Bun.build).
For native plugins implemented in Rust or C, see packages/bun-native-bundler-plugin-api/ and packages/bun-native-plugin-rs/.
CSS, HTML, and full-stack
CSS files imported from JS are extracted into a parallel CSS chunk. The CSS parser/printer is in src/css/css_parser.zig (~298 KB) — a port and adaptation of Lightning CSS's design.
HTML imports (bun build --target=browser entry.html) are scanned by src/HTMLScanner.zig, which extracts inline scripts and <link rel="stylesheet"> references and rewrites them to bundled outputs. This drives the dev-server full-stack experience documented in Bake.
Macros
Macros are JS functions called at bundle time. Mark an import with with { type: "macro" }:
import { html } from './macros.ts' with { type: 'macro' };
const greeting = html('hi'); // runs at compile timeThe parser detects macro imports and the bundler invokes them via the runtime VM during the bundle, splicing the AST result back in. Implementation is folded into src/js_parser.zig and src/bundler/bundle_v2.zig's macro pass.
Code splitting
When splitting: true (or multiple entry points share dependencies), the linker computes a "chunk closure" for each entry point and any dynamic import(). Shared dependencies that are reachable from more than one root chunk become their own shared chunk; entry-point chunks emit import() to load them on demand.
Chunk hashes are content-hashes computed after printing — file names embed the hash so they cache-bust cleanly.
Single-file executables
bun build --compile entry.ts -o myapp produces a standalone executable. The bundler:
- Bundles into a single in-memory module graph.
- Serialises the graph into
StandaloneModuleGraphformat (src/StandaloneModuleGraph.zig, ~71 KB). - Appends the serialised graph plus a footer to a copy of the
bunbinary.
At runtime, bun.zig detects the trailing footer, mounts the embedded graph as a virtual filesystem, and loads entry.ts from there.
CLI wrapper
bun build is the command-line entry, implemented in src/cli/build_command.zig. It maps CLI flags to the same Bun.build options. Output goes to --outdir or stdout (--outfile=-).
Integration points
- Parser/printer — Shared with the runtime; see Parser & transpiler.
- Resolver —
src/resolver/. Same code path as the runtime, but withtarget: "browser"overrides. - Plugins —
src/bun.js/api/JSBundler.zigruns plugin callbacks under the runtime VM. - CSS —
src/css/. - HTML —
src/HTMLScanner.zig. - Standalone —
src/StandaloneModuleGraph.zig. - Bake (dev server) — Reuses
bundle_v2for incremental builds; see Bake.
Entry points for modification
- To change a printer detail, edit
src/js_printer.zig(~260 KB). Most bundler-only options are passed throughLinkerContext. - To add a loader, register it in the loader enum in
src/options.zigand add a parse path toParseTask.zig. - To add a CSS feature, edit
src/css/css_parser.zigplus the relevant property generator undersrc/css/properties/. - To adjust tree-shaking heuristics, look at
LinkerContext.zig's symbol live-set computation andbarrel_imports.zig.
Key source files
| File | Purpose |
|---|---|
src/bundler/bundle_v2.zig |
Orchestrator; the largest single Zig file in the repo. |
src/bundler/LinkerContext.zig |
Symbol linking and chunk emission. |
src/bundler/ParseTask.zig |
Per-file parse job. |
src/bundler/Chunk.zig |
Chunk model. |
src/bun.js/api/JSBundler.zig |
Plugin host. |
src/StandaloneModuleGraph.zig |
--compile archive format. |
src/HTMLScanner.zig |
HTML entry-point handling. |
src/cli/build_command.zig |
bun build argv parsing. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.