TinaCMS v4 开发指南:`packages/v4/AGENTS.md` 架构、CLI 边界与工程规范全解析
2026/9/15 12:36:24 网站建设 项目流程

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/目录下工作的所有行为边界。它开篇即指明三条“先读”线索:

  1. README.md—— 包地图:v4 发布哪些包、发布规则,以及CLI 为什么被排除在构建管线之外
  2. 独立的 v4 架构规格仓库tinacms/tinacmsv4-docs(官方文档声明存在于外部仓库,本文以仓库内证据为准,不展开其内部内容),建议从CONTEXT.md开始,再读 ADR 系列;
  3. @tinacms/tinacms/_docs/是 v4 最终形态的事实来源(架构、插件、字段插件、逐字段规格)。

从仓库看,packages/v4/@tinacms/tinacms/_docs/确实存放了architecture.mdplugins.mdfield-plugins.md以及string-field.mdrich-text-field.mdarray-field.mdboolean-field.mddatetime-field.mdnumber-field.mdselect-field.md等逐字段规格文档。也就是说:AGENTS.md 是“规则层”,_docs/是“事实层”,二者配套使用。

二、v4 的包结构:三个包,一份包地图

AGENTS.md给出了 v4 目录下的三个包:

包名路径职责
@tinacms/tinacmspackages/v4/@tinacms/tinacmsv4 运行时 + CLI 合并在一个包里。private: true,版本4.0.0-alpha.x。通过子路径导出/react/client/server/local-data-layer以及框架适配器/adapters/next/express/astro/hono
@tinacms/rich-textpackages/v4/@tinacms/rich-textPlate 富文本编辑器。值契约(value contract)将编辑器与存储格式分离,必须守住这条边界(见src/boundary.test.ts
@tinacms/uipackages/v4/@tinacms/ui基于 shadcn/ui 的共享 UI 组件

2.1 与 v3 布局的对照(源自 README.md)

维度v3(当前)v4
根运行时 npm 名tinacms@tinacms/tinacms
工作区路径packages/tinacmspackages/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/tinacmspackages/@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 应用;
  • codegennode ./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.tstina/tina-lock.jsonsrc/app.tsxsrc/preview/vite.config.ts,并与同目录examples/barebones(一个最小可运行示例,含tina/config.tstina/tina-lock.json与自定义字段rating-field.tsx)互相印证。

四、硬性规则:CLI 边界、lock 文件与组件管理

4.1tinacmsbin 只写“人会提交的文件”

这是 v4 与 v3 最本质的架构差异。AGENTS.md的规则原文是:

tinacmsbin 只写一个人会提交的文件initcodegen)。它绝不能包装进程、开放端口或产生构建产物。没有devbuild命令。

README.md 用一张命令表进一步展开:

命令写入内容是否允许
tinacms inittina/config.ts、插件注册、admin 路由允许
tinacms codegentina/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 与项目部署从不运行tinacmsbintinacms codegen --check作为防漂移守卫存在,是项目主动选择加入的检查。仓库中 playground/tina/tina-lock.json 与 examples/barebones/tina/tina-lock.json 均已提交,正是这一规则的实例。

4.3 其他硬性规则

  • 不要为 v4 功能触碰 v3 包packages/tinacmspackages/@tinacms/*):v3 仅接受 bug 与安全修复;
  • 不要把 Level 适配器或外部集成迁入本仓库:其归属以 README.md 为准(仓库内列出mongodb-levelsqlite-levelupstash-redis-level等外部仓库);
  • shadcn 组件:一律在@tinacms/ui/内通过pnpm dlx shadcn@latest add <component>添加或更新,不得手写 shadcn 已提供的原语。仓库中 @tinacms/ui/src/components 下的button.tsxinput.tsxselect.tsxtooltip.tsxsheet.tsxsidebar.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 完整实现了FieldAddresstoFieldAddress,且校验消息为'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(携带statuscode字段,name = 'RpcError'),RequestBodyTooLargeErrorlocal-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 等源码中找到实现:

  1. 字段行(row)给字段命名admin/document-form.tsx渲染Label并用htmlFor指向控件;字段控件渲染id={address},自身不再有 label。源码第 20-27 行正是如此:先查字段注册表里metadata.labelable是否为false,再决定htmlFor={labelable ? name : undefined}

  2. 绝不在字段控件上放aria-labelaria-label优先级高于<label>,会覆盖作者写的字段名——名为 "SEO description" 的字段会被读屏器读成seoDesc。因此字段的名字只能来自行(row)。

  3. 图标按钮要aria-label:工具栏按钮没有<label>且只显示图标,aria-label是正确工具;若按钮同时有文字,aria-label必须包含该文字(WCAG 2.5.3);按钮内每个图标用aria-hidden='true'声明为装饰性。

  4. htmlFor够不到的控件用aria-labelledbymetadata.labelable: false的描述型控件没有可被 label 指向的输入框,此时行(row)给 label 一个 id,控件读取该 id。富文本字段就是典型例子

  5. 断言 accessible name,而不是 label 文本:用getByRole(role, { name });对无对应 role 的控件(如datetime-local)用toHaveAccessibleNamegetByLabelText无法发现“读屏器把aria-label读错”这类缺陷,因为它会直接命中错误的aria-label

  6. 错误消息携带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 目录工作的最小清单

  1. 读规则:先读 AGENTS.md,再读 README.md 与@tinacms/tinacms/_docs/architecture.mdplugins.mdfield-plugins.md、各字段规格);
  2. 跑命令:进入对应包目录执行pnpm dev/pnpm test/pnpm types/pnpm build/pnpm codegenpnpm test:e2e仅限@tinacms/tinacms
  3. 验证改动:用@tinacms/tinacms/playground/在浏览器里手动验证运行时/编辑器改动,改动后运行pnpm codegentina-lock.json保持最新并提交;
  4. 守边界:不触碰 v3 包、不把 Level 适配器迁入仓库、shadcn 组件用pnpm dlx shadcn@latest add <component>@tinacms/ui/中维护;
  5. 写代码:零any、标识符用Brand+to*构造器、捕获值命名causeinstanceof收窄、条件渲染用显式null三元、字段命名交给行(row)而不是aria-label
  6. 写注释:遵守 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询