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 with TypeScript and ES modules can't import "named imports" from cjs modules #11563

Open
themaskedavenger opened this issue Jun 12, 2021 · 18 comments

Comments

@themaskedavenger
Copy link

themaskedavenger commented Jun 12, 2021

🐛 Bug Report

Using TS, exports from CJS modules can be imported with syntax as if they were named ES module exports, e.g.:

import { sync } from 'glob';

However, with Jest and ES modules, when this style of import is in a test file or in a dependency of a test file, it says SyntaxError: The requested module 'glob' does not provide an export named 'sync'

Going through all one by one and changing them to import glob from 'glob'; and then calling glob.sync() seems to work, however when migrating some legacy stuff from another test runner to Jest this may not be an option, because there are a lot of those such imports in the codebase.

Is there a way around this, so that import { whatever } from 'whatever'; will work for CJS modules?

To Reproduce

Steps to reproduce the behavior:

Running jest with: node --experimental-vm-modules node_modules/jest/bin/jest.js (as described in https://jestjs.io/docs/ecmascript-modules), and using Jest config:

  resetMocks: true,
  testEnvironment: "node",
  testMatch: [
    "**/src/**/*.(spec|test).[tj]s?(x)"
  ],
  preset: 'ts-jest/presets/default-esm',
  transform: {},
  'extensionsToTreatAsEsm': [".ts", ".tsx"],
  globals: {
    'ts-jest': {
      useESM: true,
      tsconfig: {
        allowSyntheticDefaultImports: true,
        declaration: true,
        esModuleInterop: true,
        jsx: "react",
        lib: ["esnext"],
        module: "es2020",
        moduleResolution: "node",
        outDir: "build",
        sourceMap: true,
        strictNullChecks: true,
        target: "ES2020",
      }
    },
  }

Expected behavior

import { sync } from 'glob' and similar imports from CJS modules work.

Link to repl or repo (highly encouraged)

https://github.com/themaskedavenger/tsjestcjerepro

envinfo


  System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
  Binaries:
    Node: 15.14.0 - /usr/local/bin/node
    Yarn: 1.22.0 - /usr/local/bin/yarn
    npm: 7.7.6 - /usr/local/bin/npm
  npmPackages:
    jest: ^27.0.4 => 27.0.4 
@k2snowman69
Copy link

k2snowman69 commented Jun 15, 2021

In hopes that it speeds things along and removes any question as to ownership, I forked the repository provided by @themaskedavenger and removed all typescript related stuff to show it's a jest issue.

https://github.com/k2snowman69/tsjestcjerepro

Hopefully this removes any doubt that this is a ts or a ts-jest issue

Tested with node 14.17.1 and node v16.1.0

@k2snowman69
Copy link

Okay so dug into this a bit and hopefully my investigation helps whoever ends up fixing this on jest's side.

The first and important thing to note is that jest does not use nodejs to resolve files. This tripped me up a lot until I figured this out. Both jest and nodejs use cjs-module-lexer which uses basic regex to parse a contents of a file to determine what the exported functions are from a cjs library. The owner of that library had a great explanation that helped guide the rest of this investigation. Now what this means is the same package has the possibility of behaving differently between nodejs and jest and that's really important because that gap between the two is going to cause lots of confusion... so using a few examples let's take this package by package...

glob

Glob's export code looks like module.exports = glob which would require an eval on the js code to determine what the exports are. This is why cjs-module-lexer cannot determine the exports, because it's purely basing it off regex for performance reasons required by nodejs. This will fail in both nodejs and jest.

enzyme

Enzyme's export code looks like

module.exports = {
  render: _render2['default'],
  shallow: _shallow2['default'],
  mount: _mount2['default'],
  ShallowWrapper: _ShallowWrapper2['default'],
  ReactWrapper: _ReactWrapper2['default'],
  configure: _configuration.merge,
  EnzymeAdapter: _EnzymeAdapter2['default']
};

however it seems that cjs-module-lexer is only able to extract the first exported function. I commented about this bug in nodejs/cjs-module-lexer#57 and provided a unit test for reproduction. Hopefully we can see it get fixed.

tslib

tslib actually supports cjs, es6 through the module property (non-standard) and ESM through the exports property (node compatible) so this should work.

When running the following code in node in either cjs or esm there are no errors (as expected)

import { __assign } from "tslib";

const d = __assign({ a: "a" });
console.log(d.a);

However when running the following test in Jest ESM:

import { __assign } from "tslib";

test.only("General config is working", async () => {
  const d = __assign({ a: "a" });
  expect(d.a).toBe("a");
});

You'll get the following error:

 FAIL  src/tslib.test.js
  ● Test suite failed to run

    SyntaxError: The requested module 'tslib' does not provide an export named '__assign'

      at Runtime.linkAndEvaluateModule (node_modules/jest-runtime/build/index.js:669:5)

Which means that jest's resolver isn't resolving the same file that node js is resolving that eventually gets sent to cjs-module-lexer so that it can correctly determine the exports.

Summary

Hopefully that gives some guidance to someone who investigates this and maybe we can at least fix this for tslib. To fix this for glob however, you'll need to fix it in cjs-module-lexer. I'd still leave this ticket open to hopefully fix it for tslib though.

@yqrashawn
Copy link

yqrashawn commented Aug 19, 2021

This happens to me even with react.
Can't use import {useState} from 'react'
Have to write it this way

import React from 'react'
const {useState} = React

And react-use has the same problem

@smiller171
Copy link

Just ran into this myself...incredibly frustrating bug.

@zachkirsch
Copy link

+1 I am seeing the same issue with a very similar setup (jest with ESM, relying on named exports from a CJS module).

@OultimoCoder
Copy link

Is there no fix for this yet?

@pommelinho
Copy link

+1 also wondering if there is a solution for this?

@Amerr
Copy link

Amerr commented May 12, 2023

@k2snowman69 Thanks for your detailed analysis of the root cause of the problem.

Like @yqrashawn i am also facing Jest not able to do perform named export even in react package.

Current Temp solution is to import the whole package default and extract individual exports

import React from 'react'

React.useEffect

But its a pain staking process, and i would like to automate somehow, any suggestion.

Copy link

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label May 11, 2024
@viceice
Copy link

viceice commented May 12, 2024

not stale

@github-actions github-actions bot removed the Stale label May 12, 2024
@SimenB
Copy link
Member

SimenB commented May 12, 2024

Can anyone provide an example of where node is able to import using native ESM, but Jest fails?

E.g. React does not support native ESM: facebook/react#11503

@SebastienGllmt
Copy link

I can confirm the issue still exists for us. I updated the repo linked by the issue author to the latest version of jest/ts-jest and you can see the issue still persists: https://github.com/SebastienGllmt/tsjestcjerepro

myfreeer added a commit to website-local/website-scrap-engine that referenced this issue Oct 5, 2024
myfreeer added a commit to website-local/website-scrap-engine that referenced this issue Oct 5, 2024
@GeorgeCr
Copy link

This happens to me even with react. Can't use import {useState} from 'react' Have to write it this way

import React from 'react'
const {useState} = React

And react-use has the same problem

Had this issue with another package and such approach solved it for me.

@glassdimlygr
Copy link

I fixed this like so:

// @ts-ignore No types bc it's a cjs file and while types exist it's not worth the effort. See tsconfig and jest.test.config
import { hash } from 'ohash';

tsconfig:.json

"paths": {
      "ohash": ["./node_modules/ohash/dist/index.cjs"]

jest.test.config.js

  moduleNameMapper: {
    '^ohash$': '<rootDir>/node_modules/ohash/dist/index.cjs'

That is to say, I forced jest and ts to look at the cjs file when importing ohash and then this fixed the problem of jest seeing the .cjs extension and barfing.

@MirKml
Copy link

MirKml commented Jan 9, 2025

Thanks for workaround.
But I agree with @SimenB. Problem with "ESM compliant" packages is mostly on packages itself and its types. Particularly for frontend packages.
These are primary used with webpack/bundler, which is very tolerant for incorrect exports, default exports ... But in node/jest, ESM requirements are much stricter. As Simen said, importing these packages mostly doesn't work on pure ESM node.

But nowadays, in latest node, node can "require" cjs packages in ESM? Maybe this can change something, but probably jest needs own support for this feature same as for ESM.

@certainlyakey
Copy link

In my specific case, this error appeared in CI and not on local. The solution was to explicitly add the glob package to package.json.

@benquarmby
Copy link

Even though I see the exact issue from the original post in large projects, I have failed to create a simple repro so far. ESM projects I create from scratch that import named Common.js exports are working perfectly both at runtime and with Jest. That's with or without TypeScript. I'll keep pulling on the threads and adding complexity in the hope I can isolate the problem 🤞

@benquarmby
Copy link

benquarmby commented Feb 6, 2025

OK, I've found the root of the problems in my projects. It's a complicated issue and there is no single TL;DR, but I'll do my best to explain:

  • Bundlers like webpack hide ESM / Common.js interoperability issues so that it seems like runtime is working and test time is not. If the same code ran straight on Node without a bundler, it would fail in the same way with the same SyntaxError that we see with Jest.

  • TypeScript compiled Common.js exports are only sometimes available as ESM imports. It depends if a function call is nested in the generated Object.defineProperty getter. For example, if a named export references a default import, it will not work as described in this Node.js issue: The function exported using Object.defineProperty in node_modules cannot be found. nodejs/node#56304 (comment)

    Not importable (due to nested function call):

    Object.defineProperty(exports, "namedThing", { enumerable: true, get: function () { return tslib_1.__importDefault(namedThing_1).default; } });

    Importable:

    Object.defineProperty(exports, "namedThing", { enumerable: true, get: function () { return namedThing_1.namedThing; } });
  • Finally, there is this TypeScript issue that explains how TS simply doesn't know which named exports are going to be importable at runtime and which ones are not: Tracking issue: Named imports from CJS module incorrectly allowed in nodenext microsoft/TypeScript#54018

So to sum up, this is not a Jest issue. Jest and Node agree on how ESM imports from Common.js exports work because they both use cjs-module-lexer. TypeScript unfortunately doesn't add design time safety. If production seems to work anyway, my bet is there is a bundler involved, such as webpack.

Edit: My next step is to remove export default from all TypeScript libraries we own and switch entirely to named exports. For libraries we don't own, we'll need to live with namespace imports 🤮

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

No branches or pull requests