esbuild 03 ⚡ CLI Basics — Transpiling a single file without bundling
The esbuild CLI has two distinct modes that are easy to conflate. The --bundle flag walks the import graph, merges every dependency into one file, and resolves the bare specifiers against node_modules. Without --bundle, esbuild operates as a transpiler: it reads one file, strips the TypeScript or JSX syntax, optionally lowers the JavaScript target, and writes the result. The import statements are left untouched. The previous chapter covered the platform-binary installation; this chapter covers the single-file transpilation that is the simplest and most frequent esbuild invocation.
The difference is not cosmetic. A transpiled file still has its import statements pointing to the original paths, and it is the runtime’s job—the browser’s ESM loader or Node’s module resolver—to find those dependencies. The bundling mode resolves them at build time and inlines them. The transpilation mode assumes the dependencies will be available at runtime. The choice between the two depends on whether the deployment environment can resolve the imports or whether the build must produce a self-contained artifact.
Key point: The transpilation mode is the absence of --bundle. The --outfile flag writes the transformed code to a file, and the --loader flag tells esbuild how to parse the input. The input can be a .ts, .tsx, .jsx, or .js file, and the --target flag controls how much of the modern syntax is lowered. The imports are preserved verbatim, which means the transpiled output is not a standalone script unless the imports resolve at runtime.
Why single-file transpilation requires the loader
The syntax problem.
esbuild does not infer the file type from the extension alone when the file is read from stdin, and even for a file on disk, the extension is the default loader but the --loader flag overrides it. A .ts file is parsed with the TypeScript loader, which strips the type annotations and handles the enum, the namespace, and the parameter properties. A .tsx file uses the TypeScript loader with the JSX enabled. A .jsx file uses the JSX loader. The --loader flag is the explicit control, and the extension is the fallback .
The target problem.
The --target flag tells esbuild which JavaScript features the runtime supports. The es2020 target keeps the optional chaining and the nullish coalescing, and the es2015 target lowers them into the older syntax. The transpilation is the mechanism that makes the modern source code run on the older runtime, and the --target is the configuration that determines how much lowering happens .
The import-preservation problem.
Without --bundle, esbuild does not touch the import paths. A file that imports ./utils.ts keeps that import, and the transpiled output still references ./utils.ts (or ./utils.js if the extension is rewritten by the runtime). This is the intended behavior for the environments where the imports are resolved at runtime, such as the Node.js with the tsx loader or the browser with the import maps. The --bundle flag is the alternative when the imports must be inlined.
a. The basic transpilation command
The simplest invocation reads a TypeScript file and writes the JavaScript to an output file.
# Transpile a single TypeScript file
npx esbuild src/app.ts --outfile=dist/app.js
The command reads src/app.ts, strips the TypeScript syntax, and writes the JavaScript to dist/app.js. The imports are preserved, and the output is not a bundle . The command completes in milliseconds because there is no graph traversal and no dependency resolution.
The same command works for the JSX files.
# Transpile a JSX file
npx esbuild src/App.jsx --outfile=dist/App.js
The App.jsx is parsed with the JSX loader, and the JSX syntax is converted into the React.createElement calls (or the configured JSX factory). The output is the JavaScript that the runtime can execute, and the imports are preserved.
b. The loader and the target flags
The --loader flag overrides the file type inference, which is necessary when the file has a non-standard extension or when the input comes from stdin.
# Transpile a TypeScript file with the explicit loader
npx esbuild src/app.ts --loader=ts --outfile=dist/app.js
# Transpile from stdin with the TypeScript loader
cat src/app.ts | npx esbuild --loader=ts > dist/app.js
The --loader=ts tells esbuild to parse the input as TypeScript regardless of the extension. The stdin form is useful in the shell pipelines, and it requires the loader because there is no file extension to infer from .
The --target flag controls the syntax lowering.
# Lower the syntax to ES2015
npx esbuild src/app.ts --target=es2015 --outfile=dist/app.js
# Keep the modern syntax for the ES2020 runtime
npx esbuild src/app.ts --target=es2020 --outfile=dist/app.js
The es2015 target converts the optional chaining, the nullish coalescing, and the async/await into the older equivalents. The es2020 target keeps them, because the runtime supports them natively. The target is the mechanism that matches the output to the runtime’s capabilities .
c. The output format and the platform
The --format flag controls the module format of the output. The default is the iife for the browser scripts, and the esm and the cjs are the explicit choices.
# Output as ESM
npx esbuild src/app.ts --format=esm --outfile=dist/app.mjs
# Output as CommonJS
npx esbuild src/app.ts --format=cjs --outfile=dist/app.cjs
The --format=esm keeps the import and the export statements, and the output file should have the .mjs extension (or the "type": "module" in the package.json). The --format=cjs converts the ESM syntax into the require() calls and the module.exports, which is the format for the older Node.js code .
The --platform flag is the related setting for the runtime environment.
# Target the Node.js platform
npx esbuild src/app.ts --platform=node --outfile=dist/app.js
The --platform=node sets the default format to cjs and the default target to the current Node.js version. It also enables the Node-specific features, such as the __dirname and the process global. The platform is the mechanism that applies the runtime-specific defaults without the explicit flags .
Complete Example Session
# ============================================
# PART 1: THE BASIC TRANSPILATION
# ============================================
npx esbuild src/app.ts --outfile=dist/app.js
# The TypeScript is stripped, the imports are preserved
# ============================================
# PART 2: THE EXPLICIT LOADER
# ============================================
npx esbuild src/app.ts --loader=ts --outfile=dist/app.js
# The loader is explicit, regardless of the extension
# ============================================
# PART 3: THE TARGET LOWERING
# ============================================
npx esbuild src/app.ts --target=es2015 --outfile=dist/app.js
# The modern syntax is lowered to ES2015
# ============================================
# PART 4: THE ESM OUTPUT
# ============================================
npx esbuild src/app.ts --format=esm --outfile=dist/app.mjs
# The import/export statements are kept
# ============================================
# PART 5: THE NODE PLATFORM
# ============================================
npx esbuild src/app.ts --platform=node --outfile=dist/app.js
# The CommonJS format and the Node target
# ============================================
# PART 6: THE STDIN FORM
# ============================================
cat src/app.ts | npx esbuild --loader=ts > dist/app.js
# The input from stdin, the loader required
# ============================================
# PART 7: THE VERIFICATION
# ============================================
head -5 dist/app.js
# The output shows the transformed code with the imports intact
The seven parts demonstrate the transpilation surface: the basic command, the explicit loader, the target lowering, the ESM output, the Node platform, the stdin form, and the verification.
Quick Reference
Transpilation vs Bundling
| Aspect | Transpilation (no flag) | Bundling (--bundle) |
|---|---|---|
| Import graph | Not walked | Walked and merged |
| Bare imports | Preserved | Resolved and inlined |
| Output | One file per input | One file for the whole graph |
| Speed | Milliseconds | Fast, but proportional to the graph |
The Flags
| Flag | Purpose | Example |
|---|---|---|
--outfile | The output file | --outfile=dist/app.js |
--loader | The input parser | --loader=ts |
--target | The syntax lowering | --target=es2015 |
--format | The module format | --format=esm |
--platform | The runtime defaults | --platform=node |
The Loaders
| Extension | Loader |
|---|---|
.ts | ts |
.tsx | tsx |
.jsx | jsx |
.js | js |
.css | css |
The Targets
| Target | Use Case |
|---|---|
esnext | The latest syntax, no lowering |
es2020 | The modern browsers and Node 14+ |
es2015 | The older browsers and Node 6+ |
Best Practices
✅ Do This:
# Omit the --bundle for the single-file transpilation
npx esbuild src/app.ts --outfile=dist/app.js # ✅
# Use the --loader when the extension is ambiguous
npx esbuild src/app.ts --loader=ts # ✅
# Use the --target to match the runtime
npx esbuild src/app.ts --target=es2020 # ✅
# Use the --format for the explicit module system
npx esbuild src/app.ts --format=esm --outfile=dist/app.mjs # ✅
# Use the --platform=node for the Node.js scripts
npx esbuild src/app.ts --platform=node # ✅
❌ Don’t Do This:
# Expect the imports to be resolved without --bundle
npx esbuild src/app.ts --outfile=dist/app.js # Imports kept # ❌
# Use the wrong loader for the file type
npx esbuild src/app.tsx --loader=ts # The JSX not parsed # ❌
# Use the --bundle when the runtime resolves the imports
# The output is a self-contained bundle, which is unnecessary # ❌
# Forget the --format when the runtime expects ESM
npx esbuild src/app.ts --outfile=dist/app.js # IIFE default # ❌
# Assume the --target is optional
# The default target is the esnext, which may not run everywhere # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The imports are broken at runtime | The transpilation preserved the imports | Use --bundle or provide the import map |
| The JSX is not transformed | The loader is ts instead of tsx | Use --loader=tsx |
| The output uses the modern syntax | No --target | Add --target=es2015 |
| The Node.js cannot run the ESM output | The format is esm | Use --format=cjs or the .mjs extension |
The browser does not understand the require | The format is cjs | Use --format=esm |
Real-World Examples
1. Transpile a TypeScript file
npx esbuild src/app.ts --outfile=dist/app.js
2. Transpile a JSX file
npx esbuild src/App.jsx --outfile=dist/App.js
3. Transpile with the explicit loader
npx esbuild src/app.ts --loader=ts --outfile=dist/app.js
4. Lower the target to ES2015
npx esbuild src/app.ts --target=es2015 --outfile=dist/app.js
5. Output as ESM
npx esbuild src/app.ts --format=esm --outfile=dist/app.mjs
6. Output for Node.js
npx esbuild src/app.ts --platform=node --outfile=dist/app.js
7. Transpile from stdin
cat src/app.ts | npx esbuild --loader=ts > dist/app.js
8. Transpile a CSS file
npx esbuild src/styles.css --outfile=dist/styles.css
9. Transpile with the source map
npx esbuild src/app.ts --sourcemap --outfile=dist/app.js
10. The watch mode
npx esbuild src/app.ts --outfile=dist/app.js --watch
Visual
The Transpilation vs the Bundling
┌──────────────────────────────────────────────────────────────┐
│ TRANSPILATION LEAVES THE IMPORTS; BUNDLING INLINES THEM │
│ │
│ Transpilation: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ app.ts → esbuild → app.js │ │
│ │ import { x } from './utils' ← preserved │ │
│ │ The runtime resolves './utils' │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Bundling: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ app.ts → esbuild --bundle → bundle.js │ │
│ │ import { x } from './utils' ← inlined │ │
│ │ The bundle is self-contained │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Loader Selection
┌──────────────────────────────────────────────────────────────┐
│ THE LOADER MATCHES THE INPUT SYNTAX │
│ │
│ .ts → the ts loader → the types stripped │
│ .tsx → the tsx loader → the types + the JSX │
│ .jsx → the jsx loader → the JSX converted │
│ .js → the js loader → no transformation │
│ │
│ The --loader flag overrides the extension inference. │
└──────────────────────────────────────────────────────────────┘
The Target Lowering
┌──────────────────────────────────────────────────────────────┐
│ THE TARGET CONTROLS THE SYNTAX LOWERING │
│ │
│ Source: const x = obj?.foo ?? 'default' │
│ │
│ --target=es2020: │
│ └── const x = obj?.foo ?? 'default' ← kept │
│ │
│ --target=es2015: │
│ └── const x = obj == null ? 'default' : obj.foo ← lowered │
│ │
│ The target matches the output to the runtime. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Transpilation mode | The absence of --bundle |
| Output | One file per input, imports preserved |
| Loader | --loader=ts, tsx, jsx, js |
| Target | --target=es2015, es2020, esnext |
| Format | --format=esm, cjs, iife |
| Platform | --platform=node, browser |
| Stdin | --loader required |
| Bundling | The --bundle flag walks the graph |
Key takeaways:
- Transpilation is the absence of
--bundle. The single file is transformed, and the imports are preserved. The runtime is responsible for resolving the imports, not esbuild . - The
--loaderflag overrides the extension inference. It is required when the input comes from stdin, and it is the explicit control when the extension is non-standard. Thetsloader strips the types, and thetsxandjsxloaders also transform the JSX . - The
--targetflag controls the syntax lowering. Thees2015target converts the modern syntax into the older equivalents. Thees2020target keeps the optional chaining and the nullish coalescing. The target is the mechanism that matches the output to the runtime . - The
--formatflag controls the module system. Theesmoutput keeps theimportand theexport, and thecjsoutput uses therequireand themodule.exports. The--platform=nodesets the format and the target defaults for Node.js . - The transpilation is fast because it does not walk the graph. There is no dependency resolution and no merge. The command completes in milliseconds, regardless of the project size.
- The bundling is the alternative when the imports must be inlined. The
--bundleflag walks the graph, resolves the bare specifiers, and produces a self-contained file. The transpilation is the choice when the runtime can resolve the imports.
Remember: The esbuild CLI has two modes. The transpilation mode transforms one file and leaves the imports intact. The bundling mode walks the graph and inlines the dependencies. The --loader flag tells esbuild how to parse the input, and the --target flag tells it how much to lower. The --format flag controls the module system, and the --platform flag applies the runtime defaults. The result is the single-file transform that is the simplest esbuild invocation, and the prerequisite for the bundling that follows.
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!