- 测试
- 开发工具
【免费下载链接】ts-jest
A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
本文是 ts-jest 仓库内部技术文档 processing.md 的深度解读。全文以文档中的两张流程图(Jest 处理流程与 ts-jest 处理流程)为骨架,结合 src/legacy/ts-jest-transformer.ts、src/legacy/compiler/ts-compiler.ts 等源码实现,逐层拆解一个 TypeScript 文件从被
require到产出可执行代码所经过的全部环节,帮助你理解缓存机制、isolatedModules 分支、AST 变换(jest.mock 提升)、source map 修复、Babel 接力与 afterProcess 钩子等核心设计,从而在排查缓存失效、诊断报错与性能问题时有的放矢。
说明:本文属于 ts-jest 贡献者向的“内部技术文档”范畴(原文档在开头即注明:如果你只是使用 ts-jest 而非为其贡献代码,这里的内容对你没有直接价值)。普通使用者重点阅读官方文档即可;本文面向希望深入理解其内部实现的读者。
一、总览:一次文件转换的两级流水线
ts-jest 本质上是一个实现了 JestSyncTransformer接口的 transformer(见 src/index.ts 中createTransformer返回的 TsJestTransformer)。任何被测试代码require('file')的文件,都会先后经过两条流水线:
- Jest 自身的 transformer 调度流程:决定是否需要 transform、如何计算缓存 key、缓存命中还是调用
process; - ts-jest 内部的处理流程:在
tsJest.process(source)中完成字符串化、声明文件处理、TS 编译、AST 变换、source map 修复、可选 Babel 接力与 afterProcess 钩子。
下面两张图分别对应原文档中的两个 PlantUML 流程(此处以文字形式还原其分支结构),后续章节将逐一展开每个节点背后的源码证据。
1.1 Jest 侧流程(require 触发)
start → require('file') → 存在 transform ? → 是:transformer 实现了 getCacheKey ? → 是:调用 transformer.getCacheKey(...) → 否:使用 Jest 内置 cache key 逻辑 → 缓存命中? → 是:直接使用缓存内容 → 否:调用 transformer.process(...) 并更新缓存 → 执行 require() end1.2 ts-jest 侧流程(process 内部)
start → tsJest.process(source) → should stringify ?(stringifyContentPathRegex 命中) → 是:JSON stringify,更新 source → 文件名以 .d.ts 结尾? → 是:清空 source(声明文件无需编译) → 否: → isolated modules ?(compiler 缓存) → 否:创建并缓存 TS Language Service → persistent cache 命中? → 是:从持久化缓存恢复内存缓存 → 否: → isolated modules ? → 是:用 transpileModule 编译(按隔离模块处理) → 否:用 Language Service 编译(读取文件时使用内存缓存) → 执行自定义 AST transformers(jest.mock 提升 + 用户自定义变换) → 得到编译产物 → 修复 source maps → 更新内存缓存 → 更新持久化缓存 → 更新 source → should use babel ? → 是:调用 babelJest.process(source)(babel-jest 处理器接力) → 存在 afterProcess hook ? → 是:调用 hook;若返回新值则作为新的 source → 输出 transformed source end二、Jest 侧调度:getCacheKey 与缓存策略
原文档第一张图描述了 Jest 在加载每个模块前的标准调度。理解这张图需要知道:Jest 的 transform 框架先通过getCacheKey计算缓存标识,命中缓存则跳过编译,未命中才调用process并回写缓存。
在 ts-jest 中,getCacheKey的实现位于 src/legacy/ts-jest-transformer.ts,其缓存 key 由以下要素拼接后经 SHA-1 哈希得到:
- 序列化后的 Jest 配置(含 ts-jest 的配置后缀,见
_configsFor中的_transformCfgStr); rootDir;instrument标志(ts-jest 始终关闭插桩,交由 Jest 后续处理);supportsStaticESM标志;- 文件内容与文件路径;
- 当未启用 isolatedModules且配置了
tsCacheDir时,还会附加该文件所有已解析依赖模块的路径及其mtimeMs(文件修改时间),见 ts-jest-transformer.ts。这意味着任何一个被 import 的依赖发生变化,缓存 key 都会失效——这正是 watch 模式下 ts-jest 能够感知依赖变更的关键。
与此配套的配置缓存机制在 ts-jest-transformer.ts 的_configsFor中:ts-jest 会在多次测试运行之间缓存ConfigSet与编译器实例(_cachedConfigSets静态数组),避免每个文件都重新解析 tsconfig 和重建编译环境。这里还有一个值得注意的细节:Jest 先以字符串化配置调用getCacheKey,随后才传入真正的配置对象,ts-jest 通过序列化字符串匹配来复用已缓存的配置集。
三、ts-jest.process 的入口分流
process方法(src/legacy/ts-jest-transformer.ts)是 ts-jest 内部流程的起点,其内部核心逻辑在processWithTs(同文件 L210-L273)中,按文件类型与配置做三级分流:
3.1 字符串化(stringifyContentPathRegex)
当文件路径命中配置项stringifyContentPathRegex(在 config-set.ts 中归一化为正则_stringifyContentRegExp),ts-jest 不会编译该文件,而是直接将其内容序列化为module.exports=<stringify(sourceText)>。这在处理 JSON、SVG、CSS 等非代码资源时非常有用,shouldStringifyContent的实现见 config-set.ts。
3.2 声明文件(.d.ts)直接清空
若文件以.d.ts结尾(DECLARATION_TYPE_EXT),ts-jest 直接返回空代码,不进入编译管线——声明文件只描述类型,无需生成运行时代码。若误将.d.ts交给语言服务编译,TypeScript 会报UnableToRequireDefinitionFile错误(见 ts-compiler.ts),因此前置拦截是必要的。
3.3 JS/TS 文件的编译分发
- node_modules 中的 JS 文件:直接调用
ts.transpileModule快速转换(ts-jest-transformer.ts),并根据 ESM 模式决定module为ESNext还是CommonJS; - 普通 TS/TSX/JS 文件:交给
_compiler.getCompiledOutput(...)(经由 TsJestCompiler 转发到 TsCompiler); - 其他扩展名:不编译,原样返回并记录警告(若配置了 Babel 则提示应改用 babel-jest 处理该扩展名)。
四、compiler(cached):语言服务与隔离模块两条路径
原文档图中compiler (cached)泳道揭示了 ts-jest 编译器的核心架构:非隔离模式使用 TypeScript Language Service,隔离模式使用 transpileModule。
4.1 非隔离模式:Language Service + 内存缓存
在 TsCompiler 构造函数 中,当configSet.isolatedModules为假时,ts-jest 会构建:
- 内存文件内容缓存
_fileContentCache与文件版本缓存_fileVersionCache; - 模块解析缓存(
createModuleResolutionCache)与 memoize 化的readFile/fileExists等宿主接口; - 一个完整的
LanguageService(ts-compiler.ts),其getScriptSnapshot按“内存缓存 → Jest 运行时 cacheFS → 磁盘读取”的优先级取文件内容(L564-L591),这正是原文档图中“mem cache 用于读取文件”的注释所指。
编译时(getCompiledOutput,L356-L468)先_updateMemoryCache更新内存缓存(文件内容变化才递增版本号并提升_projectVersion,见 L730-L762),再通过getEmitOutput产出代码,同时收集语义/语法诊断(L767-L782)。非隔离模式因此具备跨文件的类型检查能力——这是它比 transpileModule 慢但更“完整”的原因。
4.2 隔离模式:transpileModule 快速编译
当isolatedModules为真时,不创建语言服务,直接走_transpileOutput(ts-compiler.ts):每个文件按隔离模块独立编译,不做跨文件类型检查,速度显著提升,代价是类型错误可能被漏报。原文档图中“files will be compiled as isolated modules”的注释即指此路径。相关配置项说明见官方文档 isolatedModules.md。
4.3 编译器选项的运行时修正
值得补充的一点是:ts-jest 在编译前会对用户 tsconfig 做“运行时修正”(fixupCompilerOptionsForModuleKind,ts-compiler.ts):CJS 路径强制module: CommonJS,ESM 路径保留 ESNext;同时根据 TypeScript 版本校验moduleResolution与customConditions的合法组合(L261-L354),避免产生 TypeScript 不接受的配置组合(如CommonJS + Bundler在 TS<6 下会触发 TS5095)。
五、自定义 AST transformers:jest.mock 提升发生的地方
编译完成后,无论走哪条编译路径,ts-jest 都会应用自定义 AST transformers(_makeTransformers,ts-compiler.ts),把before、after、afterDeclarations三类变换器工厂注入 TypeScript 编译流程。原文档图中“here is where hoisting of jest.mock is done, as well as user-defined transformations based on config”正是对这一环节的注脚。
默认注入的第一个before变换器是hoist-jest(在 config-set.ts 中注册),其实现位于 src/transformers/hoist-jest.ts。它负责将jest.mock、jest.unmock、jest.enableAutomock、jest.disableAutomock、jest.deepUnmock等调用(含从@jest/globals导入的形式,见 hoist-jest.ts)提升到模块顶部,从而保证 mock 在 import 执行前生效——这是 Jest 生态赖以运作的基础语义。
此外,用户可通过astTransformers配置注入自己的变换器(配置解析逻辑见 config-set.ts 及后续代码),官方文档见 astTransformers.md。
六、修复 source maps 与缓存回写
6.1 source map 修复
编译产物中的 source map 需要经过updateOutput重写(src/legacy/compiler/compiler-utils.ts):将 map 的file与sources改写为当前文件名、删除sourceRoot,并把 map 内联为data:application/json;charset=utf-8;base64,...形式替换掉编译输出末尾的sourceMappingURL=前缀。这样 Jest 在报错时才能准确定位到原始 TS 源码行。
6.2 内存缓存与持久化缓存
原文档图中“update mem cache / update persistent cache”对应源码中的两级缓存:
- 内存缓存:
_updateMemoryCache(ts-compiler.ts)在每次编译前把最新文件内容与版本写入语言服务的宿主缓存; - 持久化缓存:当
tsCacheDir配置存在时,ts-jest 会复用 Jest 的磁盘缓存体系(缓存 key 中已包含依赖模块的 mtime,见第二章),原文档图中“update mem cache from persistent cache”表示命中磁盘缓存后直接回填内存缓存,跳过编译。
七、Babel 接力与 afterProcess 钩子
processWithTs返回 TS 编译产物后,process方法还会继续执行两个可选阶段(ts-jest-transformer.ts):
- Babel 接力:若配置了
babelConfig,ts-jest 会创建babelJestTransformer(config-set.ts),对 TS 编译产物再执行一次babelJest.process(注意instrument: false,插桩留给 Jest 自己做)。这常用于需要 Babel 插件(如 JSX 定制、polyfill 注入)的场景;配置说明见 babelConfig.md。原文档图中的babelJest.process(source)节点即此环节。 - afterProcess 钩子:通过环境变量
TS_JEST_HOOKS指定钩子文件(runTsJestHook,ts-jest-transformer.ts),若钩子导出afterProcess函数且返回了新值,该返回值将作为最终 source 使用。原文档注释明确提示“如果 hook 返回了内容,它会被用作新的 source”。这是一个非公开但被部分用户使用的扩展点。
八、ESM 模式下的异步变体
当在 ESM 环境(supportsStaticESM且配置useESM)运行时,Jest 会调用processAsync(ts-jest-transformer.ts)而非同步process。两者的流程一致,差异在于:异步路径会在编译后检查processWithTsResult.diagnostics,存在诊断时直接抛出configs.createTsError(...)构造的类型错误;Babel 阶段也改用babelJest.processAsync。此外,构造函数中this.process = this.process.bind(this)等绑定(L64-L70)是为了规避 ESM 模式下方法内this丢失的问题。ESM 使用细节见官方指南 esm-support.md。
九、小结:一张流程图背后的完整调用链
将两张流程图与源码对应后,可以整理出一条完整调用链:
require('file') └─ Jest 调度:getCacheKey(配置 + 内容 + 依赖 mtime → SHA-1) ├─ 命中缓存 → 直接 require └─ 未命中 → transformer.process(source) ├─ stringifyContentPathRegex 命中 → module.exports=序列化内容 ├─ .d.ts → 返回空代码 ├─ JS/TS → TsJestCompiler.getCompiledOutput │ ├─ isolatedModules=false → Language Service(内存缓存 + 类型检查) │ └─ isolatedModules=true → transpileModule(隔离编译) ├─ 自定义 AST transformers(hoist-jest 默认注入 + 用户 astTransformers) ├─ updateOutput 修复 source map ├─ 更新内存/持久化缓存 ├─ 可选:babelJest.process 接力 └─ 可选:afterProcess 钩子改写结果 └─ require() 执行最终产物对于想要继续深挖的读者,建议从以下源码入口入手:
- 变换器入口与缓存 key:src/legacy/ts-jest-transformer.ts
- 编译核心(语言服务 / transpileModule / 选项修正):src/legacy/compiler/ts-compiler.ts
- 编译分发器:src/legacy/compiler/ts-jest-compiler.ts
- 配置归一化(stringify 正则、Babel、诊断、transformers 注册):src/legacy/config/config-set.ts
- source map 修复:src/legacy/compiler/compiler-utils.ts
- jest.mock 提升变换器:src/transformers/hoist-jest.ts
配套的端到端测试(如 e2e/tests/hoist-jest.test.ts、e2e/tests/source-map.test.ts)验证了上述流程的关键行为,可作为理解各环节实际效果的直接参考。
- 测试
- 开发工具
【免费下载链接】ts-jest
A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
相关推荐
为什么顶尖C++开发者都在抛弃DIA SDK?RawPDB的7大技术优势解析
为什么顶尖C++开发者都在抛弃DIA SDK?RawPDB的7大技术优势解析 在C++开发领域,微软的PDB(Program DataBase)文件是调试信息的
为 jest-expo 生成新的 Native 模块 Jest Mocks:从原理到完整工作流
为 jest expo 生成新的 Native 模块 Jest Mocks:从原理到完整工作流 导读 在 Expo / React Native 项目中, je
移动开发前端跨平台原生移动ts-jest 入门指南:用 Jest 测试 TypeScript 项目的 Transformer 配置与实践
ts jest 入门指南:用 Jest 测试 TypeScript 项目的 Transformer 配置与实践 ts jest 是一个带源码映射(source
测试开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考