源码级解析:从 Jest 调用链到 TypeScript 编译、Babel 与多层缓存)
测试开发工具【免费下载链接】ts-jestA 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 flow》website/versioned_docs/version-28.0/processing.md的深度展开。它面向希望理解 ts-jest 内部架构的开发者完整还原 Jest 调用 transformer 的通用链路以及 ts-jest 从拿到源码到产出transformed source的每一步决策点字符串化、声明文件跳过、Language Service /transpileModule双编译路径、jest.mock提升、source map 修复、Babel 接力与afterProcess钩子。读完本文你将能读懂任何一次.ts文件变换过程中谁在什么时候做了什么并能据此判断isolatedModules、babelConfig、stringifyContentPathRegex等配置对运行行为的具体影响。原文档以两幅 PlantUML 流程图Jest process 与 ts-jest process为骨架本节先以文字形式完整还原其流程再结合当前仓库源码逐节点印证。Jest 层面transformer 的通用调用链ts-jest 以 JestSyncTransformer的身份挂载到 Jest 的 transform 管线中入口是 src/index.ts 的createTransformer()它返回一个TsJestTransformer实例src/legacy/ts-jest-transformer.ts。Jest 每次require(file)时都会按以下顺序询问 transformer是否有 transform—— 只有当文件匹配 Jest 配置中transform的匹配规则如.ts/.tsx/.js/.jsx时Jest 才会把该文件交给 transformer否则直接交给内置的require处理。相关匹配模式常量定义在 src/constants.tsTS_TRANSFORM_PATTERN、JS_TRANSFORM_PATTERN等。transformer 是否实现了getCacheKey—— ts-jest 实现了getCacheKey/getCacheKeyAsyncsrc/legacy/ts-jest-transformer.ts因此由 ts-jest 自己计算缓存键若没有实现Jest 会退回到内置缓存键算法。是否命中缓存—— 命中则直接使用缓存内容跳过编译未命中则调用transformer.process(...)生成新内容并更新缓存。require()—— 最终加载变换后的模块。ts-jest 的缓存键由什么构成getCacheKey的源码src/legacy/ts-jest-transformer.ts展示了缓存键的构成这是理解改了什么会导致缓存失效的关键序列化后的完整 Jest 配置 由ConfigSet计算的cacheSuffix_transformCfgStrrootDirinstrument:on/offts-jest 自己不插桩恒为 offsupportsStaticESM:on/off文件内容与文件路径当isolatedModules为false且启用了文件缓存tsCacheDir时还会追加该文件解析出的依赖模块路径及其mtimeMs——即依赖文件的时间戳变化也会使缓存键失效这是 ts-jest 缓存比只看文件内容更精准的地方。cacheSuffix的计算位于 src/legacy/config/config-set.ts它把 TypeScript 编译器版本、ts-jest 自身 digest、Babel 配置、解析后的 tsconfigoptions raw、isolatedModules、diagnostics 配置以及全部 AST transformer 的name-version列表做 sha1 哈希再以cacheDirectory/ts-jest/前2位/剩余位的目录结构落盘。从源码结构看缓存键把影响输出的一切因素配置、编译器版本、依赖时间戳、ESM 模式全部纳入因此 Jest 的增量缓存对 ts-jest 是可靠的。ts-jest 内部处理流程从process()到transformed source原文档的第二个泳道图刻画了ts-jest自己的处理管线泳道分为processor与compiler (cached)。当前源码中process()src/legacy/ts-jest-transformer.ts先通过processWithTs()完成 TypeScript 侧的全部工作再视配置决定是否调用 Babel最后执行afterProcess钩子。逐节点展开如下。1. 入口tsJest.process(source)TsJestTransformer.process()接收源码文本、文件路径与 Jest 的 transform 选项。第一步是this._configsFor(transformOptions)src/legacy/ts-jest-transformer.ts它维护一个静态的_cachedConfigSets数组按序列化后的 Jest 配置在多次测试运行之间复用ConfigSet与编译实例TsJestCompiler避免每个文件都重新解析 tsconfig、重建编译器。这也是泳道图中compiler (cached)的含义——编译器和配置都是进程级缓存的。2. 分支一shouldStringifyContent字符串化ConfigSet.shouldStringifyContent(filePath)src/legacy/config/config-set.ts根据stringifyContentPathRegex选项判断文件是否需要字符串化。该选项在 src/types.ts 中注释为兼容旧版__TRANSFORM_HTML__行为任何匹配该正则的文件都会被编译成一个导出文件内容为字符串的模块。命中时processWithTs直接产出module.exports字符串化内容src/legacy/ts-jest-transformer.ts完全跳过 TypeScript 编译。配置解析在 src/legacy/config/config-set.ts字符串形式会被new RegExp(...)转换正则形式直接使用。典型场景.html、.svg、.md等非代码资源测试中需要以字符串形式读取其内容。对应选项文档见 stringifyContentPathRegex。3. 分支二.d.ts声明文件直接清空若文件名以.d.ts结尾processWithTs直接返回空代码{ code: }src/legacy/ts-jest-transformer.ts。原因正如原文档 note 所述声明文件只有类型信息没有运行时产物不需要编译。这也避免了require一个.d.ts时抛出的UnableToRequireDefinitionFile类型错误对应 src/legacy/compiler/ts-compiler.ts 中无输出文件即抛错的保护逻辑。4. 编译器TsJestCompiler与TsCompiler非字符串化、非.d.ts的ts[x]/js[x]文件进入编译器。TsJestCompilersrc/legacy/compiler/ts-jest-compiler.ts是一个薄包装真正干活的是TsCompilersrc/legacy/compiler/ts-compiler.ts。构造函数中会依据isolatedModules决定是否创建 TypeScript Language Service。5.isolatedModules分支transpileModule还是 Language Service这是 ts-jest 两条编译路径的分水岭也是文档泳道图的核心分叉isolatedModules: false默认创建并缓存 TypeScript Language Servicesrc/legacy/compiler/ts-compiler.ts_createLanguageService()在 src/legacy/compiler/ts-compiler.ts。编译走getCompiledOutput→languageService.getEmitOutput(fileName)src/legacy/compiler/ts-compiler.ts并同时获取语义/语法诊断getDiagnosticssrc/legacy/compiler/ts-compiler.ts。这一路径支持完整类型检查但第一次编译更慢。isolatedModules: true跳过 Language Service改用ts.transpileModulesrc/legacy/compiler/ts-compiler.ts逐文件独立编译不做跨文件类型检查因此更快但诊断更少且与tsc的隔离编译行为一致。此路径下自定义 transformer 仍会通过_makeTransformers注入src/legacy/compiler/ts-compiler.ts。isolatedModules的取值来自 ts-jest 选项或 tsconfig 的compilerOptions.isolatedModules解析见 src/legacy/config/config-set.ts。配置说明与权衡详见选项文档 isolatedModules 以及 typescript-7 兼容指南。可以推断如果你的测试代码依赖跨文件的类型诊断diagnostics选项默认开启应保持默认的 Language Service 路径若追求极致的启动与变换速度、且类型问题已由tsc在 CI 中兜底可开启isolatedModules: true。6. 持久化缓存persistent cache与内存缓存泳道图在编译前还有一步in persistent cache?的判断命中持久化缓存时直接从磁盘恢复内存缓存未命中则编译并回写。当前实现中内存侧TsCompiler维护_fileContentCache与_fileVersionCache_updateMemoryCache()src/legacy/compiler/ts-compiler.ts在编译前把文件内容写入缓存内容变化时递增版本号与_projectVersion驱动 Language Service 增量更新getScriptSnapshot读取优先级为内存缓存 → Jest runtime cacheFS → 磁盘 readFilesrc/legacy/compiler/ts-compiler.ts。磁盘侧tsCacheDir由 src/legacy/config/config-set.ts 按cacheSuffix计算Jest 的cacheDirectory下的ts-jest/hash前2位/hash即持久化缓存目录getCacheKey中依赖模块的mtimeMs也来自这里src/legacy/ts-jest-transformer.ts。7. 自定义 AST transformersjest.mock提升与用户变换编译产物在产出前会经过自定义 AST transformers。ts-jest 默认注册了hoist-jesttransformer 到before阶段src/legacy/config/config-set.ts其实现位于 src/transformers/hoist-jest.ts它遍历 AST把jest.mock、jest.unmock、jest.doMock、jest.dontMock等调用提升到模块顶部对应 src/transformers/hoist-jest.ts 中检查需要提升的语句并重排节点的逻辑。这正是 Jest 官方对 mock 声明必须先于 import 执行语义的实现。用户还可以通过astTransformers选项before/after/afterDeclarations定义见 src/types.ts注册自己的 transformer解析逻辑见 src/legacy/config/config-set.ts包括对.ts形式 transformer 用 esbuild 预编译后加载。详细用法见 astTransformers 选项文档 与 src/transformers/README.md。对应的端到端验证见 hoist-jest 测试 与 e2e/hoist-jest 示例工程。8. 修复 source mapfix source maps对应 src/legacy/compiler/compiler-utils.ts 的updateOutput()它会重写编译输出末尾的sourceMappingURL把 source map 以data:application/json;charsetutf-8;base64,...内联进输出并修正file、sources字段删除sourceRoot保证测试报错时能正确映射回原始.ts行号。Language Service 路径中source map 文件是outputFiles[0]代码是outputFiles[1]src/legacy/compiler/ts-compiler.ts。9. 更新缓存编译完成后内存缓存与持久化缓存都会被更新对应泳道图末尾的update mem cache/update persistent cache供下一次require命中。10. Babel 阶段babelConfig与babel-jest接力TypeScript 编译完成后若配置了babelConfigtrue/ babelrc 路径 / 内联对象ts-jest 会把产物交给babel-jest再处理一次。babelJestTransformer的创建在 src/legacy/config/config-set.ts通过importer.babelJest(...).createTransformer(babelConfig)构建Babel 配置支持.js/.cjs文件 require、JSON5 解析或内联对象三种形式。调用点位于process()src/legacy/ts-jest-transformer.tsbabelJest.process(result.code, sourcePath, {...transformOptions, instrument: false})——注意instrument: false源码注释明确说明这里不做插桩Jest 之后本来就会做。这解释了为什么babelConfig常用于补充 TS 编译之外的能力如 JSX、装饰器等 Babel 插件而覆盖率插桩统一由 Jest 自己完成。从源码结构看被stringifyContentPathRegex命中的文件会跳过 BabelbabelJest被置为undefined见 src/legacy/ts-jest-transformer.ts因为字符串化模块没有再做 Babel 变换的意义。11.afterProcess钩子TS_JEST_HOOKS流程最后runTsJestHook()src/legacy/ts-jest-transformer.ts检查环境变量TS_JEST_HOOKS若指向某个模块且该模块导出了afterProcess函数则用[sourceText, sourcePath, transformOptions.config, transformOptions]与当前编译结果调用它若钩子有返回值该返回值将作为新的输出否则沿用编译结果。源码注释强调这不应当作公开 API但保留是因为有人在使用。流程图之外的细节非ts[x]/js[x]文件的兜底processWithTs对既非字符串化、非.d.ts、也非ts[x]/js[x]的文件例如.vue等其他扩展名会走兜底逻辑src/legacy/ts-jest-transformer.ts直接原样返回源码并记录 warn 日志GotUnknownFileTypeWithBabel/GotUnknownFileTypeWithoutBabel。源码注释建议这类扩展名应直接在 Jest 的transform里配置babel-jest等专门 transformer而不是交给 ts-jest。另有一个特例来自node_modules的.js文件会用ts.transpileModule快速处理src/legacy/ts-jest-transformer.ts避免为依赖代码付出 Language Service 的全量成本。两条编译路径的取舍与配置速查决策点条件行为源码依据字符串化匹配stringifyContentPathRegex输出module.exports字符串跳过 TS/Babelsrc/legacy/ts-jest-transformer.ts声明文件文件名以.d.ts结尾输出空代码不编译src/legacy/ts-jest-transformer.ts编译方式isolatedModules: falseLanguage Service 完整类型诊断src/legacy/compiler/ts-compiler.ts编译方式isolatedModules: truetranspileModule隔离编译无跨文件检查src/legacy/compiler/ts-compiler.tsAST 变换默认 astTransformershoist-jest提升jest.mock再叠加用户 transformersrc/legacy/config/config-set.tsBabelbabelConfig非空babel-jest接力处理instrument: falsesrc/legacy/ts-jest-transformer.ts后处理设置TS_JEST_HOOKS且导出afterProcess钩子返回值可覆盖输出src/legacy/ts-jest-transformer.ts配套的官方选项文档可直接查阅tsconfig、isolatedModules、babelConfig、diagnostics、stringifyContentPathRegex、astTransformers 与 useESM完整端到端行为可参考 e2e 测试目录 下的 hoist-jest.test.ts、performance.test.ts 与 transformer-options.test.ts。结语ts-jest 的处理流程本质是一条**先快筛字符串化/声明文件、再编译Language Service 或隔离编译、后变换AST transformer source map 修复、可接力Babel、可兜底afterProcess 钩子**的流水线每一级都由配置驱动、由缓存加速。理解这张流程图不仅能解释为什么我改了 tsconfig 会全量重编译cacheSuffix变了、为什么依赖文件改了缓存会失效mtimeMs进了缓存键也能让你在遇到某类文件行为异常时沿着process()→processWithTs()→getCompiledOutput()的调用链迅速定位到对应分支——这正是这份内部文档的原始价值所在。赞分享测试开发工具【免费下载链接】ts-jestA 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 处理流程深度解析从 Jest Transformer 到 TypeScript 编译管线的完整链路ts jest 处理流程深度解析从 Jest Transformer 到 TypeScript 编译管线的完整链路 ts jest 的核心价值在于它作为 Je测试开发工具Bilibili视频解析革命一键解锁高质量视频资源管理新范式Bilibili视频解析革命一键解锁高质量视频资源管理新范式 还在为B站视频资源管理而烦恼bilibili parse作为一款革命性的视频解析工具彻底改变后端音视频ts-jest 与 Babel 7 对比babel/preset-typescript 的六大局限与 TypeScript 项目级编译的优势ts jest 与 Babel 7 对比babel/preset typescript 的六大局限与 TypeScript 项目级编译的优势 本文基于 ts测试开发工具上一篇10分钟快速上手LTX-2.3-22b-IC-LoRA-Ingredients视频生成终极教程 下一篇Jellium Desktop启动基础启动入门创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考