Nx 23.0 迁移指南:将@nx/remix插件的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 工作区中使用了@nx/remix插件推断功能(inferred plugin)的开发者,讲解 Nx 23.0 引入的自动化迁移:把@nx/remix/plugin的主导出createNodesV2重命名为规范的createNodes,并保留旧的createNodesV2作为已弃用别名。读完本文,你将掌握这条迁移的完整行为边界(会重写什么、明确跳过什么)、其源码级实现原理(基于 TypeScript AST 的文本重写机制),以及如何通过nx migrate在真实工作区中落地该变更。
迁移背景:为什么createNodesV2要改名
Nx 的插件体系通过「推断插件」在无需手写project.json的情况下,从文件系统自动发现项目并推断出 targets。@nx/remix作为 Remix 官方集成插件,其主推断导出此前命名为createNodesV2。
在 Nx 23.0.0 中,@nx/remix将这一主导出更名为createNodes,使之与 Nx 生态其他插件的命名保持一致。createNodesV2这个名字作为已弃用别名暂时保留,但新代码应当统一使用createNodes。
从源码可以确认这一「同一函数、两个名字」的实现方式:packages/remix/src/plugins/plugin.ts 中,createNodes是真正的推断插件实现(匹配**/{remix,vite}.config.{js,cjs,mjs,ts,cts,mts}配置文件并生成build、dev、start、serve-static、typecheck等 target),而紧随其后的export const createNodesV2 = createNodes;只是对同一对象的别名引用。公共入口 packages/remix/plugin.ts 同时导出createNodes与createNodesV2,因此在运行时两者完全等价,任何通过旧名字进行的调用(包括命名空间访问、动态导入等)都继续可用。
正因二者是同一引用,这次变更本质上是「更名」而非「替换实现」,所以迁移工具可以放心地把createNodesV2的全部静态命名导入重写为createNodes,而不会产生行为差异。
迁移的行为范围:会被重写的内容
该迁移由 packages/remix/migrations.json 注册,generator 名为update-23-0-0-migrate-create-nodes-v2-import,对应版本23.0.0-beta.24,实现位于 packages/remix/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.ts。
扫描范围
迁移会遍历工作区中所有未被忽略的文件,但只处理以下四种 TypeScript 扩展名:
.ts.tsx.cts.mts
对每个文件,先做一次快速文本检查(是否包含createNodesV2),不包含则直接跳过,避免不必要的解析开销(见 migrate-create-nodes-v2-to-create-nodes.ts#L39-L52)。测试用例也验证了非 TS 文件(如docs/example.md)不会被改动。
唯一命中的模块说明符
只有从@nx/remix/plugin导入或再导出的createNodesV2才会被重写(TARGET_SPECIFIERS集合中目前只有这一项)。以下来源的createNodesV2不会被触碰:
- 来自其他模块,如
import { createNodesV2 } from '@nx/other-plugin/plugin'; - 来自相对路径,如
import { createNodesV2 } from './plugin';
这两条边界均有对应单元测试覆盖(见 migrate-create-nodes-v2-to-create-nodes.spec.ts#L103-L116)。
具体的改写规则
1. 普通命名导入
// Before import { createNodesV2 } from '@nx/remix/plugin'; // After import { createNodes } from '@nx/remix/plugin';2. 混合命名导入:其他导出保持不动,仅重命名createNodesV2:
// Before import { createNodesV2, SomeOtherExport } from '@nx/remix/plugin'; // After import { createNodes, SomeOtherExport } from '@nx/remix/plugin';3. 别名导入:别名(本地绑定名)保留,只重写「原导出名」一侧:
// Before import { createNodesV2 as cn } from '@nx/remix/plugin'; // After import { createNodes as cn } from '@nx/remix/plugin';4. 冗余别名折叠:createNodesV2 as createNodes会被折叠为单个createNodes:
// Before import { createNodesV2 as createNodes } from '@nx/remix/plugin'; // After import { createNodes } from '@nx/remix/plugin';5. 重复绑定去重:若同一文件同时导入了createNodes与createNodesV2,无论先后顺序,冗余绑定都会被删除:
// Before import { createNodes, createNodesV2 } from '@nx/remix/plugin'; // After import { createNodes } from '@nx/remix/plugin';多行命名导入同样会被重排为单行形式(见 spec#L61-L66)。
6. 默认导入与 type 导入:默认导入保留;import type、内联type修饰符也被正确保留:
// Before import def, { createNodesV2 } from '@nx/remix/plugin'; import type { createNodesV2 } from '@nx/remix/plugin'; import { type createNodesV2 } from '@nx/remix/plugin'; // After import def, { createNodes } from '@nx/remix/plugin'; import type { createNodes } from '@nx/remix/plugin'; import { type createNodes } from '@nx/remix/plugin';7. 命名再导出:export { ... } from '@nx/remix/plugin'与导入同样处理,别名规则一致:
// Before export { createNodesV2 } from '@nx/remix/plugin'; export { createNodesV2 as cn } from '@nx/remix/plugin'; // After export { createNodes } from '@nx/remix/plugin'; export { createNodes as cn } from '@nx/remix/plugin';8. 文件体内对绑定名的值引用:当一个本地绑定确实被重命名(无别名的{ createNodesV2 },或被去重合并进已有createNodes)时,文件体内所有对该绑定的值引用也会同步重写,否则会留下悬空引用。测试覆盖了变量赋值、函数调用实参(如addPlugin(graph, '@nx/remix/plugin', createNodesV2, {}))、对象简写属性等场景:
// Before import { createNodesV2 } from '@nx/remix/plugin'; export const plugin = createNodesV2; // After import { createNodes } from '@nx/remix/plugin'; export const plugin = createNodes;9. 对象简写属性的展开:{ createNodesV2 }简写会被展开为{ createNodesV2: createNodes },从而保留属性键名:
// Before import { createNodesV2 } from '@nx/remix/plugin'; export const plugins = { createNodesV2 }; // After import { createNodes } from '@nx/remix/plugin'; export const plugins = { createNodesV2: createNodes };不会被重写的内容(及原因)
这是本次迁移最关键的行为边界。以下形式的createNodesV2全部保持原样,因为它们不依赖静态命名绑定,且能通过运行时别名继续正常工作:
- 命名空间导入:
import * as plugin from '@nx/remix/plugin'后通过plugin.createNodesV2访问; - 动态导入:
import('@nx/remix/plugin')之后解构; - CommonJS 解构:
const { createNodesV2 } = require('@nx/remix/plugin');(有专门测试验证其保持不变); - 属性访问 / 限定类型名:
config.createNodesV2、PluginTypes.createNodesV2; export * from再导出:export * from '@nx/remix/plugin';没有可重命名的具名绑定;- 对象字面量键:
{ createNodesV2: something }中的键名不是绑定引用; - 遮蔽声明:文件中若存在名为
createNodesV2的变量、参数或函数声明,属于遮蔽(shadowing),不会被误改; - 字符串与注释:
"createNodesV2"、// import { createNodesV2 } ...一律不动。
对于这些形式,若你希望彻底移除弃用名字,需要手工更新。由于createNodesV2只是运行时别名,即使不改,代码依然正常工作(相关边界判定逻辑见 migrate-create-nodes-v2-to-create-nodes.ts#L280-L316)。
迁移的源码实现原理
实现 migrate-create-nodes-v2-to-create-nodes.ts 的机制值得了解,便于理解上述所有行为:
整体流程:入口
migrateCreateNodesV2ToCreateNodes(tree)通过visitNotIgnoredFiles遍历工作区,过滤出 TS 扩展名文件,做createNodesV2文本预检,命中后调用核心函数rewriteCreateNodesV2Imports;若有文件实际发生变化,通过logger.info输出改动文件数,最后调用formatFiles统一格式化。累计改动文件数统计逻辑见 migrate-create-nodes-v2-to-create-nodes.ts#L34-L61。AST 解析:
rewriteCreateNodesV2Imports使用typescript的createSourceFile将源码解析为 AST(ScriptKind.TSX),遍历顶层语句,区分ImportDeclaration与ExportDeclaration分别处理。字符串补丁而非整文件重写:核心的
rewriteNamedBindings会重新渲染{ ... }绑定列表——重命名createNodesV2为createNodes、丢弃重复项、保留每个绑定上的type修饰符与别名——然后通过applyChangesToString以「删除旧区间 + 插入新文本」的方式生成补丁(见 migrate-create-nodes-v2-to-create-nodes.ts#L173-L212)。这意味着模块说明符、import/export关键字、默认导入、type修饰符等周边内容全部原样保留,只动绑定列表本身。条件性的值引用重写:只有当本地绑定名真正发生变化(无别名导入或被去重)时,才会触发
collectValueUsageRewrites重写文件体内的标识符引用,并配合isRenamableValueUsage排除属性访问、限定名、对象键、遮蔽声明等不可重写的语法位置。
如何在你的工作区执行这条迁移
@nx/remix的迁移注册于 packages/remix/migrations.json,由 Nx 迁移机制统一驱动。在升级到 Nx 23 时:
- 升级 Nx 相关依赖到 23.x(该迁移对应的
version字段为23.0.0-beta.24); - 在仓库根目录运行迁移命令,Nx 会读取各插件的
migrations.json并自动应用update-23-0-0-migrate-create-nodes-v2-import:nx migrate @nx/remix(按 Nx 常规流程,先
nx migrate更新 package.json 并安装依赖,再运行nx migrate --run-migrations执行迁移文件。) - 迁移完成后,检查
migrations.json生成的迁移文件输出日志(Renamed \createNodesV2` imports to `createNodes` in N file(s).`),并在各应用/库下运行类型检查与测试,确认无悬空引用。
迁移后的代码该长什么样
迁移完成后,你的推断插件注册代码应使用规范名。例如,一个手写插件配置或入口文件应呈现为:
import { createNodes } from '@nx/remix/plugin'; // 在 nx.json / 插件配置中使用 createNodes export default { name: '@nx/remix/plugin', createNodes, options: { buildTargetName: 'build', devTargetName: 'dev', startTargetName: 'start', typecheckTargetName: 'typecheck', serveStaticTargetName: 'serve-static', }, };同时,仓库内部源码也已全面转向新命名,可作为「迁移后的正确写法」的参考:@nx/remix自己的生成器在注册插件时使用createNodesV2(因为内部运行时别名仍存在,见 init.ts#L11、convert-to-inferred.ts#L7),而插件入口 packages/remix/plugin.ts 同时导出两个名字,其中createNodes已排在首位。
常见问题
Q:迁移后nx命令还能识别旧的createNodesV2吗?能。createNodesV2作为已弃用别名保留(export const createNodesV2 = createNodes;),运行时行为完全一致,只是新代码不应再使用。
Q:为什么我的require('@nx/remix/plugin')没被改?迁移只重写静态import/export的具名绑定。require解构、命名空间访问、动态import()等通过运行时别名继续工作,不在自动重写范围内,需要的话请手工更新。
Q:迁移会不会误改其他包导出的同名符号?不会。TARGET_SPECIFIERS只匹配@nx/remix/plugin,来自其他模块或相对路径的createNodesV2一律跳过。
Q:如果我同时导入了createNodes和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
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考