esbuild 28 ⚡ Cheatsheet
This cheatsheet covers esbuild 0.28.x, the current stable line as of mid-2026 . It is scoped to the decisions you make mid-build: the CLI flags, the API entry points, the plugin hooks, the loader mappings, and the incremental lifecycle. The version numbers matter less than the shape of the tool, but the shape has settled, and this is the shape.
The Two Entry Points
esbuild has two interfaces. The CLI is a binary that parses flags and runs a build. The JavaScript API is a module that imports esbuild and calls a function. They share the same engine, and the flags map to the options. The choice is the lifecycle, not the output.
| Interface | Entry | Use case |
|---|---|---|
| CLI | esbuild src/app.ts --bundle | One-shot build, simple flags |
| API | import * as esbuild from 'esbuild' | Plugins, context, result inspection |
| Transform API | esbuild.transform(code, { loader: 'ts' }) | In-memory single-file transform |
The CLI is the one-liner. The API is the module. The transform API is the primitive that does not touch the file system.
The Build vs Context Distinction
Since 0.17.0, build() is for one-shot builds and context() is for long-lived builds. The watch and serve options were removed from build(), and the context object is the replacement.
| Function | Returns | Watch/Serve |
|---|---|---|
esbuild.build(options) | Promise<BuildResult> | ❌ Removed |
esbuild.context(options) | Promise<BuildContext> | ✅ ctx.watch(), ctx.serve() |
The context object holds the incremental state. The rebuild(), watch(), and serve() methods operate on it. The dispose() method is the single cleanup path.
// One-shot
const result = await esbuild.build({ entryPoints: ['app.ts'], bundle: true, outdir: 'dist' });
// Long-lived
const ctx = await esbuild.context({ entryPoints: ['app.ts'], bundle: true, outdir: 'dist' });
await ctx.rebuild(); // initial build
await ctx.watch(); // start watcher
await ctx.serve({ servedir: 'www' }); // dev server
await ctx.dispose(); // cleanup
The Loaders
The loader tells esbuild how to parse a file. The --loader:.ext=name flag maps an extension to a loader. The loader option in the API does the same.
| Loader | Content type | Output |
|---|---|---|
js | JavaScript | JS module |
jsx | JSX | JS module |
ts | TypeScript | JS module |
tsx | TSX | JS module |
css | CSS | CSS |
json | JSON | JS module (default export) |
text | Raw string | JS module (default export) |
binary | Uint8Array | JS module (default export) |
file | File path | URL string |
dataurl | Data URL | Data URL string |
copy | Copy file | No transformation |
base64 | Base64 string | Base64 string |
The text, binary, file, and dataurl loaders are the asset loaders. The js, ts, and jsx loaders are the code loaders.
The Plugin Hooks
A plugin is a setup function with onResolve and onLoad. The two hooks are the entire extension surface.
| Hook | When | Returns |
|---|---|---|
onResolve | Every import path | path, namespace, external, or undefined |
onLoad | Every unique path/namespace | contents, loader, or undefined |
onStart | Build start | — |
onEnd | Build end | — |
onDispose | Context disposal | — |
The onResolve hook turns a specifier into a path. The onLoad hook turns a path into content. The namespace is the link: the onResolve returns a custom namespace, and the onLoad filters on it.
const plugin = {
name: 'my-plugin',
setup(build) {
build.onResolve({ filter: /^env$/ }, () => ({ path: 'env', namespace: 'env-ns' }));
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}));
build.onEnd((result) => console.log(result.errors.length));
},
};
The onEnd callback fires after every build, including the first. It is the replacement for the removed onRebuild callback.
The Filter Syntax
Both hooks require a filter RegExp. The filter is evaluated in Go, and the callback is only invoked when the filter matches.
| Filter | Match |
|---|---|
/\.ts$/ | Files ending in .ts |
/^env$/ | The exact specifier env |
/.*/ | Everything |
The filter syntax follows Go’s regexp engine. No look-ahead, no look-behind, no backreferences. An invalid filter causes a build error.
The External and Alias Options
The external array marks packages as external. The alias map rewrites specifiers. The define map substitutes globals.
| Option | Type | Purpose |
|---|---|---|
external | string[] | Packages to leave unbundled |
alias | Record<string, string> | Specifier rewrite |
define | Record<string, string> | Global substitution |
inject | string[] | Files to inject imports from |
await esbuild.build({
external: ['react', 'react-dom'],
alias: { 'old-lib': 'new-lib' },
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
inject: ['./shims.js'],
});
The define values must be JSON.stringify-wrapped for strings. The inject files are imported into every module that uses the injected symbols.
The Output Configuration
The output options control the format, the target, and the file names.
| Option | Values | Default |
|---|---|---|
format | iife, cjs, esm | iife (browser), cjs (node) |
platform | browser, node, neutral | browser |
target | es2015, es2020, chrome100, etc. | esnext |
outdir | Directory | — |
outfile | File | — |
splitting | boolean | false |
minify | boolean | false |
sourcemap | boolean, inline, external, both | false |
The splitting option requires format: 'esm' and outdir. The outfile option is incompatible with splitting.
The Chunking and Naming
The entryNames, chunkNames, and assetNames templates control the output filenames. The [hash] placeholder is the cache-busting mechanism.
| Option | Placeholders | Applies to |
|---|---|---|
entryNames | [dir], [name], [hash], [ext] | Entry points |
chunkNames | [name], [hash], [ext] | Shared chunks |
assetNames | [dir], [name], [hash], [ext] | File loader assets |
build: {
entryNames: '[dir]/[name]-[hash]',
chunkNames: 'chunks/[name]-[hash]',
assetNames: 'assets/[ext]/[name]-[hash]',
}
The [dir] placeholder is not supported in chunkNames. The [hash] placeholder is what makes long-term caching possible.
The Metafile
The metafile: true option produces a JSON object with inputs and outputs. The inputs object maps every module to its bytes and imports. The outputs object maps every output to its bytesInOutput and entryPoint.
const result = await esbuild.build({ metafile: true, /* ... */ });
console.log(result.metafile.inputs);
console.log(result.metafile.outputs);
The metafile is the data. The visualizers (esbuild-visualizer, esbuild-lazy-analyzer, @rnx-kit/esbuild-bundle-analyzer) are consumers of the data. The metafile is not enabled by default because it adds a small amount of work and produces a file that is not needed for the production output.
The CLI Quick Reference
# One-shot bundle
esbuild src/app.ts --bundle --outdir=dist --minify
# Watch
esbuild src/app.ts --bundle --outdir=dist --watch
# Serve
esbuild src/app.ts --bundle --outdir=www --serve --servedir=www
# External
esbuild src/app.ts --bundle --external:react --outfile=dist/app.js
# Loader
esbuild src/app.ts --bundle --loader:.png=file --outdir=dist
# Metafile
esbuild src/app.ts --bundle --metafile=stats.json --outfile=dist/app.js
# Target
esbuild src/app.ts --bundle --target=es2020 --outfile=dist/app.js
The CLI performs the initial build automatically. The API does not; the rebuild() call is required.
The Common Pitfalls
| Pitfall | Cause | Fix |
|---|---|---|
watch: true rejected | Removed in 0.17.0 | Use context() + watch() |
| The process does not exit | The watcher holds the handle | Call ctx.dispose() |
| A plugin is not called | The filter does not match | Check the regexp against the path |
| The virtual module is not loaded | The namespace does not match | Check the namespace string |
The define value is a raw string | JSON.stringify not used | Wrap the value |
The onLoad returns a string | The callback must return an object | Return { contents, loader } |
The Version Note
esbuild 0.28.0 added support for the with { type: 'text' } import proposal and integrity checks for the fallback download path . The OpenBSD ports tree shows 0.28.1 as the current version as of June 2026 . The version line is still 0.x, which means the API is not semver-stable in the strict sense, but the surface has been stable for years. The with { type: 'text' } import is the newest syntax addition, and it is the same as the existing text loader.
Summary
| Area | Key takeaway |
|---|---|
| Entry points | build() for one-shot, context() for watch/serve |
| Plugins | onResolve + onLoad, connected by namespace |
| Filters | Go regexp, no look-ahead or backreferences |
| External | external array, alias map, define map |
| Output | format, platform, target, outdir/outfile |
| Chunking | splitting: true + format: 'esm' + outdir |
| Naming | entryNames, chunkNames, assetNames |
| Metafile | metafile: true for analysis |
| Version | 0.28.x, with { type: 'text' } support |
Remember: esbuild is a bundler and a transformer with two interfaces and one engine. The CLI is the one-liner for the simple build. The API is the module for the plugin, the context, and the result object. The context is the incremental lifecycle, and the onEnd callback is the post-build hook. The filter is the performance mechanism, and the namespace is the virtual-module mechanism. The metafile is the analysis data, and the visualizers are its consumers. The tool is small, and the surface is stable.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!