Skip to main content
Technology & EngineeringBun393 lines

Bun Bundler

Bun's built-in bundler: Bun.build() API, entry points, output formats (esm, cjs, iife), plugins, loaders, tree shaking, code splitting, CSS bundling, HTML entries, and compile-time macros.

Quick Summary30 lines
You are an expert in Bun's built-in bundler, covering the JavaScript API, CLI interface, plugin system, and production optimization techniques.

## Key Points

- **esm**: Default. Produces `import`/`export` syntax. Best for modern browsers and Node/Bun.
- **cjs**: Produces `require`/`module.exports`. Use for Node.js packages that must support older consumers.
- **iife**: Wraps everything in a self-executing function. Use for `<script>` tags in browsers without module support.
- **Bundling server-side code with `target: "browser"`**: Server code that uses `node:fs` or `bun:sqlite` will fail. Use `target: "bun"` or `target: "node"` for backend bundles.
- **Using `require()` and expecting tree shaking**: Tree shaking only works with static `import`/`export`. Migrate to ESM for optimal bundle sizes.
- **Forgetting `splitting: true` for multi-page apps**: Without code splitting, shared libraries are duplicated in every entry point's output.
- **Inlining large assets with the `text` loader**: Large files imported as text increase bundle size. Use the `file` loader for assets over a few KB.
- **Not checking `result.success`**: Build errors do not throw by default. Always check `result.success` and log `result.logs` on failure.

## Quick Example

```typescript
await Bun.build({
  entrypoints: ["./index.html"],
  outdir: "./dist",
});
// Produces: dist/index.html (with updated paths), dist/app-[hash].js, dist/main-[hash].css
```

```json
{
  "sideEffects": false
}
```
skilldb get bun-skills/Bun BundlerFull skill: 393 lines
Paste into your CLAUDE.md or agent config

Bun Bundler — Fast JavaScript and CSS Bundling

You are an expert in Bun's built-in bundler, covering the JavaScript API, CLI interface, plugin system, and production optimization techniques.

Overview

Bun includes a bundler that replaces webpack, esbuild, Rollup, and Parcel for most use cases. It is invoked via the Bun.build() API or the bun build CLI command. The bundler resolves imports, performs tree shaking, supports code splitting, and handles TypeScript, JSX, CSS, and JSON out of the box. It is significantly faster than webpack and comparable in speed to esbuild.

Basic Usage

CLI

# Bundle a single entry point
bun build ./src/index.ts --outdir ./dist

# Multiple entry points
bun build ./src/index.ts ./src/worker.ts --outdir ./dist

# Specify output format
bun build ./src/index.ts --outdir ./dist --format esm

# Minify output
bun build ./src/index.ts --outdir ./dist --minify

# Generate sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap=external

# Watch mode
bun build ./src/index.ts --outdir ./dist --watch

JavaScript API

const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  format: "esm",
  minify: true,
  sourcemap: "external",
  target: "browser",
});

if (!result.success) {
  for (const log of result.logs) {
    console.error(log);
  }
  process.exit(1);
}

// result.outputs is an array of BuildArtifact objects
for (const output of result.outputs) {
  console.log(output.path);  // absolute path to output file
  console.log(output.kind);  // "entry-point", "chunk", "asset"
}

Entry Points

Entry points are the files where bundling begins. Each entry point produces at least one output file:

await Bun.build({
  entrypoints: [
    "./src/index.ts",
    "./src/worker.ts",
    "./src/service-worker.ts"
  ],
  outdir: "./dist",
});

HTML Entry Points

Bun can use HTML files as entry points, automatically finding and bundling all referenced scripts and stylesheets:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="./styles/main.css">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="./src/app.tsx"></script>
</body>
</html>
await Bun.build({
  entrypoints: ["./index.html"],
  outdir: "./dist",
});
// Produces: dist/index.html (with updated paths), dist/app-[hash].js, dist/main-[hash].css

Output Formats

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  format: "esm",  // ES modules (default) -- import/export
  // format: "cjs",  // CommonJS -- require/module.exports
  // format: "iife", // Immediately Invoked Function Expression -- for <script> tags
});
  • esm: Default. Produces import/export syntax. Best for modern browsers and Node/Bun.
  • cjs: Produces require/module.exports. Use for Node.js packages that must support older consumers.
  • iife: Wraps everything in a self-executing function. Use for <script> tags in browsers without module support.

Target Environments

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  target: "browser", // default -- strips Node/Bun-specific code
  // target: "bun",    // keeps Bun-specific APIs
  // target: "node",   // keeps Node.js APIs, resolves node: imports
});

The target affects how built-in modules are resolved. With target: "browser", imports like node:fs are marked as errors. With target: "bun", bun:sqlite and other Bun-specific imports are left as-is.

Tree Shaking

Bun performs tree shaking automatically for ESM code. Unused exports are removed from the output:

// utils.ts
export function used() { return 1; }
export function unused() { return 2; } // removed if never imported

// index.ts
import { used } from "./utils";
console.log(used());

Tree shaking only works reliably with ES module syntax (import/export). CommonJS require() calls are dynamic and cannot be statically analyzed, so they prevent tree shaking.

Mark packages as side-effect-free in package.json to enable deeper tree shaking:

{
  "sideEffects": false
}

Or mark specific files as having side effects:

{
  "sideEffects": ["./src/polyfills.ts", "*.css"]
}

Code Splitting

When multiple entry points share dependencies, Bun can extract shared code into separate chunks:

await Bun.build({
  entrypoints: [
    "./src/pages/home.ts",
    "./src/pages/about.ts",
    "./src/pages/contact.ts",
  ],
  outdir: "./dist",
  splitting: true, // enable code splitting
  format: "esm",   // splitting requires ESM format
});

This produces shared chunks like chunk-[hash].js that are imported by multiple entry points. Use dynamic import() to create split points within a single entry:

// Lazy-load a heavy module
const editor = await import("./heavy-editor.ts");
editor.init();

CSS Bundling

Bun bundles CSS files, resolving @import statements and handling CSS modules:

await Bun.build({
  entrypoints: ["./src/styles/main.css"],
  outdir: "./dist",
  minify: true,
});
/* main.css */
@import "./reset.css";
@import "./variables.css";
@import "./components/button.css";

body {
  font-family: var(--font-family);
}

CSS modules are supported when the file ends in .module.css:

/* button.module.css */
.primary {
  background: blue;
  color: white;
}
import styles from "./button.module.css";
// styles.primary is a unique class name like "button_primary_x7f3a"

Minification

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  minify: true, // minify everything

  // Or control individually:
  minify: {
    whitespace: true,   // remove whitespace
    identifiers: true,  // mangle variable names
    syntax: true,       // simplify syntax
  },
});

Sourcemaps

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  sourcemap: "external",  // .js.map file alongside output
  // sourcemap: "inline",  // embedded in the JS file as data URL
  // sourcemap: "linked",  // external file, referenced in JS comment
  // sourcemap: "none",    // no sourcemap (default)
});

Plugins

Plugins let you customize how Bun resolves and loads modules:

import type { BunPlugin } from "bun";

const envPlugin: BunPlugin = {
  name: "env-loader",
  setup(build) {
    // Custom resolver: intercept specific import paths
    build.onResolve({ filter: /^env:/ }, (args) => {
      return {
        path: args.path.slice(4), // strip "env:" prefix
        namespace: "env",
      };
    });

    // Custom loader: provide content for resolved modules
    build.onLoad({ filter: /.*/, namespace: "env" }, (args) => {
      const value = Bun.env[args.path];
      return {
        contents: `export default ${JSON.stringify(value ?? "")};`,
        loader: "js",
      };
    });
  },
};

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  plugins: [envPlugin],
});
// Now this works in your source code:
import apiUrl from "env:API_URL";
console.log(apiUrl);

YAML Plugin Example

const yamlPlugin: BunPlugin = {
  name: "yaml-loader",
  setup(build) {
    build.onLoad({ filter: /\.ya?ml$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      const { parse } = await import("yaml");
      const data = parse(text);
      return {
        contents: `export default ${JSON.stringify(data)};`,
        loader: "json",
      };
    });
  },
};

Loaders

Loaders tell Bun how to interpret files with specific extensions:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  loader: {
    ".txt": "text",    // import as string
    ".json": "json",   // import as parsed JSON (default)
    ".toml": "toml",   // import as parsed TOML
    ".svg": "file",    // copy file, import as URL path
    ".png": "file",    // copy file, import as URL path
    ".wasm": "file",   // copy file
  },
});

Built-in loaders: js, jsx, ts, tsx, json, toml, text, file, css, napi, wasm.

Macros — Compile-Time Code Execution

Macros run JavaScript at bundle time and inline the result:

// get-version.ts (runs at build time)
export function getVersion(): string {
  const pkg = require("./package.json");
  return pkg.version;
}

// index.ts
import { getVersion } from "./get-version" with { type: "macro" };

// At runtime, this becomes a string literal like "1.2.3"
console.log(getVersion());

Macros are powerful for embedding build metadata, reading configuration files at compile time, or generating code. They execute in Bun's runtime during the build step, so they have full access to the file system and environment.

Define — Compile-Time Constants

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
    "__APP_VERSION__": JSON.stringify("1.2.3"),
    "__DEV__": "false",
  },
});

Combined with tree shaking, this eliminates dead code branches:

if (__DEV__) {
  // This entire block is removed in production builds
  enableDevTools();
}

Anti-Patterns

  • Bundling server-side code with target: "browser": Server code that uses node:fs or bun:sqlite will fail. Use target: "bun" or target: "node" for backend bundles.
  • Using require() and expecting tree shaking: Tree shaking only works with static import/export. Migrate to ESM for optimal bundle sizes.
  • Forgetting splitting: true for multi-page apps: Without code splitting, shared libraries are duplicated in every entry point's output.
  • Inlining large assets with the text loader: Large files imported as text increase bundle size. Use the file loader for assets over a few KB.
  • Not checking result.success: Build errors do not throw by default. Always check result.success and log result.logs on failure.
  • Using macros for runtime-dependent values: Macros execute at build time. Do not use them for values that change at runtime (like environment-specific config that differs between staging and production).

Install this skill directly: skilldb add bun-skills

Get CLI access →