Skip to main content
Technology & EngineeringDeno Bun279 lines

Bun Bundler

Using Bun as a bundler for frontend assets and as a fast test runner

Quick Summary24 lines
You are an expert in using Bun's built-in bundler for frontend and backend asset bundling and its test runner for fast JavaScript/TypeScript testing.

## Key Points

- **Use `Bun.build` programmatically** for complex builds — it gives you access to the full output manifest and error details.
- **Enable code splitting** (`splitting: true`) for browser targets to reduce initial load size.
- **Use `--minify` in production** and `--sourcemap=external` so you can debug production errors without shipping maps to users.
- **Structure tests with `describe` blocks** for readable output and logical grouping.
- **Mock external services** (HTTP, databases) in unit tests; reserve real calls for integration tests.
- **Run `bun test --coverage`** in CI and set minimum thresholds.
- **Use `--watch` during development** for instant feedback.
- **Mark external dependencies** that should not be bundled (e.g., `react` when building a library).
- **Bundle size surprises** — without `external`, the bundler inlines everything. For libraries, always mark peer dependencies as external.
- **CSS handling limitations** — Bun bundles CSS but does not support CSS Modules or PostCSS plugins natively. Use a plugin or a separate CSS pipeline if needed.
- **Snapshot drift** — update snapshots intentionally with `bun test --update-snapshots`, not reflexively. Review snapshot diffs in PRs.
- **Mock hoisting** — `mock.module()` is hoisted to the top of the file (like Jest). Calls inside `describe` or `it` blocks do not scope the mock — it applies to the whole file.

## Quick Example

```bash
bun build ./src/index.html --outdir ./dist --minify
```
skilldb get deno-bun-skills/Bun BundlerFull skill: 279 lines
Paste into your CLAUDE.md or agent config

Bun Bundler and Test Runner — Modern JS Runtimes

You are an expert in using Bun's built-in bundler for frontend and backend asset bundling and its test runner for fast JavaScript/TypeScript testing.

Overview

Bun includes a production-grade bundler (Bun.build / bun build) and a Jest-compatible test runner (bun test). The bundler handles JavaScript, TypeScript, JSX, CSS, and static assets with tree-shaking, minification, and code splitting. The test runner is a drop-in Jest replacement that runs tests significantly faster by eliminating transpilation overhead.

Core Concepts

Bundler

The bundler is available as both a CLI command and a programmatic API. It supports multiple entry points, output formats (ESM, CJS, IIFE), and targets (browser, bun, node).

Test Runner

bun test discovers files matching *.test.{ts,tsx,js,jsx} or *.spec.* patterns. It implements the Jest expect API, lifecycle hooks, mocking, and snapshot testing natively.

Implementation Patterns

CLI Bundling

# Bundle for browser
bun build ./src/index.tsx --outdir ./dist --minify

# Bundle with source maps
bun build ./src/index.tsx --outdir ./dist --sourcemap=external

# Bundle for Node target
bun build ./src/server.ts --outdir ./dist --target node

# Bundle as single standalone file
bun build ./src/cli.ts --compile --outfile myapp

Programmatic Bundler API

const result = await Bun.build({
  entrypoints: ["./src/index.tsx", "./src/worker.ts"],
  outdir: "./dist",
  target: "browser",       // "browser" | "bun" | "node"
  format: "esm",           // "esm" | "cjs" | "iife"
  minify: {
    whitespace: true,
    identifiers: true,
    syntax: true,
  },
  splitting: true,          // enable code splitting
  sourcemap: "external",    // "none" | "inline" | "external"
  naming: "[dir]/[name]-[hash].[ext]",
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
  external: ["react", "react-dom"], // exclude from bundle
  loader: {
    ".png": "file",
    ".svg": "text",
    ".json": "json",
  },
});

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

for (const output of result.outputs) {
  console.log(`${output.path} — ${output.size} bytes`);
}

HTML Entry Points

Bun can bundle starting from an HTML file, discovering scripts and stylesheets automatically:

bun build ./src/index.html --outdir ./dist --minify
<!-- src/index.html — Bun resolves and bundles these references -->
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="./styles/main.css" />
</head>
<body>
  <div id="root"></div>
  <script type="module" src="./app.tsx"></script>
</body>
</html>

Plugins

import type { BunPlugin } from "bun";

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: "js",
      };
    });
  },
};

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

Basic Tests

// math.test.ts
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { Calculator } from "./calculator";

describe("Calculator", () => {
  let calc: Calculator;

  beforeEach(() => {
    calc = new Calculator();
  });

  it("adds two numbers", () => {
    expect(calc.add(2, 3)).toBe(5);
  });

  it("throws on division by zero", () => {
    expect(() => calc.divide(1, 0)).toThrow("Division by zero");
  });

  it("handles floating point", () => {
    expect(calc.add(0.1, 0.2)).toBeCloseTo(0.3);
  });
});

Async Tests

import { describe, it, expect } from "bun:test";

describe("API client", () => {
  it("fetches user data", async () => {
    const resp = await fetch("https://api.example.com/users/1");
    const user = await resp.json();

    expect(resp.status).toBe(200);
    expect(user).toMatchObject({
      id: 1,
      name: expect.any(String),
    });
  });

  it("handles timeout", async () => {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 100);

    expect(
      fetch("https://slow.example.com", { signal: controller.signal })
    ).rejects.toThrow();
  });
});

Mocking

import { describe, it, expect, mock, spyOn } from "bun:test";
import { sendEmail } from "./email";
import * as db from "./database";

// Mock a module
mock.module("./email", () => ({
  sendEmail: mock(() => Promise.resolve({ sent: true })),
}));

// Spy on methods
describe("user service", () => {
  it("creates user and sends welcome email", async () => {
    const dbSpy = spyOn(db, "insertUser").mockResolvedValue({ id: "u_1" });

    const result = await createUser("alice@test.com");

    expect(dbSpy).toHaveBeenCalledWith("alice@test.com");
    expect(sendEmail).toHaveBeenCalledWith(
      expect.objectContaining({ to: "alice@test.com" })
    );
    expect(result.id).toBe("u_1");
  });
});

Snapshot Testing

import { it, expect } from "bun:test";
import { renderToString } from "react-dom/server";
import { UserCard } from "./UserCard";

it("renders user card correctly", () => {
  const html = renderToString(<UserCard name="Alice" role="Admin" />);
  expect(html).toMatchSnapshot();
});

Running Tests

bun test                          # Run all tests
bun test src/utils                # Run tests in a directory
bun test --watch                  # Re-run on file change
bun test --timeout 10000          # Set timeout (ms)
bun test --bail                   # Stop on first failure
bun test --coverage               # Generate coverage report
bun test --test-name-pattern "add" # Filter by test name

Core Philosophy

Bun's bundler and test runner embody the all-in-one philosophy: the same binary that runs your code also bundles and tests it. There is no separate webpack, esbuild, or Jest to install, configure, and keep compatible. The bundler and test runner share Bun's native TypeScript and JSX transpilation, which means your build pipeline understands the same syntax your runtime does, eliminating an entire class of configuration mismatches.

The bundler is designed to be correct and fast rather than infinitely configurable. It handles the common cases (tree-shaking, minification, code splitting, source maps, multiple entry points) with minimal configuration and provides a plugin system for the uncommon ones. This opinionated approach means fewer decisions for the developer and fewer places where misconfiguration causes subtle production bugs.

The test runner follows the same principle: it implements the Jest API surface (describe, it, expect, mock, spy) natively so that existing test suites work with minimal changes. By eliminating the transpilation step that Jest requires, test startup is nearly instant. The per-file process isolation ensures that tests do not leak state to each other, while the familiar API surface means developers do not need to learn a new testing paradigm.

Anti-Patterns

  • Bundling peer dependencies into library output. Without marking packages like react or react-dom as external, the bundler inlines them into your library. Consumers end up with duplicate copies, causing runtime errors and bloated bundles.

  • Treating CSS as a solved problem in Bun's bundler. Bun handles basic CSS bundling but does not natively support CSS Modules or PostCSS plugins. If your project relies on these, use a separate CSS pipeline or a Bun plugin rather than assuming built-in support.

  • Updating snapshots reflexively without reviewing diffs. Running bun test --update-snapshots to make failing tests pass without examining what changed masks regressions. Review snapshot diffs in pull requests the same way you review code changes.

  • Scoping mock.module() inside describe or it blocks. Module mocks are hoisted to the top of the file, just like Jest. Placing them inside test blocks gives a false impression of scoping. The mock applies to the entire file regardless of where it is written.

  • Ignoring test isolation within files. Tests within a single file share state (module-scope variables, database connections, in-memory caches). Use beforeEach to reset state between tests, or accept that tests within a file may interfere with each other if you do not.

Best Practices

  • Use Bun.build programmatically for complex builds — it gives you access to the full output manifest and error details.
  • Enable code splitting (splitting: true) for browser targets to reduce initial load size.
  • Use --minify in production and --sourcemap=external so you can debug production errors without shipping maps to users.
  • Structure tests with describe blocks for readable output and logical grouping.
  • Mock external services (HTTP, databases) in unit tests; reserve real calls for integration tests.
  • Run bun test --coverage in CI and set minimum thresholds.
  • Use --watch during development for instant feedback.
  • Mark external dependencies that should not be bundled (e.g., react when building a library).

Common Pitfalls

  • Bundle size surprises — without external, the bundler inlines everything. For libraries, always mark peer dependencies as external.
  • CSS handling limitations — Bun bundles CSS but does not support CSS Modules or PostCSS plugins natively. Use a plugin or a separate CSS pipeline if needed.
  • Snapshot drift — update snapshots intentionally with bun test --update-snapshots, not reflexively. Review snapshot diffs in PRs.
  • Mock hoistingmock.module() is hoisted to the top of the file (like Jest). Calls inside describe or it blocks do not scope the mock — it applies to the whole file.
  • --compile produces a platform-specific binary — cross-compilation is supported but you must specify --target explicitly (e.g., --target=bun-linux-x64).
  • Test isolation — each test file runs in its own process, but tests within a file share state. Use beforeEach to reset state between tests.

Install this skill directly: skilldb add deno-bun-skills

Get CLI access →