esbuild 18 ⚡ Chunk Naming — Controlling output file paths
esbuild’s output filenames are controlled by three template options that look interchangeable but apply to different categories of output. entryNames controls the files generated from your entry points. chunkNames controls the shared chunks that --splitting extracts. assetNames controls the files emitted by the file loader—images, fonts, and anything else that is copied rather than bundled . The default behavior for entry points is not a template at all: esbuild preserves the input file’s path relative to outbase, which means src/pages/about.ts becomes dist/pages/about.js without any configuration. The entryNames option overrides that behavior, and the [dir] placeholder is what preserves the directory structure while adding a hash or changing the extension.
The distinction between chunkNames and entryNames is the source of the most common naming surprise. A dynamic import with --splitting produces a chunk, not an entry point, and the chunk’s filename comes from chunkNames, not entryNames. A developer who sets entryNames: '[name]-[hash]' and expects the split chunk to be hashed will find the chunk still named from the default chunkNames template. The two options must be configured independently, and the placeholders they accept differ: chunkNames supports [name], [hash], and [ext] but not [dir], because chunks are not associated with an input directory in the same way entry points are .
Key point: entryNames names the files built from entry points, chunkNames names the files extracted by code splitting, and assetNames names the files copied by the file loader. Each template accepts a specific set of placeholders, and configuring one does not configure the others.
Why the three naming options exist separately
The entry point path preservation problem. esbuild’s default entry point naming is structural: the output path mirrors the input path relative to outbase. This is useful when the build produces a directory tree that matches the source tree, but it provides no mechanism for cache busting. The entryNames option replaces the structural default with a template. The [dir] placeholder is the bridge between the two: it preserves the relative directory while allowing the filename portion to change. Without [dir], all entry points with the same basename collide in the output directory, and esbuild fails the build because two files cannot have the same path .
The chunk category problem. Chunks are not entry points. They are modules that esbuild extracted because they are shared between entry points or referenced by dynamic imports. They have no natural input path that maps to an output path, because the same module can be reached from multiple entry points. The chunkNames template is therefore purely synthetic: it generates a name from the chunk’s own content and extension. The [dir] placeholder is not supported because there is no meaningful directory to preserve . A project that wants hashed chunks must configure chunkNames explicitly; the entryNames setting does not apply.
The asset hash collision problem. Assets loaded with the file loader are copied to the output directory. Two assets in different source directories can have the same filename—components/button/icon.png and components/select/icon.png both become icon.png. Without a hash, the second overwrites the first, and the application references the wrong image. The [hash] placeholder in assetNames is what disambiguates them. The [dir] placeholder can preserve the source directory structure, but the hash is the more robust solution because it also serves as a cache-busting mechanism .
The trade-off. Every template that includes [hash] produces filenames that change when the content changes, which is what makes long-lived caching possible. But it also means the filenames are unpredictable at build time. A server that references the output files must either read the metafile to discover the hashed names or use a manifest that maps the original names to the hashed ones. esbuild does not rewrite HTML or other entry files to point at hashed chunks; that is the developer’s responsibility, or a plugin’s .
a. Configuring entryNames with [dir] and [hash]
The entryNames template accepts [dir], [name], [hash], and [ext]. The [dir] placeholder is the relative path from the entry point’s directory to outbase. The [name] is the filename without the extension. The [hash] is the content hash. The [ext] is the extension.
// build.mjs — entry names with directory preservation and hashing
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/pages/home/index.ts', 'src/pages/about/index.ts'],
outdir: 'dist',
// The [dir] placeholder preserves the relative directory.
// The [hash] placeholder adds a content hash for cache busting.
// Output: dist/pages/home/index-XXXXXX.js
// dist/pages/about/index-XXXXXX.js
entryNames: '[dir]/[name]-[hash]',
outbase: 'src',
bundle: true,
format: 'esm',
});
The outbase option is the reference point for [dir]. In the example above, outbase: 'src' means the relative directory for src/pages/home/index.ts is pages/home. If outbase is omitted, esbuild computes the lowest common ancestor of all entry points, which may produce a different relative path than expected. The [dir] placeholder is what prevents the collision that would occur if two entry points with the same basename were both written to the output root .
// build.mjs — hash only, no directory preservation
await esbuild.build({
entryPoints: ['src/app.js'],
outdir: 'dist',
// Without [dir], the output is dist/app-XXXXXX.js.
// This works for a single entry point but collides for multiple.
entryNames: '[name]-[hash]',
bundle: true,
});
For a single entry point, [name]-[hash] is sufficient. For multiple entry points with the same basename in different directories, [dir] is required to disambiguate them before the hash is applied.
b. Configuring chunkNames and assetNames
The chunkNames template applies to shared chunks extracted by --splitting. It accepts [name], [hash], and [ext]. The [dir] placeholder is not supported.
// build.mjs — chunk names with hash
await esbuild.build({
entryPoints: ['src/app.js'],
outdir: 'dist',
format: 'esm',
splitting: true,
// The chunk is a shared module extracted from the bundle.
// The [hash] ensures the chunk filename changes when its content changes.
// Output: dist/chunks/settings-XXXXXX.js
chunkNames: 'chunks/[name]-[hash]',
bundle: true,
});
The assetNames template applies to files copied by the file loader. It accepts [dir], [name], [hash], and [ext]. The [dir] placeholder is useful for preserving the asset’s source directory, but the [hash] is what prevents name collisions when assets in different directories share a filename .
// build.mjs — asset names with directory and hash
await esbuild.build({
entryPoints: ['src/app.js'],
outdir: 'dist',
// Assets are copied to dist/assets/[ext]/[name]-[hash].[ext].
// The [ext] placeholder groups assets by type.
assetNames: 'assets/[ext]/[name]-[hash]',
loader: { '.png': 'file', '.woff2': 'file' },
bundle: true,
});
The asset template does not need to include the file extension. esbuild appends the original extension automatically after the template is applied. The [ext] placeholder in the template includes the dot, so [ext] for a .png file produces .png in the output path .
Complete Example Session
// ============================================
// PART 1: THE SOURCE STRUCTURE
// ============================================
// src/
// ├── pages/
// │ ├── home/
// │ │ └── index.ts
// │ └── about/
// │ └── index.ts
// ├── shared/
// │ └── utils.ts ← imported by both pages
// ├── assets/
// │ ├── logo.png
// │ └── icon.png ← another icon.png in a different dir
// └── styles.css
// ============================================
// PART 2: THE BUILD CONFIGURATION
// ============================================
// build.mjs
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: [
'src/pages/home/index.ts',
'src/pages/about/index.ts',
],
outdir: 'dist',
outbase: 'src',
bundle: true,
format: 'esm',
splitting: true,
// Entry points preserve their directory and add a hash.
entryNames: '[dir]/[name]-[hash]',
// Shared chunks go into a chunks/ directory with a hash.
chunkNames: 'chunks/[name]-[hash]',
// Assets preserve their extension directory and add a hash.
assetNames: 'assets/[ext]/[name]-[hash]',
loader: { '.png': 'file' },
metafile: true,
});
// ============================================
// PART 3: THE OUTPUT
// ============================================
// dist/
// ├── pages/
// │ ├── home/
// │ │ └── index-ABC123.js
// │ └── about/
// │ └── index-DEF456.js
// ├── chunks/
// │ └── utils-GHI789.js
// └── assets/
// └── png/
// ├── logo-JKL012.png
// └── icon-MNO345.png
// ============================================
// PART 4: THE COLLISION WITHOUT [dir]
// ============================================
// ❌ entryNames: '[name]-[hash]'
// Both src/pages/home/index.ts and src/pages/about/index.ts
// produce index-XXXXXX.js. The hashes differ, so the filenames
// differ, and the build does not fail. But the directory structure
// is lost, and the output is flat.
// ❌ entryNames: '[name]' (no hash)
// Both produce index.js. The build fails:
// "Two output files share the same path but have different contents."
// ============================================
// PART 5: THE METAFILE AS A MANIFEST
// ============================================
// The metafile maps input paths to output paths.
const result = await esbuild.build({
// ...
metafile: true,
});
// result.metafile.outputs contains entries like:
// 'dist/pages/home/index-ABC123.js': { entryPoint: 'src/pages/home/index.ts', ... }
//
// A server can read this metafile to discover the hashed filenames
// and serve the correct asset for each route.
// ============================================
// PART 6: THE CHUNK NAME IS NOT THE ENTRY NAME
// ============================================
// If entryNames is set but chunkNames is not:
await esbuild.build({
entryPoints: ['src/app.js'],
outdir: 'dist',
splitting: true,
entryNames: '[name]-[hash]',
// chunkNames is not set. The chunk uses the default.
// Output: dist/app-XXXXXX.js (entry)
// dist/chunk-YYYYYY.js (chunk, unhashed by default?)
});
// The chunk filename does not inherit entryNames.
// chunkNames must be set explicitly.
// ============================================
// PART 7: THE ASSET HASH COLLISION
// ============================================
// Two assets with the same name in different directories:
// src/assets/logo.png
// src/other/logo.png
// ❌ assetNames: '[name]'
// Both become logo.png. The second overwrites the first.
// ✅ assetNames: '[name]-[hash]'
// Both become logo-XXXXXX.png and logo-YYYYYY.png.
// ✅ assetNames: '[dir]/[name]'
// Both preserve their relative directory.
// ============================================
// PART 8: VERIFICATION
// ============================================
// 1. Build with metafile: true.
// 2. Inspect result.metafile.outputs.
// 3. Verify each entry point has a hashed filename.
// 4. Verify the shared chunk has a hashed filename.
// 5. Verify the assets have hashed filenames.
// 6. Serve the output and verify the references resolve.
// ============================================
// PART 9: THE CACHE BUSTING CONTRACT
// ============================================
// A hashed filename is a promise: if the content changes,
// the filename changes. A server that sets
// Cache-Control: immutable, max-age=31536000
// on these files is safe because a new deployment
// produces new filenames, and the old files are never
// requested again.
//
// Without [hash], the filename is stable, and the server
// must use a short cache lifetime or revalidation.
// ============================================
// PART 10: THE TEMPLATE PLACEHOLDER SUMMARY
// ============================================
// entryNames: [dir] [name] [hash] [ext]
// chunkNames: [name] [hash] [ext]
// assetNames: [dir] [name] [hash] [ext]
//
// The absence of [dir] in chunkNames is intentional:
// chunks have no input directory to preserve.
The ten parts covered the three naming options, the directory preservation, the collision behavior, the metafile as a manifest, and the cache-busting contract. The through-line is that each output category has its own naming template, and the placeholders reflect what is meaningful for that category.
Quick Reference
Naming options
| Option | Applies to | Placeholders |
|---|---|---|
entryNames | Files built from entry points | [dir], [name], [hash], [ext] |
chunkNames | Shared chunks from --splitting | [name], [hash], [ext] |
assetNames | Files from the file loader | [dir], [name], [hash], [ext] |
Placeholder meanings
| Placeholder | Meaning |
|---|---|
[dir] | Relative path from the input file’s directory to outbase |
[name] | Filename without extension |
[hash] | Content hash of the output file |
[ext] | File extension including the dot |
Common configurations
| Goal | Configuration |
|---|---|
| Hash entry points, preserve dirs | entryNames: '[dir]/[name]-[hash]' |
| Hash chunks | chunkNames: '[name]-[hash]' |
| Hash assets, group by type | assetNames: 'assets/[ext]/[name]-[hash]' |
| Flat output, single entry | entryNames: '[name]-[hash]' |
Best Practices
✅ Do This:
// Preserve directories and hash entry points
entryNames: '[dir]/[name]-[hash]' // ✅
// Hash chunks explicitly
chunkNames: '[name]-[hash]' // ✅
// Hash assets to prevent collisions
assetNames: '[name]-[hash]' // ✅
// Use metafile to discover hashed filenames
metafile: true // ✅
❌ Don’t Do This:
// Expect chunkNames to inherit entryNames
entryNames: '[name]-[hash]' // chunkNames not set // ❌
// Use [dir] in chunkNames
chunkNames: '[dir]/[name]' // [dir] is not supported // ❌
// Omit [hash] for cache-busted assets
assetNames: '[name]' // collisions and stale caches // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Chunk filenames not hashed | chunkNames not configured | Set chunkNames: '[name]-[hash]' |
| Entry points collide | [dir] missing from entryNames | Add [dir] or use unique basenames |
| Asset overwritten | Two assets share a filename, no hash | Add [hash] to assetNames |
| Hashed files hard to reference | Filenames unpredictable at build time | Read the metafile to discover paths |
Real-World Examples
1. Hash entry points with directories
entryNames: '[dir]/[name]-[hash]'
2. Hash chunks
chunkNames: 'chunks/[name]-[hash]'
3. Hash assets by extension
assetNames: 'assets/[ext]/[name]-[hash]'
4. Single entry, flat output
entryNames: '[name]-[hash]'
5. Asset collision fix
assetNames: '[name]-[hash]' // two logo.png become unique
6. Metafile for manifest
metafile: true // result.metafile.outputs maps input to output
7. Custom build ID instead of hash
entryNames: '[name]-' + process.env.BUILD_ID
8. No hash for stable filenames
entryNames: '[name]' // requires short cache or revalidation
9. Chunk directory
chunkNames: 'chunks/[name]-[hash]'
10. Outbase for [dir] reference
outbase: 'src' // [dir] is relative to this
Visual
The Three Naming Categories
┌──────────────────────────────────────────────────────────────┐
│ WHICH OPTION NAMES WHICH FILE │
│ │
│ Source Output │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ src/pages/ │ │ dist/pages/ │ │
│ │ home/index.ts │──entryNames─▶│ home/index-XXX.js│ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ src/shared/ │ │ dist/chunks/ │ │
│ │ utils.ts │──chunkNames─▶│ utils-XXX.js │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ src/assets/ │ │ dist/assets/ │ │
│ │ logo.png │──assetNames─▶│ logo-XXX.png │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Each category has its own template. │
│ Configuring one does not configure the others. │
└──────────────────────────────────────────────────────────────┘
The [dir] Placeholder
┌──────────────────────────────────────────────────────────────┐
│ [dir] PRESERVES THE RELATIVE DIRECTORY │
│ │
│ entryPoints: ['src/pages/home/index.ts'] │
│ outbase: 'src' │
│ │
│ Input relative to outbase: pages/home/index.ts │
│ │
│ entryNames: '[dir]/[name]-[hash]' │
│ [dir] = pages/home │
│ [name] = index │
│ [hash] = ABC123 │
│ │
│ Output: dist/pages/home/index-ABC123.js │
│ │
│ Without [dir]: │
│ entryNames: '[name]-[hash]' │
│ Output: dist/index-ABC123.js │
│ Directory structure is lost. │
└──────────────────────────────────────────────────────────────┘
Chunk vs Entry Naming
┌──────────────────────────────────────────────────────────────┐
│ entryNames DOES NOT AFFECT CHUNKS │
│ │
│ entryNames: '[name]-[hash]' │
│ chunkNames: (not set) │
│ │
│ Entry point: │
│ ┌────────────────────┐ │
│ │ dist/app-XXXX.js │ ← entryNames applied │
│ └────────────────────┘ │
│ │
│ Shared chunk: │
│ ┌────────────────────┐ │
│ │ dist/chunk-YYYY.js │ ← default chunkNames │
│ │ (not hashed) │ │
│ └────────────────────┘ │
│ │
│ To hash chunks, set chunkNames explicitly. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
entryNames | Names files from entry points |
chunkNames | Names shared chunks from --splitting |
assetNames | Names files from the file loader |
[dir] | Relative path from input to outbase |
[name] | Filename without extension |
[hash] | Content hash |
[ext] | Extension including dot |
chunkNames [dir] | Not supported |
| Metafile | Maps input paths to output paths |
Key takeaways:
- The three naming options are independent.
entryNamescontrols entry point outputs,chunkNamescontrols split chunk outputs, andassetNamescontrolsfileloader outputs. Setting one does not set the others . [dir]preserves directory structure but only for entry points and assets. ThechunkNamestemplate does not support[dir]because chunks have no input directory to preserve. A project that wants hashed chunks must configurechunkNamesexplicitly .[hash]is the cache-busting mechanism. A hashed filename changes when the content changes, which allows long-lived immutable caching. Without[hash], the filename is stable and the server must use a shorter cache lifetime or revalidation .- The metafile is the manifest. Because hashed filenames are unpredictable at build time, a server that references the output files must read
result.metafile.outputsto discover the actual paths. esbuild does not rewrite HTML to point at the hashed names . [dir]prevents entry point collisions. Two entry points with the same basename in different directories collide without[dir]. With[hash]but no[dir], the filenames differ but the directory structure is lost. With[dir]and[hash], both the structure and the cache busting are preserved .
Remember: The naming template is not a cosmetic detail. It determines whether the output directory structure is navigable, whether two files collide, and whether the server can cache the files indefinitely. The three categories—entry points, chunks, and assets—each have their own rules and their own placeholders. The template that works for one category may not be valid for another. Configure each one deliberately, verify the output with the metafile, and serve the hashed names through a manifest that the server reads at startup.
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!