cheat sheet
esbuild
Daily-driver reference for esbuild — bundle CLI, transform API, JS API patterns, common build recipes.
esbuild — CLI & transform API
What it is
esbuild is the Go-implemented bundler/transformer that set the modern speed baseline. It powers Vite's dev transforms, ships embedded inside most modern toolchains, and works standalone as a CLI or JS API. This page covers the CLI and the JS transform API.
For package context, see packages-npm/npm-esbuild.
Install
npm install -D esbuild
# or
npm install -g esbuild
Output: platform-specific Go binary in node_modules/.bin/esbuild.
Day-to-day commands
| Command | What it does |
|---|---|
esbuild src/index.ts --bundle --outfile=dist/out.js | Basic bundle |
esbuild src/index.ts --bundle --platform=node | Bundle for Node (CJS resolution) |
esbuild src/index.ts --bundle --platform=browser | Bundle for browsers (default) |
esbuild src/index.ts --bundle --minify | Minified output |
esbuild src/index.ts --bundle --watch | Watch mode (rebuilds on change) |
esbuild src/index.ts --bundle --servedir=dist | Tiny dev server (no HMR) |
esbuild src/index.ts --bundle --target=es2020,chrome90,safari15 | Down-level to targets |
esbuild src/index.ts --bundle --metafile=meta.json | Emit build metadata |
esbuild src/index.ts --analyze | Print bundle composition summary |
esbuild --version | Print esbuild version |
Common scenarios
Bundle a CLI for Node
npx esbuild src/cli.ts \
--bundle \
--platform=node \
--target=node20 \
--outfile=dist/cli.cjs
Output:
dist/cli.cjs 1.2mb
⚡ Done in 47ms
For a tiny shipping binary, add --minify. For ESM output, --format=esm (and rename to .mjs).
Browser app bundle with code splitting
npx esbuild src/index.ts src/worker.ts \
--bundle \
--format=esm \
--splitting \
--outdir=dist \
--minify \
--target=es2020
Output:
dist/index.js 12kb
dist/worker.js 8kb
dist/chunk-AB12.js 4kb ← shared chunk
⚡ Done in 89ms
--splitting requires --format=esm (and --outdir, not --outfile).
Transform API (no bundling, no FS)
import { transform } from "esbuild";
const result = await transform(
"const x: number = 42; export default x;",
{ loader: "ts", target: "es2022" }
);
console.log(result.code);
Output:
const x = 42;
export default x;
transform takes a single string in, returns code + map. No filesystem access. Use for REPLs, AST tools, runtime evaluation.
Watch / context API
import esbuild from "esbuild";
const ctx = await esbuild.context({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/out.js",
});
await ctx.watch(); // rebuilds on file change
const { host, port } = await ctx.serve({ servedir: "dist" });
console.log(`http://${host}:${port}`);
serve is a minimal HTTP server — no HMR. Use Vite for SPA dev.
Sourcemap modes
npx esbuild src/index.ts --bundle --sourcemap # external (.map file + comment)
npx esbuild src/index.ts --bundle --sourcemap=inline # base64 in JS file
npx esbuild src/index.ts --bundle --sourcemap=external # .map file, no comment
npx esbuild src/index.ts --bundle --sourcemap=linked # .map file + comment (default)
Output:
out.js 12.3kb
out.js.map 18.7kb
⚡ Done in 17ms
For prod: --sourcemap=external + upload to error tracker, or --sourcemap=linked + serve maps privately.
Plugin (JS API only)
import esbuild from "esbuild";
const envPlugin = {
name: "env",
setup(build) {
build.onResolve({ filter: /^env$/ }, (args) => ({
path: args.path,
namespace: "env-ns",
}));
build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
contents: JSON.stringify(process.env),
loader: "json",
}));
},
};
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/out.js",
plugins: [envPlugin],
});
esbuild's plugin API is intentionally small: onResolve, onLoad, onStart, onEnd. Not a webpack-style hook ladder.
Mark deps external
npx esbuild src/index.ts --bundle --platform=node \
--external:express --external:pg --outfile=dist/out.js
Output:
dist/out.js 8.2kb
⚡ Done in 11ms
Or for all of node_modules/:
npx esbuild src/index.ts --bundle --platform=node \
--packages=external --outfile=dist/out.js
Output:
dist/out.js 4.7kb
⚡ Done in 9ms
The --packages=external flag (0.18+) externalises every npm package, leaving only your source bundled.
Respect tsconfig.json
npx esbuild src/index.ts --bundle --tsconfig=tsconfig.json --outfile=dist/out.js
Output:
dist/out.js 18.4kb
⚡ Done in 21ms
esbuild reads target, paths, experimentalDecorators, useDefineForClassFields. Without --tsconfig, it falls back to its own defaults.
Useful flags
| Flag | What it does |
|---|---|
--bundle | Resolve imports and bundle into one (or splittable chunks) |
--outfile=<path> | Single-file output |
--outdir=<path> | Multi-file output |
--format=<fmt> | esm, cjs, iife |
--platform=<plat> | browser (default), node, neutral |
--target=<targets> | e.g. es2020,chrome110,safari16,node20 |
--external:<name> | Exclude from bundle (resolve at runtime) |
--packages=external | All node_modules/ deps external (0.18+) |
--splitting | Code splitting (ESM-only, multi-entry) |
--minify | Whitespace + identifiers + syntax |
--minify-whitespace, --minify-identifiers, --minify-syntax | Granular |
--sourcemap[=mode] | Sourcemap generation |
--watch | Rebuild on change |
--servedir=<dir> | Tiny dev server |
--tsconfig=<path> | Use a specific tsconfig |
--loader:<.ext>=<loader> | Map extension to loader (file, text, binary, dataurl, base64, css, json) |
--define:<key>=<val> | String substitution (--define:process.env.NODE_ENV=\"production\") |
--banner:js=<str> / --footer:js=<str> | Wrap output |
--legal-comments=<mode> | none, inline, eof, linked, external |
--metafile=<path> | Emit build metadata JSON |
--analyze | Print bundle treemap summary |
--log-level=<level> | silent, error, warning, info, debug, verbose |
--color / --no-color | Control coloured output |
Configuration
esbuild has no config file. Either pass flags on the CLI or write a build.mjs:
// build.mjs
import esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "browser",
format: "esm",
outdir: "dist",
splitting: true,
minify: true,
target: ["es2020", "chrome110", "firefox110"],
sourcemap: true,
define: {
"process.env.NODE_ENV": '"production"',
},
loader: { ".png": "file", ".svg": "text" },
metafile: true,
});
node build.mjs
Output:
dist/index.js 12kb
dist/chunk-AB.js 4kb
⚡ Done in 89ms
The JS API is more flexible than the CLI — multi-entry, plugin chains, metafile inspection.
Common pitfalls
[ERROR] Could not resolve "X". Either missing install, missing--platform=nodefor Node deps, or atsconfig.jsonpath alias not loaded (pass--tsconfig).Top-level await is not available in the configured target environment. Bump--target=es2022+or refactor.The CommonJS "require" function does not work in ESM scope. Output is ESM but source usesrequire(). Switch source, change--format=cjs, or add acreateRequireshim.- Source maps don't show original source.
--sources-content=falsewas set, or IDE loads bundle from a different URL thansourceMappingURLexpects. - Bundle includes
node_modules/deps unexpectedly. Default bundles everything. Use--packages=externalor per-package--external:X. - Binary fails after install. Platform optional dep missing. Run
npm install --include=optionalornpm rebuild esbuild. - Minifier produces slightly larger output than Terser. Expected — esbuild trades ~5% bytes for ~10× speed. Run Terser as a second pass for max compression.
See also
- Packages: npm-esbuild — versioning, security, ecosystem
- JavaScript: modules — ESM/CJS interop
- JavaScript: vite — esbuild-powered dev server