mickelsonmichael / dev

0 stars 0 forks source link

Jest tests `SyntaxError: Cannot use import statement outside a module` #8

Closed mickelsonmichael closed 1 year ago

mickelsonmichael commented 1 year ago

When executing Jest tests with a particular proprietary library, I was receiving the following exception(s). For security purposes, I have obfuscated the official package names and paths.

Jest encountered an unexpected token
    This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
    By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/en/configuration.html
    Details:
    /builds/project/node_modules/@some-package/style-utils/lib/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import SomeExport from "./SomeFile";
                                                                                             ^^^^^^
    SyntaxError: Cannot use import statement outside a module
      1 | import React from "react";
    > 2 | import SomeModule from "@some-package/style-utils";
        | ^
      3 | import { render, waitFor } from "@testing-library/react";
      4 | import { mocked } from "jest-mock";
      5 |
      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14)
      at Object.<anonymous> (src/components/SomeComponent.test.tsx:2:1)

Or, a similar exception

    This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
    By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/en/configuration.html
    Details:
    /builds/project/node_modules/@some-package/lib/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export { default as Alert } from "./components/Alert";
                                                                                             ^^^^^^
    SyntaxError: Unexpected token 'export'
      1 | import React from "react";
      2 | import { render } from "@testing-library/react";
    > 3 | import { SomeModule } from "@some-package";
        | ^
      4 |
      5 | import Welcome from "./components/Welcome";
      6 |
      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14)
      at Object.<anonymous> (src/App.test.tsx:3:1)
mickelsonmichael commented 1 year ago

I found a post suggesting to use --transformIgnorePatterns and attempted to place this logic into the package.json but was getting additional errors. It seems like the package is still not being transformed.

"jest": {
  "transformIgnorePatterns: "node_modules/(?!@some-package)"
}

However, putting it in the arguments to the scripts:test command worked.

mickelsonmichael commented 1 year ago

I found another post suggesting that create-react-app does some overwriting of the Jest configuration. Looking through it's clear that it does in fact replace the default values of transformIgnorePatterns (and any other configurations) with the ones the user provides in the package.json. So, in theory, putting the patterns in the package.json should work.

More investigation uncovered that react-app-rewired actually overrides the Jest handling by create-react-app, and in it's processing, it does a concatenation of any arrays (including transformIgnorePatterns) rather than a replacement. Meaning that the default values provided by create-react-app were being maintained and the node_modules directory was still being ignored (since the latter patterns do not take precedence).

I have found an issue on GitHub corroborating this issue, where the solution was to add a line to the documentation pointing out this issue.

Note: Configuration arrays and objects are merged, rather than overwritten. See https://github.com/timarney/react-app-rewired/issues/240 and https://github.com/timarney/react-app-rewired/issues/241 for details

The actual solution is to either use the scripts method in the package.json:

"scripts": {
  "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!your-module-name)/\"",
}

Or, alternatively, to keep the scripts clean, you can add the ignore pattern to the react-app-rewired config-overrides.js:

const jestConfig = (c) => ({
  ...c,
  transformIgnorePatterns: [
    ...c.transformIgnorePatterns.reduce(
      (prev, current) =>
        current.includes("node_modules") ? prev : [...prev, current],
      []
    ),
    "[/\\\\]node_modules[/\\\\](?!@some-package).+\\.(js|jsx|mjs|cjs|ts|tsx)$",
  ],
});

The latter code snippet allows for the addition of a new pattern while still maintaining the original patterns.

mickelsonmichael commented 1 year ago

One more discovery, in order to properly attach the jestConfig function, you need to be sure it is a property of the returned exports. We are leveraging the customize-cra package, which creates a single function using the override method. So, counter-intuitively, you need to add the jest config function as a property of that function (see config-overrides.js from the react-app-rewired source code).

const options = override(); // some combination of plugins

const jestConfig = (c) => c; // some function like the one above that updates the config

options.jest = jestConfig;

module.export = option;

And it is important to note that the ... syntax does not function properly in this scenario. So, direct assignment is required.

mickelsonmichael commented 12 months ago

Encountered this issue later down the road in a different project, and found there's a new potential thread.

When using Babel to compile the tests, you cannot utilize a .babelrc file if you intend to have any node_modules assets compiled as well. You must use a babel.config.json in order for them to be compiled directly.

See this line from the Babel documentation:

You want to compile node_modules? babel.config.json is for you!