Skip to content

Instantly share code, notes, and snippets.

@524c
Forked from danpetitt/esmodules.md
Created September 14, 2023 14:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 524c/47cc70854c0249d0acf7c5bccd94ce54 to your computer and use it in GitHub Desktop.
Save 524c/47cc70854c0249d0acf7c5bccd94ce54 to your computer and use it in GitHub Desktop.
Typescript, Jest and ECMAScript Modules

Typescript, Jest and ECMAScript Modules (ESM)

Package.json

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"

tsconfig

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"
  ]
}

Filenames and Imports

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.

Jest

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"
  }

Transformation

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';

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment