esbuild 04 ⚡ Bundling — Recursive dependency graphs & –bundle
The --bundle flag is the switch that turns esbuild from a single-file transpiler into a graph processor. When it is present, esbuild reads the entry point, parses its imports, resolves each import to a file on disk or a package in node_modules, parses that file, and repeats the process until the entire reachable graph has been visited. The result is a single output file that contains the entry point and every dependency it needs, with the imports replaced by the inlined module definitions. The previous chapter covered the transpilation without the graph traversal; this chapter covers the traversal itself.
The recursion is the defining property. esbuild does not stop at the first level of imports. It follows the imports of the imports, and the imports of those, until no new files are discovered. The linker then orders the modules by the ESM evaluation order—depth-first postorder—so that the dependency is defined before the module that uses it . The tree shaking runs on the same graph, marking only the exports that are actually used and dropping the rest . The result is the minimal bundle that satisfies the entry point’s transitive requirements.
Key point: The --bundle flag resolves the bare imports against node_modules and inlines the resolved files. The --external flag is the escape hatch: a package listed as external is left as an import in the output, and the runtime must provide it. The --packages=external option marks every package import as external, which is the default when the platform is node in the current esbuild versions .
Why bundling requires the linker and the tree shaking
The resolution problem.
A source file imports ./utils or lodash-es. The relative path must be resolved to a file on disk, and the bare specifier must be resolved through the Node resolution algorithm—checking node_modules, the package.json exports field, and the file extensions. esbuild performs this resolution during the graph traversal, and the resolved path becomes a node in the graph. The linker then replaces the import statement with a reference to the inlined module .
The ordering problem.
The inlined modules must be arranged in the order the ESM specification requires. A module must be evaluated after its dependencies, and the depth-first postorder traversal guarantees this . esbuild’s linker performs the traversal from each entry point and emits the modules in the correct sequence. The cyclic imports are handled with the live bindings, which is the ESM semantics .
The tree-shaking problem.
Not every export of every dependency is used. The tree shaking marks the exports that are reachable from the entry point and drops the rest. The linker walks the parts of each module, marking the live parts and their dependencies, and the unused parts are removed from the output . The tree shaking is the mechanism that makes the bundle smaller than the concatenation of its inputs.
a. The basic bundling command
The bundling command adds the --bundle flag to the transpilation command.
# Bundle a TypeScript entry point
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
The command reads src/index.ts, resolves its imports, reads the imported files, and repeats until the graph is exhausted. The output dist/bundle.js contains the entry point and every dependency, with the imports inlined . The --outfile is the single output file for the single entry point.
The multiple entry points use the --outdir instead.
# Bundle multiple entry points into the output directory
npx esbuild src/index.ts src/admin.ts --bundle --outdir=dist
The --outdir writes one output file per entry point, preserving the directory structure. The shared dependencies are included in each output file unless the code splitting is enabled .
b. The external packages and the platform defaults
The --external flag tells esbuild not to bundle a specific package. The import stays in the output, and the runtime must resolve it.
# Bundle everything except the react package
npx esbuild src/index.ts --bundle --external:react --outfile=dist/bundle.js
The --external:react leaves the import ... from 'react' in the output. The React package must be available at runtime, either through the import map, the CDN, or the node_modules . This is the mechanism for the peer dependencies and the packages that must not be duplicated in the bundle.
The --packages=external is the blanket option.
# Mark all the package imports as external
npx esbuild src/index.ts --bundle --packages=external --outfile=dist/bundle.js
The --packages=external marks every import that does not start with /, ./, or ../ as external . This is the default behavior when the --platform=node is set, because the Node.js packages often use the runtime features that the bundler cannot replicate, and the node_modules is available at runtime anyway . The --packages=bundle restores the bundling for the packages that the default would externalize.
c. The format and the platform
The --format flag controls the module syntax of the output, and the --platform flag sets the defaults for the target environment.
# Bundle as ESM for the browser
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/bundle.mjs
# Bundle as CommonJS for Node.js
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/bundle.cjs
The --format=esm keeps the import and the export in the bundle, which is the format for the modern browsers and the Node.js ESM. The --format=cjs converts the bundle into the require and the module.exports, which is the format for the older Node.js and the CommonJS consumers . The --platform=node sets the format default to cjs and the target to the current Node.js version, and it marks the built-in modules as external .
Complete Example Session
# ============================================
# PART 1: THE BASIC BUNDLE
# ============================================
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
# ============================================
# PART 2: THE MULTIPLE ENTRY POINTS
# ============================================
npx esbuild src/index.ts src/admin.ts --bundle --outdir=dist
# ============================================
# PART 3: THE EXTERNAL PACKAGE
# ============================================
npx esbuild src/index.ts --bundle --external:react --outfile=dist/bundle.js
# ============================================
# PART 4: THE PACKAGES EXTERNAL
# ============================================
npx esbuild src/index.ts --bundle --packages=external --outfile=dist/bundle.js
# ============================================
# PART 5: THE NODE PLATFORM
# ============================================
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/bundle.cjs
# ============================================
# PART 6: THE ESM OUTPUT
# ============================================
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/bundle.mjs
# ============================================
# PART 7: THE VERIFICATION
# ============================================
grep "from 'react'" dist/bundle.js
# The react import is present only if it was marked as external
The seven parts demonstrate the bundling surface: the basic bundle, the multiple entry points, the external package, the packages external, the Node platform, the ESM output, and the verification.
Quick Reference
Bundling Flags
| Flag | Purpose |
|---|---|
--bundle | Walk the graph and inline the dependencies |
--outfile | The single output file |
--outdir | The output directory for the multiple entry points |
--external:<pkg> | Leave the package as an import |
--packages=external | Externalize all the packages |
--platform=node | The Node defaults and the built-in externalization |
--format=esm / cjs | The output module format |
The Graph Traversal
| Step | Action |
|---|---|
| 1 | Read the entry point |
| 2 | Parse the imports |
| 3 | Resolve each import to a file |
| 4 | Read the resolved file and repeat |
| 5 | Link the modules in the postorder |
| 6 | Tree-shake the unused exports |
The Tree Shaking
| Condition | Behavior |
|---|---|
| The export is used | Kept in the bundle |
| The export is unused | Removed |
| The side effects | Kept if the module has side effects |
Best Practices
✅ Do This:
# Use the --bundle for the single-file output
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js # ✅
# Use the --external for the peer dependencies
npx esbuild src/index.ts --bundle --external:react # ✅
# Use the --packages=external for the Node.js bundles
npx esbuild src/index.ts --bundle --platform=node --packages=external # ✅
# Use the --outdir for the multiple entry points
npx esbuild src/*.ts --bundle --outdir=dist # ✅
❌ Don’t Do This:
# Expect the imports to be inlined without --bundle
npx esbuild src/index.ts --outfile=dist/bundle.js # No bundle # ❌
# Bundle the built-in Node.js modules
npx esbuild src/index.ts --bundle --platform=node # External # ✅ (default)
# Use --packages=bundle to force the bundling of the packages # ❌
# Assume the tree shaking removes the side effects
# The side-effect modules are kept # ❌
# Bundle the React twice (the app and the dependency)
# Use the --external:react or the peer dependency # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The bundle is huge | The --packages=bundle bundles the node_modules | Use the default externalization |
The bundle has the __require | The CommonJS dependency in the ESM context | Use the @hyrious/esbuild-plugin-commonjs |
| The built-in module is bundled | The platform is not node | Use --platform=node |
| The multiple entry points overwrite | The --outfile with the multiple entries | Use the --outdir |
| The tree shaking does not remove | The module has the side effects | Use the sideEffects field |
Real-World Examples
1. The basic bundle
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
2. The multiple entry points
npx esbuild src/index.ts src/admin.ts --bundle --outdir=dist
3. The external React
npx esbuild src/index.ts --bundle --external:react --outfile=dist/bundle.js
4. The packages external
npx esbuild src/index.ts --bundle --packages=external
5. The Node platform
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/bundle.cjs
6. The ESM format
npx esbuild src/index.ts --bundle --format=esm --outfile=dist/bundle.mjs
7. The tree shaking check
grep "unusedExport" dist/bundle.js
# The unused export is absent if the tree shaking worked
8. The side effects
# The module with the side effects is kept
9. The CommonJS interop
# Use the @hyrious/esbuild-plugin-commonjs for the __require issue
10. The code splitting
# Use the splitting: true with the esm format
Visual
The Graph Traversal
┌──────────────────────────────────────────────────────────────┐
│ esbuild WALKS THE GRAPH FROM THE ENTRY POINT │
│ │
│ index.ts │
│ ├── import './utils' → utils.ts │
│ │ └── import 'lodash-es' → node_modules/lodash-es/... │
│ └── import 'react' → node_modules/react/... │
│ │
│ The traversal continues until no new files are discovered. │
│ The linker emits the modules in the postorder. │
└──────────────────────────────────────────────────────────────┘
The Tree Shaking
┌──────────────────────────────────────────────────────────────┐
│ THE TREE SHAKING REMOVES THE UNUSED EXPORTS │
│ │
│ utils.ts: │
│ ├── export function used() { ... } ← kept │
│ └── export function unused() { ... } ← removed │
│ │
│ The bundle contains only the used export. │
└──────────────────────────────────────────────────────────────┘
The External Packages
┌──────────────────────────────────────────────────────────────┐
│ THE --external LEAVES THE IMPORT IN THE OUTPUT │
│ │
│ Source: │
│ └── import React from 'react' │
│ │
│ Bundle with --external:react: │
│ └── import React from 'react' ← preserved │
│ │
│ Bundle without --external:react: │
│ └── var React = __require('react') ← inlined │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Bundling flag | --bundle |
| Graph traversal | The recursive resolution of the imports |
| Linker order | The depth-first postorder |
| Tree shaking | The removal of the unused exports |
| External package | --external:<pkg> |
| All packages external | --packages=external |
| Node default | The packages externalized |
| Output file | --outfile or --outdir |
Key takeaways:
- The
--bundleflag walks the recursive dependency graph. esbuild reads the entry point, resolves the imports, reads the resolved files, and repeats until the graph is exhausted. The output contains the entry point and every dependency it needs . - The linker orders the modules by the ESM evaluation order. The depth-first postorder guarantees that the dependency is defined before the module that uses it. The cyclic imports are handled with the live bindings .
- The tree shaking removes the unused exports. The linker marks only the exports that are reachable from the entry point, and the unused parts are dropped from the output .
- The
--externalflag leaves the package as an import. The runtime must resolve it. The--packages=externalmarks every package import as external, and it is the default when the platform isnode. - The
--platform=nodesets the Node defaults. It marks the built-in modules as external and sets the format tocjsand the target to the current Node version . - The
--outdiris for the multiple entry points. The--outfilewrites one file for one entry point. The--outdirwrites one file per entry point and preserves the structure.
Remember: The bundling is the recursive graph traversal. The --bundle flag tells esbuild to follow the imports, the linker orders the modules, and the tree shaking drops the unused exports. The --external and the --packages=external are the escape hatches for the packages that must not be inlined. The result is the single output file that contains the entry point and its transitive dependencies, which is the difference between the transpiled file with the broken imports and the self-contained bundle that runs.
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!