4

I created an app within a monorepo (Lerna) using yarn workspace.

The architecture of the app is as follow:

my-monorepo
├── node_modules
├── packages
│   ├── package1(shared components)
│   ├── package2(other package consuming the shared components)
│   │     └── ./jest.config.js
├── package.json

The problem

the problem is jest is throwing the following error when trying to use package1 in package2 in any test, and I haven't found a way to fix it.

● Test suite failed to run

    Jest encountered an unexpected token

    This usually means that you are trying to import a file which Jest ca
nnot parse, e.g. it's not plain JavaScript.

    By default, if Jest sees a Babel config, it will use that to transfor
m 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 spe
cify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option i
n 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:

    C:\Users\my-user\Desktop\my-monorepo\node_module
s\antd\es\date-picker\generatePicker\index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__fi
lename,global,jest){import _extends from "@babel/runtime/helpers/esm/exte
nds";

                    ^^^^^^

According the error, I'm trying to import a file which Jest cannot parse, so the problem comes from the package1, so the first thing that comes to my mind is: maybe I'm doing something wrong in rollup in package1 and the final bundle comes in some format that jest doesn't understand...

Jest config

Jest config located in package2, where i want to consume package1:

// jest.config.js in package2

const config = {
  roots: ['src'],
  setupFilesAfterEnv: ['./jest.setup.ts'],
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  testPathIgnorePatterns: ['node_modules/'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testMatch: ['**/*.test.(ts|tsx)'],
  moduleNameMapper: {
    // Mocks out all these file formats when tests are run.
    '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      'identity-obj-proxy',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
};

export default config;

Rollup config

This is the rollup configuration in package1:

// rollup.config.js

import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import copy from 'rollup-plugin-copy';
import json from '@rollup/plugin-json';
import svgr from '@svgr/rollup';
import { babel } from '@rollup/plugin-babel';
import { visualizer } from 'rollup-plugin-visualizer';

import pkg from './package.json';

export default {
  input: 'src/index.tsx',
  output: [
    {
      file: pkg.main,
      format: 'cjs',
      exports: 'named',
      sourcemap: true,
    },
    {
      file: pkg.module,
      format: 'esm',
      exports: 'named',
      sourcemap: true,
    },
  ],
  plugins: [
    peerDepsExternal({
      includeDependencies: true,
    }),
    json(),
    svgr(),
    resolve({ extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] }),
    commonjs({
      include: /\**node_modules\**/,
    }),
    // UPDATE 3: Add babel
    babel({
      babelHelpers: 'bundled',
    }),
    typescript({
      useTsconfigDeclarationDir: true,
      exclude: ['*.d.ts', '**/*.d.ts', '**/*.test.tsx'],
      rollupCommonJSResolveHack: true,
      clean: true,
    }),
    postcss({
      extensions: ['.css', '.less'],
      use: {
        less: { javascriptEnabled: true, modifyVars: {} },
      },
    }),
    visualizer({ filename: 'stats-visualizer.html', gzipSize: true }),
    copy({
      targets: [
        {
          src: 'src/styles/themes/dark-variables.less',
          dest: 'dist',
          rename: 'theme/dark.less',
        },
        {
          src: 'src/styles/themes/light-variables.less',
          dest: 'dist',
          rename: 'theme/light.less',
        },
        {
          src: 'src/assets/icons',
          dest: 'dist',
          rename: 'assets/icons',
        },
      ],
    }),
  ],
};

UPDATE 1:

I've tried to use transform rules in jest.config.js as mentioned by Matt Carlota to transpile antdbut this doesn't work:

// jest.config.js
const config = {
  // ... other jest settings
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
    'node_modules/antd/.+\\.(j|t)sx?$': 'ts-jest',
  },
  // I've tried with `antd/es` pattern too and doesn't work
  transformIgnorePatterns: ['node_modules/(?!antd)'],
};

UPDATE 2:

Change manually antd/es by antd/lib in package1 resolve the problem temporarily, but there is one problem and that is that we are a large group of people working and it could be put as a convention to use only antd/lib but I feel it would be error prone.

every time someone forgets to use antd/lib and uses antd/en all tests break with the original error.

UPDATE 3:

Add babel config file and plugin in rollup configuration...

// babel.config.js

module.exports = {
  plugins: [['import', { libraryName: 'antd', libraryDirectory: 'lib' }, 'antd']],
};

Cristian Flórez
  • 2,277
  • 5
  • 29
  • 51
  • 2
    [Similar question here](https://stackoverflow.com/questions/60714101/how-to-setup-jest-with-node-modules-that-use-es6). In short, you have to either: Transpile `antd` using babel, transpile using babel-jest (like Victors answer, although instead of creating an inverse rule, I'd recommend specifying it as a `transform` rule for just `node_modules/antd/es`) for jest, or import from `antd/lib` instead of `antd/es` (you can either directly import from `lib` or use [antd babel plugin](https://github.com/ant-design/babel-plugin-import), which targets the `lib` directory by default. – Matt Carlotta Aug 20 '21 at 23:59
  • @MattCarlotta I tried some of your recommendations, try using `transformIgnorePatterns: ['node_modules/(?!antd)']` in `jest.config.js` in `package-two` where I want to use `package-one` but this doesn't work, may be I'm doing somethign wrong. I think that transform option is the best one. can u give me a more detailed exampled with transform please or with other options that you mention. I update mi question with thing that I test... – Cristian Flórez Aug 22 '21 at 00:34
  • Some bad news, unfortunately... this goes much deeper. Apparently, `antd/es` is not the only problem. It's also: `rc-util/es`, `@ant-design/icons/es`, `rc-pagination/es`, and `rc-picker/es`. This list only covers importing `DatePicker` from `antd/es`. If you import more items from `antd/es`, I'd expect more ESM dependencies to be added to this list. The easiest workaround is import directly from `antd/lib`, which imports CJS instead of ESM. Unfortunately, jest is lagging behind when it comes to ESM support, so expect to run into a lot more third-party ESM roadblocks in the near future. – Matt Carlotta Aug 22 '21 at 02:31
  • Manually switching from `antd/es` to `antd/lib` could be a solution. there is one problem and that is that we are a large group of people working and it could be put as a convention to use only `antd/lib` but I feel it would be error prone. The final recommendation would then be to use the `antd babel plugin` in the package which you want to export to be consumed by other packages, in my case `package-one`? – Cristian Flórez Aug 24 '21 at 17:48
  • Ideally, you shouldn't be packaging anything from a 3rd party dependency into your own package. It should be marked as a `peerDependency` with version constraints: [for example](https://github.com/mattcarlotta/composable-styled-components/blob/master/package.json#L68-L72) or it can marked as a `dependency` of your internal package, but it should still not be bundled. A peerDependency makes it so that a developer must manually install it; while a dependency will be automatically installed when `npm install` or `yarn install` is run. – Matt Carlotta Aug 24 '21 at 19:14
  • but then the solution is not clear to me, if you could give me a more detailed answer I would appreciate it. I was able to solve the problem by changing in **package-one** `antd/es` for `antd/lib` but as I mentioned in my previous comment many people working one or another can go back to using `antd/es` and doing the manual change is not scalable, whenever someone forgets to `put antd/lib` and uses `antd/es` the tests in **package-two** break giving the problem mentioned in the question. – Cristian Flórez Aug 24 '21 at 19:21
  • I see. It's not recommended to package 3rd party libraries because of potential bugs and you may be packaging the entire 3rd party library (you'll have to check the build output to make sure it's just bundling the necessary code). By packaging it into your internal packge, you're now responsible for maintaining the antd library (any bugs/fixes to antd requires that you have to update the antd dependency and build a new internal package). Either way, you're going to be stuck: 1.) Custom babel config that uses `antd/lib` or 2.) Maintaining/rebuilding the package each time `antd` is updated. – Matt Carlotta Aug 24 '21 at 19:48
  • bad news, I use `babel-plugin-import` plugin in **package-one** rollup configuration and that did not solve the error, If I try to use in **package-two** again thrown the original error... – Cristian Flórez Aug 24 '21 at 21:05
  • The babel plugin has to be used across all projects that import from `antd`. Otherwise, it'll continue to import from `antd/es`. If you're still getting an ESM error, then your babel configuration is not set up correctly/not being used. Also forgot to mention, you may need to use `babel-jest` instead of `ts-jest` (unless you can configure ts-jest to use your babel config). – Matt Carlotta Aug 24 '21 at 22:26
  • @CristianFlórez could you share your babel config file? I think the problem lies in that file. Note: Jest sees the babel config before transforming any javascript and typescript file. – Subrato Pattanaik Aug 25 '21 at 10:20
  • 1
    @SubratoPatnaik I'm not using babel for now... if you check the rollup config file babel is not present... in no package is babel being used, only rollup. – Cristian Flórez Aug 25 '21 at 16:12
  • So, are you using Typescript to compile all your file into js file? – Subrato Pattanaik Aug 25 '21 at 16:23
  • yes, with typescript rollup [plugin](https://www.npmjs.com/package/rollup-plugin-typescript2). I added babel to the equation to see if I could shed some light on this but still with the same result, babel is not applying the plugin transformations... Check the question I update the rollup configuration... and set babel... – Cristian Flórez Aug 25 '21 at 17:02
  • I have a similar repo but with mocha, webpack etc. I use the following configuration and it works https://github.com/tkey/tkey/blob/master/test/setup.js. Use ts-config-paths plugin to dynamically resolve the other package and use @babel/register + ts-node for transpilation. Hope that helps – Skyuppercut Aug 31 '21 at 12:59

1 Answers1

0

I've been having the same issue for the longest time and finally found a way.

The missing piece in my case was adding moduleNameMapper. A guide mentioned that here.

The solution doesn't require babel-jest.

jest.config.js

const path = require('path');
const { lstatSync, readdirSync } = require('fs');
// get listing of packages in the mono repo
const basePath = path.resolve(__dirname, '..', '..', 'packages');
const packages = readdirSync(basePath).filter((name) => {
  return lstatSync(path.join(basePath, name)).isDirectory();
});

module.exports = {
  preset: 'ts-jest',
  verbose: true,
  moduleFileExtensions: ['js', 'json', 'jsx', 'node', 'ts', 'tsx'],
  moduleDirectories: ['node_modules', 'src'],
  moduleNameMapper: {
    ...packages.reduce(
      (acc, name) => ({
        ...acc,
        [`@xyz/${name}(.*)$`]: `<rootDir>/../../packages/./${name}/src/$1`,
      }),
      {}
    ),
  },
  rootDir: './',
  testRegex: '.spec.ts$',
  transform: {
    '^.+\\.(t)s$': 'ts-jest',
  },
  testEnvironment: 'node',
  setupFilesAfterEnv: ['./jest.setup.ts'],
  testMatch: null,
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.jest.json',
    },
  },
  roots: ['<rootDir>'],
  transformIgnorePatterns: [],
  collectCoverage: false,
  collectCoverageFrom: ['src/**/*.{js{,x},ts{,x}}', '!src/server/index.ts'],
};

tsconfig.json

{
  "compilerOptions": {
    "module": "esnext",
    "target": "ES2019",
    "lib": ["ES2019", "DOM"],
    "noEmit": true,
    "types": ["node", "jest"],
    "rootDir": "./"
    "paths": {
      "@xyz/*": ["packages/*/src"]
    },
    
  },
  "include": ["test/**/*.ts", "src/**/*.ts", "**/*.spec.ts"],
  "references": [
    { "path": "../../packages/package1" },
  ]
}

package.json

...
scripts: {
  "test": "NODE_ENV=production tsc --build ./tsconfig.jest.json && jest --env=node test --watch",
}
bhr
  • 2,279
  • 1
  • 23
  • 31