Fix: Jest Cannot Transform ES Modules — SyntaxError: Cannot use import statement
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Jest failing with 'Cannot use import statement outside a module' — configuring Babel transforms, using experimental VM modules, migrating to Vitest, and handling ESM-only packages.
The Error
Jest fails when running tests that use ES module syntax:
SyntaxError: Cannot use import statement outside a module
> 1 | import { render } from '@testing-library/react';
| ^
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:...)Or when importing a package that ships as ESM:
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies
use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
/node_modules/your-package/index.js:1
export default function ...
^^^^^^
SyntaxError: Unexpected token 'export'Or with TypeScript:
● Test suite failed to run
SyntaxError: /app/src/__tests__/utils.test.ts: Missing semicolon. (1:7)
> 1 | import { parseDate } from '../utils';
| ^Why This Happens
Jest runs in Node.js and by default uses CommonJS (require/module.exports). When your code or a dependency uses ES module syntax (import/export), Jest cannot parse it without transformation. This is a fundamental architecture issue: Jest was designed before ESM became standard, and its module loading, mocking, and snapshot systems all assume CommonJS semantics.
The transform pipeline in Jest works by intercepting require() calls and running the file through a transformer (like babel-jest or ts-jest) before executing it. By default, Jest skips node_modules to keep tests fast. This worked when every npm package shipped CommonJS, but the ecosystem has shifted. Packages like node-fetch v3, chalk v5, nanoid v4, and uuid v9 now ship ESM-only builds. When Jest hits an import or export statement in an untransformed file, Node’s CommonJS loader throws a SyntaxError.
The specific causes are:
- No Babel transform configured — Jest needs
babel-jestwith@babel/preset-env(or@babel/preset-typescript) to transform modern JS/TS to CommonJS before running tests. - ESM-only npm package — some packages ship only as ES modules. Jest’s default CommonJS runner can’t import them without special handling.
- TypeScript without
ts-jest— TypeScript files need transformation. Withoutts-jestor Babel’s TypeScript preset, Jest can’t process.tsfiles. "type": "module"inpackage.json— this makes all.jsfiles in the project ES modules. Jest’s default runner doesn’t support this without extra configuration.- Missing
transformIgnorePatternsoverride — Jest ignoresnode_modulesby default. ESM-only packages innode_modulesneed to be transformed but aren’t.
How Other Tools Handle This
Jest’s ESM problem stems from its CommonJS-first architecture. Other test runners take fundamentally different approaches to module loading, and understanding them helps you decide whether to fix Jest’s configuration or switch tools entirely.
Vitest is ESM-native from the ground up. It uses Vite’s transform pipeline, which treats every file as an ES module and transforms it on the fly using esbuild (for TypeScript) or SWC. There is no transformIgnorePatterns equivalent because Vitest transforms everything, including node_modules, using Vite’s dependency pre-bundling. ESM-only packages work without any configuration. Vitest also replaces Jest’s custom module resolution with Vite’s resolver, which handles exports field in package.json, conditional exports, and .js extension mapping for TypeScript files automatically. The migration cost is low: Vitest’s API is intentionally Jest-compatible (describe, it, expect), with vi.fn() replacing jest.fn() and vi.mock() replacing jest.mock().
Mocha + ts-node takes the approach of using Node’s own module loader hooks. With ts-node/esm registered as a loader (node --loader ts-node/esm), Node itself transforms TypeScript and handles ESM imports before Mocha sees them. This avoids the “transform pipeline” model entirely. The downside is that Node’s --loader API has gone through multiple breaking changes, and Mocha has no built-in equivalent of transformIgnorePatterns. ESM-only packages work because Node’s ESM loader handles them natively, but CJS-ESM interop issues (like __dirname not existing in ESM) become the test author’s problem.
Node’s built-in test runner (node:test, stable since Node 20) runs tests using Node’s native module system. ESM works because Node handles it. TypeScript requires either a separate compilation step (tsc before running tests) or --loader ts-node/esm. There is no transform pipeline, no transformIgnorePatterns, and no special handling for node_modules. The trade-off is that Node’s test runner has a minimal API: no built-in mocking of modules (you use node:mock or external libraries), no snapshot testing, and no watch mode equivalent to Jest’s file-watching.
Bun’s test runner (bun test) is the most opinionated. Bun natively understands TypeScript, JSX, ESM, and CommonJS without any configuration. There is no Babel, no transform step, and no transformIgnorePatterns. Bun’s module resolver handles exports fields, TypeScript path aliases, and mixed CJS/ESM packages automatically. The Jest compatibility layer (bun:test) supports describe, it, expect, mock, and snapshots. The limitation is that Bun’s Node.js compatibility is not 100%, so tests that depend on specific Node.js APIs (like vm, worker_threads, or certain fs behaviors) may not work identically.
SWC with Jest (@swc/jest) replaces Babel as Jest’s transform layer. SWC is written in Rust and transforms TypeScript and JSX 20-70x faster than Babel. The configuration is simpler (a single .swcrc file), and it handles ESM-to-CJS transformation automatically. The transformIgnorePatterns problem remains because SWC is still running inside Jest’s CJS-first architecture, but the speed improvement often makes the fix (listing ESM packages in the ignore pattern negation) more tolerable. For large test suites where Jest is otherwise working well, swapping babel-jest for @swc/jest is the lowest-cost improvement.
Fix 1: Add Babel Transform (Most Common Fix)
For JavaScript projects, configure Jest with Babel to transform ESM to CommonJS:
Install dependencies:
npm install -D babel-jest @babel/core @babel/preset-envCreate babel.config.js (not .babelrc — Jest requires a root-level Babel config):
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: { node: 'current' }, // Transform for the current Node.js version
}],
],
};For React + JSX:
npm install -D @babel/preset-react// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
],
};Jest configuration:
// jest.config.js
module.exports = {
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
};babel-jest is installed automatically with Jest — you don’t need to install it separately.
Fix 2: Configure TypeScript with ts-jest or Babel
Option A: ts-jest (recommended for TypeScript projects — preserves type checking):
npm install -D ts-jest @types/jest// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node', // or 'jsdom' for browser-like tests
};// tsconfig.json — ensure CommonJS output for Jest
{
"compilerOptions": {
"module": "CommonJS", // Jest needs CommonJS
"target": "ES2020"
}
}Option B: Babel with TypeScript preset (faster, no type checking during tests):
npm install -D @babel/preset-typescript// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};// jest.config.js
module.exports = {
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
};Option C: SWC (fastest transform, minimal configuration):
npm install -D @swc/core @swc/jest// jest.config.js
module.exports = {
transform: {
'^.+\\.[jt]sx?$': ['@swc/jest'],
},
};ts-jest vs Babel vs SWC:
ts-jestis slowest but catches type errors in tests. Babel is moderate speed but silently ignores type errors. SWC is fastest (Rust-based) and also ignores type errors. For CI that already runstsc --noEmit, SWC or Babel is usually the better choice for test speed.
Fix 3: Fix ESM-Only Packages in node_modules
Jest ignores node_modules by default (transformIgnorePatterns). When an ESM-only package is in node_modules, Jest can’t process it:
// jest.config.js
module.exports = {
// Override transformIgnorePatterns to transform specific ESM packages
transformIgnorePatterns: [
// Transform everything EXCEPT true CJS packages
'node_modules/(?!(node-fetch|nanoid|uuid|chalk|your-esm-package)/)',
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// List ESM-only packages here (pipe-separated)
],
};Finding which packages are ESM-only:
# Check if a package uses "exports" with ESM
cat node_modules/node-fetch/package.json | grep -A 5 '"exports"'
# If you see "import" in exports, it's ESM
# Or check for "type": "module"
cat node_modules/nanoid/package.json | grep '"type"'
# "type": "module" → ESM onlyAlternative: use a CommonJS-compatible alternative:
| ESM-only package | CommonJS alternative |
|---|---|
node-fetch v3 | node-fetch v2 or built-in fetch (Node 18+) |
nanoid v4 | nanoid v3 or uuid v8 |
chalk v5 | chalk v4 |
npm install node-fetch@2 # Install the CommonJS versionFix 4: Use Jest with Native ESM Support (Experimental)
For projects that are fully ESM ("type": "module" in package.json), use Jest’s experimental ESM support:
// package.json
{
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest"
}
}// jest.config.js (must also be ESM — use .js with type:module or rename to jest.config.mjs)
export default {
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', // Strip .js extension for imports
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
};Caveats of experimental ESM mode:
- Requires
--experimental-vm-modulesflag - Some Jest features (mocking,
jest.mock) work differently - Performance is generally worse than CJS mode
- Many examples and guides assume CJS mode
Fix 5: Migrate to Vitest (Recommended for Vite Projects)
If you’re using Vite (or any bundler with native ESM support), migrating to Vitest eliminates all Jest ESM configuration pain. Vitest uses Vite’s transform pipeline and supports ESM natively:
npm install -D vitest @vitest/ui// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom', // For browser-like tests
globals: true, // No need to import describe/it/expect
setupFiles: './src/test/setup.ts',
},
});Vitest is API-compatible with Jest — most Jest tests work without changes:
// Jest test
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from '@jest/globals';
// Vitest test — same code, works out of the box
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
// Or with globals: true — no imports needed at allMigration checklist:
- Replace
jest.fn()withvi.fn() - Replace
jest.mock()withvi.mock() - Replace
jest.spyOn()withvi.spyOn() - Replace
jest.useFakeTimers()withvi.useFakeTimers() - Update
jest.config.jstovitest.config.ts
Fix 6: Fix moduleNameMapper for ESM Imports
TypeScript projects often use path aliases or .js extensions in imports (required by the ESM spec but weird for TypeScript). Map these to actual files:
// TypeScript source with .js extension (ESM requirement)
import { formatDate } from './utils.js';
// But utils.js doesn't exist — utils.ts does// jest.config.js — map .js imports to actual .ts files
module.exports = {
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', // Remove .js extension
'^@/(.*)$': '<rootDir>/src/$1', // Path alias @/ → src/
'^~/(.*)$': '<rootDir>/src/$1', // Path alias ~/ → src/
},
};For CSS/image imports in component tests:
module.exports = {
moduleNameMapper: {
'\\.(css|less|scss|sass)$': '<rootDir>/__mocks__/styleMock.js',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
};// __mocks__/styleMock.js
module.exports = {};
// __mocks__/fileMock.js
module.exports = 'test-file-stub';Still Not Working?
Check package.json for "type": "module":
// If this exists and you haven't configured experimental ESM:
{ "type": "module" }Remove it or set it to "commonjs" if you don’t need full ESM mode. Jest’s CommonJS mode is more stable and widely supported.
Verify Babel config is at the project root. Jest requires babel.config.js (not .babelrc) for transformation of files outside the test directory:
ls babel.config.*
# Must be at the same level as package.jsonClear Jest’s cache — after changing transform config, old cached transforms may still run:
jest --clearCache
# Then run tests again
jestRun Jest with verbose output to see which transform is applied:
jest --verbose --showConfig 2>&1 | grep transformCheck for nested package.json files with conflicting "type" fields:
# A package.json in a subdirectory with "type": "module" can override the root
find . -name "package.json" -not -path "*/node_modules/*" -exec grep -l '"type"' {} \;Node resolves the "type" field by walking up from the file being loaded to the nearest package.json. A subdirectory with its own package.json containing "type": "module" makes all .js files in that directory ESM, regardless of the root configuration.
Verify the transform actually runs on the failing file:
# Add --no-cache to force re-transformation
jest --no-cache --verbose 2>&1 | grep -A 2 "SyntaxError"
# Check if the file path is inside node_modules — if so, it's being skipped by transformIgnorePatternsFor related testing issues, see Fix: Vitest Setup Not Working, Fix: Jest Cannot Find Module, Fix: Jest Mock Not Working, and Fix: Bun Test Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Node.js Test Runner Not Working — node --test, TypeScript, Mocks, Coverage, and Watch Mode
How to fix Node.js built-in test runner errors — node --test not finding files, ESM vs CJS imports, TypeScript with --experimental-strip-types, mock.method isolation, coverage reporting, and watch mode setup.
Fix: Bun Test Not Working — Module Mocking, DOM Setup, Coverage, and Watch Mode
How to fix Bun test runner issues — mock.module not isolating, happy-dom setup for DOM tests, --coverage missing files, timer mocks, snapshot updates, TypeScript path aliases, and preload files.
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves
How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.