evanw/esbuild · error · Error
Invalid loader extension
Error message
Invalid loader extension: ${ext} What it means
The `loader` option is a map of file extension to loader name, serialized as `--loader:.EXT=NAME`. An '=' inside the extension breaks parsing, so esbuild rejects it at lib/shared/common.ts:335. Keys should be extensions like '.js', '.ts', '.png'.
Solutions
- Use a plain extension key without '=' (e.g. '.json', '.png').
- If you meant to set a loader by filename, use the per-entry-point loader via stdin/entry options or a plugin instead.
Example fix
// before
await build({ entryPoints: ['app.ts'], loader: { '.txt=data': 'text' } })
// after
await build({ entryPoints: ['app.ts'], loader: { '.txt': 'text' } }) Defensive patterns
Strategy: validation
Validate before calling
function assertLoaderKeys(map) {
for (const k of Object.keys(map ?? {})) {
if (!k.startsWith('.') || k.includes('=')) {
throw new Error(`loader key must be an extension like '.ts', got ${k}`)
}
}
}
assertLoaderKeys(options.loader) Prevention
- Use bare extensions ('.ts', '.png') as loader keys, not filenames or globs.
- Don't embed '=' in extension keys.
- For per-file loaders, use the stdin/entry-point loader option or a plugin.
When it happens
Trigger: Passing `loader: { '.js=x': 'jsx' }` or any loader key containing '='.
Common situations: Generated extension strings that embed '='; using a full filename or glob instead of a bare extension.
Related errors
- Invalid loader
- Expected in mangle cache to map to either a string or false
- Expected value for supported
- Expected value for to be a string, got instead
- Invalid banner file type
AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09).
Data as JSON: /api/errors/bb54a165d0afa507.
Report an issue: GitHub.
Appendix: source
Thrown at lib/shared/common.ts:335
flags.push(`--alias:${old}=${validateStringValue(alias[old], 'alias', old)}`)
}
}
if (banner) {
for (let type in banner) {
if (type.indexOf('=') >= 0) throw new Error(`Invalid banner file type: ${type}`)
flags.push(`--banner:${type}=${validateStringValue(banner[type], 'banner', type)}`)
}
}
if (footer) {
for (let type in footer) {
if (type.indexOf('=') >= 0) throw new Error(`Invalid footer file type: ${type}`)
flags.push(`--footer:${type}=${validateStringValue(footer[type], 'footer', type)}`)
}
}
if (inject) for (let path of inject) flags.push(`--inject:${validateStringValue(path, 'inject')}`)
if (loader) {
for (let ext in loader) {
if (ext.indexOf('=') >= 0) throw new Error(`Invalid loader extension: ${ext}`)
flags.push(`--loader:${ext}=${validateStringValue(loader[ext], 'loader', ext)}`)
}
}
if (outExtension) {
for (let ext in outExtension) {
if (ext.indexOf('=') >= 0) throw new Error(`Invalid out extension: ${ext}`)
flags.push(`--out-extension:${ext}=${validateStringValue(outExtension[ext], 'out extension', ext)}`)
}
}
if (entryPoints) {
if (Array.isArray(entryPoints)) {
for (let i = 0, n = entryPoints.length; i < n; i++) {
let entryPoint = entryPoints[i]
if (typeof entryPoint === 'object' && entryPoint !== null) {
let entryPointKeys: OptionKeys = Object.create(null)
let input = getFlag(entryPoint, entryPointKeys, 'in', mustBeString)
let output = getFlag(entryPoint, entryPointKeys, 'out', mustBeString)View on GitHub (pinned to f6058f8364)