Nx Jest 配置迁移指南:将 jest.config.ts 从 ESM 转换为 CJS(update-22-2-0) Nx Jest 配置迁移指南将 jest.config.ts 从 ESM 转换为 CJSupdate-22-2-0【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx本文基于 Nx 仓库中nx/jest的官方迁移文档convert-jest-config-to-cjs对应版本 22.2.0-beta.2编写并结合迁移源码与单元测试深入解读其转换规则、边界情况与底层原理。读完本文你将掌握为什么 Node.js 22 的 type-stripping 会破坏 ESM 语法的.tsJest 配置、该迁移具体改写哪些语法、哪些文件会被跳过并收到警告以及迁移前后如何自查验证。一、为什么需要这次迁移Node.js type-stripping 与 CommonJS 的冲突Nx 的 Jest 迁移convert-jest-config-to-cjs解决的是一个非常具体的兼容性问题将工作区中的jest.config.ts文件转换为jest.config.ctsCommonJS TypeScript语法。问题背景如下Nx 的nx/jest/plugin在推断test目标时会以 CommonJS 语义加载 Jest 配置。在 packages/jest/src/plugins/plugin.ts 中插件为 jest 命令注入的TS_NODE_COMPILER_OPTIONS明确包含module: commonjsconst tsNodeCompilerOptions JSON.stringify({ moduleResolution: node10, module: commonjs, customConditions: null, });而 Node.js 22、24 引入了type-stripping类型剥离能力Node 会在运行.ts文件时直接剥离类型注解不再依赖 ts-node 等转译器。当项目按 CommonJS 解析、而.ts文件里却写着 ESM 语法import/export default时type-stripping 会与模块解析方式产生冲突导致 Jest 配置无法正确加载。因此对于使用 CommonJS 解析的项目把jest.config.ts改写为显式使用require/module.exports的jest.config.cts可以确保配置在 Node 原生 type-stripping 下被正确加载。适用前提根据迁移文档该迁移仅在nx.json中注册了nx/jest/plugin时运行。这是因为插件会强制以 CommonJS 方式加载 Jest 配置见上文TS_NODE_COMPILER_OPTIONS两者配套使用才能保证转换后的配置按预期工作。从实现细节看迁移代码本身并不检查插件注册情况——迁移源码 只是直接 glob 所有**/jest.config.ts文件对应的单元测试也专门验证了「未注册插件」与「nx.json不存在」两种场景下转换依然正常执行见 convert-jest-config-to-cjs.spec.ts因此在纯 executor 模式下同样安全。二、迁移做了什么事一图看懂转换前后迁移文档给出的标准示例转换前后对拍转换前jest.config.tsESM 语法import { foo } from bar; import baz from qux; export default { displayName: myapp, preset: foo, transform: baz, };转换后jest.config.ctsCommonJS 语法const { foo } require(bar); const baz require(qux).default ?? require(qux); module.exports { displayName: myapp, preset: foo, transform: baz, };核心变化一目了然转换前ESM转换后CommonJSexport default { ... }module.exports { ... }import { x } from yconst { x } require(y)import x from yconst x require(y).default ?? require(y)三、逐条解读转换规则含源码依据迁移源码在 convert-jest-config-to-cjs.ts 的 JSDoc 中明确了三条核心转换规则实际实现convertImportsToRequire与convertExportDefaultToModuleExports支持的语法远比这三条丰富。下面逐条展开。1. 具名导入named import// 转换前 import { readFileSync } from fs; // 转换后 const { readFileSync } require(fs);对应源码见 convert-jest-config-to-cjs.ts测试见 convert-jest-config-to-cjs.spec.ts。2. 默认导入default import注意兜底逻辑// 转换前 import path from path; // 转换后 const path require(path).default ?? require(path);这里有一个重要的工程细节require(path).default ?? require(path)中的??兜底是为了同时兼容同时提供 ESM 与 CJS 双入口dual package的模块——如果模块的exports映射没有default导出纯 CommonJS 包则回退到require(path)整体引用。测试见 convert-jest-config-to-cjs.spec.ts。3. 命名空间导入namespace import// 转换前 import * as fs from fs; // 转换后 const fs require(fs);4. 副作用导入side-effect import// 转换前 import reflect-metadata; // 转换后 require(reflect-metadata);源码中对没有importClause的导入直接生成require(模块名)见 convert-jest-config-to-cjs.ts。5. 重命名导入renamed import// 转换前 import { readFileSync as readFile } from fs; // 转换后 const { readFileSync: readFile } require(fs);ESM 的as别名在解构赋值中被映射为原名: 别名的对象属性语法见 convert-jest-config-to-cjs.ts。6. 类型导入import type原样保留import type { Config } from jest这类纯类型导入不会被转换原样保留。原因在源码注释中写得很清楚Node 的 type-stripping 在运行时本来就会擦除类型导入保留它可以继续为编辑器/tsc提供类型安全见 convert-jest-config-to-cjs.ts。7. 内联类型修饰符inlinetype拆分处理对于混合写法import { type Config, readConfig } from some-pkg迁移会拆成两条语句import type { Config } from some-pkg; const { readConfig } require(some-pkg);类型部分提取为独立的import type保证类型引用如const config: Config ...依然可解析值部分照常转为require解构。更复杂的组合也能正确处理例如import setup, { type Config, helper } from some-pkg会被拆成三条语句import type 默认导入 require 具名导入 require重命名的内联类型import { type Config as JestConfig, run }也会保留别名。相关实现见 convert-jest-config-to-cjs.ts测试覆盖见 convert-jest-config-to-cjs.spec.ts。8. 导出默认值不止对象字面量export default后的表达式会被整体搬运到module.exports 后面因此不仅限于对象字面量——导出函数如export default async () {...}、导出已声明变量export default config都能正确转换见 convert-jest-config-to-cjs.ts。测试中也覆盖了「函数内部使用await import()」的复杂配置模式见 convert-jest-config-to-cjs.spec.ts。四、哪些文件不会被转换重要边界条件迁移不是无条件全量改写以下三类文件会被跳过并视情况输出警告日志。1. 属于type: module项目的文件跳过并警告迁移会先读取每个jest.config.ts所在目录的package.json以及工作区根package.json判断模块类型若项目处于 ESMtype: module语义下说明该文件本就按 ESM 运行不需要也不应该改成 CJS直接跳过同时输出警告提示如果使用nx/jest/plugin它会强制 CommonJS 解析建议移除type: module或改用其他 Jest 配置方式。警告原文源码可见 convert-jest-config-to-cjs.tsThe following jest.config.ts files belong to projects with type: module in their package.json and were left as-is. If you use nx/jest/plugin, it forces CommonJS resolution, so consider removing type: module or using a different Jest configuration approach2. 使用了 ESM 专属特性的文件跳过并警告以下两种特性无法被机械地翻译为 CommonJS遇到时迁移会跳过该文件并提示手动处理见 convert-jest-config-to-cjs.tsimport.meta例如rootDir: import.meta.dirname顶层awaittop-level await。其中顶层await的检测很有意思迁移并非简单地搜索await关键字而是遍历 AST 中的每个AwaitExpression沿父节点向上追溯只有确认await不在任何函数内部时才判定为顶层await见 convert-jest-config-to-cjs.ts。因此export default async () { const config await import(./base-config.js); ... }这类「函数内 await」属于合法 CommonJS 写法会被正常转换module.exports async () {...}。对应警告原文见 convert-jest-config-to-cjs.tsThe following jest.config.ts files use ESM-only features (import.meta or top-level await) and could not be automatically converted to CommonJS. Please update them manually3. 其他扩展名的 Jest 配置完全不受影响迁移的 glob 模式是**/jest.config.ts因此jest.config.js、jest.config.cjs、jest.config.mjs、jest.config.cts、jest.config.mts等文件一律不会被动到——它们要么已是 CJS要么本就是显式 ESM无需转换。测试见 convert-jest-config-to-cjs.spec.ts。五、模块类型判定优先级迁移读取模块类型时遵循明确的优先级见 convert-jest-config-to-cjs.ts项目级package.json的type字段jest.config.ts同目录下的package.json——优先级最高工作区根package.json的type字段两者都没有时默认按commonjs处理Node 的默认行为。这一点在多文件、混合模块类型的工作区中尤其关键。测试给出了一个典型场景见 convert-jest-config-to-cjs.spec.tsapps/app1type: commonjs被转换apps/app2type: module被跳过libs/lib1无type字段默认按 CommonJS被转换。六、源码实现原理基于 AST 的机械式重写这一节从源码层面拆解迁移的工程实现帮助你理解转换的可靠性边界。1. 语法解析tsquery迁移使用phenomnomnominal/tsquery解析 TypeScript 源码为 AST并通过查询表达式ImportDeclaration、ExportAssignment、AwaitExpression、MetaProperty等定位需要改写的节点。对于顶层await检测还叠加了typescript的isFunctionLike判断。2. 倒序替换保证位置正确convertImportsToRequire中所有导入节点会按源码位置从后往前排序再逐个替换见 convert-jest-config-to-cjs.tsconst sortedImports [...importDeclarations].sort( (a, b) b.getStart() - a.getStart() );这是经典的「从后往前替换」技巧先改末尾的节点不会破坏前面节点已有的位置偏移避免连锁错位。3. 分号处理避免双分号replaceNode在替换时会检查原节点末尾是否已有;有则一并吞掉再统一追加一个分号见 convert-jest-config-to-cjs.ts保证输出不会出现;;这类瑕疵。4. 收尾格式化当有文件被修改时迁移会调用formatFiles(tree)统一格式化见 convert-jest-config-to-cjs.ts确保转换后的配置符合工作区的格式规范。5. 与nx/jest/plugin的关系插件在 packages/jest/src/plugins/plugin.ts 中声明的配置 glob 为**/jest.config.{cjs,mjs,js,cts,mts,ts}——也就是说.cts是插件本就支持的配置扩展名转换后的jest.config.cts可以无缝接入插件的目标推断流程。七、测试覆盖转换正确性的硬证据迁移配套的单元测试 convert-jest-config-to-cjs.spec.ts 提供了全面且可复现的验证矩阵按描述块划分如下测试分组覆盖要点export default conversionexport default→module.exports且对象内容完整保留import conversion具名 / 默认 / 命名空间 / 副作用 / 重命名导入的转换type-only importsimport type原样保留内联type拆分重命名保留ESM module type项目级 / 根级type: module均跳过项目级优先于根级ESM-only features detectionimport.meta与顶层await跳过并警告函数内await正常转换multiple files多文件混合场景下逐一正确处理other jest config extensions其他扩展名的配置不受影响complex config patterns真实的 SWC __dirname读配置场景executor-based setups未注册插件 / 无nx.json时依然正常转换例如SWC 场景测试见 convert-jest-config-to-cjs.spec.ts验证了真实项目中最常见的配置形态// 转换前 import { readFileSync } from fs; const swcJestConfig JSON.parse( readFileSync(${__dirname}/.spec.swcrc, utf-8) ); swcJestConfig.swcrc false; export default { displayName: app1, preset: ../../jest.preset.js, transform: { ^.\\.[tj]s$: [swc/jest, swcJestConfig], }, }; // 转换后 const { readFileSync } require(fs); const swcJestConfig JSON.parse(readFileSync(${__dirname}/.spec.swcrc, utf-8)); swcJestConfig.swcrc false; module.exports { displayName: app1, preset: ../../jest.preset.js, transform: { ^.\\.[tj]s$: [swc/jest, swcJestConfig], }, };八、如何运行这次迁移该迁移注册在nx/jest的 migrations.json 中版本号为22.2.0-beta.2描述为Convert jest.config.ts files from ESM to CJS syntax (export default - module.exports, import - require) for projects using CommonJS resolution to ensure correct loading under Node.js type-stripping.运行方式遵循 Nx 标准的迁移流程# 1. 更新 nx/jest 并生成迁移文件 nx migrate nx/jest # 2. 查看生成的 migrations.json确认包含 convert-jest-config-to-cjs # 3. 执行迁移 nx migrate --run-migrations执行后建议立即检查终端输出中的警告信息——若有文件因import.meta、顶层await或type: module被跳过请根据警告提示手动处理。九、迁移后自查清单确认转换结果检查所有jest.config.ts是否已变为jest.config.cts内容中不再有import/export default纯import type除外检查警告确认没有遗留projectsWithEsmOnlyFeatures/projectsWithTypeModule对应的警告若有按提示手动迁移或调整type字段回归测试运行npx nx test project验证配置能被正常加载、测试仍然通过特别是使用了preset、transform、setupFilesAfterEnv等外部引用的项目版本前提本次迁移针对 Node.js 22 / 24 的 type-stripping 行为若你的运行环境仍依赖旧版 Node 的 ts-node 转译转换同样无害CJS 语法在两种环境下都能正确加载。十、总结convert-jest-config-to-cjs是一次「小而关键」的自动化迁移它通过 AST 重写把 CommonJS 项目中的 ESM 风格 Jest 配置安全地转换为jest.config.cts从而与 Node.js 22 的 type-stripping 机制兼容避免配置加载静默失败。它同时做到了三个层次的稳健性语法覆盖广具名/默认/命名空间/副作用/重命名/类型导入全部处理、边界判断准type: module跳过、ESM 专属特性跳过并警告、工程细节扎实倒序替换防错位、分号去重、收尾格式化、完整测试矩阵。如果你的工作区已经注册了nx/jest/plugin并计划升级到新版本 Node这次迁移是升级路径上值得重点关注的一环。相关文件索引迁移文档packages/jest/src/migrations/update-22-2-0/convert-jest-config-to-cjs.md迁移实现packages/jest/src/migrations/update-22-2-0/convert-jest-config-to-cjs.ts迁移测试packages/jest/src/migrations/update-22-2-0/convert-jest-config-to-cjs.spec.ts迁移注册表packages/jest/migrations.jsonnx/jest/plugin实现CommonJS 强制解析与配置 globpackages/jest/src/plugins/plugin.ts【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考