Vendure CLI codemod 指南:用vendure codemod自动迁移 Dashboard 扩展代码
【免费下载链接】vendureOpen-source headless commerce platform built with TypeScript, NestJS, React, and GraphQL项目地址: https://gitcode.com/GitHub_Trending/ve/vendure
vendure codemod是 Vendure CLI(@vendure/cli)提供的自动化代码转换命令,用于对 Vendure 项目执行一次性的批量代码改写。在本文场景中,它最主要的用途是把基于 Radix UI / 旧版@vendure-io/ui编写的 Dashboard 扩展,自动迁移到 Base UI 模式(@vendure/dashboard统一入口)。读完本文,你将掌握vendure codemod的交互式与非交互式用法、dashboard-base-uitransform 的 5 类具体改写规则,以及它在源码层的执行机制与可验证的测试依据。
命令定位:codemod 在 Vendure CLI 中的角色
Vendure CLI 是驱动 Vendure 项目全生命周期的命令行工具(二进制名为vendure),包含dev、build、start、add、migrate、schema、doctor、codemod八个子命令。其中codemod的命令定义位于 command-declarations.ts,描述为 “Run codemods to update your Vendure project code”(运行 codemod 更新你的 Vendure 项目代码),它接受两个可选位置参数:
transform:要运行的 codemod 名称;path:要转换的文件或目录路径。
与add、migrate、schema一样,codemod属于“具备交互提示能力”的命令:不带参数运行时会在终端弹出选择器;而带显式参数运行时则走非交互路径,这在自动化脚本和 Agent 场景中尤为重要(详见下文)。
交互式与非交互式两种模式
运行vendure codemod且不传transform参数时,命令会进入交互模式:终端会显示🔧 Vendure Codemods标题,并通过下拉选择器列出所有已注册的 codemod,等待用户选择后执行(源码见 codemod.ts)。
但交互模式有一个关键限制:交互模式下不支持path参数,只能从当前工作目录运行。这是因为clack/prompts的选择器会阻塞等待用户输入——对无人值守的 CI 流水线或 AI Agent 而言,这会导致进程挂起。
因此,Agent 必须显式传入 transform 名称,让命令以非交互方式运行。当交互模式检测到非交互环境时(例如设置了VENDURE_CLI_NON_INTERACTIVE=true),会调用abortIfNonInteractive('vendure codemod', ['vendure codemod dashboard-base-ui'])快速失败,并打印可参考的示例命令,而不是干等终端输入。这一约定同时记录在 vendure-cli SKILL.md 的 “Critical rules for agents” 一节中。
基本用法与参数说明
vendure codemod <transform> [directory]两个参数的语义:
transform:要运行的 codemod 名称。若传入的名称不在注册表中,命令会报错Unknown codemod: "<name>",并列出所有可用的 codemod 后以非零状态码退出(codemod.ts)。directory:可选的要转换的目标目录,仅在非交互模式下支持,默认是当前工作目录。传入后,命令会先做严格校验(resolveAndValidatePath,见 codemod.ts):- 使用
path.resolve解析为绝对路径; - 若路径不存在,报错
Path does not exist: <resolved>并退出; - 若路径存在但不是目录,报错
Path is not a directory: <resolved>并退出。
- 使用
运行方式:按项目 lockfile 选择 CLI 启动器
@vendure/cli通常是项目的 devDependency,因此不应硬编码npx。请根据项目根目录的 lockfile 选择对应的运行器(规则见 vendure-cli SKILL.md):
| 项目根目录 lockfile | 包管理器 | 运行命令 |
|---|---|---|
bun.lock/bun.lockb | bun | bunx vendure <command> |
pnpm-lock.yaml | pnpm | pnpm exec vendure <command> |
yarn.lock | yarn | yarn vendure <command> |
package-lock.json | npm | npx vendure <command> |
| 未找到 | npm(回退) | npx vendure <command> |
若@vendure/cli是全局安装的,可直接调用vendure <command>;用vendure --help可列出全部命令。下文示例为便于阅读使用裸vendure前缀,实际执行时请按上表补上对应运行器。
可用 transforms 一览
| Transform | 描述 |
|---|---|
dashboard-base-ui | 将 Dashboard 扩展从 Radix UI 迁移到 Base UI 模式 |
目前 codemod 注册表中仅有一个 transform(codemod.ts),并且是按需动态加载的:dashboard-base-ui的run函数内部通过await import('./dashboard-ui/dashboard-ui-migration')懒加载迁移实现,避免 CLI 启动时加载昂贵的 TSX 解析依赖。新增 codemod 时只需在CODEMODS注册表中添加一个条目,注册表结构为:
const CODEMODS: Record<string, { description: string; run: (targetPath?: string) => Promise<void> }> = { 'dashboard-base-ui': { description: 'Migrate dashboard extensions from Radix UI to Base UI patterns', run: async (targetPath?: string) => { /* ... */ }, }, };若 transform 名称未被识别,可运行不带参数的vendure codemod查看交互式列表,或直接查看上述注册表获取最新清单。
基础示例
# 对当前工作目录下的整个项目执行迁移 vendure codemod dashboard-base-ui # 仅对指定插件目录执行迁移(非交互模式) vendure codemod dashboard-base-ui ./src/plugins/my-plugin第二条命令会把./src/plugins/my-plugin解析为绝对路径并校验其为存在的目录,然后仅对该目录内的 TSX 文件执行迁移。
深入dashboard-base-ui:五类源码级转换
dashboard-base-ui的实际执行入口是dashboardUiMigration函数(dashboard-ui-migration.ts)。它基于ts-morph构建 TypeScript/TSX 抽象语法树(AST)进行分析与改写——值得注意的实现细节是:它刻意没有复用getTsMorphProject(),因为后者会做 Vendure 特有的 monorepo/package.json 探测,在外部项目中会失败;codemod 只需要加载 TSX 文件,因此自行创建Project。
迁移会对每个.tsx文件依次执行 5 个 transform,其顺序是精心设计的(源码注释明确说明了依赖关系):
- asChild → render(
as-child-to-render.ts):必须先于 import 合并执行,确保asChild属性在 import 被重写前已经消失; - FormField → FormFieldWrapper(
form-components.ts):移除旧表单组件的 import 并引入FormFieldWrapper,同样必须先于 import 合并,以便第三方的残留表单 import 能被后续步骤捕获; - Import 合并(
import-consolidation.ts):把@radix-ui/*、@vendure-io/ui、@base-ui/react等 import 统一改写为@vendure/dashboard,并重写命名空间成员访问点。放在 JSX 转换之后,才能看到最终的 import 集合; - Accordion prop 清理(
accordion-props.ts):独立转换,顺序无关; - Select items 提示/改写(
select-items-prop.ts):只读为主,不产生破坏性修改。
在遍历源码文件之前,迁移会先定位 tsconfig(findTsConfig,dashboard-ui-migration.ts):优先向上查找tsconfig.dashboard.json(Dashboard 扩展的首选配置),其次查找tsconfig.json,一直回溯到文件系统根目录;若都找不到则抛出明确错误,提示“请从包含 tsconfig.json 的目录运行,或通过第二个参数传入项目目录路径”。加载项目后,它会过滤出目标目录下所有.tsx文件;如果 tsconfig 的 include 模式没有覆盖到 Dashboard 扩展文件(sourceFiles.length === 0),则回退用**/*.tsxglob 手动扫描并添加。
每次转换完成后,命令会打印摘要:Found N TSX files (using <tsconfig>)、每个改动文件的Updated: <path> (N changes),最后Done! N changes across M files;若未发现任何 Radix UI 模式,则输出No Radix UI patterns found. Your code is already up to date!。单个文件处理出错只记录警告(Error processing <path>: <message>)并继续,不会中断整体迁移。
1.asChildprop →renderprop
Radix 风格的<Button asChild><Link>...</Link></Button>组合子模式,在 Base UI 中改为显式的renderprop:
// 迁移前 <Button asChild> <Link to="./new"> <PlusIcon /> New </Link> </Button> // 迁移后 <Button render={<Link to="./new" />}> <PlusIcon /> New </Button>转换逻辑(as-child-to-render.ts)会反复扫描源码中的asChild属性(每次替换都会使 AST 位置失效,因此采用“替换一次、重新扫描”的循环策略),并处理多种边界情况:
asChild出现在自闭合元素上:仅删除属性并给出警告;asChild的子节点数量不为 1(JSX 要求恰好一个子元素):跳过并警告;- 子节点是 JSX 表达式或纯文本:无法自动转换,跳过并警告;
- 子元素是普通 JSX 元素时,会用原始源码文本提取内部内容(保留
{value.firstName} {value.lastName}这类内联空白),并对内部内容做基于缩进的 dedent,保证格式化后缩进正确。
2. 旧表单组件 →FormFieldWrapper
shadcn 风格的FormField + FormItem + FormLabel + FormControl + FormMessage嵌套结构,被合并为更简洁的FormFieldWrapper单组件:
// 迁移前 <FormField control={form.control} name="slug" render={({ field }) => ( <FormItem> <FormLabel>Slug</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormDescription>The URL slug.</FormDescription> <FormMessage /> </FormItem> )} /> // 迁移后 <FormFieldWrapper control={form.control} name="slug" label="Slug" description="The URL slug." render={({ field }) => ( <Input {...field} /> )} />该转换(form-components.ts)基于 ts-morph AST 解析,逐个处理FormField(每次修改后重新查询,最多迭代 100 次防止死循环);对于无法自动转换的模式,会回退写入TODO注释提示人工处理。
3. Import 合并:统一到@vendure/dashboard
转换的核心目标(import-consolidation.ts)是让所有 UI 组件 import 统一来自@vendure/dashboard:
- 识别
@radix-ui/*、@vendure-io/ui*、@base-ui/react*三类 import,收集其默认/命名 import(保留别名),删除原声明,去重后合并写入import { ... } from '@vendure/dashboard'; - 处理命名空间 import(如
import * as Dialog from '@radix-ui/react-dialog'):通过NAMESPACE_MEMBER_MAP将Dialog.Root → Dialog、Dialog.Trigger → DialogTrigger、Dialog.Content → DialogContent等成员访问改写为扁平组件名,并自动补充对应命名 import(如Dialog.Root映射为Dialog本身、Dialog.Overlay映射为DialogOverlay);遇到未收录的成员则按NamespaceMember命名并输出警告; - 处理第三方包中被
@vendure/dashboard重新导出的符号:源码中的REEXPORTED_SYMBOLS表精确列出了可迁移的符号集合,例如react-hook-form(useForm、Controller、useWatch等)、@tanstack/react-query(useQuery、useMutation、queryOptions等)、@tanstack/react-router(Link、useNavigate等)、@tanstack/react-table、@lingui/react的useLingui、lucide-react的LucideIcon类型以及sonner的toast。只把表内符号搬移到@vendure/dashboard,其余保留在原包,避免破坏性的错误重写; - 注释中特别说明:
@lingui/react/macro路径下的Trans、useLingui宏不被迁移(Babel 宏无法被 re-export),lucide-react的实际图标组件也不迁移(仅LucideIcon类型被 re-export)。
4. Accordion 过时 prop 清理
Base UI 的 Accordion 不再需要 Radix 风格的type与collapsibleprop(accordion-props.ts):
// 迁移前 <Accordion type="single" collapsible className="w-full"> // 迁移后 <Accordion className="w-full">实现上会查找所有<Accordion>开闭标签与自闭合标签,逆序移除type="single"、type="multiple"属性以及collapsible布尔属性(逆序处理避免位置偏移),并保留其余属性。
5. Select 缺失itemsprop 补齐
Base UI 的 Select 推荐通过itemsrecord 声明选项(select-items-prop.ts)。对于静态的<SelectItem>子节点(字符串valueprop + 文本内容),转换会自动生成 items record:
// 迁移前 <Select value={value} onValueChange={setValue}> <SelectContent> <SelectItem value="draft">Draft</SelectItem> <SelectItem value="published">Published</SelectItem> </SelectContent> </Select> // 迁移后 <Select value={value} onValueChange={setValue} items={{ draft: 'Draft', published: 'Published' }}> <SelectContent> <SelectItem value="draft">Draft</SelectItem> <SelectItem value="published">Published</SelectItem> </SelectContent> </Select>对于动态模式(如.map()生成的选项),无法静态推断,则输出警告提示人工处理。
测试与验证:可运行的证据链
dashboard-base-ui的全部 5 个转换均有 vitest 单元测试覆盖,测试文件位于 dashboard-ui-migration.spec.ts。测试采用共享的 ts-morphProject(useInMemoryFileSystem: true,避免重复初始化 TypeScript 编译器)和内存源码文件进行断言。例如transformAsChildToRender的用例验证:
- 改动计数为 1;
- 输出文本包含
render={<Link to="./new" />}; - 不再包含
asChild; - 子内容
<PlusIcon />与文本New被保留; - 外层仍然是
<Button。
仓库还附带了一套面向 Agent 的迁移操作手册 radix-to-base-ui-migration,它与 codemod 的执行顺序一致:扫描asChild用法、旧表单组件、直接@radix-ui/*/@vendure-io/ui/*/@base-ui/react/*import、带type/collapsible的Accordion、缺items的Select,然后参考01-asChild-to-render.md、02-form-components.md、03-import-consolidation.md、04-component-api-changes.md四份细则逐项转换,最后验证“所有 UI 组件 import 均来自@vendure/dashboard、不再残留三类直接 import、第三方 import 仅使用白名单符号”。
使用建议与注意事项
- Agent / CI 场景必须传 transform 名,并建议设置
VENDURE_CLI_NON_INTERACTIVE=true,让无参数调用快速失败并打印示例,而不是阻塞在终端提示符; - 迁移前先提交或备份:codemod 是直接改写文件的批量操作(通过
project.save()落盘),虽然单文件出错不会中断整体,但建议先在干净的工作区运行git diff审查改动; - 目录参数只在非交互模式生效,交互模式请直接在目标项目目录下运行;
- 确保能找到 tsconfig:迁移依赖
tsconfig.dashboard.json或tsconfig.json(从目标目录向上回溯查找),找不到时会报错并提示传入项目目录路径; - 查看最新 transform 清单:运行不带参数的
vendure codemod查看交互式列表,或查看 codemod.ts 中的CODEMODS注册表。
【免费下载链接】vendureOpen-source headless commerce platform built with TypeScript, NestJS, React, and GraphQL项目地址: https://gitcode.com/GitHub_Trending/ve/vendure
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考