Nx 23 迁移指南:将 `@nx/rsbuild` 的 `createNodesV2` 导入统一重命名为 `createNodes`
2026/9/12 5:48:36 网站建设 项目流程

Nx 23 迁移指南:将@nx/rsbuildcreateNodesV2导入统一重命名为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.createNodesV2plugin.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 流程:

  1. 按扩展名过滤visitNotIgnoredFiles遍历整个工作区,只处理.ts.tsx.cts.mts文件(常量TS_EXTENSIONS定义);命中前还会用original.includes('createNodesV2')做一次快速预筛,未包含旧名称的文件直接跳过;
  2. 解析源码:通过ts.createSourceFile把文件内容解析为带父节点引用的 AST,并设置setParentNodes: true(后续判断“值引用”时需要向上回溯父节点);
  3. 收集变更:遍历顶层语句,对ImportDeclaration调用collectImportRewrite、对ExportDeclaration调用collectExportRewriterewriteNamedBindings会重新渲染{ ... }中的绑定列表——重命名createNodesV2createNodes、按seen集合去重、保留type前缀与别名——然后以ChangeType.Delete+ChangeType.Insert的方式记录精确的字符区间变更;
  4. 重命名值引用:仅当本地绑定确实被改名(单独的{ createNodesV2 }或与已有createNodes合并)时,才触发collectValueUsageRewrites遍历整个文件体,用isRenamableValueUsage排除属性访问、限定名、对象键、遮蔽声明等安全位置;
  5. 应用变更applyChangesToString一次性把收集到的所有字符串变更应用到源码上(所有位置基于原文件偏移,互不干扰),最后调用formatFiles统一格式化;
  6. 按需写回:只有内容实际发生变化(updated !== original)的文件才会被tree.write写回,并累加touchedCount作为日志统计。

由于createNodescreateNodesV2在 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.tsb.tsxc.ctsd.mts四种扩展名文件,断言migration(tree)后各文件内容符合预期,同时验证不含旧名称的文件与.md等非 TS 文件不被修改。

迁移后的手动收尾清单

运行迁移后,建议按以下清单收尾:

  1. 运行测试与构建:由于别名仍然存在,未改写的require/动态导入/命名空间访问不会报错,但请通过nx run-many -t build test确认整体无回归;
  2. 搜索残留引用:在仓库中全局搜索createNodesV2(注意排除packages/rsbuild中迁移自身的源码与测试),若仍存在,则属于文档声明的手动处理范围;
  3. 规划 Nx 24 升级createNodesV2别名将在 Nx 24 移除,建议在本次升级中就把require(...)解构、动态导入、属性访问等场景一并改为createNodes,为下次升级扫清障碍;
  4. 同步插件内部引用:如果你在nx.jsonplugins配置中通过字符串引用@nx/rsbuild,该配置无需改动(推断式插件通过包名自动加载,不涉及具名导入);只有在你自己的工具库/脚本中显式import { createNodesV2 }时才需要关注本次迁移。

小结

createNodesV2createNodes是 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),仅供参考

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

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

立即咨询