Plane Monorepo Lint 体系实战:OxLint 单一根配置、逐包警告预算与 Pre-commit 自动修复
Plane 是一个采用 pnpm + Turbo 组织的 TypeScript 单仓库(monorepo),其代码风格与潜在缺陷的静态检查完全由 OxLint 承担。本篇基于仓库内的 docs/linting.md 展开,系统讲解 Plane 如何用一份根级 .oxlintrc.json 覆盖全部应用与包、如何通过 pnpm check:lint / pnpm fix:lint 与 Turbo 过滤器执行检查、如何用逐包 --max-warnings 预算逐步收敛存量告警,以及 Husky + lint-staged 如何在提交前自动格式化并修复。读完本文,你可以完整理解并复用这套"单一根配置 + 任务编排 + 警告预算 + 提交前门禁"的 Lint 工程化方案。
为什么选择 OxLint:单二进制、免构建的 Lint 管线
docs/linting.md 开篇即给出 Plane 选用 OxLint 的三条核心理由:
- Single Root Config(单一根配置):仓库根部只有一份 .oxlintrc.json,即可处理所有 packages 与 apps,无需在每个子包里重复维护 ESLint 配置;
- No Build Required(无需构建产物):OxLint 不依赖 TypeScript 的构建产物,Lint 与 build 相互独立——即使
dist/、类型产物尚未生成,检查也能照常运行; - Plugin Coverage(插件覆盖):一次启用
react、typescript、jsx-a11y、import、promise、unicorn、oxc七个插件族,覆盖 React 组件规范、TS 类型安全写法、无障碍(a11y)、模块导入顺序、Promise 误用与代码风格等常见检查维度。
文档同时提到,OxLint 是一个 Rust 编写的单二进制工具,官方宣称速度可达 ESLint 的 50~100 倍,运行时零 Node.js 依赖。这两点对大型 monorepo 意义很大:检查耗时不再随 tsc 编译链路增长,且 CI 中不需要先装完整 Node 工具链就能跑 Lint。
这套选型在仓库中有明确的落点——根 package.json 的 devDependencies 通过 pnpm catalog 统一声明了 oxlint、oxfmt、turbo、husky、lint-staged,而具体版本集中锁定在 pnpm-workspace.yaml 的 catalog 段中:oxlint 为 1.51.0、oxfmt 为 0.35.0、turbo 为 2.9.18、husky 为 9.1.7、lint-staged 为 16.2.7。也就是说,全部工作区共享同一版本,避免各子包引入不同 OxLint 造成的规则行为漂移。
如何运行 Lint:根脚本、Turbo 过滤与逐包命令
根目录命令
按照 docs/linting.md,在仓库根目录执行:
# 检查 lint 错误
pnpm check:lint
# 自动修复 lint 错误
pnpm fix:lint
这两个脚本定义在根 package.json 中:
"check:lint": "turbo run check:lint",
"fix:lint": "turbo run fix:lint",
"check": "turbo run check",
"fix": "turbo run fix"
它们并不是直接调用 oxlint,而是交给 Turbo 做任务编排。turbo.json 中对应的任务定义为:
"check:lint": {
"inputs": ["$TURBO_DEFAULT$", "!**/*.md"],
"outputs": []
},
"fix:lint": {
"inputs": ["$TURBO_DEFAULT$", "!**/*.md"],
"outputs": []
}
有三个细节值得注意:
**/*.md被显式排除在缓存输入之外——修改 Markdown 不会使 Lint 缓存失效;outputs: []表明 Lint 不产生构建产物,Turbo 仅按输入文件哈希做增量缓存;- turbo.json 将
.oxlintrc.json列入globalDependencies,意味着根配置文件一旦变化,所有包的 Lint 缓存会整体失效重跑——这正是"单一根配置"模式下防止配置改动被缓存掩盖的保险设计。
同时,check 任务聚合了三类静态检查,构成完整的质量门禁:
"check": {
"dependsOn": ["check:format", "check:lint", "check:types"]
}
即格式检查(oxfmt --check)、Lint 检查(oxlint)与类型检查(tsc --noEmit)三者缺一不可。
对指定包运行
当只需要验证某一个包时,文档给出的方式是:
pnpm turbo run check:lint --filter=@plane/ui
这里的 @plane/ui 对应 packages/ui/package.json(该包 name 即为 @plane/ui)。同样可以用 --filter 精确到 web、@plane/editor 等任意工作区成员。
每个子包内的真实 oxlint 命令与"警告预算"
文档只展示了入口命令,而各子包的 check:lint 脚本揭示了仓库的一个重要实践——逐包设置 --max-warnings 警告预算。各子包的实际脚本为(均直接调用 oxlint):
| 包(name) | 脚本 | 警告预算 |
|---|---|---|
apps/web (web) |
oxlint --max-warnings=11957 . |
11957 |
apps/admin (admin) |
oxlint --max-warnings=759 . |
759 |
apps/space (space) |
oxlint --max-warnings=676 . |
676 |
| apps/live | oxlint --max-warnings=119 . |
119 |
| packages/editor | oxlint --max-warnings=416 . |
416 |
| packages/propel | oxlint --max-warnings=3605 . |
3605 |
packages/ui (@plane/ui) |
oxlint --max-warnings=66 . |
66 |
| packages/utils | oxlint --max-warnings=38 . |
38 |
| packages/i18n | oxlint --max-warnings=9 . |
9 |
| packages/services | oxlint --max-warnings=6 . |
6 |
| packages/constants | oxlint --max-warnings=2 . |
2 |
| packages/types | oxlint --max-warnings=1 . |
1 |
| packages/shared-state | oxlint --max-warnings=0 . |
0 |
| packages/logger | oxlint --max-warnings=0 . |
0 |
--max-warnings=N 的语义是:错误(error)仍然会导致检查失败,而警告(warning)的数量超过 N 时任务才会失败。这是一种典型的"存量收敛"策略——迁移到 OxLint 初期存量警告较多,先把预算锁死在现状值上,之后只减不增,最终把预算压到 0(如 packages/logger、packages/shared-state 已经做到零容忍)。新增代码引入的新警告会立刻超出预算导致 CI 失败,从而保证代码库质量单调改善。
对应的 fix:lint 脚本则统一为 oxlint --fix .(如 apps/web/package.json),即对当前包目录做尽力自动修复。
根配置 .oxlintrc.json 逐项解析
docs/linting.md 称"一份根配置处理所有包和应用",下面结合 .oxlintrc.json 的实际内容逐项说明。
插件与环境
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "jsx-a11y", "import", "promise", "unicorn", "oxc"],
"env": {
"browser": true,
"node": true,
"es2024": true
}
plugins与文档的"Plugin Coverage"一一对应;env同时声明browser与node——因为仓库里既有面向浏览器的 React 应用(apps/web、apps/space、apps/admin),也有面向 Node 的服务端代码(apps/live中的 Hocuspocus/Yjs 协作服务,以及packages/decorators、packages/logger等);es2024则允许使用最新 ES 语法内置对象而不被误报no-undef。
类别化规则等级
"categories": {
"correctness": "warn",
"suspicious": "warn",
"perf": "warn"
}
文档中给出的类别表为:
| 类别 | 文档中的等级 | 作用 |
|---|---|---|
correctness |
error | 会导致运行时错误的真实缺陷 |
suspicious |
warn | 大概率是写错的代码模式 |
perf |
warn | 性能反模式 |
需要注意:当前仓库的 .oxlintrc.json 中三个类别实际均设为 warn,而非文档表格中 correctness 的 error。结合上文各包脚本中的 --max-warnings 预算即可理解这一取舍:统一按 warning 上报,再用逐包预算控制总量。若希望某条规则更严格,可在 rules 段单独覆盖。
框架级设置
"settings": {
"react": { "version": "19.0" },
"jsx-a11y": { "polymorphicPropName": "as" }
}
react.version: 19.0告诉 React 插件按 React 19 语义检查(仓库 catalog 中 React 版本为 19.2.8,与之一致),从而不会误报已废弃的旧版约束;jsx-a11y.polymorphicPropName: "as"是 Plane 这类重度使用"多态组件"(通过as属性切换渲染标签)代码库的关键配置:声明了它之后,<Link as="button">这类写法在无障碍规则下的标签匹配判断才能正确工作。
忽略路径
"ignorePatterns": [
".cache/**",
".next/**",
".react-router/**",
".storybook/**",
".turbo/**",
".vite/**",
"*.config.{js,mjs,cjs,ts}",
"build/**",
"coverage/**",
"dist/**",
"**/public/**",
"storybook-static/**"
]
与 docs/linting.md 列出的忽略范围(node_modules/、dist/、build/、.next/、.turbo/、*.config.{js,mjs,cjs,ts}、public 目录、coverage、storybook-static)一致并有所扩展。配置文件的忽略规则值得注意:所有 vite.config.ts、postcss.config.js 等构建配置都不参与 Lint,避免工具生成的样板代码产生噪音。
规则覆盖(rules)
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"unicorn/filename-case": "off",
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"ignoreRestSiblings": true
}
]
}
对照文档的"Additional rule overrides",可以逐条确认其动机:
react/prop-types: off——文档说明"TypeScript 已经负责 props 校验",在 TS 项目中保留 prop-types 只会产生噪音;同理react/react-in-jsx-scope关闭是因为新版 JSX Transform 不再要求import React;no-unused-vars保留为warn,但通过argsIgnorePattern/varsIgnorePattern等^_前缀模式,允许用下划线前缀显式标记"有意未使用"的参数、变量与捕获的异常;ignoreRestSiblings: true则放行const { a, ...rest } = obj中解构透传的场景——这在 React props 转发中极为常见;- 三条
unicorn规则(filename-case、no-null、prevent-abbreviations)关闭,对应文档所说的"Several noisy unicorn rules disabled":文件名大小写约定、禁止null(JS/TS 项目中常需显式空值)与命名缩写限制(如禁止obj、info这类缩写)在大型存量代码库中会产生海量误报级别的告警,逐条关闭是务实选择。
覆盖范围:哪些代码被 Lint,哪些不会
文档明确 Lint 配置应用于以下全部 TypeScript / JavaScript 文件:
- 应用层:
apps/web、apps/admin、apps/space、apps/live; - 包层:
packages/下的所有包(ui、propel、editor、i18n、types、utils、services、constants、shared-state、hooks、logger、decorators、codemods等)。
这与 pnpm-workspace.yaml 中工作区声明吻合:
packages:
- apps/*
- packages/*
- "!apps/api"
- "!apps/proxy"
apps/api(Django 后端)与 apps/proxy(Caddy 网关)不属于 pnpm 工作区,因此天然不在 JS/TS Lint 范围内。
抑制警告:eslint-disable 的向后兼容
docs/linting.md 专门强调了向后兼容:OxLint 支持 eslint-disable 注释,从 ESLint 迁移而来仓库中已有的内联抑制无需批量改写即可继续生效。文档给出的两种写法:
// 单行抑制
// eslint-disable-next-line no-unused-vars
const data = response;
// 块级抑制
/* eslint-disable no-unused-vars */
// ... code
/* eslint-enable no-unused-vars */
文档同时给出了使用约束:"Please use sparingly —— most warnings indicate real issues that should be fixed."(请克制使用,多数警告指向应当修复的真实问题)。结合上文 --max-warnings 预算机制可以理解:每一次注释抑制都在消耗该包的警告预算额度,滥用会让预算失真、削弱 CI 门禁的判别力。因此抑制注释应当只用于确认无误报的边界场景(如类型断言、跨模块契约代码),并尽量附注释说明原因。
Pre-commit 钩子:Husky + lint-staged + oxfmt + oxlint
docs/linting.md 指出提交时 Lint-staged 会经由 Husky 自动运行,做两件事:
- oxfmt 格式化本次暂存的文件;
- OxLint 自动修复可修复项,且带
--deny-warnings——即修复后若仍存在警告,提交直接失败。
仓库中的实际链路可以完整验证:
- 根 package.json 的
"prepare": "husky"脚本在pnpm install时激活钩子; - 钩子本体 .husky/pre-commit 只有一行:
pnpm lint-staged; - 真正执行规则由根 package.json 的
lint-staged配置定义:
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts,json,css,md}": [
"pnpm exec oxfmt --no-error-on-unmatched-pattern"
],
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
"pnpm exec oxlint --fix --deny-warnings"
]
}
两条规则按文件类型分工:格式规则(含 JSON、CSS、Markdown)交给 oxfmt;JS/TS 代码则交给 oxlint --fix --deny-warnings 做"能修则修、修不干净则拦下"的门禁。这意味着一旦提交因 Lint 失败,开发者必须先修复警告再提交,存量警告不可能被悄悄带进主干。
配套的格式化配置 .oxfmtrc.json 同样位于根目录,被 turbo.json 列入 globalDependencies(与 .oxlintrc.json 同等待遇)。其关键项为:printWidth: 120、tabWidth: 2、trailingComma: es5,并对 cn/clsx/cva 三个函数内的 Tailwind 类名做排序(sortTailwindcss,样式表锚定在 packages/tailwind-config/index.css);另外 packages/codemods 下的文件单独覆写为 printWidth: 80。
编辑器集成与完整检查流程
文档建议安装 VS Code 的 OxLint extension,以获得与 CI 规则完全一致的逐行错误/警告提示,避免"本地编辑器一片绿、CI 却挂掉"的偏差。
把上述机制串起来,Plane 的一次完整质量检查流程是:
- 本地逐次提交:Husky 触发
lint-staged,对暂存文件执行oxfmt格式化 +oxlint --fix --deny-warnings修复门禁; - 本地全量/按包检查:
pnpm check:lint(全仓)或pnpm turbo run check:lint --filter=<pkg>(单包),由 Turbo 做增量缓存、由各包--max-warnings预算把关; - CI 三合一门禁:
pnpm check等价于依次跑check:format(oxfmt --check)、check:lint、check:types(tsc --noEmit),覆盖格式、静态缺陷与类型三个维度。
参考文件
- docs/linting.md —— 本文档的主题源文档
- .oxlintrc.json —— OxLint 根配置(插件、类别、env、settings、ignorePatterns、rules)
- .oxfmtrc.json —— oxfmt 格式化根配置
- package.json —— 根脚本(
check:lint/fix:lint)与lint-staged配置 - turbo.json ——
check/check:lint/fix:lint任务定义与globalDependencies - pnpm-workspace.yaml —— 工作区范围与 oxlint/oxfmt/turbo/husky/lint-staged 的 catalog 版本锁定
- .husky/pre-commit —— 提交前钩子入口
- apps/web/package.json、packages/ui/package.json、packages/shared-state/package.json —— 逐包
check:lint脚本与警告预算示例
需要说明的适用前提:以上全部命令基于仓库根 package.json 声明的运行环境(Node >=22.22.0、pnpm@11.3.0),各包警告预算(--max-warnings 数值)是当前仓库快照下的存量值,后续随着警告清零会持续下调。
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 StartedRust0625
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