首页
/ Twenty 的 ESLint 到 Oxlint 迁移复盘:降级规则、插件替代与再激活计划

Twenty 的 ESLint 到 Oxlint 迁移复盘:降级规则、插件替代与再激活计划

2026-09-07 17:17:47作者:田桥桑Industrious

Twenty(The open alternative to Salesforce)的仓库根下维护着一份特殊的文档——packages/twenty-oxlint-rules/OXLINT_MIGRATION_TODO.md,它不是普通的功能说明,而是一份“迁移对账单”:记录了 Twenty 从 ESLint 切换到 Oxlint 过程中哪些规则被临时禁用、哪些规则完成了再激活、哪些 ESLint 插件因为 Oxlint 没有对应实现而被丢弃,以及后续的再激活路径。读完后,你将理解在超大型前端/后端 monorepo 中做 Linter 替换时如何评估“安全自动修复”的边界(尤其是 NestJS 装饰器依赖与 import type 的冲突),以及如何用 Oxlint 的 jsPlugins 机制把项目私有规则(twenty/*)重新实现为自定义插件。

迁移的背景与工程载体

这份 TODO 文档所在的 twenty-oxlint-rules 包是 Twenty 的私有 Oxlint 规则包。从仓库结构看,它包含:

  • oxlint-plugin.ts:插件注册入口,通过 @oxlint/pluginsdefinePluginmeta: { name: 'twenty' } 注册了约 20 条 twenty/* 自定义规则(folder-structuremax-consts-per-fileenforce-module-boundariesno-hardcoded-colors 等),见 插件注册段
  • rules/ 目录:每条规则一个实现文件加一个 .spec.ts 测试文件,例如 folder-structure.tsmax-consts-per-file.ts
  • project.json:构建目标用 esbuild 把 oxlint-plugin.ts 打包成 dist/oxlint-plugin.mjs(ESM、node 平台,并注入 createRequire banner 以兼容 CJS 依赖),测试目标跑 vitest。

各业务包通过各自 .oxlintrc.json 里的 jsPlugins 字段引用这个产物。以 twenty-server/.oxlintrc.jsontwenty-front/.oxlintrc.json 为例,两者均声明:

"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"]

也就是说,迁移 TODO 中提到的“规则禁用/再激活”,最终都落在这些 per-package 的 .oxlintrc.json 配置上——这正是理解下面每一节的钥匙。

被临时禁用的规则(twenty-server)

TODO 文档的第一张表列出了迁移后在 twenty-server 中被临时禁用的两条规则:

规则 原因 违规数 可自动修复
typescript/consistent-type-imports 存量违规 3814 处。无法自动修复,因为 NestJS 依赖 emitDecoratorMetadata 实现依赖注入——自动修复会把构造函数参数上的类型导入改写为 import type,这些导入在编译产物中会被擦除,从而直接破坏 DI 3814 否(自动修复不安全)
twenty/max-consts-per-file 24 个常量文件中存在 94 处“单文件多导出常量”的存量违规 94

consistent-type-imports:与 NestJS 依赖注入的冲突

这条规则要求类型导入统一写成 import type。在前端包里这样做完全安全,但在 twenty-server/.oxlintrc.json 中它被显式关闭:

"typescript/consistent-type-imports": "off",

而同一规则在 twenty-front/.oxlintrc.json 里是打开且强制的:

"typescript/consistent-type-imports": [
  "error",
  { "prefer": "type-imports", "fixStyle": "inline-type-imports" }
]

这个前后端差异正是 TODO 文档强调的技术背景:NestJS 控制器/服务大量使用构造函数注入(constructor(private readonly repo: SomeRepository)),编译依赖 emitDecoratorMetadata 在运行时反射出参数的真实类型。若把 SomeRepository 的导入改成 import type,运行时该符号不再存在于模块作用域,装饰器元数据拿不到它,DI 容器就会注入失败。因此 3814 处违规不能--fix 一把梭,只能人工甄别“纯类型用途”的位置再改——这也是 TODO 把它标记为 “unsafe auto-fix” 的原因。

max-consts-per-file:常量文件拆分约定

twenty/max-consts-per-file 是 Twenty 的私有规则,实现在 max-consts-per-file.ts:它监听 VariableDeclaration 节点逐次计数,超过选项 max(schema 要求为 minimum: 0 的整数)就报 tooManyConstants

对照两个业务包当前的配置,可以看到这条规则的实际落地状态:

  • 前端包通过 overrides 在常量文件上以 max: 1 强制执行(一个常量文件只允许 1 个导出常量)——见 twenty-front/.oxlintrc.json 的 overrides
  • 后端包的 twenty-server/.oxlintrc.json 的 overrides**/constants/*.ts**/*.constants.ts 显式关闭该规则,且全局 rules 段并未启用它——与 TODO 所述“twenty-server 中暂时禁用,等 24 个常量文件手工拆分后再开启”的状态一致。

完成再激活的规则(twenty-front)

TODO 的第二张表记录了一条成功走通“自动修复 → 再激活”流程的规则:

规则 修复的违规数 方法
twenty/sort-css-properties-alphabetically 578 通过 npx nx lint twenty-front --configuration=fix 自动修复

即:先统计存量违规(578 处 styled-components CSS 属性未按字母序排列),用 Nx 的 lint fix 配置批量自动修复,随后在配置中恢复强制。当前 twenty-front/.oxlintrc.json 中该规则确实处于 "error" 状态。这条路径给出了迁移的标准操作范式:能安全自动修复的规则,修完存量立即恢复原严格度;不能的,挂起并记录原因

被丢弃的 ESLint 插件及其替代策略

Oxlint 对部分 ESLint 插件生态没有等价实现,TODO 用一张完整表格记录了 11 个受影响插件,这是本篇最值得保留的“对照资产”:

插件 使用位置 原本职责 替代 / 现状
eslint-plugin-project-structure 前端 强制 src/modules/ 的目录命名与结构约定(kebab-case 目录、允许的子目录如 hooks/utils/components、hooks/utils 的文件命名)。配置仍在 folderStructure.json 重写为 twenty/folder-structure 自定义 oxlint 规则。以 "warn" 启用——存量违规 403 处(160 处非 kebab-case 命名、215 处嵌套深度 > 4、28 处命名违规)
lingui/* 前端、邮件模块 i18n 词条抽取与一致性 无等价
@stylistic/* 后端 格式规则(缩进、空格) 改用 Prettier
import/ordersimple-import-sort/imports 后端 import 排序 无等价
prefer-arrow/prefer-arrow-functions 前端 强制箭头函数而非 function 声明 无等价
eslint-plugin-mdx 文档站 MDX 文件 lint oxlint 不支持
@next/eslint-plugin-next 官网 Next.js 专属规则(禁止裸 <img>、链接处理等) oxlint 不支持
eslint-plugin-unused-imports 前端 保存时删除未使用 import no-unused-vars 部分覆盖
eslint-plugin-storybook 前端 Storybook 最佳实践(story 结构、命名) 无等价
eslint-plugin-jsx-a11y 前端 可访问性(alt 文本、aria、roles 等) Oxlint 内置 jsx-a11y 插件有部分等价,但覆盖有限
eslint-plugin-react-refresh 前端 React Refresh 边界校验(HMR) 无等价

这张表透露出的迁移决策逻辑很清晰:

  1. 职责可被其他工具承接的,直接让渡(格式化交给 Prettier、未使用符号交给 no-unused-vars);
  2. 纯目录结构约束这类“与 AST 语义无关”的规则,重写为自定义规则project-structuretwenty/folder-structure);
  3. 框架强绑定插件(lingui、Next.js、Storybook、React Refresh、MDX)则接受缺口,在 TODO 中显式留档,避免团队误以为这些约束仍然生效。

folder-structure:从 ESLint 插件到自定义 oxlint 规则

这是本次迁移里“有损”最小、也最能体现 jsPlugins 能力的一个案例。旧的约束声明保留在 folderStructure.jsonsrc 下允许任意一级目录,modules 下每个模块目录须为 kebab-case、允许递归(folderRecursionLimit: 4),并只接受 hooksutilsstatestypesgraphqlcomponentseffect-componentsconstants 等固定子目录;hooks 内文件须匹配 use{PascalCase}.(ts|tsx)utils 内文件须匹配 {camelCase}.ts

新的实现 folder-structure.ts 把这套约定改写成了一个逐段校验路径的状态机,几个关键常量与旧配置一一对应:

每条违规都配有可读的诊断文案(moduleNameNotKebabCasemoduleTooDeephookFileNamingutilFileNaming 等,见 meta.messages),例如要求模块目录 graphWidgetBarChart 改名为 graph-widget-bar-chart、hook 文件必须形如 useMyHook.ts

TODO 给出的 403 处存量违规构成如下:160 处非 kebab-case 模块目录名、215 处嵌套过深的模块、28 处 util/hook 文件命名违规(如残留的 .util.ts 后缀、kebab-case 文件名、PascalCase 文件名)。规则先以 "warn" 接入,等违规清零后再提升为 "error"

一个值得注意的事实差异:TODO 文档记录的是迁移进行时点的状态,而从当前仓库快照看,twenty-front/.oxlintrc.jsontwenty/folder-structure 已经是 "error"——可以推断文档计划中的“违规清零后提升为 error”这一步已在当前代码中完成,TODO 文件本身则保留为过程记录。

IDE 集成现状:jsPlugins 的已知短板

TODO 单独用一节说明了开发体验上的缺口:oxc.oxc-vscode 扩展对 oxlint 内置规则提供行内诊断,但尚不支持 jsPlugins(即 twenty/* 自定义规则)。这意味着自定义规则的违规在编辑器里看不到,只能依赖 nx lint 或 CI 兜底。这也是 Twenty 选择把 twenty/* 规则整体挂在各包 jsPlugins 上的代价:规则逻辑跑得快、配置集中,但 IDE 实时性弱于内置规则。对读者而言,如果你在项目中复用这种“自定义 oxlint 插件”模式,需要把这条体验限制纳入团队预期。

剩余再激活计划与决策依据

TODO 最后一节给出了三条未完成项的处置方案,每条都附了为什么这么做:

  1. consistent-type-imports(twenty-server):不能安全自动修复,给出三个选项——
    • 只在对 DI 构造函数参数之外、确认安全的位置手工补 import type
    • 启用 TypeScript 的 verbatimModuleSyntax(代价是一次大迁移);
    • 保持禁用,直到 oxlint 支持“装饰器感知的 type-import 分析”。
  2. max-consts-per-file(twenty-server):手工拆分 24 个常量文件,使每个文件最多 1 个导出常量,然后恢复启用(前端包已按 max: 1 的口径执行,可作为后端的验收参照)。
  3. folder-structure(twenty-front):已重写为 twenty/folder-structure 规则(当时以 "warn" 接入),处理 403 处存量违规(160 处目录命名、215 处深度超限、28 处文件命名),解决后提升为 "error"——当前仓库配置中该规则已处于 "error",说明这一收尾已落地。

小结:这份 TODO 对做 Linter 迁移的工程参考价值

把 TODO 文档与仓库现状对照起来看,Twenty 的 ESLint → Oxlint 迁移遵循了三条可复用的原则:

  • 对账驱动:所有“失去保护”的规则(禁用规则、被丢插件、自动修复不安全的规则)都有表格化的记录、违规计数和再激活条件,迁移不是一锤子买卖而是可审计的过程;
  • 安全边界优先于修复率:宁可让 consistent-type-imports 带着 3814 处违规挂起,也不执行会破坏 NestJS DI 的批量自动修复;
  • 私有规则用 jsPlugins 重写承接:像目录结构、常量文件拆分这类组织级约束,通过 twenty-oxlint-rulestwenty/* 命名空间重建,并保留 vitest 单测(rules/ 下每条规则均有 .spec.ts,注册表本身也有 plugin-registry.spec.ts 校验),保证“换引擎”不丢失既有约束力。

对正在评估 Linter 替换的团队,这份文档与其说是一个 TODO,不如说是一份现成的迁移风险评估模板:先盘点存量违规与自动修复安全性,再按“让渡 / 重写 / 接受缺口”三分法处理插件缺口,最后用 warn → 清零 → error 的节奏恢复严格度。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391