Testing ECMAScript Modules with Jest

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.

Takahiro Iwasa
5 min read

When testing ECMAScript Modules (ESM) using Jest, you might encounter the error:

SyntaxError: Cannot use import statement outside a module

This issue arises because the target modules use the import keyword. Fortunately, Jest provides experimental support for ESM to resolve such errors.

Creating ESM Package

First, create a new ESM package with the following commands:

Terminal window
mkdir jest-esm && cd jest-esm
npm init -y

Install the necessary development dependencies:

Terminal window
npm i -D typescript jest @types/jest ts-node ts-jest

Add "type": "module" to the package.json.

Your package.json should look like this:

package.json
{
"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:

Terminal window
npx tsc --init

Update 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
Terminal window
npm init jest@latest

Edit 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 ESM

For demonstration, install the hast-util-from-html package:

Terminal window
npm i hast-util-from-html

The module hast-util-from-html is an ECMAScript Module (ESM) and uses the export keyword for its exports. This can be confirmed by examining the file located at node_modules/hast-util-from-html/index.js. Here’s a snippet of the module’s export structure:

/**
* @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:

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:

index.spec.ts
import JestEsm from './index';
test('case1', () => {
JestEsm();
});

Run the tests:

Terminal window
npm run test

If 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:

Terminal window
npm i -D jest-environment-jsdom

Run the tests again:

Terminal window
npm run test

Expected output:

PASS ./index.spec.ts
✓ case1 (20 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1 s
Ran 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 rewrite for .js extensions is easy to forget and just as easy to misdiagnose later, since TypeScript source imports without an extension while compiled ESM output requires one; if that mapping is ever removed, the failure mode won’t look anything like the original SyntaxError, so it’s worth documenting why it’s there.

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.