How to Fix Webpack 'Module parse failed: Unexpected token' for .mjs Files in node_modules
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins
TL;DR
- What broke: Webpack hit an
.mjsfile innode_modulesthat uses ES Module syntax (import/export). No loader rule covers.mjs, so the default parser bails withUnexpected token. - How to fix it: Add an explicit
module.rulesentry for.mjsfiles that either setstype: 'javascript/auto'or routes throughbabel-loaderwith@babel/preset-env. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
webpack.config.jsand get the patched rules block instantly, no data ever leaves your browser.
The Incident (What Does the Error Mean?)
Raw error output from a failing CI build:
ERROR in ./node_modules/some-esm-package/index.mjs 3:0
Module parse failed: Unexpected token (3:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
> export const foo = () => {};
|
@ ./src/app.js 1:0-38
Immediate consequence: The entire Webpack compilation halts. Nothing is emitted. Your CI pipeline fails at the build step, blocking deploys. If this surfaces in a monorepo, every downstream app depending on the shared bundle is also dead.
The root cause is deterministic: Webpack 4 and early Webpack 5 configs commonly exclude node_modules from babel-loader via exclude: /node_modules/. That blanket exclusion made sense in the CommonJS era. It breaks the moment any dependency ships native ESM (.mjs or "type": "module" in its package.json).
The Attack Vector / Blast Radius
This isn't a security vulnerability — it's a build-time hard failure with a wide blast radius:
- Dependency upgrades are now landmines. Any
npm updatethat bumps a package to an ESM-only release will detonate your build. No warning, no deprecation — just a broken pipeline. - Transitive dependencies are invisible. You didn't install the offending
.mjsfile directly. It's three levels deep in your lock file.npm lswon't surface it without deliberate auditing. - Affects all environments equally. Local dev (
webpack-dev-server), staging builds, and production bundles all share the same config. The failure is not environment-specific — it's total. - Monorepo multiplier. In an Nx or Turborepo setup, one broken shared library config propagates the failure to every app in the workspace simultaneously.
- Tree-shaking assumptions break. Developers sometimes "fix" this by downgrading the offending package to its last CJS release. That workaround silently disables tree-shaking benefits and can balloon bundle size by 30–200% depending on the package.
How to Fix It (The Solution)
Basic Fix — type: 'javascript/auto' (Webpack 5)
Tell Webpack to treat .mjs files as standard JavaScript modules without enforcing strict ESM semantics:
// webpack.config.js
module.exports = {
module: {
rules: [
+ {
+ test: /\.mjs$/,
+ include: /node_modules/,
+ type: 'javascript/auto',
+ },
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
};
type: 'javascript/auto' disables Webpack 5's automatic module type detection for matched files and lets the file parse without enforcing strict ESM import/export validation at the module boundary level.
Enterprise Best Practice — Scoped babel-loader with sourceType: 'unambiguous'
For teams that need transpilation of ESM node_modules (e.g., targeting IE11 or older Node runtimes, or using Babel plugins for instrumentation/coverage):
// webpack.config.js
module.exports = {
module: {
rules: [
{
- test: /\.(js|jsx)$/,
- exclude: /node_modules/,
+ test: /\.(js|jsx|mjs)$/,
+ exclude: {
+ and: [/node_modules/],
+ not: [
+ // Whitelist specific ESM packages that need transpilation
+ /node_modules[\\/](some-esm-package|another-esm-lib)[\\/]/,
+ ],
+ },
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }],
],
+ // Critical: lets Babel auto-detect CJS vs ESM per file
+ sourceType: 'unambiguous',
},
},
},
],
},
};
Why sourceType: 'unambiguous'? Without it, Babel assumes all files are scripts (CJS). When it hits an import statement in an .mjs file, it throws its own parse error on top of Webpack's. unambiguous tells Babel to sniff the file and select module or script mode automatically.
Why whitelist instead of blanket-include node_modules? Transpiling all of node_modules through Babel adds 30–90 seconds to cold build times in large projects. Whitelist only the offending packages. Use npm ls --all | grep <package> to identify the full transitive chain.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Fail fast with a pre-build ESM audit script
Add this to your package.json scripts and run it in CI before webpack build:
# Detect .mjs files in node_modules not covered by webpack rules
npx webpack --config webpack.config.js --dry-run 2>&1 | grep 'Module parse failed'
If exit code is non-zero, block the pipeline.
2. Lock dependency ESM surface with syncpack
npx syncpack list-mismatches
Enforce version ranges in CI to prevent silent ESM-only upgrades from slipping through npm update.
3. Add a Jest/Vitest config parity check
Your test runner needs the same .mjs handling. Add transformIgnorePatterns override:
// jest.config.js
module.exports = {
- transformIgnorePatterns: ['/node_modules/'],
+ transformIgnorePatterns: [
+ '/node_modules/(?!(some-esm-package|another-esm-lib)/)',
+ ],
};
Mismatches between Webpack and Jest configs cause "works in dev, fails in test" ghost bugs.
4. Enforce via ESLint import/no-unresolved + eslint-import-resolver-webpack
Wire your ESLint resolver to your Webpack config. If Webpack can't resolve a module, ESLint flags it at lint time — before CI even reaches the build step.
npm install --save-dev eslint-import-resolver-webpack
// .eslintrc
{
"settings": {
"import/resolver": {
"webpack": { "config": "webpack.config.js" }
}
}
}
This turns a runtime build failure into a pre-commit lint error.