Plate 模板 CI 修复实战:Registry 辅助依赖与 RSC 客户端边界的完整排查指南
Plate 模板 CI 修复实战:Registry 辅助依赖与 RSC 客户端边界的完整排查指南
导读
本文以 Plate 仓库中 2026-04-06-fix-template-ci-registry-helper-deps.md 这份修复计划为骨架,完整复盘一次"模板 CI 构建失败"的真实排查过程:注册表(registry)UI/插件重构引入了一批辅助文件后,plate-template 与 plate-playground-template 两个模板无法再构建。读完本文,你将掌握三个关键工程能力:一是理解 shadcn 风格 registry 元数据如何驱动模板生成(以及为什么"应用源码能编译"不等于"模板能编译");二是学会用"显式登记或内联"两种策略处理 registry 辅助文件依赖;三是理解 Next.js/Turbopack 下 RSC 客户端边界的放置原则,以及如何利用 pnpm --filter www rd、pnpm brl、pnpm turbo build 等命令组合完成验证闭环。
一、问题背景:重构引发模板 CI 连锁故障
1.1 现象:CI 失败发生在模板,而非主应用
本次故障的典型特征是:apps/www 应用源码一切正常,CI 却在生成的模板中持续报错。计划文档明确记录:
CI is failing in generated templates, not in
apps/www.
也就是说,故障点在 templates/plate-template 与 templates/plate-playground-template 两个模板工程里,它们是 Plate 提供给用户脚手架使用的独立项目(分别位于 templates/plate-template 与 templates/plate-playground-template)。
1.2 根因:registry 辅助文件没有进入模板依赖图
模板是由 registry 元数据驱动生成的,而不是从 apps/www/src 直接复制。当 registry UI/插件重构给若干注册项(registry item)引入了新的辅助文件(helper file)导入后,这些辅助文件并没有同步出现在 registry 定义中,导致生成的模板代码里出现 Module not found。
计划文档列出了缺失的五个模块:
| 缺失模块 | 归属 |
|---|---|
block-discussion-index | block-discussion 注册项下的辅助逻辑 |
get-annotation-click-target | 注释点击目标解析工具 |
suggestion-line-break-anchor | suggestion 换行锚点组件 |
suggestion-styles | suggestion 样式变体 |
trailing-block-kit | 尾部块 Kit(引用 @platejs/suggestion/react) |
1.3 一份失败案例的技术教训
配套解决方案文档 2026-04-06-registry-helper-refactors-must-update-template-registry-dependencies.md 总结了三条"行不通"的做法,对理解故障本质极有帮助:
- 只检查应用源码导入:本地源文件存在 ≠ registry 定义覆盖。真正的契约是:registry 注册项必须显式携带生成消费者所需的每一个辅助文件与 registry 依赖。
- 把模板 CI 失败当作"依赖过期"处理:
Module not found是确定性的"文件从未被复制进模板",重装依赖无济于事。 - 无差别新增辅助文件:对于只被单一组件使用的辅助文件,先判断是否应内联,而不是无条件扩大 registry 表面积。
二、原理剖析:registry 元数据是模板生成的唯一事实来源
2.1 生成式 registry 的数据流
apps/www 的 registry 体系建立在 shadcn 的 Registry/RegistryItem schema 之上。核心数据流为:
- apps/www/src/registry/registry.ts 通过
createPlateRegistry()汇总各分类注册表(registry-ui.ts、registry-kits.ts、registry-components.ts、registry-lib.ts、registry-examples.ts、registry-hooks.ts、registry-pro.ts等); - apps/www/scripts/build-registry.mts 读取该汇总结果,经
registrySchema.parse()校验,并用withPublicRegistryDependencies()把内部依赖说明符改写为公开的 registry URL(REGISTRY_BASE_URL,开发环境为http://localhost:3000/rd,生产为https://platejs.org/r); - 最终输出
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 描述的 files 与 registryDependencies。只要注册项定义中缺少某个本地辅助文件,生成出来的模板就必然缺文件——这正是本次故障的机制根源。
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 显示,最终采用了"显式登记 + 内联"双管齐下的方案。对于需要保留为独立文件的辅助模块,登记内容如下:
lib/block-discussion-index.ts挂到block-discussion注册项下(当前实现在 apps/www/src/registry/ui/block-discussion.tsx 与 apps/www/src/registry/lib/block-discussion-index.ts,并配有 apps/www/src/registry/lib/block-discussion-index.spec.tsx 测试);lib/get-annotation-click-target.ts作为独立的 registry lib 项登记;trailing-block-kit.tsx作为独立的 registry kit 项登记;- 同步补齐
comment-kit、suggestion-kit、editor-kit、editor-ai等注册项上的registryDependencies,使生成消费者的依赖图与apps/www完全一致。
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.mts 的 toPublicRegistryDependencySpecifier() 改写为完整的公开地址。这正是 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-anchor 与 suggestion-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 记录了三条未解决问题的路径:
- 给
@platejs/react加'use client'——掩盖坏导入图而非移除; - 把指令挪到
@platejs/core/react各文件——同理是错误地扩大客户端边界; - 当作普通页面问题处理——忽略了 registry 路由这条决定性线索。
5.3 正确修复:客户端边界放在"离源码最近处"
最终方案是把 trailing-block-kit 实现为带 'use client' 的真实客户端 Kit:
- 在 Kit 文件顶部声明
'use client',把SuggestionPlugin的访问限制在该客户端边界内; - 由 apps/www/src/registry/components/editor/editor-kit.tsx 中的
EditorKit消费TrailingBlockKit; - 仅在边界就绪后才通过 apps/www/src/registry/registry-kits.ts 暴露该 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.json 的 typecheck 脚本还额外执行了 check-docs-source-parity.mts 与 check-registry-source.mts 两个源码一致性校验,这意味着 registry 源与文档源的漂移也会被 CI 抓住。而最后一步 pnpm brl && pnpm turbo build 则是本次修复最有价值的收尾动作——它同时验证了"自动工具重写 barrel"与"生产构建"两者可以共存,杜绝边界被工具链悄悄破坏。
七、可复用的工程法则与排查清单
7.1 三条核心法则
- 模板生成由 registry 元数据驱动,而非应用源码:
apps/www中存在的文件不会自动进入模板。每个被注册项使用的本地辅助文件,只有两种合法表达方式——内联进宿主注册文件,或显式登记为 registry 文件/registry 依赖。 - 一次性辅助函数优先内联:独立成文件会放大 registry 簿记成本与 CI 影响面;除非真正被多个注册项共享,否则折叠回宿主文件。
- 客户端专属模块必须自带客户端边界:任何会进入 registry 索引的注册项,要么服务端安全,要么自己声明
'use client';不要把'use client'打在包入口上掩盖坏图。
7.2 故障排查快速清单
- 确认 CI 失败在模板还是在应用:
apps/www通过不代表模板通过; - 对
apps/www源码执行rg找到全部新增辅助文件导入; - 逐一检查对应 registry 注册项(registry-ui.ts、registry-kits.ts、registry-lib.ts 等)是否覆盖这些文件;
- 判断辅助文件是"独立登记"还是"内联回宿主";
- 重新生成 registry:
pnpm --filter www rd,检查public/rd/registry.json的文件清单; - 若报 RSC 钩子错误,沿
src/app/api/registry/[name]/route.ts→src/lib/rehype-utils.ts→src/__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 一跑就挂"的怪圈里空耗时间。
更多推荐
所有评论(0)