拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Jest 代码转换(Code Transformation)全指南:从 babel-jest 默认转换到自定义 Transformer

Jest 代码转换Code Transformation全指南从 babel-jest 默认转换到自定义 Transformer【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest导读Jest 本身运行的是纯 JavaScript当项目中使用 JSX、TypeScript、Vue 模板等 Node.js 原生不支持的语法时就必须在测试执行前将这些代码翻译为普通 JavaScript——这个过程就是代码转换Code Transformation。本文以 Jest 29 的官方文档 CodeTransformation.md 为主线系统讲解transform配置项的作用、开箱即用的babel-jest默认转换器以及如何编写同步/异步自定义 Transformer同时结合本仓库jest-transform、babel-jest等包的源码实现帮助读者真正理解转换流程、缓存机制与性能优化手段。读完本文你将能够独立配置 Jest 的转换管线、编写符合官方接口的自定义转换器并解决语法无法识别node_modules 不转换缓存失效等常见问题。为什么需要代码转换Jest 在 Node 环境中运行项目代码但 Node.js 原生只支持标准 JavaScript。如果你的代码包含以下语法就需要在测试前将其转换为纯 JavaScript这与前端构建时对浏览器代码做转译是类似的思路JSXReact 等框架的模板语法TypeScript类型注解、接口、泛型等Vue 模板未来版本的 JavaScript 新特性如尚未被当前 Node 版本支持的新语法。Jest 通过transform配置项支持这一机制为不同的文件匹配规则RegExp指定对应的转换器transformer。转换器transformer是一个模块它提供一个用于转换源文件的方法。例如如果你想在模块或测试中使用 Node 尚不支持的新语言特性可以接入一个代码预处理器code preprocessor把未来版本的 JavaScript 转译成当前可运行的版本。Jest 会缓存转换结果并根据一系列因素尝试使缓存失效invalidate例如被转换文件的源码内容、配置是否发生变化等。这一设计保证了重复运行测试时无需反复转译大幅提升执行速度详见下文转换缓存与缓存键一节。配置入口transform配置项transform的完整类型定义见 Configuration.md为[objectstring, pathToTransformer | [pathToTransformer, object]]即一个正则表达式 → 转换器路径的映射对象。值有两种形式字符串形式\\.[jt]sx?$: babel-jest——直接指定转换器模块路径或包名元组形式\\.[jt]sx?$: [babel-jest, {rootMode: upward}]——第一个元素是转换器路径第二个元素是传给该转换器的配置对象。官方文档给出的例子是{\\.js$: [babel-jest, {rootMode: upward}]}。例如为.css文件挂一个自定义 CSS 转换器同时保留默认的 JS 转换transform: { \\.[jt]sx?$: babel-jest, \\.css$: some-css-transformer, }需要特别说明的是转换器只在文件发生变化时执行一次a transformer only runs once per file unless the file has changed。转换结果写入磁盘缓存后续运行直接读取缓存这是 Jest 测试性能的关键保证。开箱即用的默认转换器babel-jestJest 自带了唯一一个开箱即用的转换器——babel-jest。它的职责是加载你项目中的 Babel 配置babel.config.js、.babelrc等转换所有匹配/\.[jt]sx?$/正则的文件即任意.js、.jsx、.ts、.tsx文件注入 mock 提升mock hoisting所需的 Babel 插件——这正是 ES Module mocking 中jest.mock调用会被提升到模块顶部的原因。与多个转换器共存时必须显式声明:::tip 官方提示 如果你想在默认babel-jest之外再挂载其他代码预处理器务必显式包含默认的babel-jest转换器否则它会因未匹配任何正则而不会生效。 :::transform: { \\.[jt]sx?$: babel-jest, \\.css$: some-css-transformer, }源码视角babel-jest 的 createTransformer 实现查看 packages/babel-jest/src/index.ts可以看到它导出一个createTransformer工厂函数而非直接导出 Transformer 实例这是为了让用户可以在 Jest 配置中传入转换器配置。核心要点预设注入babel-jest默认会注入babel-preset-jest预设源码第 181-184 行它负责注入jest.mock提升插件。若你显式传入excludeJestPreset: true则跳过该预设——但官方明确警告这会破坏jest.mock的提升机制见 packages/babel-jest/README.md。支持标志透传它会将 Jest 传来的supportsDynamicImport、supportsStaticESM、supportsTopLevelAwait等标志合并进 Babel 的caller对象让 Babel 决定输出 ESM 还是 CJS源码第 198-214 行。覆盖率插桩当instrument为真时它会自动追加babel-plugin-istanbul插件并对cwd之外的目录不做插桩源码第 53-81 行。这一点与ScriptTransformer中的自定义插桩逻辑canInstrument标志相互配合详见下文。source mapsourceMaps: both同时生成内联与外置 source map。配置示例来自 babel-jest READMEtransform: { \\.[jt]sx?$: [babel-jest, { extends: ./babel.config.js, plugins: [babel-plugin-transform-import-meta] }] }以及关闭内置预设transform: { \\.[jt]sx?$: [babel-jest, { excludeJestPreset: true }], }编写自定义 Transformer接口全解你可以编写自己的转换器。官方文档给出了完整的接口定义这里按类型逐一拆解完整源码见 packages/jest-transform/src/types.ts。TransformOptions传给转换器的上下文interface TransformOptionsTransformerConfig unknown { supportsDynamicImport: boolean; supportsExportNamespaceFrom: boolean; /** * 取值说明 * - falseJest 未带 Node ESM 标志 --experimental-vm-modules 运行 * - true文件扩展名在 [extensionsToTreatAsEsm](https://link.gitcode.com/i/260ec5cf41f590d0fa5acafbb38a4271) * 中定义且 Jest 带 --experimental-vm-modules 运行。 * * 详见 [ECMAScript Modules](https://link.gitcode.com/i/53664b4f349e2085f4b7dd0d882e1d36) */ supportsStaticESM: boolean; supportsTopLevelAwait: boolean; instrument: boolean; /** 缓存文件系统由 jest-runtime 使用以提升性能。 */ cacheFS: Mapstring, string; /** 当前运行项目的 Jest 配置。 */ config: ProjectConfig; /** config 的字符串化版本——用于缓存失效判断。 */ configString: string; /** 用户通过 transform 配置项传给转换器的配置。 */ transformerConfig: TransformerConfig; }TransformedSource转换输出type TransformedSource { code: string; map?: RawSourceMap | string | null; };code是转换后的 JavaScript 代码map是可选的外部 source map对象或字符串形式。SyncTransformer与AsyncTransformer同步/异步两套接口interface SyncTransformerTransformerConfig unknown { canInstrument?: boolean; getCacheKey?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) string; getCacheKeyAsync?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) Promisestring; process: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) TransformedSource; processAsync?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) PromiseTransformedSource; } interface AsyncTransformerTransformerConfig unknown { canInstrument?: boolean; getCacheKey?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) string; getCacheKeyAsync?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) Promisestring; process?: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) TransformedSource; processAsync: ( sourceText: string, sourcePath: string, options: TransformOptionsTransformerConfig, ) PromiseTransformedSource; }组合后得到统一的类型type TransformerTransformerConfig unknown SyncTransformerTransformerConfig | AsyncTransformerTransformerConfig;createTransformer工厂支持配置化转换器除了直接实现Transformer接口你还可以导出createTransformer工厂函数来动态创建转换器。这样做的目的是允许在 Jest 配置中为转换器传入配置配置会被放在transform选项的元组形式里最终注入transformerConfigtype TransformerCreator X extends TransformerTransformerConfig, TransformerConfig unknown, (transformerConfig?: TransformerConfig) X; type TransformerFactoryX extends Transformer { createTransformer: TransformerCreatorX; };从源码结构看ScriptTransformer在加载转换器时会先requireOrImportModule该模块再判断模块上是否存在createTransformer函数isTransformerFactory判断见 ScriptTransformer.ts若存在则以transformerConfig为参数调用工厂得到真正的转换器实例同文件 L296-L299。babel-jest正是采用这一模式。同步 vs 异步require 与 import 的转换路径差异Jest 中有三种引入代码的方式CommonJS 的require、ESM 的静态import与动态import()动态import()在 CJS 模块中也可用。Jest 会按需on demand对文件做转换——即当某个require或import被执行时才转换。因此require触发同步转换processimport/import()触发异步转换processAsync。这正是接口同时提供process{Async}与getCacheKey{Async}两组方法的原因。其中getCacheKey{Async}先被调用用来判断是否真的需要调用process{Async}如果缓存命中就直接读取跳过转换。回退规则务必牢记异步转换可以回退到同步process若未实现processAsyncJest 会退而使用同步process同步转换不能使用异步processAsync因为require的语义决定了它不可能等待一个 Promise。据此给出两条实践建议如果你的代码库是纯 ESM实现异步变体processAsync、getCacheKeyAsync就足够了如果任何代码会通过require加载包括 ESM 内部的createRequire则必须实现同步process变体。在 types.ts 的类型注释中也有同样的说明requirewill always useprocess, andimportwill useprocessAsyncif it exists, otherwise fall back toprocess。supports*标志控制输出 ESM 还是 CJS与同步/异步直接相关的是我们传入的supports*标志即TransformOptions中supportsDynamicImport、supportsStaticESM等。这些标志应该被转换器用来决定输出 ESM 还是 CJS但与转换本身是同步还是异步没有直接关系。含义如下supportsDynamicImport: true表示转换器可以输出import()表达式这对 ESM 和 CJS 都受支持supportsStaticESM: true表示支持顶层import语句转换后的代码会被解释为 ESM 而非 CJS。这些标志来源于 Jest 运行环境supportsStaticESM为true的条件是文件扩展名在extensionsToTreatAsEsm中定义且Jest 以--experimental-vm-modules标志运行。转换缓存与缓存键性能的关键Jest 会根据以下因素使转换缓存失效官方文档列举的a number of factors被转换文件的源码source text变化Jest 配置变化configString即配置的字符串化版本是否开启覆盖率插桩instrument转换器自身声明的缓存键getCacheKey返回值。ScriptTransformer 的缓存键构造在 packages/jest-transform/src/ScriptTransformer.ts 中可以看到缓存键的实际构造逻辑若转换器实现了getCacheKey则用 SHA-1 哈希transformerCacheKey callerSupport(transformOptions) CACHE_VERSION否则退化为哈希fileData configString instrument callerSupport filename CACHE_VERSION截取前 32 位十六进制作为缓存键。缓存文件存放在jest-transform-cache-id目录下L225-L239并按缓存键首字符分子目录以避免单目录文件过多。强烈建议实现 getCacheKey虽然接口并未强制要求官方文档强烈推荐实现getCacheKey否则每次运行都不得不重新转译无法从磁盘读取上次的转换结果白白浪费资源。你可以使用jest/create-cache-key-function来帮助你实现它根据Jest 版本 项目配置 被转换文件源码 文件路径等输入计算出稳定、正确的缓存键避免手工拼装哈希出错。babel-jest的getCacheKeyFromConfig见 packages/babel-jest/src/index.ts就是这种做法的典型代表它对转换器自身源码、Babel 配置 JSON、源码文本、相对路径、configString、instrument标志、NODE_ENV/BABEL_ENV环境变量乃至 Node 版本号逐项做 SHA-1 哈希——任何一项变化都会导致缓存失效从而保证缓存结果永远与转换产物一致。开发期排障--no-cache在开发一个转换器期间可以配合--no-cache运行 Jest 来绕过缓存避免旧缓存干扰调试。当怀疑缓存异常时还可以直接清空 Jest 缓存目录详见 Troubleshooting.md 中的缓存问题处理。作用域控制node_modules 默认不转换需要特别警惕默认配置下node_modules是不参与转换的。如果你想转换node_modules中的文件必须修改transformIgnorePatterns配置。transformIgnorePatterns的默认值是[/node_modules/, \\.pnp\\.[^\\/]$]见 Configuration.md。它是一组正则字符串与所有源文件路径匹配只要路径匹配其中任意一个模式该文件就不会被转换。常见场景React Native 或 TypeScript 项目中某些第三方模块以未转译的源码发布默认规则下 Jest 会因无法理解这些语法而报错。此时可用如下方式放行特定包// jest.config.js /** type {import(jest).Config} */ const config { transformIgnorePatterns: [/node_modules/(?!(foo|bar)/)], }; module.exports config;该模式表示/node_modules/下的文件除foo/、bar/两个目录外都不转换。官方同时给出一个反例警示——模式互相重叠会导致意外的不转换transformIgnorePatterns: [/node_modules/(?!(foo|bar)/), /bar/],第二个模式会匹配任何包含/bar/的路径导致/node_modules/bar/下的文件即便被第一个模式豁免仍然因命中第二个模式而不被转换。更稳妥的做法是使用rootDir令牌限定根目录避免在不同环境下因根路径差异而误伤全部文件const config { transformIgnorePatterns: [ rootDir/bower_components/, rootDir/node_modules/, ], };另外注意transform只会对匹配transform正则的扩展名生效而transformIgnorePatterns是第二道过滤——两个条件同时满足才执行转换。这一判断逻辑可以在 ScriptTransformer.ts 的calcTransformRegExp与calcIgnorePatternRegExp构造ProjectCache时生成L102-L105以及shouldTransform中看到对应实现。返回 Source Map可调试性的最佳实践官方文档给出两条重要建议确保process{Async}返回 source map与转换后的代码一起这样代码覆盖率报告和测试错误才能准确定位到原始源码的行号信息而非转换后的代码内联 source map 也能工作但性能更慢优先返回外部 map 对象。从实现上看若转换器没有返回 mapScriptTransformer会尝试从转换产物中解析内联 source mapsourcemapFromSource失败时输出makeInvalidSourceMapWarning警告见 ScriptTransformer.ts。同时source map 还决定了覆盖率插桩的质量只有当转换产生了 source map 时istanbul 插桩结果才能映射回原始代码同文件 L444-L456 的shouldEmitSourceMaps逻辑如果转换器声明了canInstrument: true则插桩责任完全交给转换器自己例如 ts-jest 直接输出已插桩代码Jest 不会再额外插桩。实战示例示例一需要类型检查的 TypeScript默认的babel-jest虽然会转译 TypeScript 文件但Babel 不做类型检查——它只剥离类型注解不验证类型正确性。如果你希望测试时进行真正的类型检查可以使用ts-jest与 coverage-remapping 等端到端用例它们演示了 TypeScript 转换与覆盖率/源码映射的配合方式。示例二把图片转换为路径字符串在浏览器构建中import img from ./logo.png会把图片作为资源打包但图片本身不是合法 JavaScriptJest 解析到这类导入时会报语法错误。一种处理方式是编写转换器把导入的图片值替换为其文件名const path require(path); module.exports { process(sourceText, sourcePath, options) { return { code: module.exports ${JSON.stringify(path.basename(sourcePath))};, }; }, };然后在jest.config.js中将其挂到常见静态资源扩展名上module.exports { transform: { \\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$: rootDir/fileTransformer.js, }, };注意这里的转换器导出的正是process(sourceText, sourcePath, options) {code, map?}的最小形态——只需实现一个同步process方法即可满足最常见的使用方式这类无需缓存键的简单转换器Jest 会退化为默认缓存键逻辑见上文。真实仓库参考handlebars 转换器本仓库的端到端测试 e2e/coverage-handlebars/transform-handlebars.js 提供了一个真实的自定义转换器样例它把.hbs模板文件转换为可执行的 JavaScript 模块并配套展示了coverageProvider与自定义转换器组合下的覆盖率收集行为——这正是编写自定义 Transformer 以支持非 JS 语法在真实项目中的完整落地形态值得参考。常见问题速查问题原因解决方案测试报 Unexpected token 语法错误文件匹配了transform正则但未命中任何转换器或转换器未处理该语法检查transform正则覆盖范围确认自定义转换器实现正确必要时用--no-cache排除旧缓存干扰第三方包中的 JSX/TS 语法报错node_modules默认不转换修改transformIgnorePatterns放行特定包如[/node_modules/(?!(foo|bar)/)]jest.mock提升不生效自定义 Babel 配置排除了babel-preset-jestexcludeJestPreset: true恢复默认预设或改用支持 mock 提升的转换方案覆盖率行号定位不准转换器未返回 source map让process{Async}返回map字段优先使用外部 map 而非内联 map测试运行缓慢、反复转译未实现getCacheKey缓存键不稳定实现getCacheKey推荐使用jest/create-cache-key-function纯 ESM 项目仍被要求实现同步process某些依赖通过require/createRequire加载补实现同步process变体回退规则异步可退同步同步不可用异步结语代码转换是 Jest 支持现代前端语法JSX、TypeScript、Vue 等的基石默认的babel-jest覆盖了最常见的.js/.jsx/.ts/.tsx场景并自动处理 mock 提升当默认能力不够时你可以依据SyncTransformer/AsyncTransformer/createTransformer这套公开接口编写任意语法的转换器并通过getCacheKey与 source map 保证性能与调试体验。理解同步/异步回退规则、supports*标志语义与transformIgnorePatterns作用域是写出健壮转换配置的关键。更多底层细节可继续阅读本仓库的 jest-transform 源码 与 ECMAScript Modules 文档。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门