Material UI 仓库 AGENTS.md 深度解读:为 AI Agent 定制的 Monorepo 开发协作规范
Material UI 仓库根目录的 AGENTS.md 是一份面向 AI Agent(同时对人有效)的工程协作指南,规定了 pnpm 工作区命令、测试与代码质量流程、组件结构约定、错误信息规范与无障碍测试机制。本文以该文档为核心骨架,结合仓库源码与配置逐项展开,帮助你在动手修改 MUI 源码前建立完整的操作认知:从正确执行每条 pnpm 命令,到遵循组件目录结构与 minify-error 错误压缩约定,再到理解视觉回归中的 axe 无障碍检查流程。
只允许 pnpm:命令执行的第一原则
文档开宗明义:仅支持 pnpm,yarn/npm 会直接失败。这不是偏好而是硬约束——根 package.json 中声明了 "preinstall": "npx only-allow pnpm",任何非 pnpm 的安装都会在 preinstall 钩子处被拦截;"packageManager": "pnpm@11.22.0" 与 engines 字段进一步锁定了 pnpm 11.22.0、Node >= 22.23.2 的运行时环境。
针对工作区内的包操作,应使用 -F 参数而非 cd 进入包目录(文档明确禁止用 cd 导航):
pnpm -F @mui/material add some-package # 给某个包添加依赖
pnpm -F @mui/material build # 构建特定包
这种写法的价值在于:命令始终在仓库根目录执行,工作区解析(workspace 协议、pnpm-workspace.yaml)与 Nx 缓存命中都不受工作目录切换影响。
常用命令速查:从开发到构建
开发
pnpm install # 按需安装依赖
pnpm docs:dev # 仅启动文档开发服务器
对照 package.json 的 scripts 字段,docs:dev 实际转发为 pnpm --filter docs dev,即只启动文档站而不动其他包。
构建
pnpm release:build # 构建全部包(不含 docs)
pnpm docs:build # 构建文档站
源码层面 release:build 展开为 lerna run --concurrency 8 --no-private build --skip-nx-cache——两个关键细节:--no-private 只构建待发布的公开包,--skip-nx-cache 跳过 Nx 缓存强制执行。docs:build 则串联了 pnpm docs:llms:build(生成面向 LLM 的文档产物)与 pnpm --filter docs build。
测试
pnpm test:unit # 运行全部单元测试(jsdom)
pnpm test:unit ComponentName # 按模式匹配运行测试
pnpm test:unit -t "test name" # 按测试名 grep 过滤
pnpm test:browser # 在真实浏览器中运行(Chrome、Firefox、WebKit)
pnpm test:e2e # 端到端测试
pnpm test:regressions # 视觉回归测试
实际实现上,test:unit 是 cross-env TZ=UTC vitest,通过固定时区保证日期相关断言的稳定性;test:browser 则切换 TEST_SCOPE=browser 环境变量,利用仓库根与各包下的 vitest.config.browser.mts 双配置在真实浏览器中跑同一套用例;test:e2e 转发到独立的 test/e2e 工作区包(pnpm -F ./test/e2e start)。
代码质量
pnpm prettier # 格式化暂存的改动
pnpm eslint # 带缓存的 Lint
pnpm typescript # 对全部包做类型检查
对应实现:prettier 使用 pretty-quick --ignore-path .lintignore --branch master(只对相对 master 分支的暂存改动格式化);eslint 带有 --cache 与 --max-warnings 0(零容忍);typescript 通过 lerna run --no-bail typescript 并行调度所有包各自的类型检查。
API 文档
修改组件 props 或 TypeScript 声明后,需重新生成 API 文档:
pnpm proptypes && pnpm docs:api
proptypes 指向 tsx ./scripts/generateProptypes.ts(基于 packages-internal/scripts/typescript-to-proptypes 从类型声明抽取 proptypes);docs:api 先清理 docs/pages/**/api-docs 旧产物再执行 tsx ./scripts/buildApiDocs/index.ts 重建。
文档 Demo
文档规范:始终编写 TypeScript 版本的 demo,JavaScript 变体由脚本生成:
pnpm docs:typescript:formatted
该命令执行 tsx ./docs/scripts/formattedTSDemos.mjs,将 *.tsx demo 编译为格式化的 *.js 版本供文档站的双语言 Tab 展示(可在 docs/data/material/components 下看到大量成对的 .tsx/.js demo 文件)。
Monorepo 架构:Lerna + Nx 的包组织
文档给出的架构结论是:这是一个由 Lerna 管理、以 Nx 做缓存的 monorepo。仓库证据完全印证:lerna.json 声明 "npmClient": "pnpm" 与 "version": "independent"(各包独立版本号),而 package.json 中 devDependencies 同时包含 lerna@10.0.1 与 nx@22.7.6。
核心公开包及其职责:
| 包 | 职责 | 源码位置 |
|---|---|---|
@mui/material |
核心 Material UI 组件 | packages/mui-material/src |
@mui/system |
样式系统(sx prop、styled、theme) | packages/mui-system/src |
@mui/lab |
实验性组件(新组件先在这里孵化) | packages/mui-lab/src |
@mui/icons-material |
Material Design 图标 | packages/mui-icons-material/lib |
@mui/utils |
内部工具函数 | packages/mui-utils/src |
@mui/styled-engine |
CSS-in-JS 抽象层(默认 Emotion) | packages/mui-styled-engine/src |
不发布的内部包以 @mui-internal/*、@mui/internal-* 命名,对应 packages-internal/ 目录(如 api-docs-builder、core-docs、markdown)。从 tsconfig.json 的 paths 别名可以看到内部包与公开包的完整映射关系,例如 "@mui/internal-api-docs-builder": ["./packages-internal/api-docs-builder/src/index.ts"]。
TypeScript 约定与路径别名
三条硬性约定:
- 组件 props 用
interface而不是type; - 从组件文件导出
{ComponentName}Props接口; - 路径别名可用,如
@mui/material→./packages/mui-material/src。
第 3 点在 tsconfig.json 中有完整落实:"@mui/material": ["./packages/mui-material/src"] 与 "@mui/material/*": ["./packages/mui-material/src/*"] 两级别名让编辑器与测试直接解析到源码,无需预构建即可跨包引用。这也解释了为什么 pnpm test:unit 不需要先 release:build——vitest 环境下各包直接以源码形态互相依赖。
错误信息规范与 minify-error 机制
三段式错误文案
文档要求公开包抛出的每个错误信息必须满足三段式:
- 说明发生了什么——清晰描述问题;
- 说明为什么是问题——解释后果;
- 指向解决方向——给出可操作的指引。
格式约束:以 MUI: 前缀开头;用字符串拼接提升可读性;适用时附文档链接(https://mui.com/r/... 短链)。
minify-error babel 插件
用 /* minify-error */ 注释激活 babel 插件的压缩能力:
throw /* minify-error */ new Error(
'MUI: Expected valid input target. ' +
'Did you use a custom `inputComponent` and forget to forward refs? ' +
'See https://mui.com/r/input-component-ref-interface for more info.',
);
源码中确有此实践:InputBase.js 的受控输入变更处理中抛出的正是这条 "Expected valid input target" 错误,SelectInput.js 也是同款写法。插件本体是 @mui/internal-babel-plugin-minify-errors(见 package.json devDependencies)。其工作方式:构建时将完整错误消息抽走,运行时只保留一段可定位的短错误码,用户可在文档站的错误码页反查完整文案——从而在不牺牲开发体验的前提下显著压缩产物体积。文档还特别指出该 minifier 同时兼容 Error 与 TypeError 两种构造函数。
错误码提取流程
新增或更新错误后运行:
pnpm extract-error-codes
它调用 code-infra extract-error-codes,把各包源码中带 minify-error 标注的消息抽取到 docs/public/static/error-codes.json。当前该文件首条记录正是上面 InputBase 的完整文案:"1": "MUI: Expected valid input target. Did you use a custom \inputComponent` and forget to forward refs? ..."`。
一个容易踩坑的细节:如果新消息与原消息参数个数相同且语义未变,应直接更新原错误码的文案,而不是新增一个编码——错误码编号是对外稳定契约,随意增码会让旧版本用户的排查链接失效。
组件目录结构约定
文档给出的标准组件目录布局:
packages/mui-material/src/Button/
├── Button.tsx # 组件实现
├── Button.d.ts # TypeScript 声明(供 JSDoc API 文档使用)
├── Button.test.js # 单元测试
├── buttonClasses.ts # CSS 类名
└── index.ts # 公开导出
对照实际的 packages/mui-material/src/Button/ 目录:Button.d.ts、Button.spec.tsx(类型测试)、Button.test.js、buttonClasses.ts、index.d.ts、index.js 一应俱全(实现文件在此仓库版本中为 Button.js,文档中的 .tsx 命名代表约定方向)。
其中 buttonClasses.ts 展示了该约定的完整形态:ButtonClasses 接口为每个类名键(root、text、contained、loading 等 27 个)提供 JSDoc 注释,getButtonUtilityClass(slot) 委托给 @mui/utils/generateUtilityClasses 统一生成 MuiButton-* 类名。这套 CSS 类同时是用户通过 componentsProps/sx 覆样式的官方钩子,因此每个类名都必须有可检索的文档注释。
单元测试约定
- 使用
@mui/internal-test-utils的createRenderer()创建渲染器; - 使用 Chai BDD 风格断言(
expect(x).to.equal(y)); - 自定义断言
toErrorDev()、toWarnDev()用于捕获开发期 console 输出; - 优先用
user.*方法做完整交互测试,尽量避免fireEvent与setProps; - 需要浏览器布局测量的用例,用
it.skipIf(isJsdom())/describe.skipIf(isJsdom())限定到 Chromium 环境(不确定时先搜索其他测试的用法)。
文档附带的示例:
import { createRenderer } from '@mui/internal-test-utils';
describe('Button', () => {
const { render } = createRenderer();
it('renders children', async () => {
const handleClick = vi.fn();
const { getByRole, user } = render(<Button onClick={handleClick}>Hello</Button>);
const button = getByRole('button');
expect(button).to.have.text('Hello');
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
这个模式可直接在 Button.test.js 等既有测试中找到同构写法:createRenderer() 返回 render 及其作用域内的查询 API 与 user 句柄,断言库则是 vitest 环境下混入 Chai 风格 matcher 的组合,配合仓库根 test/setupVitest.ts 完成全局初始化。
无障碍(a11y)测试:截图与 axe 双轨并行
运行机制
axe-core 的无障碍检查运行在视觉回归的 Playwright 循环内(test/regressions/index.test.js),不单独开浏览器会话。截图与 a11y 相互独立:一个 demo 可以退出其中一项而保留另一项。
三个关键文件
- test/regressions/demoMeta.ts:
SCREENSHOT_RULES与A11Y_RULES两个独立规则数组,按最后匹配生效(last-wins)原则对docs/data/material/components/{slug}/{Demo}路径做 minimatch glob 匹配。规则间没有继承——覆盖规则必须重述它关心的每个字段。 - test/regressions/a11y/axe.ts:默认断言
color-contrast与link-in-text-block两条 axe 规则;当匹配的 a11y 规则设置assertions: 'all'时,断言该 demo 触发的全部 axe 规则;skipAssertions可屏蔽指定规则。 - test/regressions/a11y/a11yReporter.ts:按 slug 每个组件写一个
docs/data/material/components/{slug}/{slug}.a11y.json,内部以 demo 名为一级键、axe 规则 ID 为二级键,每条规则记录status(pass/fail/incomplete)与 WCAG 标签。
从 demoMeta.ts 源码可以看到 A11yRule 接口的精确形状:test(glob)、enabled、assertions: 'visual' | 'all'(默认 'visual')、skipAssertions: string[];ScreenshotRule 额外支持 waitForSelector(Playwright 在导航后等待该选择器出现,再执行 axe + 截图)与 viewportWidth(仅宽度可配,因为截图截取的是 testcase 元素全高,只有宽度影响结果)。
接入一个组件
按 slug 整体纳入,或用 brace-glob 收窄范围:
// test/regressions/demoMeta.ts
{ test: 'docs/data/material/components/alert/*', enabled: true, skipAssertions: ['color-contrast'] },
{ test: 'docs/data/material/components/buttons/{BasicButtons,ColorButtons}', enabled: true, assertions: 'all' },
覆盖特定 demo:在 slug 级规则之后追加 per-demo 规则(last-match-wins,且覆盖必须重述所有字段):
{ test: 'docs/data/material/components/popover/AnchorPlayground', enabled: false }, // Redux isolation
规则作用的对象就是 docs/data/material/components/ 下的 demo 文件(如 alert/ 目录里的 BasicAlerts.tsx、ActionAlerts.tsx 等),glob 匹配的是文档数据路径而非源码路径。
刷新结果与 CI 校验
pnpm test:regressions # 刷新所有 *.a11y.json;CI 会因文件过期(stale)而失败
本地迭代时用 vitest 的 -t 名称过滤缩小范围(匹配 it() 字符串中携带的路由,不匹配的测试体根本不执行,浏览器不会导航到那些路由):
# 终端一
pnpm test:regressions:server
# 终端二 —— 注意没有 `--`,pnpm 直接转发参数
pnpm test:regressions:run -t '/docs-components-buttons/' # 单个 slug
pnpm test:regressions:run -t '/docs-components-buttons/BasicButtons$' # 单个 demo
pnpm test:regressions:run -t '/docs-components-(buttons|chips)/' # 多个 slug
注意:过滤后的运行只会刷新匹配到 slug 的 *.a11y.json,推送前务必跑一次无过滤的 pnpm test:regressions。对照 package.json,test:regressions 的完整编排是:cross-env NODE_ENV=production pnpm test:regressions:build(vite 构建回归站点)+ concurrently 并行拉起 run 与 server + 最后 prettier --write 归一化所有 *.a11y.json。
导入约定:一级深度导入
包内代码必须使用一级深度导入,避免把整个包打进依赖者的 bundle:
import Button from '@mui/material/Button'; // 正确
import { Button } from '@mui/material'; // 包内避免
这与 packages/mui-material/src/Button 的 index.js 逐文件导出结构、以及 tsconfig.json 中 @mui/material/* 二级别名共同保证了深层导入在源码态与构建态都可用。
Agent Skills:按主题分包的集成指南
文档指出,面向常见集成主题的分包指南存放在 skills/ 目录下,每个 skill 是一个自包含目录:
| Skill | 主题 |
|---|---|
| skills/material-ui-styling | sx、styled()、主题覆写、slots、全局 CSS |
| skills/material-ui-theming | createTheme、设计令牌、colorSchemes、CSS 变量 |
| skills/material-ui-nextjs | App/Pages Router、Emotion 缓存、next/font、Link、SSR |
| skills/material-ui-tailwind | Tailwind v4 @layer、enableCssLayer、v3 互操作 |
处理这些主题时应读取对应 skill 的 AGENTS.md。按 skills/README.md,每个 skill 目录遵循固定布局:AGENTS.md(完整指南,规范事实源)、SKILL.md(入口与索引)、README.md(人读概览)、metadata.json(机器可读元数据)、reference.md(速查表)。发现机制正是根 AGENTS.md 的这张表格——任何会读取 AGENTS.md 的 Agent 都能据此跳转到细分技能。
Pre-PR 检查清单与 PR 标题格式
文档结尾给出了七步提交前检查:
pnpm prettier— 格式化代码pnpm eslint— 通过 Lintpnpm typescript— 通过类型检查pnpm test:unit— 通过单元测试- 若改了 API:
pnpm proptypes && pnpm docs:api - 若改了 demo:
pnpm docs:typescript:formatted - 若改了
.md文件:pnpm vale <file1> <file2> ...— 检查行文风格与语法
第 7 步的 vale 规则在 package.json 中实现为 code-infra vale(版本锁定 3.18.0),仅对 .Level=="error" 级别报错。
PR 标题格式为 [component] Imperative description,示例:
[button] Add loading state[docs] Fix typo in Grid documentation
小结
AGENTS.md 本质上把 MUI 仓库的"隐性工程知识"显性化为一份可被 Agent 与新人直接执行的契约:命令层只认 pnpm -F 与 lerna/nx 编排,规范层锁定 interface props、三段式 MUI: 错误文案与 minify-error 压缩、一级深度导入,测试层则以 createRenderer() + user.* 交互、jsdom/浏览器双轨、以及截图/axe 独立开关的回归体系兜底。对照仓库中 lerna.json、package.json、tsconfig.json、demoMeta.ts 等真实实现,每一条约定都有落点;按 Pre-PR 清单走完七步,即可保证一次改动在格式、类型、单测、API 文档与文档站 demo 之间的一致性。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00