Nx 23 迁移指南:将@nx/rsbuild的createNodesV2导入统一重命名为createNodes
【免费下载链接】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/rsbuild在 Nx 23 中将推断式插件(inferred plugin)的主导出从createNodesV2正式更名为createNodes,旧名称仅作为已废弃的运行时别名保留。本文以 Nx 仓库中该迁移的官方文档为骨架,结合迁移实现源码(packages/rsbuild/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.ts)、单元测试与迁移注册配置(packages/rsbuild/migrations.json),完整讲解迁移的背景、可自动改写与不可自动改写的边界、底层 AST 实现原理及验证方式,帮助你安全、无损地完成这一升级动作。
为什么需要这次重命名
createNodesV2是 Nx 插件 API 中推断式插件的早期命名。在 Nx 23 中,@nx/rsbuild将其主推断导出正式定名为createNodes,与 Nx 生态其他插件(如@nx/webpack、@nx/vite)的命名约定保持一致。
在 插件实现文件 中可以清楚地看到两者的关系:
export const createNodes: CreateNodes<RsbuildPluginOptions> = [ rsbuildConfigGlob, async (configFilePaths, options, context) => { // ... 扫描 rsbuild.config.* 并生成 build/dev/preview/inspect/typecheck 等 targets }, ]; /** * @deprecated Use {@link createNodes} instead. This will be removed in Nx 24. */ export const createNodesV2 = createNodes;关键事实:
createNodes是新的主导出,createNodesV2只是指向同一实现的运行时别名,二者在行为上完全等价;- 源码注释明确标注
@deprecated,并警告该别名将在 Nx 24 中被移除; - 因此,本次迁移不只是“改名”这么简单,而是为 Nx 24 的彻底移除提前清理旧引用,属于一次面向未来的代码卫生(hygiene)工作。
迁移会改写什么
根据 官方迁移文档,该迁移会扫描工作区中每一个.ts、.tsx、.cts、.mts文件,并将从@nx/rsbuild导入/再导出的createNodesV2具名绑定统一重写为createNodes。
基础改写:单独的具名导入
迁移前:
import { createNodesV2 } from '@nx/rsbuild';迁移后:
import { createNodes } from '@nx/rsbuild';保留别名
createNodesV2 as cn这类别名导入会被改写为createNodes as cn,局部绑定名cn保持不变,因此文件体内所有cn(...)的引用都无需改动:
// 迁移前 import { createNodesV2 as cn } from '@nx/rsbuild'; export const plugin = cn; // 迁移后 import { createNodes as cn } from '@nx/rsbuild'; export const plugin = cn;合并去重
如果文件里同时导入了两个名称,例如import { createNodes, createNodesV2 } from '@nx/rsbuild';,改写后冗余的绑定会被直接删除,只保留createNodes:
import { createNodes, createNodesV2 } from '@nx/rsbuild'; // 迁移后: import { createNodes } from '@nx/rsbuild';顺序无关紧要,createNodesV2, createNodes同样会被合并为单个createNodes。多行写法也会被统一收敛为单行格式化输出(迁移末尾会调用formatFiles)。
其他被安全处理的场景
从 测试用例 中可以看到,迁移对以下场景均有覆盖:
| 场景 | 迁移前 | 迁移后 |
|---|---|---|
| 纯类型导入 | import type { createNodesV2 } from '@nx/rsbuild'; | import type { createNodes } from '@nx/rsbuild'; |
| 内联 type 修饰符 | import { type createNodesV2 } from '@nx/rsbuild'; | import { type createNodes } from '@nx/rsbuild'; |
| 混合默认导入 | import def, { createNodesV2 } from '@nx/rsbuild'; | import def, { createNodes } from '@nx/rsbuild'; |
| 具名再导出 | export { createNodesV2 } from '@nx/rsbuild'; | export { createNodes } from '@nx/rsbuild'; |
| 别名再导出 | export { createNodesV2 as cn } from '@nx/rsbuild'; | export { createNodes as cn } from '@nx/rsbuild'; |
| 冗余别名折叠 | import { createNodesV2 as createNodes } from '@nx/rsbuild'; | import { createNodes } from '@nx/rsbuild'; |
其中“冗余别名折叠”值得特别说明:createNodesV2 as createNodes这种写法在改写后导入名与本地名相同,会自然塌缩为简洁的createNodes,不会留下createNodes as createNodes这种冗余别名。
文件体内的值引用同步重命名
这是迁移最微妙的部分:当未使用别名的createNodesV2本地绑定被改名后,文件体内对它的值引用也必须同步改名,否则会产生悬空引用。迁移实现中的renameLocalUsages逻辑会遍历整个文件的 AST,把以下形式的引用一并改写:
// 迁移前 import { createNodesV2 } from '@nx/rsbuild'; export const plugin = createNodesV2; addPlugin(graph, '@nx/rsbuild', createNodesV2, {}); // 迁移后 import { createNodes } from '@nx/rsbuild'; export const plugin = createNodes; addPlugin(graph, '@nx/rsbuild', createNodes, {});对象简写属性会被展开为完整键值对以保留属性键名:
// 迁移前 import { createNodesV2 } from '@nx/rsbuild'; export const plugins = { createNodesV2 }; // 迁移后 import { createNodes } from '@nx/rsbuild'; export const plugins = { createNodesV2: createNodes };而以下几类位置会被安全跳过(见 isRenamableValueUsage 实现):
- 属性访问
config.createNodesV2、plugin.createNodesV2(这是成员名,不是绑定引用); - 限定类型名
Foo.createNodesV2; - 对象字面量键
{ createNodesV2: ... }; - 遮蔽了导入绑定的变量、参数、函数、类声明;
- 字符串字面量与注释(它们不是
Identifier节点,天然不受影响)。
什么不会被改写
官方文档明确划定了改写边界:只改写来自@nx/rsbuild的静态import/export具名绑定。以下形式一律保持原样:
| 不被改写的写法 | 原因 |
|---|---|
import * as nxRsbuild from '@nx/rsbuild'; nxRsbuild.createNodesV2 | 命名空间导入,整体引用模块,仍可通过运行时别名工作 |
const m = await import('@nx/rsbuild'); m.createNodesV2 | 动态import(...) |
const { createNodesV2 } = require('@nx/rsbuild'); | require解构 |
plugin.createNodesV2 | 属性访问 |
export * from '@nx/rsbuild'; | 没有具名绑定可供改写 |
import { createNodesV2 } from '@nx/other-plugin/plugin'; | 模块说明符不匹配(非@nx/rsbuild) |
import { createNodesV2 } from './plugin'; | 相对路径说明符不匹配 |
这些写法之所以被“放过”,是因为createNodesV2作为运行时别名依然存在、依然可用。迁移实现中的TARGET_SPECIFIERS常量(new Set(['@nx/rsbuild']))精确限定了唯一被识别的模块说明符。如果你希望在整个仓库中彻底消除已废弃名称,需要手动处理上述场景。
迁移如何被触发与注册
该迁移通过 Nx 的迁移框架(nx migrate)运行,注册信息位于 migrations.json:
{ "generators": { "update-23-0-0-migrate-create-nodes-v2-import": { "version": "23.0.0-beta.24", "description": "Rename imports of `createNodesV2` from `@nx/rsbuild` to the canonical `createNodes` export.", "implementation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes", "documentation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.md" } } }也就是说,当你的工作区执行nx migrate @nx/rsbuild@23完成版本升级后,Nx 会自动依据version: "23.0.0-beta.24"匹配到这条迁移并在nx migrate --run-migrations阶段执行。它没有requires字段(不像同目录下其他三条迁移那样依赖@rsbuild/core >= 2.0.0),因为重命名@nx/rsbuild自身的导出与 Rsbuild 核心版本无关。
迁移执行完成后,控制台会输出类似Renamed \createNodesV2` imports to `createNodes` in N file(s).的日志,N` 为实际被触碰的文件数。
底层实现原理:基于 TypeScript AST 的精准改写
迁移的核心是 rewriteCreateNodesV2Imports 函数,它没有使用脆弱的字符串替换,而是走完整的 TypeScript AST 流程:
- 按扩展名过滤:
visitNotIgnoredFiles遍历整个工作区,只处理.ts、.tsx、.cts、.mts文件(常量TS_EXTENSIONS定义);命中前还会用original.includes('createNodesV2')做一次快速预筛,未包含旧名称的文件直接跳过; - 解析源码:通过
ts.createSourceFile把文件内容解析为带父节点引用的 AST,并设置setParentNodes: true(后续判断“值引用”时需要向上回溯父节点); - 收集变更:遍历顶层语句,对
ImportDeclaration调用collectImportRewrite、对ExportDeclaration调用collectExportRewrite。rewriteNamedBindings会重新渲染{ ... }中的绑定列表——重命名createNodesV2为createNodes、按seen集合去重、保留type前缀与别名——然后以ChangeType.Delete+ChangeType.Insert的方式记录精确的字符区间变更; - 重命名值引用:仅当本地绑定确实被改名(单独的
{ createNodesV2 }或与已有createNodes合并)时,才触发collectValueUsageRewrites遍历整个文件体,用isRenamableValueUsage排除属性访问、限定名、对象键、遮蔽声明等安全位置; - 应用变更:
applyChangesToString一次性把收集到的所有字符串变更应用到源码上(所有位置基于原文件偏移,互不干扰),最后调用formatFiles统一格式化; - 按需写回:只有内容实际发生变化(
updated !== original)的文件才会被tree.write写回,并累加touchedCount作为日志统计。
由于createNodes与createNodesV2在 plugin.ts 中是同一个函数对象的两个导出名,重命名不会改变任何运行时行为——这保证了迁移的“零风险”特性:即使某个文件被遗漏,旧名称别名依然可用,不会破坏构建。
如何验证迁移结果
仓库提供了完整的单元测试(migrate-create-nodes-v2-to-create-nodes.spec.ts),共覆盖约 30 个用例,是理解迁移边界的最佳参考。几个关键分组:
- rewriteCreateNodesV2Imports 分组:直接测试纯函数的改写逻辑,覆盖单独的具名导入、别名保留/折叠、双向去重、多行导入、
import type、内联type修饰符、默认导入共存、具名/别名再导出等语法形态; - 安全边界分组:验证来自其他模块说明符(
@nx/other-plugin/plugin)、相对路径、export * from、字符串与注释、require解构中的createNodesV2一律不被触碰; - 值引用分组:验证函数体引用(含作为调用参数)、去重场景下的引用、对象简写展开、属性访问保留等行为;
- 迁移运行器分组:用
createTreeWithEmptyWorkspace构造虚拟工作区,写入a.ts、b.tsx、c.cts、d.mts四种扩展名文件,断言migration(tree)后各文件内容符合预期,同时验证不含旧名称的文件与.md等非 TS 文件不被修改。
迁移后的手动收尾清单
运行迁移后,建议按以下清单收尾:
- 运行测试与构建:由于别名仍然存在,未改写的
require/动态导入/命名空间访问不会报错,但请通过nx run-many -t build test确认整体无回归; - 搜索残留引用:在仓库中全局搜索
createNodesV2(注意排除packages/rsbuild中迁移自身的源码与测试),若仍存在,则属于文档声明的手动处理范围; - 规划 Nx 24 升级:
createNodesV2别名将在 Nx 24 移除,建议在本次升级中就把require(...)解构、动态导入、属性访问等场景一并改为createNodes,为下次升级扫清障碍; - 同步插件内部引用:如果你在
nx.json的plugins配置中通过字符串引用@nx/rsbuild,该配置无需改动(推断式插件通过包名自动加载,不涉及具名导入);只有在你自己的工具库/脚本中显式import { createNodesV2 }时才需要关注本次迁移。
小结
createNodesV2→createNodes是 Nx 23 为 Nx 24 移除废弃别名所做的铺垫性迁移。它依托 TypeScript AST 实现了对具名导入、再导出、别名、去重与文件体值引用的精准改写,同时对命名空间导入、动态导入、require解构等场景保持安全跳过。理解迁移的改写边界与底层实现,既能让你放心地一键升级,也能在手动处理残留引用时做到心中有数。
【免费下载链接】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),仅供参考