Testing ECMAScript Modules with Jest
Fixing Jest's "Cannot use import statement outside a module" error by wiring up ESM support across package.json, TypeScript, and Jest config.
When testing ECMAScript Modules (ESM) using Jest, you might encounter the error:
SyntaxError: Cannot use import statement outside a module
This happens when Jest runs a file containing import as CommonJS. Jest provides experimental ESM support, but it must be enabled consistently across Node.js, TypeScript, and Jest.
Creating ESM Package
First, create a new ESM package with the following commands:
mkdir jest-esm && cd jest-esmnpm init -yInstall the necessary development dependencies:
npm i -D typescript jest @types/jest ts-node ts-jestAdd "type": "module" to the package.json.
Your package.json should look like this:
{ "name": "jest-esm", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "@types/jest": "^29.5.12", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" }}Configuring TypeScript
Generate a tsconfig.json file with the following command:
npx tsc --initUpdate the tsconfig.json file:
@@ -14 +14 @@- "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */+ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */@@ -28 +28 @@- "module": "commonjs", /* Specify what module code is generated. */+ "module": "es6", /* Specify what module code is generated. */@@ -30 +30 @@- // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */+ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */Configuring Jest
Run the following command to set up Jest configuration:
- Use Jest for the “test” script: Yes
- Use TypeScript for the configuration: Yes
- Test environment: jsdom
- Add coverage reports: No
- Provider for coverage: v8
- Automatically clear mock calls…: No
npm init jest@latestEdit the generated jest.config.ts to include:
// preset: undefined,preset: 'ts-jest',Supporting ESM
Update the test script in package.json to enable Node’s experimental VM modules:
"scripts": { "test": "jest" "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"}Add the extensionsToTreatAsEsm configuration in jest.config.ts to indicate which file types Jest should treat as ESM:
extensionsToTreatAsEsm: ['.ts'],To configure ESM in ts-jest, refer to the official documentation.
Update jest.config.ts with moduleNameMapper to handle .js extensions:
// moduleNameMapper: {},moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1',},Update the transform setting to support ESM:
// transform: undefined,transform: { '^.+\\.tsx?$': [ 'ts-jest', { useESM: true, }, ],},Creating an ESM Module
For demonstration, install the hast-util-from-html package:
npm i hast-util-from-htmlThe hast-util-from-html package is an ESM-only package. You can confirm this by examining node_modules/hast-util-from-html/index.js; its exports use ESM syntax:
/** * @typedef {import('hast-util-from-parse5')} DoNotTouchItRegistersData * * @typedef {import('./lib/index.js').ErrorCode} ErrorCode * @typedef {import('./lib/index.js').ErrorSeverity} ErrorSeverity * @typedef {import('./lib/index.js').OnError} OnError * @typedef {import('./lib/index.js').Options} Options */
export { fromHtml } from './lib/index.js';Create index.ts:
import { fromHtml } from 'hast-util-from-html';
export default function JestEsm(): void { const root = fromHtml( '<span><a href="https://github.com">GitHub</a></span>', { fragment: true } ); console.info(root);}Testing ESM
Create index.spec.ts:
import JestEsm from './index';
test('case1', () => { JestEsm();});Run the tests:
npm run testIf you see the error:
● Validation Error:
Test environment jest-environment-jsdom cannot be found. Make sure the testEnvironment configuration option points to an existing node module.
Configuration Documentation: https://jestjs.io/docs/configuration
As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sure to install it separately.Install the missing package:
npm i -D jest-environment-jsdomRun the tests again:
npm run testExpected output:
PASS ./index.spec.ts ✓ case1 (20 ms)
Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 1 sRan all test suites.Conclusion
Configuring package.json, TypeScript, and Jest together got a test importing a real ESM package passing instead of failing with SyntaxError: Cannot use import statement outside a module.
Getting to that passing test required changes across four different layers — package.json’s type field, the TypeScript module settings, Jest’s own config, and the Node invocation flag for experimental VM modules — which is a good illustration of why “just add type: module” alone doesn’t fix the original error.
The moduleNameMapper rule removes a .js suffix from relative imports so Jest can resolve the corresponding TypeScript source file. Because this workaround is specific to how the test environment resolves ESM-style imports, document why it exists before reusing it in another project.
Related posts
Sign in with Slack Using Cognito User Pools and OIDC
Federating Cognito user pools with Slack over OIDC and wiring "Sign in with Slack" into a Next.js app with Amplify.
Deploying FastAPI on AWS Lambda with Lambda Web Adapter
Containerizing a FastAPI backend and deploying it to a single Lambda function with Lambda Web Adapter and AWS CDK.
React Project Structure for Modularity and Reusability
Organize a React project into components, core, features, and shared directories with explicit dependency boundaries.
Angular Project Structure for Modularity and Reusability
Organize an Angular application into core, features, and shared directories with clear dependency boundaries.
Spying on Mock Object Properties in Jasmine
Working around Jasmine's "already been spied upon" error when spying on a mocked object's property with Object.getOwnPropertyDescriptor.
