Bun Bundler
Using Bun as a bundler for frontend assets and as a fast test runner
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 linesBun 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
reactorreact-domasexternal, 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-snapshotsto 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()insidedescribeoritblocks. 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
beforeEachto reset state between tests, or accept that tests within a file may interfere with each other if you do not.
Best Practices
- Use
Bun.buildprogrammatically 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
--minifyin production and--sourcemap=externalso you can debug production errors without shipping maps to users. - Structure tests with
describeblocks for readable output and logical grouping. - Mock external services (HTTP, databases) in unit tests; reserve real calls for integration tests.
- Run
bun test --coveragein CI and set minimum thresholds. - Use
--watchduring development for instant feedback. - Mark external dependencies that should not be bundled (e.g.,
reactwhen 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 hoisting —
mock.module()is hoisted to the top of the file (like Jest). Calls insidedescribeoritblocks do not scope the mock — it applies to the whole file. --compileproduces a platform-specific binary — cross-compilation is supported but you must specify--targetexplicitly (e.g.,--target=bun-linux-x64).- Test isolation — each test file runs in its own process, but tests within a file share state. Use
beforeEachto reset state between tests.
Install this skill directly: skilldb add deno-bun-skills
Related Skills
Bun Basics
Bun runtime fundamentals including speed optimizations, built-in APIs, and package management
Compatibility
Node.js compatibility layers in Deno and Bun for running existing npm packages and Node APIs
Deno Basics
Deno runtime fundamentals including permissions, module system, and built-in tooling
Deno Deploy
Deno Deploy edge functions for globally distributed serverless applications
Elysia Bun
Elysia web framework on Bun for type-safe, high-performance HTTP APIs
Fresh Framework
Fresh full-stack web framework for Deno with islands architecture and zero client JS by default