kulshekhar / ts-jest

A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
https://kulshekhar.github.io/ts-jest
MIT License
6.98k stars 455 forks source link

Support `Node16/NodeNext` value for `moduleResolution` #4198

Open alumni opened 1 year ago

alumni commented 1 year ago

Version

29.1.1

Steps to reproduce

I have a monorepo setup with multiple apps and libraries. One of the apps (the largest, the only one with isolatedModules: true) fails after updating to TS 5.2 with the following error: error TS5110: Option 'module' must be set to 'Node16' when option 'moduleResolution' is set to 'Node16'.

Currently using @tsconfig/node18/tsconfig.json for the monorepo base config, which contains the following:

{
    "module": "node16",
    "moduleResolution": "node16"
}

The module option is overridden by ts-jest:

Expected behavior

I would expect that module is not changed.

Actual behavior

module is changed first to 100 and later to 1 (CommonJs)

Debug log

extracted info above

Additional context

No response

Environment

System:
    OS: Windows 10 10.0.19044
    CPU: (8) x64 Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
  Binaries:
    Node: 18.17.0 - ~\AppData\Local\fnm_multishells\16532_1692954691139\node.EXE
    Yarn: 1.22.19 - ~\AppData\Local\fnm_multishells\16532_1692954691139\yarn.CMD
    npm: 9.8.1 - ~\AppData\Local\fnm_multishells\16532_1692954691139\npm.CMD
    pnpm: 8.6.12 - ~\AppData\Local\fnm_multishells\16532_1692954691139\pnpm.CMD
  npmPackages:
    jest: ^29.6.4 => 29.6.4
zapteryx commented 1 year ago

getting this as well after updating to TS 5.2.2

andrew-pledge-io commented 1 year ago

I have the same issue. As a temporary workaround I've set moduleResolution to classic in the ts-jest configuration:

module.exports = {
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      { tsconfig: { moduleResolution: "classic" } },
    ],
  },
};
chentsulin commented 1 year ago

The code overrides module in getCompiledOutput():

https://github.com/kulshekhar/ts-jest/blob/9f1439a97588ddfb32abb75003041835877fc910/src/legacy/compiler/ts-compiler.ts#L155-L170

DavidRigglemanININ commented 1 year ago

I'm running into this as well and it looks like it's been a month since the last comment. Is there a fix coming out anytime soon for this?

erunion commented 1 year ago

This patch fixed this issue for our use-case of using @tsconfig/node16 with module: node16 and moduleResolution: node16:

--- ./node_modules/ts-jest/dist/legacy/compiler/ts-compiler.js  2023-11-01 13:05:20.000000000 -0700
+++ ./node_modules/ts-jest/dist/legacy/compiler/ts-compiler.fixed.js    2023-11-01 13:09:38.000000000 -0700
@@ -132,7 +132,7 @@
             allowSyntheticDefaultImports = true;
         }
         else {
-            moduleKind = this._ts.ModuleKind.CommonJS;
+            moduleKind = this._compilerOptions.module || this._ts.ModuleKind.CommonJS;
         }
         this._compilerOptions = __assign(__assign({}, this._compilerOptions), { allowSyntheticDefaultImports: allowSyntheticDefaultImports, esModuleInterop: esModuleInterop, module: moduleKind });
         if (this._languageService) {

It seems like ModuleKind.CommonJS is pretty hardwired throughout ts-jest though so I don't know if anything is being missed here but this at least lets us run our tests with modern resolutions.

raczkerry commented 1 year ago

I am experiencing this as well and @erunion 's patch is fixing this issue for me.

darkbasic commented 1 year ago

node16 works "fine" (at least with "noEmitOnError": false) for me even without the patch: https://github.com/kulshekhar/ts-jest/issues/4207#issuecomment-1827413759

patrickshipe commented 1 year ago

I only encounter the issue if I do:

  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        isolatedModules: true,
      },
    ],
  },

If I leave out the isolatedModules option, it works just fine.

lpc921 commented 11 months ago

In addition to setting "noEmitOnError": false, I have to leave moduleResolution unspecified when "module": "Node16" or "module": "NodeNext" is set. If I set moduleResolution to Node16 or NodeNext, ts-jest loads the CommonJS version of some packages. See this issue here: Error TS2351 for new SchemaBuilder when testing with ts-jest

DavidRigglemanININ commented 10 months ago

Does anyone have a workaround for when your jest config file and global setup files are in TS? It does work if my production tsconfig file sets "noEmitOnError" to false, but I don't really want to do that for production code. But with the ts-jest code seeming to invoke ts-node (from what I read) and using my default tsconfig, some of the above workarounds don't seem to work such as the transform or a custom tsconfig file.

Also, is this project dead? There hasn't been a release for over 6 months. I'm trying to figure out if we need to start looking at other projects if this project is indeed no longer being maintained.

tobice commented 9 months ago

I came here because I wanted to speed up my ts-jest tests by applying isolatedModules: true, which is when I started receiving this error.

I solved by it putting the following into my tsconfig.js:

        "module": "ESNext",
        "moduleResolution": "Node",

I don't fully understand the practical difference between NodeNext and ESNext but it seems to work fine for my case (I just had to change how I import certain npm packages).

Running a trivial test went from 5 seconds to 1 ms.

InExtremaRes commented 7 months ago

Why does ts-jest change module to CommonJS and why this is only apparent if using isolatedModules: true in the jest config?

If I have to guess I would say it is to change away from ESNext if the config doesn't specify useESM but I could be totally wrong. Also this doesn't say why only if isolatedModuels is set.

Is it possible to fix ts-jest so it keeps the given module option as is if it is one of CommonJS, Node16 or NodeNext, defaulting to CommonJS only in other cases? Would that break something?

For people asking what the difference is between CommonJS and NodeNext (beware I'm not an expert) it has to do with CJS and ESM. Basically, under CommonJS TypeScript asumes everything that uses an import can be called with require(); however ESM modules can't be requireed and have to be imported. Under NodeNext TypeScript tries to understand the difference between a CJS and an ESM module so it generates the right JS code. For example this code:

// source code: in a .ts file.
const x = import('./module.mjs');

generates this under CommonJS:

// TypeScript uses `require` but wraps in a Promise. This would fail at runtime.
const x = Promise.resolve().then(() => require('./module.mjs'));

but generates this under NodeNext:

// TypeScript uses `import`  and this code is correct.
const x = import('./module.mjs');

NodeJS differentiates between the two from the extension .mjs and TypeScript under NodeNext makes the same distinction (plus some other things like understanding the field "type" in package.json).

lazarljubenovic commented 7 months ago

Simply not explicitly specifying moduleResolution solved the issue for me, leaving only "module": "NodeNext". tsc at least guarantees the same behavior with this config: https://www.typescriptlang.org/docs/handbook/modules/reference.html#implied-and-enforced-options

What a mess. Layer upon layer of re-interpreting configs and patching together 20 years of legacy lunacy. What a time to be alive!

unional commented 6 months ago

In summary, ts-jest doesn't really work with TypeScript 5.2 or above.

@erunion workround is for non ESM (without NODE_OPTIONS=--experimental-vm-modules or useESM is false): https://github.com/kulshekhar/ts-jest/issues/4198#issuecomment-1789652347

Inferring moduleResolution is the same: https://github.com/repobuddy/repobuddy/pull/150

When setting NODE_OPTIONS=--experimental-vm-modules and useESM is true, It will meet this condition and moduleKind is set to ESNext:

        if ((this.configSet.babelJestTransformer || (!this.configSet.babelJestTransformer && options.supportsStaticESM)) &&
            this.configSet.useESM) {
            moduleKind =
                !moduleKind ||
                    (moduleKind &&
                        ![this._ts.ModuleKind.ES2015, this._ts.ModuleKind.ES2020, this._ts.ModuleKind.ESNext].includes(moduleKind))
                    ? this._ts.ModuleKind.ESNext
                    : moduleKind;
            // Make sure `esModuleInterop` and `allowSyntheticDefaultImports` true to support import CJS into ESM
            esModuleInterop = true;
            allowSyntheticDefaultImports = true;
        }

Meaning there is no way to use moduleResolution: Node16|NodeNext.

If you patch that and allow the moduleKind to stay at Node16|NodeNext, then you will get ReferenceError: exports is not defined errors.

rthreei commented 6 months ago

https://swc.rs/docs/usage/jest has been a good drop-in replacement for us.

camsteffen commented 6 months ago

This blocks me from doing:

  1. Use moduleResolution: bundler in main tsconfig.json
  2. Use moduleResolution: nodenext in tsconfig.test.json
  3. Use isoluatedModules: true for ts-jest
erunion commented 4 months ago

Confirmed that the work in v29 resolves this for us on a module and moduleResolution of node16. Thanks @ahnpnl!

drweizak commented 4 months ago

This fix i breaking our packages. We have some packages in our monorepo that fail with the following setup:

package.json:

"type": "module",

tsconfig.json:

  "compilerOptions": {
    "outDir": "./dist",
    "lib": ["ES2023"],
    "module": "NodeNext",
    "target": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "NodeNext",
    "incremental": true,
    "declaration": true,
}

tsconfig.test.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["node", "jest"],
    "typeRoots": ["../../node_modules/@types"],
    "moduleResolution": "Node",
    "resolveJsonModule": true
  },
  "include": ["./src/index.spec.ts"],
  "exclude": []
}

jest.config.cjs:

const config = {
  preset: 'ts-jest',
  testEnvironment: 'node',

  extensionsToTreatAsEsm: ['.ts'],

  transform: {
    '^.+\\.(ts)$': [
      'ts-jest',
      {
        useESM: true,
        tsconfig: 'tsconfig.test.json',
        diagnostics: {
          ignoreCodes: ['TS151001'],
        },
      },
    ],
  },

  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },

  testMatch: ['**/?(*.)+(spec).[tj]s?(x)'],

  testPathIgnorePatterns: ['/node_modules/', '/dist/'],

  verbose: true,

  reporters: ['<rootDir>/minimalReporter.cjs'],
}

module.exports = config

Can anyone please help me understand what i am doing wrong?

I am having issues importing .json files with "import * as xxx from 'x/x/x.json'" They are convertes into objects, while they are defined as arrays.

ahnpnl commented 4 months ago

@drweizak would you please create a reproduce problem with example apps https://github.com/kulshekhar/ts-jest/tree/main/examples It would be easier to investigate. Thank you.

drweizak commented 4 months ago

@drweizak would you please create a reproduce problem with example apps https://github.com/kulshekhar/ts-jest/tree/main/examples It would be easier to investigate. Thank you.

Thank you for your time! i found out the problem was related how i was importing my files. All is good now :)

before: import * as x from 'file.json'

now: import x from 'file.json'

ahnpnl commented 4 months ago

@drweizak is it related to https://www.typescriptlang.org/tsconfig/#allowSyntheticDefaultImports? We did change a bit the behavior there in https://github.com/kulshekhar/ts-jest/commit/ff4b302

kirkwaiblinger commented 1 week ago

The issue that I'm encountering is that JSON modules, especially when imported dynamically, do not seem to be resolved the same way as tsc/node when using ts-jest. See https://github.com/kirkwaiblinger/typescript-default-json/ for some examples.

It's been hard to figure out what behavior is even correct here, since it touches on

But the ts-jest and tsc/node behavior definitely do not match.

Note that the behavior when using swc does more or less seem to match node's runtime behavior: https://github.com/kirkwaiblinger/typescript-default-json/tree/swc-variant

amitbeck commented 20 hours ago

It's been hard to figure out what behavior is even correct here, since it touches on

  • moduleResolution
  • node runtime semantics
  • TS's compatibility mechanisms (synthetic default imports and esmodule interop)
  • resolveJsonModule (and import attributes?)

But the ts-jest and tsc/node behavior definitely do not match.

I've encountered an issue similar to the one @kirkwaiblinger is describing, but not with importing a JSON module. The issue I'm having is actually related to importing the type Delta which is declared as the default export of the quill-delta package.

tsc passes, but jest with ts-jest fails with 'Delta' only refers to a type, but is being used as a namespace here. This alleged TypeScript error is for using Delta.default because Delta's type definition uses declare class, and setting the module TSConfig option to 'NodeNext' causes the imported Delta type to be interpreted as a namespace.