最近项目工程中成员反馈单测运行报错,看报错信息其实显而易见,但当魔法叠加魔法后,解决问题还是花了不少时间,同时也发现了一些有趣的事情。
原因
报错原因和你直接使用 jest 去测试 esm 模块是一样的,你需要 babel 转译成 cjs 模块才能顺利完成测试。
项目使用 cra 进行创建,jest 相关的配置,脚手架已经帮我们内化了,通常不会报这种错误,这时候报这种错误的原因就是,使用的第三方模块是 esm 模块导致的。因为无论是 jest 的默认配置,还是 cra 里面提供的配置项 transformIgnorePatterns
都会将 node_modules 过滤掉,因为没有进行 babel 转译,从而报错了。
OK,既然知道了原因,那么解决起来应该很轻松,且 jest 官方文档中也提供了说明,甚至提供了 React Native 的案例如下
{
"transformIgnorePatterns": [
"node_modules/(?!(react-native|my-project|react-native-button)/)"
]
}
尝试解决
我项目中主要是两个模块导致的
- three 模块:项目中使用 three 中 example 封装好的 esm 模块,具体路径为:
three/examples/jsm
- antd 模块:自己的子包中有使用 antd 组件,经由
babel-plugin-import
后会转为使用 antd/es 模块
如果只是单独解决 antd 的问题,可以通过 moduleNameMapper 的配置项进行解决,因为 antd 同时提供了 lib 模块,通过如下配置将 es 使用映射为 lib 即可,具体如下
{
"moduleNameMapper": {
"^antd/es/(.*)$": "antd/lib/$1",
}
}
的确,通过 package.json 中设置 jest 字段,然后添加 moduleNameMapper
配置后,关于 antd 的报错就不见了。
但上述方式对于 three/examples/jsm
并不生效,因为其并没有提供 cjs 的模块,没办法,只能通过设置 transformIgnorePatterns
的方式了,于似乎设置如下
{
"transformIgnorePatterns": [
"node_modules/(?!(three/examples/jsm)/)"
]
}
但解决并不如人意,依旧报错,尝试过各种可能性,比如怀疑 jest 对于 transformIgnorePatterns
的支持度是不是有问题,但都没有生效,于是怀疑是不是 cra 传给 jest 的配置有问题,于是只能深入源码查看一下,接下来才是有趣的部分。
react-scripts
react-scripts 处理 jest 的逻辑十分简单,test.js 模块调用 createJestConfig.js 模块获取配置,组装好相关参数后,调用 jest 的 API 执行。
于是乎,我在 createJestConfig 模块中进行相关打印
module.exports = () => {
//…… logic
console.log(config);
return config;
}
打印出来的结果发现,package.json 下的 jest 配置没有生效,得到的还是 cra 中内置的 jest 配置。如果我继续打印 package.json 中内容如下
module.exports = () => {
//…… logic
console.log(paths.appPackageJson);
//…… logic
console.log(config);
return config;
}
打印结果中得到 package.json 中所有内容,唯独没有 jest 字段,哎,可是我关于 antd 配置是生效的了哇,不服气,继续在 jest 中打印 createJestConfig 返回值。
const createJestConfig = require('./utils/createJestConfig');
console.log(createJestConfig(
relativePath => path.resolve(__dirname, '..', relativePath),
path.resolve(paths.appSrc, '..'),
false
))
有点颠覆三观事情发生了,配置生效了,但并不完全生效,为什么说不完全生效呢,因为 transformIgnorePatterns 的配置不符合预期,结果为
{
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$",
"node_modules/(?!(three/examples/jsm)/)"
]
}
由于两个 node_modules 同时存在,按照 jest 定义的规则,类似并集的操作,因此 node_modules 还是被完全过滤掉了,为什么说不符合呢,因为 createJestConfig 中明确写明了,对于 array 类型,会直接整个替换,而现在被合并了。createJestConfig 中代码如下
supportedKeys.forEach(key => {
if (Object.prototype.hasOwnProperty.call(overrides, key)) {
console.log(overrides[key])
if (Array.isArray(config[key]) || typeof config[key] !== 'object') {
// for arrays or primitive types, directly override the config key
config[key] = overrides[key];
} else {
// for object types, extend gracefully
config[key] = Object.assign({}, config[key], overrides[key]);
}
delete overrides[key];
}
});
震惊之余,突然想起我并不是直接通过 react-script 进行 test 的,而是使用了 react-app-rewired
模块。
react-app-rewired
之所以发生这种现象,是因为项目使用 react-app-rewired
去动态修改 webpack、jest 等相关配置。接下来直接看一段源码,解释为什么是上述的表现
// hide overrides in package.json for CRA's original createJestConfig
const packageJson = require(paths.appPackageJson);
const jestOverrides = packageJson.jest;
delete packageJson.jest;
// load original createJestConfig
const createJestConfig = require(createJestConfigPath);
// run original createJestConfig
const config = createJestConfig(
relativePath => path.resolve(paths.appPath, "node_modules", paths.scriptVersion, relativePath),
path.resolve(paths.appSrc, ".."),
false
);
// restore overrides for rewireJestConfig
packageJson.jest = jestOverrides;
// override createJestConfig in memory
require.cache[require.resolve(createJestConfigPath)].exports =
() => overrides.jest(rewireJestConfig(config));
// Passing the --scripts-version on to the original test script can result
// in the test script rejecting it as an invalid option. So strip it out of
// the command line arguments before invoking the test script.
if (paths.customScriptsIndex > -1) {
process.argv.splice(paths.customScriptsIndex, 2);
}
// run original script
require(paths.scriptVersion + '/scripts/test');
相关解释
- 正是由于它会将 package.json jest 字段进行删除,所以我们在 createJestConfig 打印时,只能得到默认的配置
- 那为什么在方法外打印显示生效了呢,因为它会使用 jest 配置进行整合,得到最终配置后,使用关键魔法 require.cache 对模块进行了动态修改
- 那为什么又不符合预期呢,因为它的 rewire 规则和 cra 不一样,具体看 rewireJestConfig 源码
rewireJestConfig 源码
// Jest configuration in package.json will be added to the the default config
Object.keys(overrides)
.forEach(key => {
//We don't overwrite the default config, but add to each property if not a string
if(key in config) {
if(typeof overrides[key] === 'string' || typeof overrides[key] === 'number' || typeof overrides[key] === 'boolean') {
config[key] = overrides[key];
} else if(Array.isArray(overrides[key])) {
config[key] = overrides[key].concat(config[key]);
} else if(typeof overrides[key] === 'object') {
config[key] = Object.assign({}, config[key], overrides[key]);
}
} else {
config[key] = overrides[key];
}
});
总结
所有不可思议的事情得到了解释,要解决问题,可以通过手动声明 overrides.jest 字段的方式进行修改,这样就能完全管控了。