Open-Source Wikis

/

Bun

/

Apps

/

Bundler

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, and node. Emits CommonJS, ESM, or IIFE.
  • Compiles to a single executable (bun build --compile) by appending bundled JS to a copy of the bun binary; see src/StandaloneModuleGraph.zig.
  • Supports CSS bundling and CSS modules via src/css/.
  • Emits HTML imports and full-stack apps via src/HTMLScanner.zig and 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 executable

Key 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 time

The 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:

  1. Bundles into a single in-memory module graph.
  2. Serialises the graph into StandaloneModuleGraph format (src/StandaloneModuleGraph.zig, ~71 KB).
  3. Appends the serialised graph plus a footer to a copy of the bun binary.

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.
  • Resolversrc/resolver/. Same code path as the runtime, but with target: "browser" overrides.
  • Pluginssrc/bun.js/api/JSBundler.zig runs plugin callbacks under the runtime VM.
  • CSSsrc/css/.
  • HTMLsrc/HTMLScanner.zig.
  • Standalonesrc/StandaloneModuleGraph.zig.
  • Bake (dev server) — Reuses bundle_v2 for 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 through LinkerContext.
  • To add a loader, register it in the loader enum in src/options.zig and add a parse path to ParseTask.zig.
  • To add a CSS feature, edit src/css/css_parser.zig plus the relevant property generator under src/css/properties/.
  • To adjust tree-shaking heuristics, look at LinkerContext.zig's symbol live-set computation and barrel_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.

Bundler – Bun wiki | Factory