Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
felipeplets opened this issue May 26, 2021 · 62 comments · Fixed by #12397
Closed

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

felipeplets opened this issue May 26, 2021 · 62 comments · Fixed by #12397

Comments

@felipeplets
Copy link

🐛 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 
@cspotcode
Copy link
Contributor

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.*

@cspotcode
Copy link
Contributor

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.

@karlhorky
Copy link
Contributor

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
Copy link
Contributor

If anyone from the jest team has a minute to offer some feedback on the solution proposed in #11453 (comment) and #11453 (comment), I would greatly appreciate it.

See also: TypeStrong/ts-node#1342 tracking this feature on the ts-node side.

@SimenB
Copy link
Member

SimenB commented Jun 7, 2021

I wonder if we should just spawn ts-node, somehow to load the config, execute the exported function (if any), then return that to the main process rather than using the programmatic API.

A flag allowing us to truly force CJS is all well and good until the user imports an ESM native package. I assume we cannot use import() like we do for ESM for TS as we'll need the loader, right?

@cspotcode
Copy link
Contributor

Yeah, that works. You are correct that import(pathToTsFile) fails without the loader, since node's ESM resolver does not know how to classify the .ts file extension.

I think it depends on philosophy: in the general sense, I want ts-node to be user-configurable so that tools such as gulp, webpack, and jest can support it without too much code, and users can set the necessary flags in their tsconfig.json. For those tools, require('ts-node').register() will pull all ts-node options from tsconfig, so users have full control and the CLI tool doesn't need to configure anything. We can put docs and sample tsconfigs on our website so users will know how to setup tsconfig so that ts-node uses CJS for their config files. (Gulpfile.ts, webpack.config.ts, etc)

On the other hand, if jest is already overriding module: commonjs then spawning a subprocess fits jest's philosophy here, since jest wants to control lifecycle. And it doesn't preclude ts-node from adding the flag I proposed to support webpack, gulp, etc. Jest is an interesting case because it wants to control the bootstrapping of ts-node but not the version of ts-node being used.

When jest loads the config, is that happening in the very first CLI process? Or has jest already spawned a subprocess at that point?

This might technically be possible in a worker_thread, too? I think worker_threads let you add --loader but I'm not sure.

@SimenB
Copy link
Member

SimenB commented Jun 7, 2021

This is essentially the first thing that happens yeah. I'm happy to keep using the programmatic API, I'm just afraid as mentioned that it's just pushing the issue in front of us (like ESM source JS, which will break if loaded via some transpiled require). After evaluating the config file (including potentially calling some exported function), everything in the exported config must be serializable, so I thought just loading outside of the main process should be fine since we can return a JSON.stringify from the other process. Will impact startup time of Jest, but if you've written your config in TS you've pretty much already gone down that route and it won't impact anybody else

@cspotcode
Copy link
Contributor

Yeah, it won't impact startup much if users are smart and enable transpileOnly and if jest keeps the amount of code executing in the child process to a minimum.

To get a sense for timing:

ts-node --transpile-only -e 'console.log("hello world" as const)'

@SimenB
Copy link
Member

SimenB commented Jun 7, 2021

We can probably pass --transpileOnly ourselves if we're spawning the binary

@cspotcode
Copy link
Contributor

For --loader, you'll need to do something like this:

TS_NODE_TRANSPILE_ONLY=true node --loader ts-node/esm ...
# or
node --loader ts-node/esm-transpile-only ...

@SimenB
Copy link
Member

SimenB commented Jun 8, 2021

Semi-related: microsoft/TypeScript#44501. How that shakes out might affect how ts-node does things and also how we should deal with it in Jest?

@cspotcode
Copy link
Contributor

cspotcode commented Jun 10, 2021

TypeStrong/ts-node#1371 should give users a way to force CJS when they need it, solving this use-case. I just finished the implementation minutes ago and haven't reviewed it, but it has basic tests that are passing.

If anyone is feeling adventurous, they can install from git:

npm install -D TypeStrong/ts-node#ab/override-module-type

Semi-related: microsoft/TypeScript#44501. How that shakes out might affect how ts-node does things and also how we should deal with it in Jest?

Unfortunately files like jest.config.ts and webpack.config.ts are going to be governed by the root package.json. A user would need to set <root>/package.json {"type": "commonjs", ...} and <root>/src/package.json {"type": "module"}

@vahdet
Copy link

vahdet commented Jun 25, 2021

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

Applying this workaround, plus:

  1. Setting babel.config.cjs (Note the .cjs extension here, too) as:

    // eslint-disable-next-line no-undef
    module.exports = {
      presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        '@babel/preset-typescript',
      ],
    }

Was a go for me. Still missing my .ts configs though..

@cspotcode
Copy link
Contributor

@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.

@cspotcode
Copy link
Contributor

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

@karlhorky
Copy link
Contributor

karlhorky commented Jul 10, 2021

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

@karlhorky
Copy link
Contributor

karlhorky commented Jul 10, 2021

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;

@karlhorky
Copy link
Contributor

@cspotcode Would be nice to see the content of the webpack.config.ts file in the docs here: https://typestrong.org/ts-node/docs/module-type-overrides/

I'd be happy to do a PR for this as soon as I know how this is working :)

@karlhorky
Copy link
Contributor

karlhorky commented Jul 10, 2021

Ah from the webpack.config.ts in the tests it seems like the source input module format is ESM.

Opened a PR: TypeStrong/ts-node#1394

@SimenB
Copy link
Member

SimenB commented Feb 15, 2022

See #12397 for a working example.

So that's B from your use cases. C is free since people can just enable that as well in their config.

And A I think can wait for more stabilization in typescript itself (at some point we can just spawn ts-node as mentioned (possibly with a loader), rather than require users to run jest entirely with a loader)

@cspotcode
Copy link
Contributor

cspotcode commented Feb 15, 2022

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

@SimenB
Copy link
Member

SimenB commented Feb 15, 2022

I tried passing ts-node config to the register call, but it didn't take effect... I might have messed something up, as that would be ideal (since we already force CJS as you mention).

EDIT: Oh, from the types we shouldn't have the ts-node wrapper in this case

@cspotcode
Copy link
Contributor

cspotcode commented Feb 15, 2022

Yeah, no ts-node wrapper. Docs are rendered here if you prefer reading on a website:
https://typestrong.org/ts-node/api/index.html#Register
https://typestrong.org/ts-node/api/interfaces/RegisterOptions.html

@SimenB
Copy link
Member

SimenB commented Feb 15, 2022

I was fooled by not getting a type error when passing ts-node - I just assumed the setup was the same as when in tsconfig.json. When you assume... 😅

@cspotcode
Copy link
Contributor

Yeah, no ts-node wrapper. Docs are rendered here if you prefer reading on a website:
https://typestrong.org/ts-node/api/index.html#Register
https://typestrong.org/ts-node/api/interfaces/RegisterOptions.html

These docs are also outdated because they say TS can't use cts nor mts, which is wrong.

You didn't get a type error due to dynamic require()? Or is it a bug in ts-node's declarations?

@SimenB
Copy link
Member

SimenB commented Feb 15, 2022

You didn't get a type error due to dynamic require()? Or is it a bug in ts-node's declarations?

The require 🙂 fixing it while I'm here

@SimenB
Copy link
Member

SimenB commented Feb 15, 2022

https://github.com/facebook/jest/releases/tag/v28.0.0-alpha.1

mateus-f-torres added a commit to mateus-f-torres/barefoot that referenced this issue Mar 24, 2022
mateus-f-torres added a commit to mateus-f-torres/barefoot that referenced this issue Mar 24, 2022
@moonman239
Copy link

I'm bumping this issue because it doesn't seem to have been solved. Since a lot of people are probably going to be using ES6 modules, and they are no longer considered experimental by the NodeJS team, it is important that this issue is resolved.

@cspotcode
Copy link
Contributor

cspotcode commented Mar 30, 2022

ESM loaders are still considered experimental by the NodeJS team. This means if you want to write a jest config file that requires transpilation, and you're unwilling to use the configs that allow it to be executed as CommonJS, then you're relying on an experimental feature of node. Nothing that Jest can do about that.

@moonman239
Copy link

moonman239 commented Mar 30, 2022 via email

@SimenB
Copy link
Member

SimenB commented Apr 1, 2022

It is solved, you can try the reproduction in the OP and it works correctly. If it's still not working for you, please create a new issue with a minimal reproduction

@gugol2
Copy link

gugol2 commented May 4, 2022

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 ?

@zachkirsch
Copy link

And A I think can wait for more stabilization in typescript itself (at some point we can just spawn ts-node as mentioned (possibly with a loader), rather than require users to run jest entirely with a loader)

@SimenB am I understanding correctly that "use case A" (Users want to write config files that import ESM dependencies and thus must execute as ESM) is not currently possible?

@mprinc
Copy link
Contributor

mprinc commented Nov 13, 2022

#11453 (comment)

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?

@Zainal2

This comment was marked as spam.

@NicolasThierion
Copy link

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

@sharky98
Copy link

Hi, I feel like I am missing something between this thread, TypeStrong/ts-node#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...)

@ValentinGurkov
Copy link

#11453 (comment)

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?

I found this thread experiencing the exact same issue, nothing I tried has worked :/

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

Successfully merging a pull request may close this issue.