TinaCMS v4 开发指南:packages/v4/AGENTS.md架构、CLI 边界与工程规范全解析
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
导读:本文以 TinaCMS 仓库中 packages/v4/AGENTS.md 为骨架,系统梳理 v4(
@tinacms/tinacms)的包结构、命令体系、硬性规则、类型约束、错误处理、React/JSX 规范与无障碍(A11y)要求,并结合packages/v4/下的真实源码、测试与配置逐条印证。读完本文,你将理解 v4 与 v3 的关键差异(CLI 退出构建管线、tina-lock.json提交而非构建产物)、掌握tinacmsbin 的合法命令边界、熟悉 v4 的 branded type 模式、unknown错误处理、无障碍命名规则等工程规范,可直接用于 v4 源码阅读、插件开发与代码评审。
一、AGENTS.md的定位:v4 目录的“代理指令”
packages/v4/AGENTS.md是一份面向 AI Agent 与贡献者的目录级说明文档,规定了在packages/v4/目录下工作的所有行为边界。它开篇即指明三条“先读”线索:
README.md—— 包地图:v4 发布哪些包、发布规则,以及CLI 为什么被排除在构建管线之外;- 独立的 v4 架构规格仓库
tinacms/tinacmsv4-docs(官方文档声明存在于外部仓库,本文以仓库内证据为准,不展开其内部内容),建议从CONTEXT.md开始,再读 ADR 系列; @tinacms/tinacms/_docs/是 v4 最终形态的事实来源(架构、插件、字段插件、逐字段规格)。
从仓库看,packages/v4/@tinacms/tinacms/_docs/确实存放了architecture.md、plugins.md、field-plugins.md以及string-field.md、rich-text-field.md、array-field.md、boolean-field.md、datetime-field.md、number-field.md、select-field.md等逐字段规格文档。也就是说:AGENTS.md 是“规则层”,_docs/是“事实层”,二者配套使用。
二、v4 的包结构:三个包,一份包地图
AGENTS.md给出了 v4 目录下的三个包:
| 包名 | 路径 | 职责 |
|---|---|---|
@tinacms/tinacms | packages/v4/@tinacms/tinacms | v4 运行时 + CLI 合并在一个包里。private: true,版本4.0.0-alpha.x。通过子路径导出/react、/client、/server、/local-data-layer以及框架适配器/adapters/next、/express、/astro、/hono |
@tinacms/rich-text | packages/v4/@tinacms/rich-text | Plate 富文本编辑器。值契约(value contract)将编辑器与存储格式分离,必须守住这条边界(见src/boundary.test.ts) |
@tinacms/ui | packages/v4/@tinacms/ui | 基于 shadcn/ui 的共享 UI 组件 |
2.1 与 v3 布局的对照(源自 README.md)
| 维度 | v3(当前) | v4 |
|---|---|---|
| 根运行时 npm 名 | tinacms | @tinacms/tinacms |
| 工作区路径 | packages/tinacms | packages/v4/@tinacms/tinacms |
| 发布状态 | 支持模式:仅修 bug 和安全问题 | 预发布(4.0.0-alpha.x),private: true直至 alpha 发布 |
| CLI | @tinacms/cli(独立包提供tinacmsbin) | 并入@tinacms/tinacms,由它提供tinacmsbin |
@tinacms/tinacms的 package.json 印证了这一点:"version": "4.0.0-alpha.0"、"private": true,并在bin字段声明"tinacms": "./bin/tinacms.mjs";exports字段逐条列出/react、/admin、/preview、/client、/server、/local-data-layer、/local-data-layer/vite与四个框架适配器子路径,且当前 alpha 脚手架直接通过exports指向src/*(ADR-001 规定正式版编译到dist/)。
AGENTS.md特别强调:v3(packages/tinacms与packages/@tinacms/*)处于支持模式,禁止为 v4 功能改动 v3 包;Level 适配器与外部集成也不得迁入本仓库(README.md中列有其外部归属)。
2.2 富文本的值契约边界:boundary.test.ts的实际约束
AGENTS.md用一句话概括了@tinacms/rich-text的核心设计,而测试 boundary.test.ts 用代码把这条边界固化成可验证的规则:
- 编辑器只能引用
@tinacms/schema-tools与@tinacms/ui两个共享包,不得 import 宿主运行时,也不得 import 任何读写存储格式的包; - 相对导入不得逃出
src/目录(resolved.startsWith(SRC)检查); - 测试自证其有效:源码树内总导入数大于 200,
@tinacms/*导入非空——防止“正则失配导致检查空转”。
这让“值契约分离编辑器与存储格式”不再是口头约定,而是一条由 vitest 守护的包边界。
三、命令体系:在包目录内运行
AGENTS.md要求所有命令在包目录内执行,并列出六个命令:
pnpm dev # 仅 @tinacms/tinacms —— vite playground(playground/) pnpm test # vitest pnpm test:e2e # 仅 @tinacms/tinacms —— playwright pnpm types # tsc + tsconfig.test.json pnpm build # tinacms-scripts build pnpm codegen # 仅 @tinacms/tinacms —— 重新生成 playground 的 tina-lock对照 @tinacms/tinacms/package.json 的 scripts 段,除test:e2e外全部一一对应,且能看到更多细节:
dev实际为vite playground,即用 Vite 直接启动 playground 应用;codegen为node ./bin/tinacms.mjs codegen --root playground,说明 codegen 面向 playground 目录生成tina/tina-lock.json;types是双跑:tsc后再用tsconfig.test.json校验测试类型。
AGENTS.md明确 playground(@tinacms/tinacms/playground/)是手动测试台,用于在浏览器中验证运行时/编辑器改动。仓库中 playground 确实包含tina/config.ts、tina/tina-lock.json、src/app.tsx、src/preview/与vite.config.ts,并与同目录examples/barebones(一个最小可运行示例,含tina/config.ts、tina/tina-lock.json与自定义字段rating-field.tsx)互相印证。
四、硬性规则:CLI 边界、lock 文件与组件管理
4.1tinacmsbin 只写“人会提交的文件”
这是 v4 与 v3 最本质的架构差异。AGENTS.md的规则原文是:
tinacmsbin 只写一个人会提交的文件(init、codegen)。它绝不能包装进程、开放端口或产生构建产物。没有dev或build命令。
README.md 用一张命令表进一步展开:
| 命令 | 写入内容 | 是否允许 |
|---|---|---|
tinacms init | tina/config.ts、插件注册、admin 路由 | 允许 |
tinacms codegen | tina/tina-lock.json | 允许 |
tinacms codegen --check | 不写入;lock 过期时以退出码 1 结束 | 允许 |
tinacms dev | — | 否。应运行框架自己的开发服务器,Vite 插件与适配器负责其余工作 |
tinacms build | — | 否。lock 已提交,无需构建 |
设计动机(README 原文概括):v3 中tinacms dev -c "next dev"让 Tina 成为框架的父进程,tinacms build && next build让 Tina 成为构建步骤——Tina 的任何故障或工具链冲突都会阻断与内容无关的构建。v4 反转了这一关系:项目拥有自己的管线,由项目调用 Tina。
每个能力因此挂载到项目已有的服务器上:
- 本地数据层是项目加入自身配置的 Vite 插件,或是一个适配器路由;
dispatchContentRequest不绑定任何传输层,为其他打包器写宿主只是一个小文件; - RPC 处理器是
(Request) => Promise<Response>,框架适配器把它挂为项目的一个路由; - admin UI 是一个 React 组件,由项目在自己的路由上渲染,而不是构建时拷进
public/的 bundle。
4.2tina-lock.json是提交物,不是构建产物
tina-lock.json是已提交文件而非构建输出(ADR-016),因此 CI 与项目部署从不运行tinacmsbin;tinacms codegen --check作为防漂移守卫存在,是项目主动选择加入的检查。仓库中 playground/tina/tina-lock.json 与 examples/barebones/tina/tina-lock.json 均已提交,正是这一规则的实例。
4.3 其他硬性规则
- 不要为 v4 功能触碰 v3 包(
packages/tinacms、packages/@tinacms/*):v3 仅接受 bug 与安全修复; - 不要把 Level 适配器或外部集成迁入本仓库:其归属以 README.md 为准(仓库内列出
mongodb-level、sqlite-level、upstash-redis-level等外部仓库); - shadcn 组件:一律在
@tinacms/ui/内通过pnpm dlx shadcn@latest add <component>添加或更新,不得手写 shadcn 已提供的原语。仓库中 @tinacms/ui/src/components 下的button.tsx、input.tsx、select.tsx、tooltip.tsx、sheet.tsx、sidebar.tsx等正是 shadcn 风格的受控组件。
五、类型规范:零any与 Branded Type
5.1 零any纪律
- 不允许任何
any:不做注解、不做断言。改用unknown再收窄,或写出真实类型; @tinacms/tinacms/src当前零any,必须保持;rich-text/src/plate中的any是继承自 Plate 的代码:不得新增,动到这些文件时顺手移除。
5.2 Branded Type:标识符不是裸string
规则:标识符要有具体的 branded type,而不是裸string。使用 core/brand.ts 的Brand,并为每个 ID 提供在边界处做校验的to*构造函数——cast 只存在于构造函数里,别无他处:
// core/brand.ts export type Brand<T, K extends string> = T & { readonly __brand: K }; // core/field/address.ts export type FieldAddress = Brand<string, 'FieldAddress'>; export const toFieldAddress = (path: string): FieldAddress => { invariant(path.length > 0, 'field-address-empty', '...'); return path as FieldAddress; };仓库源码逐一对应:
- core/brand.ts 第 1 行就是
Brand的定义; - core/field/address.ts 完整实现了
FieldAddress与toFieldAddress,且校验消息为'A field address must be a non-empty path.'; - form/form-store.ts 定义了
FormId = Brand<string, 'FormId'>与toFormId,校验消息为'A form id must be a non-empty path.',并让FormValues = Record<FieldAddress, unknown>、store 状态与 hooks 全部以FormId/FieldAddress为键——见form-store.test.ts(第 392-393 行验证toFormId('')抛错)与form-store.hooks.test.tsx(用toFormId('posts/a.mdx')构造表单); - config.ts 用
ResolvedConfig = Brand<ComposedConfig, 'ResolvedConfig'>,经asResolvedConfig收口,defineConfig返回该类型。
结论正如 AGENTS.md 所述:一个FormId不会因为类型错误被当作FieldAddress使用,类型系统在编译期就堵住了“字符串互相乱传”的隐患。
六、注释与行文:ASD-STE100 简化技术英语
v4 所有文字性内容——代码注释、_docs/、README——遵循ASD-STE100 简化技术英语,本目录的 README.md 是词汇表的参考基准。STE 中最重要的规则:
- 主动语态、现在时:写 “The bin writes files”,不写 “files are written by the bin”;
- 一句一个指令或事实:句子保持短(约 20 词以内);
- 一词一义:同一事物始终用同一个词——document 就是 document,不要随文件不同换成 "page"、"entry" 或 "record";
- 无废话:不要 "simply"、"just"、"note that"、"in order to"。
注释政策(此前一次清理确立):
- 只注释陷阱、不变式与 ADR 指针,删除复述代码的注释;
- 当某个决策解释了代码时,按编号引用 ADR。
AGENTS.md给出的两个示例在源码中能找到呼应,例如 rpc/proxy.ts 第 1-4 行的注释直接引用 ADR-007 并说明 “types cross through animport type…no server code and no secret reaches the browser”,第 29-31 行引用 ADR-023 §4 解释 bearer token 的挂载方式,第 52-55 行解释RESERVED_PROXY_KEYS是为了防止await client.media挂起或调试器误发 POST——每一处都指向“代码为何如此”,而非复述“代码做了什么”。
七、错误处理:unknown捕获值与自定义错误类
7.1 捕获值一律视为unknown
规则:捕获的值是unknown,绝不假设它是Error。捕获变量按代码库惯例命名为cause,用instanceof Error收窄,兜底用String(cause):
try { await save(document); } catch (cause) { if(cause instanceof Error){ logError(cause.message) }else{ logError(String(cause)) } }这一惯例同样见于 rpc/proxy.ts:RpcError通过instanceof区分已知失败。
7.2 用自定义错误类区分已知失败
规则:区分已知失败用自定义错误类,而不是匹配错误消息字符串。做法是继承Error并用instanceof检查。AGENTS.md给出的实例:
if (cause instanceof RequestBodyTooLargeError) { res.statusCode = 413; }其中RpcError定义在 src/rpc/proxy.ts(携带status与code字段,name = 'RpcError'),RequestBodyTooLargeError在local-data-layer.vite.ts中定义。instanceof模式的好处是:错误语义与字符串解耦,重构提示文案不会破坏上游的类型化判断。
八、React / JSX:条件渲染禁用手写&&
规则:条件渲染不用&&,改用显式null的三元表达式。原因是&&会把假值(如空数组时的0)泄漏进 DOM:
// ❌ Bad — renders "0" when items is empty, leaks falsy values into the DOM {items.length && <List items={items} />} // ✅ Good {items.length > 0 ? <List items={items} /> : null}这一规则与document-form.tsx中“脏状态徽标”的渲染方式互相呼应——仓库实现里对脏标记的渲染同样遵循显式判断(dirty为真才渲染徽标 span),避免把假值泄漏到页面。
九、无障碍(A11y):名字从哪来,aria-label就用在哪
AGENTS.md的无障碍章节是 v4 admin 表单可访问性的精确技术规范,共五条,且全部能在 admin/document-form.tsx 等源码中找到实现:
字段行(row)给字段命名:
admin/document-form.tsx渲染Label并用htmlFor指向控件;字段控件渲染id={address},自身不再有 label。源码第 20-27 行正是如此:先查字段注册表里metadata.labelable是否为false,再决定htmlFor={labelable ? name : undefined}。绝不在字段控件上放
aria-label:aria-label优先级高于<label>,会覆盖作者写的字段名——名为 "SEO description" 的字段会被读屏器读成seoDesc。因此字段的名字只能来自行(row)。图标按钮要
aria-label:工具栏按钮没有<label>且只显示图标,aria-label是正确工具;若按钮同时有文字,aria-label必须包含该文字(WCAG 2.5.3);按钮内每个图标用aria-hidden='true'声明为装饰性。htmlFor够不到的控件用aria-labelledby:metadata.labelable: false的描述型控件没有可被 label 指向的输入框,此时行(row)给 label 一个 id,控件读取该 id。富文本字段就是典型例子。断言 accessible name,而不是 label 文本:用
getByRole(role, { name });对无对应 role 的控件(如datetime-local)用toHaveAccessibleName。getByLabelText无法发现“读屏器把aria-label读错”这类缺陷,因为它会直接命中错误的aria-label。错误消息携带
role='alert':表单校验错误必须能被辅助技术即时播报。
仓库中 e2e/rich-text-field.spec.ts 与admin/field-labels.test.tsx等测试即是围绕“accessible name 而非 label 文本”这一断言原则展开的用例。
十、相关配套文档:发布、弃用与集成路径
AGENTS.md是 v4 目录的入口规则文件,与同目录三份文档配套:
- README.md:包地图 + 发布规则 + 所有权(所有 v4 包由 TinaCMS 核心团队拥有)。“CLI 退出管线”的完整论证也在此;
- DEPRECATIONS.md:定义
deprecate(留在 npm、加deprecated字段、停新功能)、remove(从 monorepo 删除源码)、fold(能力并入保留包)三种状态,并给出决策表。关键迁移点包括:tinacms变为@tinacms/tinacms、@tinacms/cli被吸收、@tinacms/datalayer折叠为@tinacms/tinacms的 store + local-content 插件 + 外部 Level 适配器、@tinacms/schema-tools折叠进t助手函数与 codegen 模块;用户侧升级只需把package.json里的tinacms换成@tinacms/tinacms,再按表格改写 import; - INTEGRATIONS.md:列出迁往独立仓库的 provider 包与集成包。
十一、实战速查:在 v4 目录工作的最小清单
- 读规则:先读 AGENTS.md,再读 README.md 与
@tinacms/tinacms/_docs/(architecture.md、plugins.md、field-plugins.md、各字段规格); - 跑命令:进入对应包目录执行
pnpm dev/pnpm test/pnpm types/pnpm build/pnpm codegen;pnpm test:e2e仅限@tinacms/tinacms; - 验证改动:用
@tinacms/tinacms/playground/在浏览器里手动验证运行时/编辑器改动,改动后运行pnpm codegen让tina-lock.json保持最新并提交; - 守边界:不触碰 v3 包、不把 Level 适配器迁入仓库、shadcn 组件用
pnpm dlx shadcn@latest add <component>在@tinacms/ui/中维护; - 写代码:零
any、标识符用Brand+to*构造器、捕获值命名cause并instanceof收窄、条件渲染用显式null三元、字段命名交给行(row)而不是aria-label; - 写注释:遵守 ASD-STE100(主动语态、现在时、短句、一词一义、无填充词),只注释陷阱/不变式/ADR 指针,按编号引用 ADR(如 ADR-007、ADR-016、ADR-023、ADR-024)。
<输出文章> (说明:以上正文即为完整文章。)
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考