Skip to content

esbuild Reference: CLI, JavaScript API, Loaders, Plugins & Build Scripts

esbuild is a JavaScript/TypeScript bundler written in Go. It’s 10–100x faster than webpack or Rollup for the same job. It doesn’t do everything — no TypeScript type-checking, no CSS modules, no HMR — but for bundling and transpiling it’s the fastest option by far. Vite uses esbuild for dependency pre-bundling in dev and for transpiling TypeScript. esbuild is best used directly when you need a fast, predictable build pipeline without a framework layer.

1. CLI Basics

Bundle, transform, target, format, and common CLI flags
# Install:
npm install -D esbuild

# Bundle entry point to output file:
npx esbuild src/index.ts --bundle --outfile=dist/out.js

# Common flags:
npx esbuild src/index.ts \
  --bundle \                    # resolve + inline all imports
  --outfile=dist/bundle.js \
  --platform=browser \          # browser (default) | node | neutral
  --target=es2020 \             # output syntax target (es2015, es2020, node18, chrome96...)
  --format=esm \                # esm | cjs | iife
  --minify \                    # minify whitespace + identifiers + syntax
  --sourcemap \                 # generate .js.map
  --define:process.env.NODE_ENV='"production"'  # replace global identifiers

# Output directory (multiple entry points):
npx esbuild src/app.ts src/worker.ts \
  --bundle \
  --outdir=dist \
  --splitting \                 # code splitting (esm only)
  --format=esm

# Watch mode (rebuilds on file change):
npx esbuild src/index.ts --bundle --outfile=dist/out.js --watch

# Transform only (no bundling — transpile one file):
npx esbuild src/component.tsx --loader=tsx --target=es2019

# Named exports for library builds:
npx esbuild src/index.ts \
  --bundle \
  --format=esm \
  --external:react \            # don't bundle react (peer dep)
  --external:react-dom \
  --outfile=dist/index.esm.js

2. JavaScript API

build(), transform(), context() for watch/serve, and options reference
import * as esbuild from "esbuild";

// One-shot build:
await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outfile: "dist/out.js",
  platform: "browser",
  target: ["es2020", "chrome90", "firefox88"],
  format: "esm",
  minify: true,
  sourcemap: true,
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
    "__VERSION__": JSON.stringify("1.0.0"),
  },
  loader: {
    ".png": "dataurl",    // inline images as base64
    ".svg": "text",       // inline SVGs as strings
    ".wasm": "binary",    // inline WASM as Uint8Array
  },
  external: ["react", "react-dom"],
  splitting: true,   // code splitting (esm only)
});

// Transform a code string (no filesystem):
const result = await esbuild.transform(`
  const x: number = 42;
  export default x;
`, {
  loader: "ts",
  target: "es2018",
});
console.log(result.code);   // const x = 42; export default x;

// Watch + serve (dev server):
const ctx = await esbuild.context({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outdir: "dist",
  format: "esm",
});
await ctx.watch();
await ctx.serve({ port: 3000, servedir: "." });
// Dispose when done:
await ctx.dispose();

3. Loaders & CSS

Loaders for different file types, CSS bundling, CSS modules limitation
// Built-in loaders (set per extension):
// js, jsx, ts, tsx  — JavaScript/TypeScript (auto-detected by extension)
// json              — imports as JS object
// text              — imports as UTF-8 string
// base64            — imports as base64-encoded string
// dataurl           — imports as "data:..." URI (inline)
// binary            — imports as Uint8Array
// file              — copies to outdir, imports as URL string
// css               — CSS files (bundled with @import resolution)
// local-css         — CSS Modules (scoped class names)
// empty             — treats import as empty module (stub)

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outdir: "dist",
  loader: {
    ".png": "file",        // copied to dist/, imported as URL string
    ".svg": "dataurl",     // inlined as base64 data URI
    ".txt": "text",        // inlined as string
    ".wasm": "binary",     // inlined as Uint8Array
    ".css": "css",         // bundled CSS
    ".module.css": "local-css",   // CSS Modules (generates scoped names)
  },
});

// CSS bundling — esbuild resolves @import and bundles:
// src/styles.css: @import "./variables.css"; @import "./reset.css";
// → dist/out.css with all @imports inlined

// CSS limitations:
// - No CSS-in-JS (styled-components, emotion) — needs plugin
// - CSS Modules supported via "local-css" loader but no :composes
// - No PostCSS processing (no autoprefixer) — needs plugin
// - For full CSS pipeline, pair with Vite or use esbuild as transpiler only

4. Plugins

Plugin API — onResolve, onLoad, onEnd hooks for custom loaders and transforms
import * as esbuild from "esbuild";

// Plugin structure:
const myPlugin: esbuild.Plugin = {
  name: "my-plugin",
  setup(build) {
    // Intercept import resolution:
    build.onResolve({ filter: /^env$/ }, (args) => ({
      path: args.path,
      namespace: "env-ns",
    }));

    // Load a custom namespace as a virtual module:
    build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
      contents: JSON.stringify(process.env),
      loader: "json",
    }));

    // Transform .md files to JS:
    build.onLoad({ filter: /\.md$/ }, async (args) => {
      const text = await fs.promises.readFile(args.path, "utf8");
      return {
        contents: `export default ${JSON.stringify(text)}`,
        loader: "js",
      };
    });

    // Run after build completes:
    build.onEnd((result) => {
      if (result.errors.length) console.error("Build failed");
      else console.log("Build succeeded");
    });
  },
};

// Use plugin:
await esbuild.build({ entryPoints: ["src/index.ts"], plugins: [myPlugin] });

// Popular community plugins:
// esbuild-plugin-alias    — path aliases without tsconfig
// esbuild-plugin-postcss  — PostCSS (autoprefixer, etc.)
// esbuild-plugin-svgr     — SVG to React components
// @fal-works/esbuild-plugin-global-externals  — global window.React externals

5. Build Scripts & CI Patterns

Node.js build scripts, multi-format library builds, and type checking separately
// build.mjs — typical library build script:
import * as esbuild from "esbuild";

const sharedConfig = {
  entryPoints: ["src/index.ts"],
  bundle: true,
  external: ["react", "react-dom"],   // peer deps
  sourcemap: true,
};

await Promise.all([
  // ESM for bundlers (tree-shakeable):
  esbuild.build({ ...sharedConfig, format: "esm", outfile: "dist/index.esm.js" }),
  // CJS for Node.js / older tooling:
  esbuild.build({ ...sharedConfig, format: "cjs", outfile: "dist/index.cjs.js" }),
]);

// package.json:
// "main": "dist/index.cjs.js",
// "module": "dist/index.esm.js",
// "exports": { ".": { "import": "./dist/index.esm.js", "require": "./dist/index.cjs.js" } }

// IMPORTANT: esbuild does NOT type-check. Run tsc separately:
// package.json scripts:
// "build": "node build.mjs",
// "type-check": "tsc --noEmit",
// "ci": "npm run type-check && npm run build"

// GitHub Actions:
// - run: node build.mjs
// - run: npx tsc --noEmit   # fails if type errors

// Metadata analysis:
const result = await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outdir: "dist",
  metafile: true,    // generate metadata
});
const text = await esbuild.analyzeMetafile(result.metafile!);
console.log(text);  // shows which files contribute to bundle size

Track Node.js and build tooling releases at ReleaseRun. Related: Vite 6 Reference | Turborepo Reference | TypeScript Reference

🔍 Free tool: npm Package Health Checker — check esbuild and related build tool packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com