Radix UI -> Base UI 迁移指南
将 shadcn 包装器、手写的 radix 组合组件及其消费者代码迁移至 @base-ui/react,并保证项目在每一步都可构建。请精确操作;切勿猜测映射关系。当某个属性或部件不在以下参考文件中时,请在转换前检查 node_modules/@base-ui/react/**/*.d.ts,并在报告中记录缺失项。
预检步骤(始终执行)
- 运行
npx shadcn@latest info --json(或项目所用的运行器):获取当前基础库、STYLE(例如radix-lyra)、tailwind 版本、别名、已安装的组件以及包管理器。信任此输出,而非推断。 - 检测包管理器(packageManager 字段 / 锁文件:pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json),并使用它执行所有安装操作。绝不遗留过时的锁文件。
- 要求干净的 git 工作树;在分支上工作;每个组件一个提交。
- 在接触依赖项之前进行基线检查:运行项目的类型检查/构建,确保预先存在的失败不会归咎于你。
- 将
@base-ui/react与 radix 一同安装。仅当最后一个组件迁移完成后才移除 Radix 包(两者可共存)。
策略:优先使用黄金对,其次使用转换引擎
- 通过 CLI 获取黄金对(首选)。 如果项目是 shadcn 且具有已知风格(
radix-<style>),则 shadcn CLI 本身就是黄金对执行器:- 首先对每个 ui 包装器进行分类:将用户的文件与其源文件进行差异比较,使用 components.json 中的风格原样拼入 URL(
https://ui.shadcn.com/r/styles/<style>/<component>.json,files[0].content)。这适用于带前缀的风格(如 radix-nova)以及旧版无前缀的风格(如 new-york, new-york-v4, default),这些仍被服务。 - 全项目模式:立即将
components.json中的风格从radix-<style>翻转为base-<style>。渐进模式:不要立即翻转(项目大部分仍是 radix;翻转仅在最后一个组件迁移后执行一次);改为直接通过 URL 获取 base 变体(https://ui.shadcn.com/r/styles/base-<style>/<component>.json)。 - 未定制包装器,全项目模式:运行
shadcn add <component> --overwrite,它将使用项目确切的图标/字体/预设解析交付 base 变体。切勿使用--all --overwrite;应逐个组件进行,否则会被无关的注册表版本漂移淹没。渐进模式:切勿使用--overwrite(它会破坏消费者仍在导入的原始文件);改为将获取的 base 变体内容写入<component>-base.tsx。 - 定制包装器:获取 base 变体并将用户的差异重放上去(他们的定制必须保留;
--overwrite会破坏它们)。可规模化实施的机械方法:git merge-file user.tsx radix-golden.tsx base-golden.tsx(三路合并,以 radix 黄金对为祖先),可自动解决大多数文件;使用参考表手动解决冲突。 - 在每个黄金对文件(包括“干净”合并的文件)上强制进行残留扫描:
grep -n "radix-ui\|@radix-ui\|IconPlaceholder"每个文件。注册表有时会在变体之间重新排列函数,这可能导致三路合并报告零冲突,同时留下陈旧的 radix 代码段。干净的合并并不证明文件是干净的。 该策略比重建转换更可靠;只要黄金对存在,就使用它。消费者/应用代码没有 CLI 机制:始终根据consumer-props.md手动迁移。
- 首先对每个 ui 包装器进行分类:将用户的文件与其源文件进行差异比较,使用 components.json 中的风格原样拼入 URL(
- 旧版样式(new-york, new-york-v4, default):仅分类,不重放。 这些样式没有 base 对应项(不存在 base-new-york),将其重定向到 base- 变体会改变用户应用的外观。仅使用 radix 黄金对来检测定制,然后对用户自己的文件运行转换引擎:重写原语,保留其确切的类,应用类映射重命名。用户的外观保持不变。在旧版全项目迁移结束时,标记(不要修复):风格名称对 CLI 来说仍然读作 radix,因此将来的
shadcn add将交付 radix 变体;用户决定是切换风格还是手动添加。 - 转换引擎(备用方案)。 对于手写的 radix 代码、非 shadcn 项目、未知风格:使用
universal-patterns.md(两种形式的导入:radix-ui和@radix-ui/react-*;asChild -> render 及工作示例;Portal > Positioner > Popup;positioner FORWARD 规则;部件重命名)、按族分类的属性表(overlays.md,menus.md,form-controls.md,disclosure.md,display-misc.md)、用于 data-attribute/CSS-var 重写的class-mapping.md,以及wrapper-shapes.md获取精确的目标形状(tooltip arrow, SubContent 默认值, select 解剖结构)。
模式
渐进式(默认)。 命令如“迁移 accordion”= 单个组件,采用绞杀者模式:
- 首先检测进行中的状态:是否存在
<component>-base.tsx,消费者是否在旧/新导入之间拆分。文件本身就是状态;继续,绝不重启。 - 如果该组件导入其他仍在使用 radix 的 ui 包装器(例如 select -> button),则停止并建议先迁移那些底层包装器。
- 将迁移后的版本写入
<component>-base.tsx(原始文件保持不变;根据上述策略,黄金对内容通过 URL 获取或手动转换);进行类型检查。一次一个地重指消费者(导入 +consumer-props.md中的调用点属性);每次进行类型检查。当没有消费者再导入原始文件时:删除它,将-base重命名为原始名称,将导入翻转回来,进行最终检查,提交。当项目中最后一个 radix 包装器完成时,将components.json翻转为base-<style>并移除 radix 依赖。
全项目模式(仅当明确要求时):按依赖顺序执行相同组件级工作(叶子/共享包装器如 button 和 label 优先)。包装器完成后,根据 consumer-props.md 扫描所有应用代码——调用点的破坏范围比 asChild 大得多。然后移除 radix 依赖,安装,完整构建。
硬性规则
- 绝不触碰非 radix 库或其包装器:cmdk(command), vaul(drawer), sonner, input-otp, react-day-picker(calendar), recharts(chart)。报告它们为故意未触碰。
- 无 Base UI 对应项:AspectRatio -> CSS aspect-ratio div;Label -> 原生
<label>;VisuallyHidden ->sr-only;Direction -> Direction Provider(使用direction属性,而非dir)。Popover Anchor 和 NavigationMenu Indicator 没有等价物:惰性传递 + 标记。 button.tsx迁移至真正的@base-ui/react/button原语,而非手写的 useRender 包装器。- 行为差异标记,而非静默修复(例如 tabs 手动激活、菜单项点击不关闭、nav-menu 50ms 延迟)。目标是符合 shadcn base 注册表的惯用 Base UI。
- 诚实报告:跳过/还原的文件列为已标记,而非已迁移。预先存在的失败应标明为预先存在。
验证与报告
每个文件进行类型检查,每批进行构建,最后进行完整构建并与基线比较。
报告存放在项目根目录下的 .migration/ 目录中,每个组件一个文件:.migration/<component>.md(例如 .migration/accordion.md)。
规则:
- 每次运行都会写入(或完全覆盖)其所迁移的每个组件的文件。重新运行某个组件会替换其报告;绝不触碰其他组件的文件。
- 一次多组件运行(如“迁移 alert-dialog 和 dropdown-menu”)会为每个组件写入一个文件,每个文件独立;共享的消费者扫描说明将在每个受影响的文件中重复。
- 全项目模式除了每个组件的文件外,还会写入
.migration/project.md(依赖交换、应用代码扫描摘要、最终构建结果)。 - 没有索引文件。 迁移状态来源于磁盘,而非维护:当被问及“还剩什么”时,扫描项目的 ui 目录(来自 shadcn info 的
ui别名,例如 components/ui 或 src/components/ui)以查找剩余的 radix 导入。每次运行总结时输出该派生计数(“N 个包装器仍在使用 Radix”)。
每个 .migration/<component>.md 严格使用以下结构(该结构公开记录;报告必须匹配):
# <component>
<日期,使用的策略(通过 CLI 的黄金对 / 合并 / 引擎),一行结论>
## 更改的
<每个接触过的文件,更改了什么以及原因;对任何值得注意的内容包含 file:line。确认残留扫描干净:
对此组件的文件执行 grep -n "radix-ui\|@radix-ui">
## 保留的
<看起来相关但有意未触碰的文件,以及原因(cmdk/vaul/sonner 不是 radix;无关漂移等)>
## 行为变更
<编译通过但行为不同的差异;已标记,未静默修复(tabs 激活、菜单点击关闭、延迟...)。若无则此节为空>
## 手动验证
<此原语族的简短手动 QA 检查清单:对话框的焦点返回、菜单/选择上的键盘导航+类型搜索、tooltip 延迟感受、slider 提交事件。具体步骤,大约一分钟点击操作>