范围:本导览只管 A01-A08 这八篇架构层深读之间的跨篇路由——一个改动需求该落在哪一层、撞上架构层硬约束时前人的坑写在哪一篇。不管具体组件的行为(那在
03-components),也不管_util单设施(那在02-infrastructure)。
A 层八篇的共同抽象只有一句话:把「人写的源码」变成「消费者拿到的确定性」。它不管某个按钮长什么样,管的是「按钮的样式凭什么能到 DOM 上」「按钮的名字凭什么能被 import 到」。按状态流向,八篇可以切成四段:交付段(A01、A02、A08)、样式段(A03、A04)、配置段(A05、A06)、交互段(A07)。选型表与坑索引都按这四段分组,找入口时先定段再定篇。
交付段的方向是单向的:源码目录 → 生成物 → 包根导出面 → 消费者的解析结果。A02 把这条链讲透——「哪些目录算组件」是三条规则叠加,而且这套判断在仓库里有四份互不同步的副本(A02 论点 1);A01 则给出这条链的最外侧形状:整个工作区是 src-first,发布态靠第二份 manifest 顶上(A01 论点 1),因此任何只在发布态成立的问题,本地都必须显式 pack 才能复现(A01 坑 4)。A08 是这条链的下游终点:自动导入只认元数据里的一条 from,样式路径靠拼字符串生成(A08 论点 3、A08 论点 4)。三段读下来的结论是同一条:改交付面的正确顺序是「先动源码事实,再跑生成器,最后看消费侧解析」,中间任何一步手改都是在伪造事实。
样式段是一条数值流水:Seed → Map → Alias → Component Token(A03 论点 1、A03 论点 4),再交给 cssinjs 把 styleFn 的结果算成一张带 hashId 的表(A04 论点 2、A04 论点 4)。这一段的两个关键落点在 A04:hashId 是双重哈希加环境前缀的产物,而 tokenKey 才是总开关(A04 论点 4);wrapSSR 在本库是恒等函数,SSR 样式的唯一出口是 extractStyle(cache)(A04 论点 5)。换句话说,样式层的身份问题(谁和谁共用一张表)与渲染问题(谁把样式写进 HTML)分属两篇,混在一篇里找会找不到。
配置段是纵向的:一份全局配置从 ConfigProvider 出发,经四个并行 InjectionKey 汇到 useConfigInject(A05 论点 1),再被每个组件的 props 工厂消费(A06 论点 1)。这一段的危险点在于兜底写法:A05 的嵌套合并语义分三类(A05 论点 4),A06 的默认值注入会把「未传」和「传了假值」抹平(A06 坑 5),两者叠起来就是「全局配置被静默吃掉」这一族症状的完整成因。
交互段 A07 是 A 层里唯一横跨多组件的总览篇:它只讲一条浮层链的组装方式(trigger 决定何时显示、align 决定显示在哪,两者之间只有一个对象作为全部发言权,A07 论点 1),具体某个浮层组件的行为归 C 层。它与 C03-08 的分工已由 PROMPT 的冲突仲裁记录定案(A07 无误,悬念由源码关闭,链条改写进了 C03-08 论点 5 末段),本导览按该现状引用、不重新裁定。
最后要说明这一层为什么值得单独有一份导览:A 层的坑和 C 层的坑不同型。C 层的坑通常是「某个 prop 的某个分支不可达」,读者可以在自己组件的边界内绕开;A 层的坑是闸门型的——它们规定了「你以为改好了」和「机器认为你改好了」之间的距离。A02 论点 7 说得最清楚:--check 校验的是生成物等于生成器的不动点,不是生成物等于意图。同一条形状在别处反复出现:A01 论点 10 记录了 AGENTS.md 要求的四项检查里三项没有机械强制、A03 坑 3 记录了 Token 派生逻辑零测试、A04 §七 记录了 cssinjs 目录下零测试文件、A05 §七 记录了注入链的嵌套覆盖与回落没有任何测试、A06 §七 记录了 props 工具的回归只能靠组件层用例间接兜住。因此本层的「改动路线」几乎都要求先记下改动前的可观测事实(数字、类名、抽出的字符串),再动代码,这是八篇共同的收口方式,不是某一篇的偏好。
先定段再定篇:下表每一行是一个改动意图,「该动哪一篇的哪一层」给的是最小落点,「不该动哪里」给的是这一层最常见的越界(架构层的越界代价通常是换掉公开 API 或换掉缓存身份,而不是换个外观)。
| 想达成的改动 | 该动哪一篇的哪一层 | 不该动哪里 | 判据 |
|---|---|---|---|
| 只改一个组件的内边距、圆角、色值 | A03 的 Component Token 层(theme.components.Xxx) | 不动 A04 引擎,也不调整组件样式文件里二次 mergeToken 的键顺序 | 组件 Token 不走 theme.token,Alias 层只认 AliasToken 的键;框架兜底只在键不相交时留下可见效果(A03 坑 1、A03 论点 4) |
| 全站间距整体变紧凑 | A03 的 Seed 层(sizeUnit 与 sizeStep) | 不改 util/alias.ts 里的 9 档复制,也别指望断点跟控件高度一起动 | 9 档 size* 被复制成 padding* 与 margin* 两套公开命名空间,断点是硬编码常量、控件高度另走一条乘数链(A03 论点 7、A03 坑 7) |
| 加一套暗色或紧凑主题 | A03 的 Map 层局部重写(themes/dark、compact) | 不在 Seed 层新增「另一套种子」,不只改一份 colorAlgorithm | dark 与 compact 是 Map 层重写而非换种子;深浅两份 colorAlgorithm 是同名兄弟、方向相反(A03 论点 2、A03 论点 8、A03 坑 8) |
| 新增一个组件目录 | A02 的生成物链(入口契约加 pnpm gen:entries) | 不手改 src/components.ts、typings/global.d.ts 与 package.json 的 exports | 三份生成物由静态文本扫描产生,--check 只证明有人跑过生成器、不证明它判对了(A02 论点 7、A02 坑 1) |
| 让新组件能被自动导入 | A08 的 metadata 与 resolver 层,并核对 A02 生成的两个样式键 | 不要用 pnpm typecheck 或文档站来判定成功 | resolver 与上游的契约只有一条 from(子组件另加 exp 具名导出格),toMeta 的兜底原先按「小写不插连字符」拼路径、实测 82 条失配(2026-09-30 已改成 kebab 并当晚修完:from 失配清零、虚构条目删除),而类型检查对这条链完全无感(A08 论点 1、A08 论点 4、A08 坑 1) |
| 改浮层「显示在哪」 | A07 的 placements 表与 I01 的求解层 | 不碰 I02 的弹出时序,也不在组件篇里改判定表 | trigger 到定位之间的全部发言权就是一个 align 对象,坐标求解整体外包在库外、翻转另有前提(A07 论点 1、A07 论点 5、A07 论点 6) |
| 改浮层「何时显示、事件怎么穿透」 | I02 的时序与事件层 | 不改 A07 的层级账本与 placements 表 | 「请求显示」与「提交显示」分两段、pending 期可整体撤销(A07 论点 2);同症状在 I 层已单独立条(I02 坑 4) |
| 全站换组件前缀 | A05 的前缀派生链(prefixCls、rootPrefixCls、iconPrefixCls)加 A02 的生成物 | 不改组件里的解构默认值,不给 Group 传它自己的前缀 | 三条前缀用途不同且父级继承在缺省路径不可达(A05 论点 2、A05 坑 1);样式缓存按前缀分键,喂错前缀就另注册一张表(A04 论点 1、C04-03 论点 6) |
| SSR 首屏带样式 | A04 的 extractStyle(cache) 与那三个 data-* 属性名 | 不改 wrapSSR,也不指望自动导入链补组件样式 | wrapSSR 在本库是恒等函数、样式唯一出口是 extractStyle;注水识别读回 CSS content,内联映射消费即删(A04 论点 5、A04 论点 6、A04 坑 4) |
| 让某个 prop 支持「未传即继承全局」 | A06 的 props 工厂层(该键不写 default) | 不把它放进 initDefaultProps 的 defaults,不用 PropTypes.bool | 组件级配置合并与受控判定都以「不等于 undefined」为判据,PropTypes.bool 自带真值默认(A06 论点 2、A06 坑 5、A06 坑 9) |
| 决定嵌套 ConfigProvider 的覆盖语义 | A05 的合并规则(父链回落、就地覆盖、深合并三类) | 不新增第四种读法,不读主链那两个不继承字段 | 主链字段不合并、只有子链回落,同一语义两套读法的差异是设计取舍而非缺陷(A05 论点 3、A05 论点 4、A05 坑 2) |
| 同一页面两套主题并存 | A04 的 cache 三轴身份(per-app、per-节点、per-算法数组) | 不走 CSS 变量旁路 | 样式身份由三个互不相干的轴共同决定(A04 论点 8);哈希类名方案的隔离力来自 token 进缓存键,而 CSS 变量是全局单值、最后写入者赢,本库该旁路还明确不支持 SSR(A03 §六、A03 论点 3) |
| 决定浮层的层叠顺序 | A07 的层级结论加 I10 的编排机制 | 不在组件样式里再手写一个偏移常数 | 层级是三套互不知情的账本,trigger 这条链用的是最静态的那套;同值时只能由 DOM 顺序决定(A07 论点 7、A07 坑 11、I10 坑 4) |
族谱〇:这一层的七条硬约束(改任何 A 层范围内的东西前先背下来)
src/components.ts、typings/global.d.ts、package.json 的 exports 都是生成物,改动只能经 pnpm gen:entries;这条红线在 A02 §六(生成物进版本控制的理由)与 A05 §七 的安全边界里各写了一次,A08 §七 又第三次重申「不可以顺手改 exports」。gen:entries 与 check:entries 的分工:前者写盘、后者只校验新鲜度并在漂移时退出码 1,prebuild 走的是 --check 而非静默改写(A02 论点 7、A02 坑 6);同一份 pin 在两种模式下一边报错退出、一边只 warn 后照样写盘(A02 坑 7)。--check 会报错要求删除(A02 论点 6、A02 坑 7);自动定主用的是目录名、就地声明与字典序这类弱信号(A02 论点 4)。A03 论点 1、A03 论点 2、A03 论点 4)。hashId 与 wrapSSR 的落点:hashId 注入有三个互斥分支、它由双重哈希与环境前缀决定,而 wrapSSR 在本库是恒等函数(A04 论点 2、A04 论点 4、A04 论点 5);组件里把返回值第一位丢掉并不报错,只是 SSR 收集包装没接上——组件侧的普遍写法是 const [wrapSSR, hashId] = useStyle(prefixCls) 然后照样包一层空转调用(活例 button/Button.tsx:48,见 A04 论点 5)。extractStyle(cache),注水识别靠读回 CSS content,内联映射节点消费即删、无法二次注水(A04 论点 5、A04 论点 6、A04 坑 4);自动导入链在 SSR 首屏组件样式这一半修前是空的,2026-09-30 由 Nuxt 模块补上了提取与注入(机制有测试、端到端因 playground 其它断链未跑通,A08 §六 与 §论点 7)。prefixCls 就是缓存键的一部分:组件层的路径是 component、prefixCls、iconPrefixCls 三段,传别的目录的前缀就会另生成一张表(A04 论点 1);仓内活例是 C04-03 论点 6,同族正确写法在 C04-04 一侧。族谱一:生成物与目录契约(A02 主场,交付段)
<xy-foo> 无类型,但所有检查全绿 → A02 坑 1(入口写成 index.js,findEntry 只认 .ts/.tsx 且该分支无日志);同族上游症状见 A08 论点 4(toMeta 兜底推导失手时曾影响 82 条组件条目;2026-09-30 已改 kebab 规则并修完,余量清零,防回归靠 packages/xiaoye-ui/tests/metadata-entries.test.js 的双向对赌)。A02 坑 3(declaredHere 只看具名导出后面有没有 from,一行转发就能改归属,且 typecheck 与 --check 全过);A02 坑 2 是同一判定链的另一半(export { default } from 同时打掉两处生成物)。pnpm build 在 prebuild 中止,报 package.json exports 漂移 → A02 坑 4(编辑器重排 manifest 即触发,比较是整份 stringify)与 A02 坑 6(真忘了跑 pnpm gen:entries,期望行为)。XYXxx 声明,模板里补全仍不生效 → A02 坑 5(global.d.ts 既不在 tsconfig 的 include 内,也不随包发布)。A02 坑 7(--check 报错退出,--write 只 warn 照样写盘,同一份代码两种模式一个过一个失败)。EXCLUDE_DIRS 名单,单测炸在两个硬编码数字上 → A02 坑 9;缺单测保护的那两份生成物见 A02 坑 11。族谱二:Token 与样式引擎(A03 与 A04)
theme.token 里写组件专属 Token 不生效,也不报错 → A03 坑 1(Alias 层只认 AliasToken 的键,组件 Token 的落点在 components 层);同一个键在不同组件表现不同时,看的是它有没有被二次 mergeToken 列进去(A03 论点 4)。padding: undefined 这种静默失效的样式 → A03 坑 2;同类改动面 A03 坑 7(改 sizeUnit 会平移 9 档间距却不动控件高度系列)。A03 坑 3(派生链零测试);引擎侧同样为零 → A04 §七 开头的测试现状,唯一相关的用例还整块被注释掉。A03 坑 8(两份同名反向的 colorAlgorithm 加各自硬编码的透明度阶梯)。borderRadius 传大于 16 的值被静默截断 → A03 坑 9;照着 JSDoc 的 @default 数字改配置却发现对不上 → A03 坑 10(没有工具读这些注释)。css-dev-only-do-not-override-* 类名做的覆盖上生产即失效 → A03 坑 4,实现级依据(同一份 token 两种 hashId)在 A04 坑 3。A04 坑 2(styleFn 不进缓存键,HMR 兜底只写了 webpack);StyleProvider 的 transformers 与 linters 改了没反应是同一成因的另一格,A04 坑 6。A04 坑 4(映射表与节点必须成对存在,内联节点消费即删);症状出现在自建 SSR 环境且首个组件注册样式就抛错时,是 A04 坑 1。A04 坑 5(effect 节点既不写 token 属性也不写实例属性)。A04 坑 9(测试环境把随机数钉死,实例身份退化)。autoClear 或 ssrInline 能替你清 DOM → A04 坑 8(这一族开关要么自注未用、要么唯一读取在注释里)。useStyleRegister 传 order 想让样式排在后面 → A04 坑 7(SSR 产物里排对了、客户端零效果,是一处两侧不一致的缺口)。族谱三:注入链与 props 体系(A05 与 A06)
ConfigProvider 想继承父级前缀,类名却退回默认前缀 → A05 坑 1(解构默认值让父链回落分支不可达)。A05 坑 2(主链字段不合并、只有子链回落),运算符不对称的补充见 A05 坑 3。Modal.confirm 这类静态方法不吃组件树配置 → A05 坑 4(新建的 ConfigProvider 层是孤立根);忘记挂 appContext 导致样式缓存分裂是同族下一格,A05 坑 5,组件侧的对应结论在 C07-01 论点 6。csp 取到 undefined、inject('$xiaoyeUI') 恒为空 → A05 坑 7 与 A05 坑 6(后者全仓只有读取点、没有提供方)。A06 坑 5;同族在组件层的活例是 C04-08 坑 1(全局 RTL 对该组件零效果)、C11-01 论点 5(全局尺寸被默认值截断)、C05-01 坑 10(全局虚拟滚动开关无效)。ConfigProvider 的组件级配置被组件自己的默认值顶掉 → A06 论点 2 与 A06 坑 5;该 hook 的适用面在 I 层归 I13,两处口径要一起读。A06 坑 3(emit 与 props.onXxx 双派发);组件层的可见后果是 C07-01 坑 3(关闭事件无法分辨来源)。data-*、aria-*、@click 在中间层被吞 → A06 坑 2;同族两道白名单的组件侧证据在 C04-03 论点 2。A06 坑 1,公开面放大成组件新增 API 的形态见 C05-01 坑 1。PropTypes.bool 变成「默认开启」且「已传入」判定恒真 → A06 坑 9;initDefaultProps 兜底分支造出非法 prop 定义 → A06 坑 4。A06 坑 7;改 _util 根级文件却发现另一份副本没动 → A06 坑 8。A05 论点 6、A02 坑 10 与 A06 §五 末尾的结案是同一条的三次记录:A06 给出了「115 个目录里 107 个带注册机制、8 个没有」的可复现脚本,A02 提醒单条 grep 会低估,两者口径不冲突(前者是「有没有注册代码」,后者担心的是「标签是否真注册上了」)。族谱四:交付与发布链(A01 与 A08)
A01 坑 4(发布态是第二份 manifest);同一形状在自动导入链上表现为 A08 坑 1(新增组件只改 metadata,生成指向不存在模块的 import,而文档站与类型检查全绿),根因侧的 kebab 化缺口在 I16 坑 1。exports 覆盖不到的子路径本地能跑、发布后 404 → A01 坑 3;按需样式链路在 6 个目录上根本不存在,且被另一条缺陷互相遮挡,A02 坑 8。A01 坑 1(版本号进了样式缓存的 salt,机制在 A03 论点 3)。A01 坑 2 与 A08 坑 4 是同一条外部名单缺口的两个视角;metadata 是否随 pnpm gen:entries 更新的疑问由 I16 坑 4 回答。prefix: 'xy' 配下去自动导入静默不生效 → A08 坑 2;Nuxt 项目里指令默认全不可用 → A08 坑 5;默认 Nuxt 装配必然生成两条不存在的服务 import → A08 坑 6。@xiaoye-ui/vite-plugin 的新工程报缺包 → A08 坑 7;改 icons 产物布局就让深路径用法 404 → A01 坑 9,同族在 I15 坑 6 与 I15 坑 8。A01 坑 7 与 A01 论点 10(该篇自己把这条降级为「只用静态证据支撑、未查运行历史」,复核方法在它的 §附);构建把 tracked 的 manifest 改脏 → A01 坑 8。llms.txt 与文档组件目录不同步(新增或删除组件后没重跑生成器,死条目或缺失都无告警) → A08 坑 3。族谱五:浮层与层级(A07,与 I 层合读)
<xy-dropdown placement="bottom"> 让浮层完全不定位 → A07 坑 1(厂商表只有六个键、业务层先做了去 Center 化);这条链已由仲裁改写进 C03-08 论点 5 末段,RTL 镜像死码那一半在 C03-08 坑 8。builtinPlacements 传进去被丢掉 → A07 坑 2(同一个 prop 在两个族里语义相反,取决于展开顺序)。A07 坑 3;每次打开都在挂载点留下一个空 div → A07 坑 4,容器只增不减在 I 层另立一条 I02 坑 3。open 但浮层不出现、也没有任何告警 → A07 坑 5(闸门无超时兜底),同症状的 I 层条目是 I02 坑 4 与 I01 坑 4。A07 坑 6;三套机制对同一场景三种答案的对照表在 A07 坑 10,Modal 侧的按下—抬起记账在 C07-01 论点 3。A07 坑 7;换容器但不换 target 引用时不重算 → I01 坑 2。getContainer 指到局部容器后背景照旧能滚、body 却被误锁 → A07 坑 9(闸门是一次空判),组件侧可见结论在 C07-01 坑 5 与 C07-01 论点 5。A07 坑 11(内联层级覆盖 CSS 层级),I 层的三种号段表现见 I10 坑 3 与 I10 坑 4。vc-* 内核会拿到上游前缀、样式全丢 → A07 坑 8(四个内核各有一个 rc- 默认值),A07 篇内已注明它与 C07-01 坑 1 同源,属同一条红线。A07 坑 12(全仓只有一个消费者用了焦点圈定能力)。A07 论点 3(两个 visible 与一条三格队列)与 A07 论点 9(重定位靠四种监听)进入,组件特例回 C07-03、C05-01 等对应篇。(骨架占位段:待填条目。)