Dissecting the hell that is Jest setup with ESM and Typescript

Jen Chan - Dec 31 '23 - - Dev Community

Jest: Not So Delightful Anymore

Several months ago, I had the hardest time setting up Jest with a React 18 Typescript project as part of a spike to help teams who wanted to use Jest to test web components that were built with ESM dependencies.

As I polled my peers working on React products, many mentioned they had moved over to Vitest.

The outlook for Jest leading up to this moment hasn't been great. In 2022, one of Jest's OSS maintainers mentioned "no one at Facebook... worked on Jest for years". The project was subsequently transferred to the OpenJS foundation for maintenance and development has slowly fizzled since.

As of December 2023, Jest support for esmodules is still experimental due to its unfortunate reliance on node's vm module for test isolation.

If you discover yourself at a large company where tech isn't a core competency, you might realize there's a world where many revenue-generating products are still chugging along, one-Jenga-move-from-collapse, on ancient versions of Angular and React. A significant amount of time working as a dev at such a company will be spent helping teams figure out how to integrate extremely outdated frameworks and tools with modern web things.

If I'm appropriately tactful, I might eventually persuade them to migrate and upgrade their already end-of-life-d framework if they realize the diminishing returns on clinging to legacy technical choices that only compounds in friction to new feature delivery...

More often, organizational politics make it such that you have to just make something irrelevantly old work with something rather new (and possibly immature) because upgrading or changing tools isn't an imaginable option for said team.

Investigating how last 2 versions of Create React App (deprecated) and Jest could seamlessly import and test ESM web component library was the laughable situation I found myself in.

Oh yeah here's the sample StackBlitz if you don't feel like reading.

Overcoming Configuration Hell

Rotating through some hell combinations of ts-jest, ts-node, and babel plugins as recommended by different official documentation sources, I rehashed the following to get it working:

  1. Use the node --experimental-vm-modules when running the jest as part of your package.json test script due to Jest's API relying on a node vm API that's still in experimental stage for esm scripts.
// package.json
"scripts": {
  "test": "node --experimental-vm-modules node_modules/.bin/jest"
}
Enter fullscreen mode Exit fullscreen mode
  1. We have to tell Node our project uses ESM, so we add "type": "module" to package.json just to support ESM import statements.

This leads to a cascade of necessary changes to turn a CommonJS project to ESM:

  • changing all CommonJS files with requires("module-name") statements to import x from 'module-name'
  • changing module.exports to export default {}
  • change any .js file extensions to .cjs or ESM config files to .mts.

Should be fine right? ...Not if you already have a variety of components and views importing from CJS and ESM dependencies.

At this point you have to decide whether you will isolate the folders that use ESM from the rest of the project to create a different package which can be imported into your CJS project, or if you will just convert the entire project to ESM.

  1. Surprise! Whenever you have package.json type set to module, you'll encounter many errors:
  ReferenceError: module is not defined in ES module scope
  This file is being treated as an ES module because it has a '.js' file extension and '/Users/username/projectname/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
Enter fullscreen mode Exit fullscreen mode

This next one is easy; just rename jest.config.js to jest.config.cjs.

  1. Add Typescript support.

Install ts-node and ts-jest to help with the execution and transformation of .ts files to .js files before running tests.

npx npx ts-jest config:init creates a jest.config.js file which you can configure to your liking.

// jest.config.cjs
module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  transformIgnorePatterns: ['node_modules'],
  testMatch: ['**/*.test.ts'],
};
Enter fullscreen mode Exit fullscreen mode

Using ts-jest and the transform property in jest.config.ts will help with recognizing .ts or .tsx test files.

But don't use the transform property if you're also using a ts-jest preset! (See the module writer's disclaimer on that)

  1. But wait, there's more! Our test-runner complains about the import/export syntax:
  SyntaxError: Cannot use import statement outside a module
Enter fullscreen mode Exit fullscreen mode

Jest (with some support from Babel) transforms all files to CommonJS before running tests; it doesn't support import/export statements from ES2015.

This is especially infuriating if you import a component or util function that has a direct dependency on an ESM export.
You literally will be blocked on running Jest tests on that particular component. (Even if you already configured your jest config to exclude node_modules).

  1. If you by now, have decided to convert the entire project to ESM, you'll need to use babel to transform all .js files to .cjs files before running tests.

If you haven't already, you'll want to use a babel.config.js or babel.config.json to help with recognizing React .jsx files and transforming them to .js files before running tests.

Our friend is @babel/preset-env

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      '@babel/preset-react'
    ]
  ]
};
Enter fullscreen mode Exit fullscreen mode

I've personally found babel/preset-typescript to have no impact on Typescript transformation.

Side note: the maintenance-abandoned nature of CRA means installing @babel/plugin-proposal-private-property-in-object as a devDependency or plugin to be helpful for reducing console noise.

Summary

At the end of the day, you catch yourself needing to decide when linting, transforming need to happen before tests run. I settled on:

  • typechecking it first with tsc , OR using ts-jest to ensure the types are correct as tests run.
  • transpiling with babel to CJS before running tests

If all else fails, I also had some success downgrading to Jest 27.x or 28.x instead of bashing my head on bloated config and confusing doc on 29.x.

Related Reading

BigMan73. "Answer: Jest Typescript with ES Module in node_modules error - Must use import to load ES Module", StackOverflow. Jan 4, 2023

r/node. "Jest Not Getting Support in Backend Node?"

"Jest Typescript with ES Module in node_modules error - Must use import to load ES Module", StackOverflow,

SimenB. "Support ESM versions of all pluggable modules" Mar 7, 2021

Sindre Sorhus, "Pure ESM package", 2023

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .