☰
ts-jest 处理流程全解析:从 Jest Transformer 到 TypeScript 编译管线的内部工作原理
2026/10/7 2:29:04 网站建设 项目流程
  • 测试
  • 开发工具

【免费下载链接】ts-jest

A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.

项目地址:https://gitcode.com/gh_mirrors/ts/ts-jest
点击查看免费下载

本文是 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')的文件,都会先后经过两条流水线:

  1. Jest 自身的 transformer 调度流程:决定是否需要 transform、如何计算缓存 key、缓存命中还是调用process;
  2. 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() end

1.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):

  1. Babel 接力:若配置了babelConfig,ts-jest 会创建babelJestTransformer(config-set.ts),对 TS 编译产物再执行一次babelJest.process(注意instrument: false,插桩留给 Jest 自己做)。这常用于需要 Babel 插件(如 JSX 定制、polyfill 注入)的场景;配置说明见 babelConfig.md。原文档图中的babelJest.process(source)节点即此环节。
  2. 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.

项目地址:https://gitcode.com/gh_mirrors/ts/ts-jest
点击查看免费下载
上一篇:5分钟掌握SPT-AKI Profile Editor:高效管理你的离线塔科夫存档
下一篇:KMS_VL_ALL_AIO:Windows和Office智能激活的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询