Kumo 自定义 Lint 规则揭秘:设计系统如何用 5 条规则从源头锁住团队代码一致性
【免费下载链接】kumoCloudflare's component library for building modern web applications.项目地址: https://gitcode.com/gh_mirrors/kumo5/kumo
Kumo 是 Cloudflare 开源的 React 组件库(设计系统)。它的做法不是靠 Code Review 口头约定,而是用5 条自定义 Lint 规则从源头强制颜色令牌、暗色模式、组件规范、包边界与组件契约——所有"设计系统违规"都在你写代码的那一刻被机器拦截。这篇文章带你拆解这 5 条 Kumo 自定义 Lint 规则各自解决什么问题,以及如何借鉴到你的团队。
为什么设计系统需要自定义 Lint 规则?
组件库中,"视觉一致"是硬要求。但人工审查很难兜住这些情况:
- 有人图省事,直接写原生颜色类,绕过设计令牌;
- 有人手动用
dark:前缀切换暗色模式,导致暗色表现不一致; - 新组件不遵循团队约定的变体(variant)命名与导出结构;
- Monorepo 里用
../../相对路径"偷渡"到兄弟包内部实现; - 已废弃的 prop 被新代码重新使用。
通用解法就是自定义 Lint 规则:把规则写成插件,挂到 Lint 检查器上(Kumo 用的是基于 Rust、速度更快的 Oxlint),让机器在每次检查时自动报告违规。入口文件 lint/kumo-plugin.js 一次性注册了 5 条规则:
| 规则名 | 拦截什么 |
|---|---|
no-primitive-colors | Tailwind 原生色(如bg-blue-500) |
no-tailwind-dark-variant | 手动用dark:类切换颜色 |
enforce-variant-standard | 组件变体导出不符合命名规范 |
no-cross-package-imports | 相对路径跨包导入 |
no-flow-node-custom-render | Flow 自定义节点未透传 props 和 ref |
规则一:no-primitive-colors——封杀"裸颜色",强制设计令牌
这是逻辑最重的一条,位于 lint/no-primitive-colors.js。
问题:如果每个组件都写bg-blue-500、border-red-500,主题一换品牌色就全乱了。
做法:
- 规则启动时直接读取两份主题 CSS 文件(
theme-kumo.css与theme-fedramp.css,位于 packages/kumo/src/styles/),解析出所有--color-*与--text-color-*自定义属性,构成"合法令牌白名单"——白名单与主题文件天然保持同步,改主题不用改规则; - 遍历 JSX 中所有类名字符串(模板串、字符串拼接、三元表达式都能被递归提取出来),一旦发现
bg-、text-、border-、ring-等颜色前缀:- 命中 Tailwind 原生色系(red、blue、slate 等 21 个色族)→ 报
no-primitive-colors,提示改用 Kumo 语义令牌; - 命中"未知令牌"(比如语义色拼错)→ 报
invalid-color-token,错误信息里直接点出具体是哪个令牌没定义在主题文件里。
- 命中 Tailwind 原生色系(red、blue、slate 等 21 个色族)→ 报
亮点:bg-white、text-black被刻意放行(它们是通用色);text-sm、bg-clip-padding这类"长得像颜色但其实不是颜色"的工具类,通过一张非颜色工具表加正则模式精准排除,避免误报。这种"白名单驱动"的写法,是设计令牌强制落地的好范本。
规则二:no-tailwind-dark-variant——封杀dark:,暗色模式统一收口
位于 lint/no-tailwind-dark-variant.js。规则一句话:任何className里的dark:bg-...、dark:text-...都不允许。
问题:设计系统的暗色模式应该由主题令牌统一处理(换主题即自动换色),而不是每个页面自己写dark:变体,否则会出现"双重暗色"或暗色表现不一致。
实现细节:
- 正则要求
dark:后面必须跟着合法的工具名(如dark:bg-blue-500),这样就不会误报{ dark: "vesper" }这种普通对象键; extractStrings函数能递归收集字面量、模板字符串、字符串拼接、数组、对象、函数参数、三元表达式、甚至 JSX 文本中的类名片段——想用动态字符串绕过都行不通;- JSX 的
className/class属性被专门监听,其余代码位置同样扫描。
报错信息直接给出改法:"请使用设计系统令牌或组件 API 处理暗色模式"。
规则三:enforce-variant-standard——变体导出必须符合命名契约
位于 lint/enforce-variant-standard.js。
背景:Kumo 的组件采用"机器可读"的变体体系——每个组件必须导出KUMO_{组件名}_VARIANTS(变体定义)与KUMO_{组件名}_DEFAULT_VARIANTS(默认值),文档站、AI 组件注册表、代码生成都依赖这份结构。
规则做什么:
- 只对
src/components/{name}/{name}.tsx这类组件文件生效,组件名直接由文件路径解析(连字符自动转下划线); - 逐个检查导出名是否与
KUMO_{COMPONENT}_VARIANTS、KUMO_{COMPONENT}_DEFAULT_VARIANTS、可选的KUMO_{COMPONENT}_BASE_STYLES完全一致; - 命名写错时,报错会同时给出你写的名字和应该写的名字;文件末尾若缺少必需导出,还会列出你实际导出了哪些变体相关符号。
这类"把命名约定当契约"的规则,把团队文档里的承诺变成了机器硬校验。
规则四:no-cross-package-imports——封杀相对路径"爬出包"
位于 lint/no-cross-package-imports.js。
问题:Monorepo 里写import x from "../../kumo/src/button"很方便,但它绕过了包的公开 API,目录一重构就全线报错。
判定逻辑:
- 正则匹配"若干层
../+ 已知包目录名(kumo / kumo-docs-astro / kumo-figma)"; - 要求至少向上两级(
../../):只向上一级(../kumo/)大概率是同包内恰好同名的本地目录,不报——这个细节巧妙避免了误报; - 静态
import、动态import()、require()、export from、export * from五种入口全覆盖。
错误信息直接给出替代方案:改用包名(如@cloudflare/kumo)导入。
规则五:no-flow-node-custom-render——用静态分析检查"组件契约"
位于 lint/no-flow-node-custom-render.js,是最"硬核"的一条。
问题:Flow 组件的<Flow.Node render={...}>里放入自定义节点组件时,该组件必须把收到的props展开透传、把ref转发给根元素,否则节点无法聚焦、拖拽和测量,整个流程图行为会异常。靠人肉检查几乎不可能覆盖所有组合。
规则做法(一次小型静态数据流分析):
- 收集文件里的组件候选:命名函数组件、
const X = (props) => ...、forwardRef包裹的函数(支持重命名导入); - 逐个分析组件渲染体:是否出现了 props 展开、是否把 ref 绑定到了元素上;
- 找出所有出现在
<Flow.Node>的render属性里的自定义组件; - 文件结束时对照结论:缺 ref 报
missingRef,缺 props 报missingProps,都缺则合并报告。
为应对真实代码,规则还处理了 TypeScript 断言(as、satisfies、!)解包、forwardRef重命名导入、A.B形式的 JSX 成员表达式、ref与属性访问的区分等边界情况——这是一条"真正理解 React"的 Lint 规则。
彩蛋:第 6 条只存在于包内的规则
packages/kumo/lint/no-deprecated-props.js 从自动生成的 AI 组件注册表(ai/component-registry.json)里读取废弃信息,谁用了废弃 prop(例如Select的hideLabel、Banner的text)就报错,消息里直接附上替代方案。
这展示了一个很妙的模式:规则的数据源来自生成物——prop 在注册表里标记 deprecated,Lint 规则自动获得,无需手工维护任何清单,"文档"与"执行"就是同一份数据。
规则如何接线:两层配置
- 仓库根目录vite.config.ts:把
packages/kumo/lint/kumo-plugin.js作为 jsPlugin 加载,4 条通用规则(no-cross-package-imports、no-primitive-colors、no-tailwind-dark-variant、no-flow-node-custom-render)全部设为"error"——Lint 不过,提交就过不去; - 包内packages/kumo/vite.config.ts:把
enforce-variant-standard也设为"error",这条规则只在组件源码目录生效,所以放在包里; - 每条规则都配有测试文件(如 enforce-variant-standard.test.ts),保证规则本身不漂移。
这种"根目录管通用规则、包内补充专属规则"的分层,是 monorepo 的通用好模式。
可借鉴到你的团队的清单
- 白名单优于黑名单:令牌表直接从主题源文件解析(参考
no-primitive-colors),规则与设计系统自动同步; - 报错信息写出改法:给出正确的导出名、包名、替代 prop,而不只是"这里错了";
- 误报工程化:非颜色工具类白名单表、
../层级 ≥ 2 的判定、dark:正则边界——好规则的价值一半在防误报的细节里; - 规则读元数据:废弃标记、变体定义等由构建期生成,Lint 规则运行时读取,文档即规则;
- 规则要有测试:每条规则配一个
.test.ts,防止规则自身回归; - 设为 error 级别:能挡住合并的 Lint 才真正生效。
总结
Kumo 对"如何让团队代码保持一致"的回答,不是写更多文档,而是5 条自定义 Lint 规则 + 1 份注册表元数据:颜色令牌、暗色模式、变体命名、包边界、组件契约,全部在源头被机器强制。任何在做设计系统或组件库的团队,都可以直接抄这套"规则即规范"的作业。
【免费下载链接】kumoCloudflare's component library for building modern web applications.项目地址: https://gitcode.com/gh_mirrors/kumo5/kumo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考