jest: SyntaxError: Cannot use import statement outside a module, with TypeScript and ES Modules

šŸ› Bug Report

Filing a separate issue at @SimenBā€™s suggestion.

After following the steps to get native ESM support, Iā€™m running into the following error in a project that transpiles TypeScript files using @babel/preset-typescript:

/home/dandv/jets/lib.test.ts:1
import { foo } from './lib';
^^^^^^

SyntaxError: Cannot use import statement outside a module

  at Runtime._execModule (node_modules/jest-runtime/build/index.js:1074:58)

To Reproduce

  1. git clone https://github.com/dandv/jest-typescript-es-modules.git
  2. cd jest-typescript-es-modules
  3. npm install
  4. npm test

Expected behavior

Test passes.

Link to repl or repo (highly encouraged)

https://github.com/dandv/jest-typescript-es-modules

envinfo

System:
    OS: Linux 5.3 Ubuntu 18.04.4 LTS (Bionic Beaver)
    CPU: (8) x64 Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
  Binaries:
    Node: 13.13.0 - /usr/bin/node
    npm: 6.14.4 - ~/.local/bin/npm
  npmPackages:
    jest: ^25.4.0 => 25.4.0

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 9
  • Comments: 45 (34 by maintainers)

Most upvoted comments

I had the same problem. Iā€™ve fixed that by including js files in ts-node:

"transform": { "^.+\\.(ts|tsx|js|jsx)?$": "ts-jest" }

What Iā€™m thinking makes sense is to make that last one behave like js, i.e. infer from the type field.

Hi, I stumbled across this issue and thought Iā€™d say hi. Iā€™m the maintainer of CoffeeScript and Iā€™ve been dealing with this same issue. In my case, there are plenty of legacy .coffee files out there that use require/CommonJS, including the CoffeeScript codebase itself. My plan is to treat .coffee as equivalent to .js, as in, ā€œbehave however a .js file would at this pathā€ā€”so if itā€™s in a "type": "module" package scope, itā€™s ESM. I think thatā€™s the safest approach; even if you think all TypeScript youā€™ll ever encounter uses import/export, I wouldnā€™t be surprised if there are some folks out there who have been mixing import and require statements occasionally, since presumably they currently work.

Thanks for chiming in! I think that makes sense for us as well - itā€™s what we already do for CJS - we require you to transform whatever you import to CJS before Jest will load it. And if youā€™ve opted into ESM for .js files I think itā€™s a pretty safe assumption that you want the same behavior for your ts (or .coffee, .vue) files. Happy to hear people in the modules WG are thinking along the same lines šŸ‘

Iā€™ve published jest@next now, please give it a whirl! šŸ™‚

Yeah, thatā€™s suggested syntax, itā€™s not implemented yet. Will be included in Jest 27 which will come sometime before Christmas (hopefully earlier, but I donā€™t wanna make any promises Iā€™m not able to keep)

Yeah, Iā€™m currently working (on and off) on adding native ESM support, see #9430. This issue is a small, but important part of that work šŸ™‚

What Iā€™m thinking makes sense is to make that last one behave like js, i.e. infer from the type field.

Hi, I stumbled across this issue and thought Iā€™d say hi. Iā€™m the maintainer of CoffeeScript and Iā€™ve been dealing with this same issue. In my case, there are plenty of legacy .coffee files out there that use require/CommonJS, including the CoffeeScript codebase itself. My plan is to treat .coffee as equivalent to .js, as in, ā€œbehave however a .js file would at this pathā€ā€”so if itā€™s in a "type": "module" package scope, itā€™s ESM. I think thatā€™s the safest approach; even if you think all TypeScript youā€™ll ever encounter uses import/export, I wouldnā€™t be surprised if there are some folks out there who have been mixing import and require statements occasionally, since presumably they currently work.

cc @lmiller1990

Yup. Itā€™s quite simple to fix, but I donā€™t think we want to special case .ts, .tsx or .jsx for that matter. What about people using .coffee, .vue etc.?

Right now the logic is

  • .mjs is always ESM
  • .cjs is always CJS
  • .js is ESM if closest package.json has a type: 'module' field, otherwise CJS
  • all other extensions are CJS

What Iā€™m thinking makes sense is to make that last one behave like js, i.e. infer from the type field. But it might be better to add an option that overrides this check and says ā€œtreat ts and tsx as ESMā€?

@SimenB Is it safe to use this approach for ts-jest as ts-jest only needs to transform ts/tsx to mjs/cjs depending on tsconfig target ?

Maybe only the case when transforming files from node_modules, it is necessary to check type: module in package.json

ā€˜transplantationā€™, nice autocorrect šŸ˜… but with Babel, TS is a completely different thing than CJS, if you use plugin-typescript but not plugin-transform-modules-commonjs, types get stripped but it remains ESM

yeyy tests run now šŸ˜ƒ

Only real drawback is that itā€™s a breaking change. Should be fine tho, letā€™s go for it. PR welcome, if not Iā€™ll get to this in a few weeks šŸ‘

I like that suggestion! We already support passing config to transformers, like so

{
  "transform": {
    "\\.[jt]sx?$": ["babel-jest", { "rootMode": "upward" }]
  }
}

I guess we could have a third argument which is ā€œconfig for jestā€? Then stick moduleFormat in there.

{
  "transform": {
    "\\.[jt]sx?$": [
      "babel-jest",
      { "rootMode": "upward" },
      { "moduleFormat": "ESM" }
    ]
  }
}

We could also go for your suggestion of accepting an object - simple enough to normalize the user config into that as well.

{
  "transform": {
    "\\.[jt]sx?$": {
      "transformer": "babel-jest",
      "transformerOptions": { "rootMode": "upward" },
      "moduleFormat": "ESM"
    }
  }
}

My current thinking is that a transformer should return the format of the code it has transpiled.

Right now a transformer returns:

https://github.com/facebook/jest/blob/1535af7659e0392b3f7c6124fa58d230907ee38d/packages/jest-types/src/Transform.ts#L9-L14

Iā€™m thinking in addition it can return a moduleFormat?: 'ESM' | 'CJS' which we default to CJS. Thoughts?

This one is very simple indeed and easy to do.

A problem with this approach is that it might not be possible for a transformer to know - e.g. with the babel-jest transform shipped by default we can specify to Babel that we support ESM, but the userā€™s Babel config can still transpile to CJS code. So Iā€™m back to perhaps just a top-level configuration option allowing the user to say ā€œall the files should be interpreted as ESMā€ (via some glob)

Top level configuration option is also fine by me. Currently, ts-jest has internal logic to detect whether users want to use babel-jest.

I think we can also have

{
     "\\.[jt]sx?$": {
           transformer: "babel-jest",
           moduleFormat: "ESM"
      }
}

??

Yup. Itā€™s quite simple to fix, but I donā€™t think we want to special case .ts, .tsx or .jsx for that matter. What about people using .coffee, .vue etc.?

Right now the logic is

  • .mjs is always ESM
  • .cjs is always CJS
  • .js is ESM if closest package.json has a type: 'module' field, otherwise CJS
  • all other extensions are CJS

What Iā€™m thinking makes sense is to make that last one behave like js, i.e. infer from the type field. But it might be better to add an option that overrides this check and says ā€œtreat ts and tsx as ESMā€?