外部パッケージに ESM が使われているコードを ts-jest でテストしたい
とりあえず、そのまま実行してみる。
$ yarn test src/components/post
yarn run v1.22.10
$ jest src/components/post
FAIL src/components/post/Post/Post.test.tsx
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/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/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
~/blog/node_modules/react-markdown/index.js:6
import {ReactMarkdown} from './lib/react-markdown.js'
^^^^^^
SyntaxError: Cannot use import statement outside a module
5 | import { PrismAsync as SyntaxHighlighter } from 'react-syntax-highlighter';
6 | import { css } from '@emotion/react';
> 7 | import ReactMarkdown, { Options } from 'react-markdown';
| ^
8 | import gfm from 'remark-gfm';
9 | import rehypeRaw from 'rehype-raw';
10 | import { Post as PostEntity } from '../../../entities/Post';
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1479:14)
at Object.<anonymous> (src/components/post/Post/Post.tsx:7:1)
SyntaxError: Cannot use import statement outside a module
とシンタックスエラーが発生。
react-markdown
が v7 から ESM(ECMAScript Module) で提供されるようになったが、jest は Node.js 環境で実行されるので、何も設定していないと ESM は読み込めずシンタックスエラーとなる。
CommonJS: module.exports/require
ESM: export/import
Jest では ESM をサポートするために --experimental-vm-modules
を Node.js の実行オプションとして指定する必要がある。
{
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
}
}
Node.js で ESM を有効にするために、pakcage.json に "type": "module"
を追加
{
"type": "module"
}
実行してみる。
$ yarn test src/components/post
yarn run v1.22.10
$ NODE_OPTIONS=--experimental-vm-modules jest src/components/post
ReferenceError: module is not defined
at file:///Users/tomohiro/workspace/blog/jest.config.js:1:1
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at async Loader.import (internal/modules/esm/loader.js:166:24)
at async requireOrImportModule (/Users/tomohiro/workspace/blog/node_modules/jest-util/build/requireOrImportModule.js:65:32)
at async readConfigFileAndSetRootDir (/Users/tomohiro/workspace/blog/node_modules/jest-config/build/readConfigFileAndSetRootDir.js:109:22)
at async readConfig (/Users/tomohiro/workspace/blog/node_modules/jest-config/build/index.js:227:18)
at async readConfigs (/Users/tomohiro/workspace/blog/node_modules/jest-config/build/index.js:412:26)
at async runCLI (/Users/tomohiro/workspace/blog/node_modules/@jest/core/build/cli/index.js:220:59)
at async Object.run (/Users/tomohiro/workspace/blog/node_modules/jest-cli/build/cli/index.js:163:37)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
jest.config.js が CommonJS スタイルなのでエラーになる。
module.exports = {
preset: 'ts-jest’,
(省略)
}
jest.config.js も ESM に変更
export default {
preset: 'ts-jest’,
(省略)
};
テスト実行
エラーの内容が変わった => Must use import to load ES Module: ~/blog/node_modules/react-markdown/index.js
$ yarn test src/components/post
yarn run v1.22.10
$ NODE_OPTIONS=--experimental-vm-modules jest src/components/post
(node:30646) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
FAIL src/components/post/Post/Post.test.tsx
● Test suite failed to run
Must use import to load ES Module: ~/blog/node_modules/react-markdown/index.js
5 | import { PrismAsync as SyntaxHighlighter } from 'react-syntax-highlighter';
6 | import { css } from '@emotion/react';
> 7 | import ReactMarkdown, { Options } from 'react-markdown';
| ^
8 | import gfm from 'remark-gfm';
9 | import rehypeRaw from 'rehype-raw';
10 | import { Post as PostEntity } from '../../../entities/Post';
at Runtime.requireModule (node_modules/jest-runtime/build/index.js:791:21)
at Object.<anonymous> (src/components/post/Post/Post.tsx:7:1)
at Object.<anonymous> (src/components/post/Post/Post.test.tsx:4:1)
TypeScript からコンパイルされた JS のコード が CommonJS の形式でパッケージを読み込んでいるのが原因っぽい。
// この形にコンパイルされて実行されている
const ReactMarkdown = require('react-markdown')
ts-jest の ESM のサポートを追加
export default {
preset: 'ts-jest/presets/default-esm',
(省略),
globals: {
'ts-jest': {
useESM: true,
tsconfig: 'tsconfig.test.json',
},
},
};
ts-jest/presets/default-esm
は 「(.ts, .tsx) のファイルは ts-jest で ESM のシンタックスにコンパイルされ、(.js, .jsx)はそのまま」
テスト実行。できた!
yarn run v1.22.10
$ NODE_OPTIONS=--experimental-vm-modules jest src/components/post
(node:31330) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
PASS src/components/post/Post/Post.test.tsx (6.216 s)
Post
✓ renders post title (67 ms)
"type": "module"
に変更したことで next.config.js
を上手く扱えず Next.js がビルドできない問題が発生!( ´•̥ω•̥` )
考え方を変更する。
node_modules/react-markdown を babel-jest でコンパイルして ESM => CJS に変更して読み込み可能にする。
ts-jest の presets を ts-jest/presets/js-with-babel
に変更する。
TypeScript files (.ts, .tsx) will be transformed by ts-jest to CommonJS syntax, and JavaScript files (.js, jsx) will be transformed by babel-jest.
(.ts, .tsx) は ts-jest で CJS スタイルに変換して、(.js, .jsx) は babel-jest で変換する。
module.exports = {
preset: 'ts-jest/presets/js-with-babel',
(省略),
}
jest は デフォルトで /node_modules
配下を変換の対象外としているので、 /node_modules/react-markdown
を babel-jest の変換対象として含める必要がある。
module.exports = {
// /node_modules/react-markdown だけ transform の対象とする
transformIgnorePatterns: ['/node_modules/(?!react-markdown)/'],
(省略),
}
babel-jest
は .babelrc
を設定ファイルとして認識してくれないので、babel.config.js
にファイル名を変更する。
(ここの仕様でハマっていた)
(ここは自分の勘違いで .babelrc での問題ないかも?)
.babelrc
=> babel.config.js
babelの設定は Next.js のまま
module.exports = {
presets: ['next/babel', '@emotion/babel-preset-css-prop'],
};
テスト実行
通った🎉
$ yarn test src/components/post
yarn run v1.22.10
$ jest src/components/post
PASS src/components/post/Post/Post.test.tsx (7.847 s)
Post
✓ reners post title (287 ms)
Next.js の ビルド実行
通った🎉
$ yarn build
yarn run v1.22.10
$ next build
(省略)
✨ Done in 33.52s.
VitestはESMをサポートしているので、可能であればVitestを使うのも選択肢として良さそう
Vitestに置き換えたことで、同じ状況に遭遇した時の都度対応が不要になった