Nx 23 迁移:`ensure-vitest-package-migration` 如何将 Vitest 从 `@nx/vite` 平滑迁移到 `@nx/vitest`
2026/9/12 3:09:59 网站建设 项目流程

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.0vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0

Nx 官方为这条破坏性变更设计了两段式迁移路径:

  1. 可选迁移(v22)migrate-vitest-to-vitest-package,对应packages/vite/src/migrations/update-22-2-0/migrate-vitest-to-vitest-package.ts,负责将 Vitest 使用迁移到@nx/vitest包。
  2. 兜底安全网(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.jsondevDependencies

实现位于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 }); }

关键行为细节:

  • 幂等:如果dependenciesdevDependencies中已存在@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 标识,不动任何既有选项configFilewatchtestFiles等原有配置全部兼容。

典型的迁移前后对比:

// 迁移前 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.jsonplugins数组的每一个@nx/vite/plugin条目,将其 options 中与测试相关的三个字段——testTargetNameciTargetNameciGroupName——抽取出来,生成一个新的@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.jsontargetDefaults的两种写法:

  • 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继承全部原配置(cacheinputsoptions等);
  • 若两个键同时存在,旧键值通过Object.assign覆盖新键的重叠字段,非重叠字段保留——这是实现注释中明示的"对有意配置过旧键的用户更安全"的默认行为;
  • Array.isArray(targetConfig)的条目会被跳过(该迁移早于过滤数组形态的 targetDefaults,此处值均为普通对象)。

何时才真正执行:Vitest 使用检测逻辑

迁移第 4 步依赖workspaceUsesVitest函数判断工作区是否真的在使用 Vitest(源码 L229-L256)。该判断按优先级依次检查:

  1. 依赖声明package.jsondependenciesdevDependencies中存在vitest
  2. 配置文件信号:通过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后无需任何手动操作

验证迁移结果

迁移完成后,重点检查三个文件:

  1. package.jsondevDependencies中应出现@nx/vitest,版本与当前 Nx 版本一致;
  2. nx.json
    • plugins中不应再有携带testTargetName/ciTargetName/ciGroupName@nx/vite/plugin条目;
    • 应存在对应的@nx/vitest插件条目(包括为裸注册补齐的条目);
    • targetDefaults中不应残留@nx/vite:test键,应被@nx/vitest:test替代;
  3. project.json:所有executor: "@nx/vite:test"均已变为executor: "@nx/vitest:test",且options原样保留。

验证测试目标仍可正常推断与运行

迁移后,vitest 测试目标的发现与运行遵循@nx/vitest插件的新规则(参见 packages/vitest/PLUGIN.md)。该文档给出了两种运行方式:

模式检测顺序(先命中者生效):

模式检测依据
推断(Inference)nx.jsonplugins数组中存在@nx/vitest@nx/vite/plugin
执行器(Executor)project.jsontargets中存在@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.tsnx 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/plugininclude/exclude,保证测试目标推断的覆盖范围与迁移前一致;
  • 安全优先:Vitest 使用检测偏向于"过度安装",宁可多装@nx/vitest,也不让依赖推断的测试目标意外丢失。

小结

ensure-vitest-package-migration是 Nx 23 升级链路中的一道自动安全网:它兜底处理了 v22 可选迁移遗漏的所有@nx/viteVitest 残留——替换 executor、拆分插件配置、迁移 targetDefaults、按需注册@nx/vitest插件并安装依赖。理解它的触发条件、检测逻辑与幂等行为,能让你在nx migrate升级后自信地核对package.jsonnx.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),仅供参考

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

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

立即咨询