jest: Jest fails to load jest.config.ts in a ESM project using ts-node 10

šŸ› Bug Report

In a project using TypeScript, Jest and setup as ESM (the output of the transpiler is ESM so Node will run ESM instead of CJS) JEST is failing with ts-node 10 but works with ts-node 9.

Error:

Error: Jest: Failed to parse the TypeScript config file /projects/ts-jest-ts-node-10/jest.config.ts
  Error: Must use import to load ES Module: /projects/ts-jest-ts-node-10/jest.config.ts
require() of ES modules is not supported.
require() of /projects/ts-jest-ts-node-10/jest.config.ts from /projects/ts-jest-ts-node-10/node_modules/ts-node/dist/index.js is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules.
Instead change the requiring code to use import(), or remove "type": "module" from /projects/ts-jest-ts-node-10/package.json.

    at readConfigFileAndSetRootDir (/projects/ts-jest-ts-node-10/node_modules/jest-config/build/readConfigFileAndSetRootDir.js:118:13)
    at readConfig (/projects/ts-jest-ts-node-10/node_modules/jest-config/build/index.js:216:18)
    at readConfigs (/projects/ts-jest-ts-node-10/node_modules/jest-config/build/index.js:405:26)
    at runCLI (/projects/ts-jest-ts-node-10/node_modules/@jest/core/build/cli/index.js:220:59)
    at Object.run (/projects/ts-jest-ts-node-10/node_modules/jest-cli/build/cli/index.js:163:37)

error Command failed with exit code 1

To Reproduce

Steps to reproduce the behavior:

  • Create a new folder
  • Run npm init -y
  • Run npm i -D ts-node typescript jest
  • Add "type": "module" to your package.json
  • Create a jest.config.ts file with the content below:
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
    verbose: true
};

export default config;
  • Run npx jest

Expected behavior

The config file should be loaded.

Link to repl or repo (highly encouraged)

I have reproduced the issue in a StackBlitz:

In order to run the code please run npm test in the console and the error above will be raised.

Both projects are identical aside from the ts-node version.

Keep in mind that this project is using StackBlitz WebContainers and Turbo package manager (which is not NPM, though it have an alias) if you want to tweak my example but not go to learn about specifics about Turbo or WebContainers it may be better to just clone the repo.

envinfo

This is my local environment:

  System:
    OS: macOS 11.3.1
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Binaries:
    Node: 16.2.0 - ~/.nvm/versions/node/v16.2.0/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v16.2.0/bin/yarn
    npm: 7.14.0 - ~/.nvm/versions/node/v16.2.0/bin/npm
  npmPackages:
    jest: ^27.0.1 => 27.0.1 

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 36
  • Comments: 62 (43 by maintainers)

Commits related to this issue

Most upvoted comments

Workaround for anyone running into this with ESM + TypeScript + Jest v27 (until this issue is addressed):

  1. Change jest.config.ts to jest.config.cjs
  2. Rewrite the file contents to CommonJS + JSDoc

@cspotcode So if I understand the release notes and docs correctly, the following configuration would allow loading an ESM jest.config.ts like the one below?

tsconfig.json

{
  "ts-node": {
    "moduleTypes": {
      "jest.config.ts": "cjs"
    }
  },
  "compilerOptions": {
    "module": "es2020",
    "target": "es2020"
  }
}

jest.config.ts

import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  moduleNameMapper: {
    '^(.*)\\.js$': '$1',
  },
  testEnvironment: 'jest-environment-node',
  transformIgnorePatterns: [
    'node_modules/(?!aggregate-error|clean-stack|escape-string-regexp|indent-string|p-map)',
  ],
};

export default config;

Hm, seems like I donā€™t even need to configure moduleTypes at all, ts-node@10.1.0 just automatically works for jest.config.ts šŸ¤” šŸ™Œ

See this for more details: https://github.com/TypeStrong/ts-node/discussions/1390#discussioncomment-988597


Edit: Ah, I think there was something wrong with the version that I had, now that I upgraded, I needed to specify this configuration in the tsconfig.json:

{
  "ts-node": {
    "moduleTypes": {
      "jest.config.ts": "cjs"
    }
  }
}

But why not just rename it to

Workaround for anyone running into this with ESM + TypeScript + Jest v27 (until this issue is addressed):

  1. Change jest.config.ts to jest.config.cjs
  2. Rewrite the file contents to CommonJS + JSDoc

But why not just change it to jest.config.js ?

Iā€™m still facing this issue. It happens in a monorepo setup, when I launch tests on a sub package. The tests run fine when running from the root package.

Please find a reproduction on this repository

ts-node v10.1.0 adds a new moduleTypes configuration which can be used to support this use-case.

Hi, Iā€™m the ts-node maintainer and someone pointed this out on our issue tracker as well. I can explain whatā€™s happening here, and I want to share my proposed solution to see if the jest team agrees.

In node, if you try to require() a .js file in an ESM package (package.json "type": "module") node will throw ERR_REQUIRE_ESM Must use import to load ES Module

ts-node 10 aligns with nodeā€™s behavior, so if you try to require() a .ts file in an ESM package, youā€™ll get the same error, because .ts corresponds to .js. (Release notes mention this under ERR_REQUIRE_ESM)

I see that when jest installs ts-node, it overrides compilerOptions.module = commonjs. I believe that jest would like to have a mode for ts-node that forces commonjs, so that your jest.config.ts is executed as commonjs, ignoring nodeā€™s ESM behavior.

This could be exposed as an API flag, used like this:

registerer = require('ts-node').register({
  compilerOptions: {
    module: 'CommonJS',
    allowCommonjsInEsmPackage: true // <-- name of this option TBD; this is an example.  Name could be allowLoadEsmAsCommonjs or allowRequireEsm or something like that
  },
});

https://github.com/facebook/jest/blob/00888027257e5a751ffb7002805248b1fc758681/packages/jest-config/src/readConfigFileAndSetRootDir.ts#L84-L91

If it were possible to programmatically install --loaders, I would suggest that we allow jest.config.ts to be executed as ESM, but I donā€™t think that would be ergonomic.

Do you think itā€™s important to support when someone is using --loader ts-node/esm? I donā€™t think so, since itā€™s only for the config file. But Iā€™m not sure if projects commonly import other ESM files from their jest.config.*

I see that jest is already hardcoding compilerOptions: module: commonjs https://github.com/facebook/jest/blob/95f49691d3472d8187640f1b703209b93c2bcecc/packages/jest-config/src/readConfigFileAndSetRootDir.ts#L90-L94

Based on that code, it sounds like jest wants jest.config.ts ā€“ and any other TS files loaded by it ā€“ to always be treated like a CommonJS module. Itā€™s forcing compilation to emit CommonJS. Is that correct?

If so, should jest be passing a moduleTypes config, too?

Yeah, maybe Jest can just set all of these ts-node options internally? So that there is no need to configure ts-node when you want to use it with Jest?

For what itā€™s worth, ts-node has an swc integration built-in. Also, once TS 4.5 is stable, youā€™ll be able to use the new cts file extension for your jest config, which will eliminate the need for moduleTypes.

There are plans to put the swc integration under a much terser flag, so youā€™ll be able to do this in tsconfig.json, combined with the new cts file extension:

{
//....
"ts-node": {
    "swc": true
  }
}

Just a heads up for anyone coming across this issue:

You can solve this problem by adding this to your tsconfig.json file:

{
//....
"ts-node": {
    "moduleTypes": {
      "jest.config.ts": "cjs"
    }
  }
}

Iā€™ve thought about this for quite some time and Iā€™m pretty sure itā€™s quite difficult to implement support on the Jest side. The best solution would be one where the user does not need to modify their tsconfig.json, as I find that fairly intrusive. Using ts-node requires the end user sets module: "esnext" in your tsconfig.json when you try to implement support in Jest. Perhaps using a different tool ( like a transpiler such as SWC ) that doesnā€™t do type-checking. would be better here? The user can do type-checking themselves, and it makes it easier to import the file. Just transpile, write to a temporary file in node_modules, require that, then delete the file.

If this is an approach that any of the maintainers like, Iā€™d be happy to open a PR.

Let me know šŸ˜Š

Workaround for anyone running into this with ESM + TypeScript + Jest v27 (until this issue is addressed):

  1. Change jest.config.ts to jest.config.cjs
  2. Rewrite the file contents to CommonJS + JSDoc

This worked to me, i need to export the file as a module, just like that: export.modules = jestConfigs Before i do that i wasnā€™t able to access the CLI jest commands

Or does the moduleTypes config mean that you need to write CommonJS in your TypeScript config files? (šŸ¤” this seems weird - not even sure if this is valid syntax below)

jest.config.ts

import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  moduleNameMapper: {
    '^(.*)\\.js$': '$1',
  },
  testEnvironment: 'jest-environment-node',
  transformIgnorePatterns: [
    'node_modules/(?!aggregate-error|clean-stack|escape-string-regexp|indent-string|p-map)',
  ],
};

module.exports = config;

@vahdet You can also downgrade to ts-node 9, wait for TypeStrong/ts-node#1371 to be released, and then upgrade to ts-node 10.

This should save on effort and allow you to use .ts configs the entire time.

Another option is a config that says ā€œtreat files matching these globs as .cjsā€

Something like "moduleTypes": {"jest.config.ts": "cjs"} or "moduleTypes": {".": "cjs"}

This might be more intuitive, since it maps cleanly to a node concept. We will behave as if those .ts files compile into .cjs so that node runs them like CommonJS.

Hi, I feel like I am missing something between this thread, https://github.com/TypeStrong/ts-node/issues/1342 (linked in one of the comments here) and the docs for 29.5 of jest. The basic configuration in typescript is shown to be as follow.

import type {Config} from 'jest';

const config: Config = {
  verbose: true,
};

export default config;

I have the following error when running jest: jest.config.ts:23:1 - error TS1286: ESM syntax is not allowed in a CommonJS module when 'verbatimModuleSyntax' is enabled..

To be honest, I have no clues if this is related or not. The thing is if we write TS files with ESM syntax, but jest is forcing commonjs but we are using the esm base configuration for tsconfig.json, verbatimModuleSyntax is true, so we must override through "ts-node" field of tsconfig.json.

I made a stackblitz to show what I am saying. When adding verbatimModuleSyntax to false, it reads the configuration file. (ignore the failing configuration and failing testsā€¦ I just found out about reading that typescript ESM configuration file after 8 hoursā€¦)

https://github.com/facebook/jest/issues/11453#issuecomment-877653950

This works for me and even though I have tsconfig.json properly set:

"compilerOptions": {
    "module": "es2020",
    "target": "es2020"
  }

when I use import.meta.url in order to do an ESM workaround for the __dirname:

// ESM workaround for the missing `__dirname`:
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

I get an error:

Error: Jest: Failed to parse the TypeScript config file /Users/mprinc/data/development/colabo-zontik/colabo/src/services/puzzles/flow/go/jest.config.ts
  TSError: āØÆ Unable to compile TypeScript:
jest.config.ts:16:34 - error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

16 const __filename = fileURLToPath(import.meta.url);
                                    ~~~~~~~~~~~

    at readConfigFileAndSetRootDir (/Users/mprinc/.config/yarn/global/node_modules/jest-config/build/readConfigFileAndSetRootDir.js:118:13)
    at async readConfig (/Users/mprinc/.config/yarn/global/node_modules/jest-config/build/index.js:233:18)
    at async readConfigs (/Users/mprinc/.config/yarn/global/node_modules/jest-config/build/index.js:420:26)
    at async runCLI (/Users/mprinc/.config/yarn/global/node_modules/@jest/core/build/cli/index.js:132:59)
    at async Object.run (/Users/mprinc/.config/yarn/global/node_modules/jest-cli/build/cli/index.js:155:37)

@cspotcode, @SimenB could you please investigate and help? Thanks a lot! If it is not straightforward, please let me know if I need to create a fully reproducible repo?

Yeah, makes sense to me. I wasnā€™t sure quite how jest handles bootstrapping itself and if it was able to pass --loader to a child process. But sounds like config files are executed within the main jest process, not a child process? EDIT nevermind this is answered by your comment above

Re: delegation, I see that jest forces CommonJS emit. In this case, seems like a good thing: pair it with the correct moduleTypes override, and users are in business by being forced to write CJS configs. In the future, when jest wants to support ESM configs, forcing CommonJS emit will be an issue for users, so we might want to remove that flag and instead delegate fully to tsconfig.

Based on that integration test, are you planning to recommend that users enable that flag in their tsconfigs, or are you planning for jest to pass that option as an override similar to how it is forcing CJS emit? If the latter, probably best to make the override a catch-all "**" or merge the override with the userā€™s own moduleTypes config. For example, if jest.config.ts imports config-helpers.ts

My thinking, at a very high level, is that ts-nodeā€™s sole job is executing TS directly, and we take care of all the gotchas. So anything we can do for jest to delegate responsibility to ts-node is good for both jest, ts-node, and the users. Iā€™ve seen other projects think they can implement TS execution themselves, hit issues and complexity, and eventually deprecate their solution in favor of ts-node.

  • ts-node is controlled via tsconfig, allowing users to copy-paste well-known flags to workaround issues without intervention from jest
    • especially given how ESM is churning, I think this is good
  • Users may already be using ts-node for other things in their project, for example, DB migration scripts, or developer tooling
    • they may already have familiarity with the necessary ts-node flags in tsconfig and want to use the same config for jest
    • if jest passes overrides to ts-node, this potentially causes confusion or limitations that users are unable to workaround

ts-nodeā€™s website has a dedicated section for ā€œRecipes;ā€ we can add one or more for Jest. For example, our AVA recipe

Sounds like there are 3x issues in this ticket:

A) Users want to write config files that import ESM dependencies and thus must execute as ESM

Thisā€™ll require jest to use node --loader ts-node/esm. With that, you can import or require any TS file and itā€™ll work. If users hit issues, there are well-known recipes for configuring ts-node via their tsconfig.json, which avoid jest complexity.

If you want, we could also expose a no-op loader that can be initialized on-demand by jest. For example node --loader ts-node/esm-lazy and then at runtime, when jest realizes it needs to load an ESM ts file, call process.lateBindingLoader.install('ts-node/esm') Maybe we could make it fully automatic? Happy to discuss.

B) Users do not need config files to execute as ESM, but theyā€™re in a project with "type": "module"

I think those users can be unblocked today with https://typestrong.org/ts-node/docs/module-type-overrides, and in the future with jest.config.cts. Only requires change from jest would be to recognize the cts file extension. ts-node should hopefully have cts support merged by that point.

C) Users want faster execution of config files

ts-node has "swc": true to use SWC, or "transpileOnly": true to use the TS compilerā€™s transpilation. Both avoid typechecking. Users may ask why weā€™re not using esbuild since it may have better name recognition, but for this use case, esbuild and swc are effectively identical.

Coming back to my earlier thought about how users might already be using ts-node for other scripts, thereā€™s a good change they already have "ts-node": {"swc": true} in their tsconfig, so theyā€™ll get this benefit without any intervention from jest.


Sorry this wound up more verbose than intended. I hope it helps!

@cspotcode Iā€™ve completely lost track of this (and any context I might have possessed at some point). Whatā€™s your recommendation for moving this forward? Next release of Jest is a major, so if thereā€™s some change thatā€™s ts-node@10 only we can drop v9.

Weā€™re also dropping node 10, so loading ESM should be fine