Plate 模板 CI 修复实战:Registry 辅助依赖与 RSC 客户端边界的完整排查指南

【免费下载链接】plate Rich-text editor with AI and shadcn/ui 【免费下载链接】plate 项目地址: https://gitcode.com/GitHub_Trending/pl/plate

导读

本文以 Plate 仓库中 2026-04-06-fix-template-ci-registry-helper-deps.md 这份修复计划为骨架,完整复盘一次"模板 CI 构建失败"的真实排查过程:注册表(registry)UI/插件重构引入了一批辅助文件后,plate-templateplate-playground-template 两个模板无法再构建。读完本文,你将掌握三个关键工程能力:一是理解 shadcn 风格 registry 元数据如何驱动模板生成(以及为什么"应用源码能编译"不等于"模板能编译");二是学会用"显式登记或内联"两种策略处理 registry 辅助文件依赖;三是理解 Next.js/Turbopack 下 RSC 客户端边界的放置原则,以及如何利用 pnpm --filter www rdpnpm brlpnpm turbo build 等命令组合完成验证闭环。

一、问题背景:重构引发模板 CI 连锁故障

1.1 现象:CI 失败发生在模板,而非主应用

本次故障的典型特征是:apps/www 应用源码一切正常,CI 却在生成的模板中持续报错。计划文档明确记录:

CI is failing in generated templates, not in apps/www.

也就是说,故障点在 templates/plate-templatetemplates/plate-playground-template 两个模板工程里,它们是 Plate 提供给用户脚手架使用的独立项目(分别位于 templates/plate-templatetemplates/plate-playground-template)。

1.2 根因:registry 辅助文件没有进入模板依赖图

模板是由 registry 元数据驱动生成的,而不是从 apps/www/src 直接复制。当 registry UI/插件重构给若干注册项(registry item)引入了新的辅助文件(helper file)导入后,这些辅助文件并没有同步出现在 registry 定义中,导致生成的模板代码里出现 Module not found

计划文档列出了缺失的五个模块:

缺失模块归属
block-discussion-indexblock-discussion 注册项下的辅助逻辑
get-annotation-click-target注释点击目标解析工具
suggestion-line-break-anchorsuggestion 换行锚点组件
suggestion-stylessuggestion 样式变体
trailing-block-kit尾部块 Kit(引用 @platejs/suggestion/react

1.3 一份失败案例的技术教训

配套解决方案文档 2026-04-06-registry-helper-refactors-must-update-template-registry-dependencies.md 总结了三条"行不通"的做法,对理解故障本质极有帮助:

  1. 只检查应用源码导入:本地源文件存在 ≠ registry 定义覆盖。真正的契约是:registry 注册项必须显式携带生成消费者所需的每一个辅助文件与 registry 依赖。
  2. 把模板 CI 失败当作"依赖过期"处理Module not found 是确定性的"文件从未被复制进模板",重装依赖无济于事。
  3. 无差别新增辅助文件:对于只被单一组件使用的辅助文件,先判断是否应内联,而不是无条件扩大 registry 表面积。

二、原理剖析:registry 元数据是模板生成的唯一事实来源

2.1 生成式 registry 的数据流

apps/www 的 registry 体系建立在 shadcn 的 Registry/RegistryItem schema 之上。核心数据流为:

  1. apps/www/src/registry/registry.ts 通过 createPlateRegistry() 汇总各分类注册表(registry-ui.tsregistry-kits.tsregistry-components.tsregistry-lib.tsregistry-examples.tsregistry-hooks.tsregistry-pro.ts 等);
  2. apps/www/scripts/build-registry.mts 读取该汇总结果,经 registrySchema.parse() 校验,并用 withPublicRegistryDependencies() 把内部依赖说明符改写为公开的 registry URL(REGISTRY_BASE_URL,开发环境为 http://localhost:3000/rd,生产为 https://platejs.org/r);
  3. 最终输出 public/r/registry.json(生产)或 public/rd/registry.json(开发),同时生成 src/__registry__/index.tsx 索引文件(见 build-registry.mts 中的 buildRegistryIndex(),内含 React.lazy 懒加载组件映射)。
registry.ts ──> build-registry.mts ──> public/r/registry.json + src/__registry__/index.tsx
                                          │
                                          └──> shadcn CLI / 模板生成器 ──> templates/* 源码

关键推论:模板 CI 的编译对象来自 registry JSON 描述的 filesregistryDependencies。只要注册项定义中缺少某个本地辅助文件,生成出来的模板就必然缺文件——这正是本次故障的机制根源。

2.2 开发/生产双目标输出

apps/www/scripts/build-registry.mts 中按环境区分输出目标:

const isDev = process.env.NODE_ENV === 'development';
const TARGET = isDev ? 'public/rd/registry.json' : 'public/r/registry.json';
const REGISTRY_BASE_URL = isDev ? 'http://localhost:3000/rd' : `${HOMEPAGE}/r`;

因此日常调试时使用 pnpm --filter www rd(内部执行 NODE_ENV=development tsx scripts/build-registry.mts,见 apps/www/package.json)即可快速重建开发版 registry 载荷,验证生成的 JSON 不再指向被删除或缺失的辅助文件。

三、修复策略一:为独立辅助文件补齐 registry 覆盖

3.1 三条关键登记

计划文档的 Findings 与 Progress 显示,最终采用了"显式登记 + 内联"双管齐下的方案。对于需要保留为独立文件的辅助模块,登记内容如下:

registry 依赖在 apps/www/src/registry/registry-kits.ts 中体现为 dependencies(npm 依赖)与 registryDependencies(同 registry 内部引用)两类字段,例如 suggestion-kit 的注册项同时声明了对 @platejs/suggestion 的 npm 依赖和对 @plate/suggestion-base-kit@plate/suggestion-node@plate/suggestion-toolbar-button 的 registry 依赖。

3.2 依赖改写:让模板可远程安装

生成模板时,这些内部 registry 依赖会被 apps/www/scripts/registry-dependencies.mtstoPublicRegistryDependencySpecifier() 改写为完整的公开地址。这正是 withPublicRegistryDependencies() 在 build-registry.mts 中逐项执行的转换:

function withPublicRegistryDependencies(item: RegistryItem): RegistryItem {
  return {
    ...item,
    registryDependencies: item.registryDependencies?.map((dependency) =>
      toPublicRegistryDependencySpecifier(dependency, REGISTRY_BASE_URL)
    ),
  };
}

理解这一点很重要:模板安装命令(如 npx shadcn@latest add)读取的就是这份改写后的 JSON,任何内部路径书写错误都会直接表现为模板构建时的解析失败。

四、修复策略二:把一次性辅助文件内联回宿主文件

4.1 内联的决策原则

计划文档指出:suggestion-line-break-anchorsuggestion-styles 两个文件只服务于 ui/suggestion-node.tsx,把它们保留为独立文件意味着 registry 要为两个无独立设计价值的文件多记两笔账,徒增模板表面(template surface)与 CI 影响面。因此方案选择将它们内联进宿主文件,然后删除额外的 registry 文件条目

配套方案文档给出了判断标准:如果一个辅助函数只被一个 registry 组件使用,优先折叠回该文件;除非它是真正被共享的,否则不要独立成文件。

4.2 内联后的实际代码形态

apps/www/src/registry/ui/suggestion-node.tsx 中,三者现已成为同一文件内的成员:

  • suggestionVariants:基于 cva() 定义的样式变体(见该文件第 30 行附近);
  • getBlockSuggestionWrapperClassName({ ... }):根据状态计算 suggestion 块外层 class 的辅助函数(第 57 行附近);
  • SuggestionLineBreakAnchor:行内换行锚点组件(第 110 行附近),并被主组件在第 330 行与第 343 行处实际渲染使用。
// apps/www/src/registry/ui/suggestion-node.tsx(结构示意,节选)
export const suggestionVariants = cva(/* ... */);

export function getBlockSuggestionWrapperClassName({ /* ... */ }) {
  return suggestionVariants({ /* ... */ });
}

export function SuggestionLineBreakAnchor({ /* ... */ }) { /* ... */ }

这样既消除了两个 registry 条目,又保证了生成模板只需安装 suggestion-node 一个文件即可获得全部能力。

五、同源故障(二):registry 项目把客户端代码拉进 RSC 服务端图

5.1 症状与误诊陷阱

计划文档记录了与模板修复并行的第二个故障:pnpm turbo build --filter=./apps/www 在生产构建时报:

You're importing a module that depends on `useEffect` into a React Server Component module.

报错文件位于 packages/core/src/react/**,极易被误判为 @platejs/core/react 包入口问题。但真正的原因是:trailing-block-kit 被加入 registry 后,生成索引 apps/www/src/registry/index.tsx 开始暴露该 Kit,而它静态导入 @platejs/suggestion/react,把整个 React 编辑器图拖进了服务端可见的 registry 图(server-visible registry graph)——Turbopack 于是把最先遇到的 packages/core/src/react/** 钩子文件报告出来。

核心结论:包入口只是"传话筒",真正的 bug 是客户端专属辅助模块进入了服务端可见的 registry 图

5.2 三种无效尝试

解决方案文档 2026-04-06-next-turbopack-needs-client-boundaries-at-react-package-entrypoints.md 记录了三条未解决问题的路径:

  1. @platejs/react'use client'——掩盖坏导入图而非移除;
  2. 把指令挪到 @platejs/core/react 各文件——同理是错误地扩大客户端边界;
  3. 当作普通页面问题处理——忽略了 registry 路由这条决定性线索。

5.3 正确修复:客户端边界放在"离源码最近处"

最终方案是把 trailing-block-kit 实现为带 'use client' 的真实客户端 Kit:

5.4 边界为何必须落在 packages/plate 而非 packages/core

计划文档的 Progress 记录了一个关键迭代:第一次修复把持久客户端边界放在 packages/core/src/react/index.ts,但 pnpm brl(barrel 文件重写工具)会重写这个生成式 barrel 并重新引入漂移(drift);随后把边界迁移到 packages/plate/src/react/index.tsx,该文件以 export * from ... 显式转发 @platejs/core/react@platejs/utils/react@udecode/react-hotkeys@udecode/react-utils,能够经受 pnpm brl 的重写而保持稳定。这一取舍与 2026-04-13-rsc-client-boundary-root-cause.md 记录的后续根因分析一脉相承:边界要放在不会被自动生成工具重写的稳定入口上

六、验证闭环:一套可复现的命令清单

计划文档与两份配套方案文档共同给出完整的验证命令序列,按执行顺序整理如下:

# 1. 安装依赖(确保工作区状态干净)
pnpm install

# 2. 重建 registry 输出(开发模式,写入 public/rd/registry.json)
pnpm --filter www rd

# 3. 类型检查(含 docs-source parity、registry-source parity 双校验)
pnpm turbo typecheck --filter=./apps/www

# 4. 代码风格修复
pnpm lint:fix

# 5. 全量构建(验证 RSC/客户端边界修复,并触发模板相关构建路径)
pnpm turbo build --filter=./apps/www

# 6. barrel 重写后再次构建,确认边界不被自动工具破坏
pnpm brl && pnpm turbo build --filter=./apps/www

其中 apps/www/package.jsontypecheck 脚本还额外执行了 check-docs-source-parity.mtscheck-registry-source.mts 两个源码一致性校验,这意味着 registry 源与文档源的漂移也会被 CI 抓住。而最后一步 pnpm brl && pnpm turbo build 则是本次修复最有价值的收尾动作——它同时验证了"自动工具重写 barrel"与"生产构建"两者可以共存,杜绝边界被工具链悄悄破坏。

七、可复用的工程法则与排查清单

7.1 三条核心法则

  1. 模板生成由 registry 元数据驱动,而非应用源码apps/www 中存在的文件不会自动进入模板。每个被注册项使用的本地辅助文件,只有两种合法表达方式——内联进宿主注册文件,或显式登记为 registry 文件/registry 依赖。
  2. 一次性辅助函数优先内联:独立成文件会放大 registry 簿记成本与 CI 影响面;除非真正被多个注册项共享,否则折叠回宿主文件。
  3. 客户端专属模块必须自带客户端边界:任何会进入 registry 索引的注册项,要么服务端安全,要么自己声明 'use client';不要把 'use client' 打在包入口上掩盖坏图。

7.2 故障排查快速清单

  •  确认 CI 失败在模板还是在应用:apps/www 通过不代表模板通过;
  •  对 apps/www 源码执行 rg 找到全部新增辅助文件导入;
  •  逐一检查对应 registry 注册项(registry-ui.tsregistry-kits.tsregistry-lib.ts 等)是否覆盖这些文件;
  •  判断辅助文件是"独立登记"还是"内联回宿主";
  •  重新生成 registry:pnpm --filter www rd,检查 public/rd/registry.json 的文件清单;
  •  若报 RSC 钩子错误,沿 src/app/api/registry/[name]/route.tssrc/lib/rehype-utils.tssrc/__registry__/index.tsx 链路定位是哪个注册项拉入了客户端图;
  •  运行 pnpm brl && pnpm turbo build --filter=./apps/www 确认边界在 barrel 重写后依然成立。

八、结语

本次"模板 CI registry 辅助依赖修复"表面上是五个缺失模块的补齐,实质是一次对生成式 registry 体系的正确性教育:应用源码的可编译性、registry 元数据的完备性、模板生成结果的可编译性,是三个必须分别验证的层次。而 trailing-block-kit 引发的 RSC 客户端边界问题则提醒我们,在 Next.js/Turbopack 时代,registry 索引被服务端代码读取是常态,任何注册项在上架前都要回答"我是否服务端安全"这个问题。将这两条经验沉淀为本文的排查清单,可以帮助同类工程避免在"本地一切正常、CI 一跑就挂"的怪圈里空耗时间。

【免费下载链接】plate Rich-text editor with AI and shadcn/ui 【免费下载链接】plate 项目地址: https://gitcode.com/GitHub_Trending/pl/plate

更多推荐