Nx 23 迁移:ensure-vitest-package-migration如何将 Vitest 从@nx/vite平滑迁移到@nx/vitest
【免费下载链接】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 23 中随nx migrate自动运行的ensure-vitest-package-migration迁移机制。自 Nx 23 起,原先寄居于@nx/vite包中的 Vitest 能力(@nx/vite:testexecutor、@nx/vite:vitestgenerator、以及@nx/vite/plugin中的测试目标推断)被整体移除,改由独立的@nx/vitest包独家提供。本文以仓库中该迁移的文档、实现源码与完整测试用例为依据,详细讲解其自动执行的四项工作、底层判断逻辑、典型场景下的前后配置变化,以及升级后如何验证迁移结果,帮助你理解并掌控这条"安全网"迁移路径。
迁移背景:Vitest 为什么从@nx/vite中独立出来
在 Nx 22 及更早版本中,Vitest 支持是@nx/vite包的一部分。工作区通过以下方式使用 Vitest:
- 在
project.json的 target 中使用@nx/vite:testexecutor; - 使用
@nx/vite:vitestgenerator 生成测试配置; - 依赖
@nx/vite/plugin的插件推断自动生成 vitest 测试目标。
到了 Nx 23,这些能力被全部移除并迁移至全新的packages/vitest包。该包的定位在package.json中写得很明确:"The Nx Plugin for Vitest to enable fast unit testing with Vitest",其 peerDependencies 声明支持vitest: ^3.0.0 || ^4.0.0与vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0。
Nx 官方为这条破坏性变更设计了两段式迁移路径:
- 可选迁移(v22):
migrate-vitest-to-vitest-package,对应packages/vite/src/migrations/update-22-2-0/migrate-vitest-to-vitest-package.ts,负责将 Vitest 使用迁移到@nx/vitest包。 - 兜底安全网(v23):本文的主角
ensure-vitest-package-migration,在nx migrate升级到 Nx 23 时自动运行,专门处理那些跳过了可选迁移、或迁移后仍残留@nx/viteVitest 痕迹的工作区。
二者的注册信息都位于 packages/vite/migrations.json:
"migrate-vitest-to-vitest-package": { "version": "22.2.0-beta.2", "description": "Migrate Vitest usage from @nx/vite to @nx/vitest package.", "implementation": "./dist/src/migrations/update-22-2-0/migrate-vitest-to-vitest-package" }, "ensure-vitest-package-migration-23": { "version": "23.0.0-beta.10", "description": "Safety net: ensure any remaining @nx/vite:test executor usages are swapped to @nx/vitest:test and @nx/vitest is installed.", "implementation": "./dist/src/migrations/update-23-0-0/ensure-vitest-package-migration", "documentation": "./dist/src/migrations/update-23-0-0/ensure-vitest-package-migration.md" }注意ensure-vitest-package-migration-23的 description 明确将其定位为 "Safety net"(安全网),这解释了它存在的意义:不是替代 v22 迁移,而是保证任何工作区升级到 Nx 23 后都不会出现"测试目标静默丢失"的断崖。
迁移做了什么:四项自动操作
迁移的入口函数位于 ensure-vitest-package-migration.ts,其执行流程清晰划分为四个步骤:
const migratedExecutors = migrateExecutorUsages(tree); const migratedPlugins = migratePluginConfigurations(tree); const migratedTargetDefaults = migrateTargetDefaults(tree); const registeredVitestPlugin = await ensureVitestPluginRegistration(tree); if (migratedExecutors || migratedPlugins || migratedTargetDefaults || registeredVitestPlugin) { const installTask = installVitestPackage(tree); await formatFiles(tree); return installTask; } else { return () => {}; }以下逐一展开每一项的具体行为与触发条件。
1. 安装@nx/vitest到 devDependencies
当检测到工作区确实在使用 Vitest、且尚未安装@nx/vitest时,迁移会将其以nxVersion(即当前@nx/vite包版本,见 packages/vite/src/utils/versions.ts)写入package.json的devDependencies。
实现位于installVitestPackage函数:
function installVitestPackage(tree: Tree): GeneratorCallback { const packageJson = readJson(tree, 'package.json'); const hasNxVitest = packageJson.dependencies?.['@nx/vitest'] || packageJson.devDependencies?.['@nx/vitest']; if (hasNxVitest) { return () => {}; } return addDependenciesToPackageJson(tree, {}, { '@nx/vitest': nxVersion }); }关键行为细节:
- 幂等:如果
dependencies或devDependencies中已存在@nx/vitest,直接跳过安装,不会覆盖或升级既有版本; - 条件安装:只有迁移四项中的任何一项实际发生了变化,才会触发安装(由主入口的
if分支控制),纯 Vite 构建场景不会被强行引入@nx/vitest。
2. 将@nx/vite:testexecutor 替换为@nx/vitest:test
migrateExecutorUsages使用forEachExecutorOptions遍历整个工作区,收集所有仍在project.json的 target 中使用@nx/vite:testexecutor 的项目,然后将 executor 字段改写为@nx/vitest:test:
forEachExecutorOptions(tree, '@nx/vite:test', (_options, projectName) => { projectsToUpdate.add(projectName); }); // ... for (const target of Object.values(projectConfig.targets ?? {})) { if (target.executor === '@nx/vite:test') { target.executor = '@nx/vitest:test'; } } updateProjectConfiguration(tree, projectName, projectConfig);这一点有测试直接验证(见 ensure-vitest-package-migration.spec.ts):配置了executor: '@nx/vite:test'且带options.configFile的 target,迁移后 executor 变为@nx/vitest:test,而options原样保留。也就是说只改 executor 标识,不动任何既有选项,configFile、watch、testFiles等原有配置全部兼容。
典型的迁移前后对比:
// 迁移前 project.json { "targets": { "test": { "executor": "@nx/vite:test", "options": { "configFile": "libs/my-lib/vite.config.ts" } } } } // 迁移后 project.json { "targets": { "test": { "executor": "@nx/vitest:test", "options": { "configFile": "libs/my-lib/vite.config.ts" } } } }3. 拆分@nx/vite/plugin注册:Vitest 选项归 Vitest,Vite 选项归 Vite
migratePluginConfigurations遍历nx.json中plugins数组的每一个@nx/vite/plugin条目,将其 options 中与测试相关的三个字段——testTargetName、ciTargetName、ciGroupName——抽取出来,生成一个新的@nx/vitest插件条目;原@nx/vite/plugin条目则只保留 build/serve/preview 相关选项:
const { testTargetName, ciTargetName, ciGroupName, ...viteOptions } = options; if (!testTargetName && !ciTargetName && !ciGroupName) { newPlugins.push(plugin); // 没有测试选项,原样保留 continue; } const vitestPlugin: PluginEntry = { plugin: '@nx/vitest' }; if (Object.keys(vitestOptions).length > 0) { vitestPlugin.options = vitestOptions; } if (plugin.include) vitestPlugin.include = plugin.include as string[]; if (plugin.exclude) vitestPlugin.exclude = plugin.exclude as string[];这一拆分在测试中有完整覆盖(见 ensure-vitest-package-migration.spec.ts)。迁移前后的nx.json变化如下:
// 迁移前 { "plugins": [ { "plugin": "@nx/vite/plugin", "options": { "buildTargetName": "build", "testTargetName": "unit-test", "ciTargetName": "unit-test-ci", "ciGroupName": "unit-tests" }, "include": ["apps/**/*"], "exclude": ["apps/legacy/*"] } ] } // 迁移后 { "plugins": [ { "plugin": "@nx/vite/plugin", "options": { "buildTargetName": "build" }, "include": ["apps/**/*"], "exclude": ["apps/legacy/*"] }, { "plugin": "@nx/vitest", "options": { "testTargetName": "unit-test", "ciTargetName": "unit-test-ci", "ciGroupName": "unit-tests" }, "include": ["apps/**/*"], "exclude": ["apps/legacy/*"] } ] }实现要点:
include/exclude作用域会被镜像到新的@nx/vitest条目上,保证 vitest 目标推断与原 Vite 插件覆盖相同项目范围;- 若抽取后
viteOptions为空,options字段会被整体删除(而非留下空对象); - 拆分会按
include/exclude组合成的作用域(scopeKey)去重,避免重复注册。
4. 注册@nx/vitest插件:默认配置场景的自动补齐
这是安全网中最关键、也最容易被忽视的一步。ensureVitestPluginRegistration专门处理"用了@nx/vite/plugin但没有配置任何 vitest 选项"的工作区——即nx.json中以字符串形式注册"@nx/vite/plugin",或注册为{ "plugin": "@nx/vite/plugin" }且 options 为空。
由于这类裸注册不携带testTargetName等信号,第 3 步的拆分逻辑不会为它创建@nx/vitest条目。如果不做处理,升级后这些项目的 vitest 测试目标推断就会在 Nx 23 中静默丢失。ensureVitestPluginRegistration正是为了堵住这个缺口:
const vitePluginRegistrations = nxJson.plugins.filter((p) => typeof p === 'string' ? p === '@nx/vite/plugin' : p.plugin === '@nx/vite/plugin' ); // Only register @nx/vitest when the workspace actually uses vitest. if (!(await workspaceUsesVitest(tree))) { return false; }它按作用域(include/exclude 组合)逐一为裸注册的@nx/vite/plugin配对生成对应的@nx/vitest条目,并通过coveredScopes集合去重,避免与第 3 步已拆分出的条目重复。混合形态的配置(一个作用域带testTargetName、另一个裸注册)也能被正确处理,这在测试用例 "should pair @nx/vitest with each scoped @nx/vite/plugin even when one scope already had testTargetName and another was bare"(spec 第 246 行起)中有完整覆盖。
5. 迁移targetDefaults:executor 键与 target 名键双模式
migrateTargetDefaults处理nx.json中targetDefaults的两种写法:
- executor 键模式:
"@nx/vite:test": { ... }整体重命名为"@nx/vitest:test"; - target 名键模式:
"test": { "executor": "@nx/vite:test", ... }仅将executor字段改为@nx/vitest:test。
if (targetOrExecutor === '@nx/vite:test') { nxJson.targetDefaults['@nx/vitest:test'] ??= {}; Object.assign(nxJson.targetDefaults['@nx/vitest:test'], targetConfig); delete nxJson.targetDefaults['@nx/vite:test']; } else if (targetConfig.executor === '@nx/vite:test') { targetConfig.executor = '@nx/vitest:test'; }三个行为细节(均有测试佐证,见 spec 的 migrateTargetDefaults describe 块):
- 迁移后
@nx/vite:test旧键被删除,@nx/vitest:test继承全部原配置(cache、inputs、options等); - 若两个键同时存在,旧键值通过
Object.assign覆盖新键的重叠字段,非重叠字段保留——这是实现注释中明示的"对有意配置过旧键的用户更安全"的默认行为; Array.isArray(targetConfig)的条目会被跳过(该迁移早于过滤数组形态的 targetDefaults,此处值均为普通对象)。
何时才真正执行:Vitest 使用检测逻辑
迁移第 4 步依赖workspaceUsesVitest函数判断工作区是否真的在使用 Vitest(源码 L229-L256)。该判断按优先级依次检查:
- 依赖声明:
package.json的dependencies或devDependencies中存在vitest; - 配置文件信号:通过
globAsync扫描**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}:- 任何
vitest.config.*文件 → 判定为使用 Vitest; vite.config.*中出现顶层的test:键(正则/(^|[\s,{])test\s*:/m)→ 判定为使用 Vitest。
- 任何
该函数的注释明确说明了一种有意为之的偏置:正则可能把注释掉的test:也误判为命中,但这种"过度安装"(over-install)是安全的——相比漏判真实用法导致推断出的测试目标丢失,多装一个包要稳妥得多。
只有当工作区使用了 Vitest 时,迁移才会注册@nx/vitest插件并触发依赖安装;纯 Vite 构建的工作区则完全不受影响(对应测试见 spec L143-L157 与 L224-L244)。
升级操作与迁移结果验证
执行迁移
该迁移是 Nx 迁移链的一部分,无需手动干预。在仓库根目录依次运行:
nx migrate latest随后执行自动生成的迁移脚本(Nx 通常会提示具体的 migration runner 命令,如nx migrate --run-migrations=migrations.json),迁移即自动完成。原文明确指出:运行nx migrate后无需任何手动操作。
验证迁移结果
迁移完成后,重点检查三个文件:
package.json:devDependencies中应出现@nx/vitest,版本与当前 Nx 版本一致;nx.json:plugins中不应再有携带testTargetName/ciTargetName/ciGroupName的@nx/vite/plugin条目;- 应存在对应的
@nx/vitest插件条目(包括为裸注册补齐的条目); targetDefaults中不应残留@nx/vite:test键,应被@nx/vitest:test替代;
- 各
project.json:所有executor: "@nx/vite:test"均已变为executor: "@nx/vitest:test",且options原样保留。
验证测试目标仍可正常推断与运行
迁移后,vitest 测试目标的发现与运行遵循@nx/vitest插件的新规则(参见 packages/vitest/PLUGIN.md)。该文档给出了两种运行方式:
模式检测顺序(先命中者生效):
| 模式 | 检测依据 |
|---|---|
| 推断(Inference) | nx.json的plugins数组中存在@nx/vitest或@nx/vite/plugin |
| 执行器(Executor) | project.json的targets中存在@nx/vitest:testexecutor |
运行指定测试文件:
# 推断模式 nx test <project> -- <path/to/file.spec.ts> # 执行器模式 nx run <project>:test --testFile=<path/to/file.spec.ts>常用快捷命令:
| 任务 | 推断模式 | 执行器模式 |
|---|---|---|
| 运行指定文件 | nx test proj -- path/file.spec.ts | nx run proj:test --testFile=path/file.spec.ts |
| 按名称模式运行 | nx test proj -- -t "pattern" | nx run proj:test --testNamePattern="pattern" |
@nx/vitest包的 executor 注册于 packages/vitest/executors.json,即@nx/vitest:test。
边界情况与行为保证
基于源码与测试,该迁移具备以下可预期、可验证的行为保证:
- 幂等性:工作区已注册
@nx/vitest插件且@nx/vite/plugin无测试选项时,迁移是纯 no-op,不会重复注册(spec L159-L172); - 不重复安装:
@nx/vitest已存在时保持原版本不变(spec L188-L207); - 只处理相关项目:未使用
@nx/vite/plugin或@nx/vite:test的工作区(例如仅用@nx/eslint/plugin)完全不受影响(spec L174-L186); - 作用域一致:无论哪种迁移路径,新生成的
@nx/vitest条目都继承原@nx/vite/plugin的include/exclude,保证测试目标推断的覆盖范围与迁移前一致; - 安全优先:Vitest 使用检测偏向于"过度安装",宁可多装
@nx/vitest,也不让依赖推断的测试目标意外丢失。
小结
ensure-vitest-package-migration是 Nx 23 升级链路中的一道自动安全网:它兜底处理了 v22 可选迁移遗漏的所有@nx/viteVitest 残留——替换 executor、拆分插件配置、迁移 targetDefaults、按需注册@nx/vitest插件并安装依赖。理解它的触发条件、检测逻辑与幂等行为,能让你在nx migrate升级后自信地核对package.json、nx.json与各project.json,确保 vitest 测试目标在 Nx 23 中无缝延续。
参考文件索引:
- 迁移文档:packages/vite/src/migrations/update-23-0-0/ensure-vitest-package-migration.md
- 迁移实现:packages/vite/src/migrations/update-23-0-0/ensure-vitest-package-migration.ts
- 迁移测试:packages/vite/src/migrations/update-23-0-0/ensure-vitest-package-migration.spec.ts
- 迁移注册表:packages/vite/migrations.json
- 前序 v22 迁移:packages/vite/src/migrations/update-22-2-0/migrate-vitest-to-vitest-package.ts
@nx/vitest包元信息:packages/vitest/package.json- 测试运行指引:packages/vitest/PLUGIN.md
【免费下载链接】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),仅供参考