Add the type
property to package.json
to ensure modules are supported:
{
"type": "module",
}
Ensure you are using latest jest
and ts-jest
; I have been testing using the following versions:
- "ts-jest": "^29.0.5"
- "jest": "^29.4.3"
Use esnext
module type; this is my basic tsconfig for building:
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"module": "esnext",
"resolveJsonModule": true,
"removeComments": true,
"outDir": "../dist",
"sourceMap": false,
"types": ["node", "jest"],
"strict": false,
"esModuleInterop": true
},
"include": [
"./**/*",
"./**/*.json"
],
"exclude": [
"tsconfig.json",
"tsconfig-build.json"
]
}
There is a fundamental difference to how you name and import your files when using ESM.
What you do will depend if you want to use:
- mostly esm, but have some common js files
- mostly use commonjs, but have some esm files
Basically, I leave all my files as .ts
and will use them all as ESM; and the handful of files that need to be treated as CommonJS will be named .cjs
(or .mts
).
The following from the Typescript Manual helps give more information determination of this.
The type field in package.json is nice because it allows us to continue using the .ts and .js file extensions which can be convenient; however, you will occasionally need to write a file that differs from what type specifies. You might also just prefer to always be explicit.
Node.js supports two extensions to help with this: .mjs and .cjs. .mjs files are always ES modules, and .cjs files are always CommonJS modules, and there’s no way to override these.
In turn, TypeScript supports two new source file extensions: .mts and .cts. When TypeScript emits these to JavaScript files, it will emit them to .mjs and .cjs respectively.
Furthermore, TypeScript also supports two new declaration file extensions: .d.mts and .d.cts. When TypeScript generates declaration files for .mts and .cts, their corresponding extensions will be .d.mts and .d.cts.
Using these extensions is entirely optional, but will often be useful even if you choose not to use them as part of your primary workflow.
Once this has been determined, the last thing you need to do is change your imports to reference the js
built files:
import { export } from './source.file.js'
This can be annoying, but once you have changed your first import, most editors (I use VSCode) will auto add any further imports with the extension included. However it is something you need to keep a track of when you create a new file and add the first import.
Any files which have to be treated as CommonJS files should be renamed .cjs
; common files will be eslintrc.cjs
, or any integration startup scripts like pre-test-integration.cjs
.
Change your test start instruction in package.json
to set the experimental-vm-modules
support which is required as at v18.8.0
of node:
"scripts": {
"pretest": "npm run lint && tsc -p tsconfig.json --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose --coverage --config jest-unit.config.json"
}
As we are using ts-jest
as our transformer; the setup config, either as a module.export
or via a .json
file (as shown below), needs some additional properties and changes to the structure of the setup properties.
As you can see, we need to include an additional moduleNameMapper
so that imports work; a new extensionsToTreatAsEsm
property and to change the structure of transform
to include the new .cjs
files but also allow us to tell ts-jest to emit ESModule compatible code.
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
"extensionsToTreatAsEsm": [".ts"],
"transform": {
"^.+\\.(mt|t|cj|j)s$": [
"ts-jest",
{
"useESM": true
}
]
},
One last thing is we now need to import the jest global variable by adding to the top of our test files:
import { jest } from '@jest/globals';
Anyone who arrives here looking for a means to set this up for Projen, check out my own Gist for an example
.projenrc.ts
with these settings and other changes to make it all work together.