Oxlint-only fix workflow
仅使用 Oxlint 的修复工作流
The [repository linter migration](2026-07-29-oxlint-linter.md) retained a formatting-only ESLint invocation because Oxlint's JavaScript-plugin bridge was treated as validation-only. The pinned Oxlint toolchain executes the safe fixers supplied by `@stylistic/eslint-plugin`, so the separate formatter duplicates a configuration boundary, command startup, and direct `eslint` plus `@typescript-eslint/parser` dependencies
English
Problem
The repository linter migration retained a formatting-only ESLint invocation because Oxlint's JavaScript-plugin bridge was treated as validation-only. The pinned Oxlint toolchain executes the safe fixers supplied by @stylistic/eslint-plugin, so the separate formatter duplicates a configuration boundary, command startup, and direct eslint plus @typescript-eslint/parser dependencies.
A single Oxlint invocation is not an equivalent replacement. Overlapping plugin fixes can apply one change while leaving a newly exposed diagnostic; the repository's semi and object-curly-spacing fixture requires a second pass before it is clean. The workflow must retry that case without printing obsolete first-pass diagnostics.
Decision
All repository lint and fix workflows invoke Oxlint through scripts/run-oxlint.ts. Normal validation remains one process with inherited output. An invocation containing --fix, --fix-suggestions, or --fix-dangerously captures the first Oxlint result; success emits its stdout and stderr on their original channels, while a completed non-zero run discards its potentially obsolete diagnostics and runs the same command once more with inherited output. The runner re-raises a child signal instead of retrying or converting it to an exit code, and the second process completion is final.
The lint:fix package script and staged lefthook job use that runner directly. The type-aware root profile still ignores preserved TypeGraph fixture shapes that oxlint-tsgolint cannot analyze; the project-free staged profile re-includes that directory, carries its intentional any and quote exceptions, and applies its style fixes before the full type-aware fix pass. The formatting-only ESLint configuration and the direct eslint and @typescript-eslint/parser development dependencies are absent. @stylistic/eslint-plugin and eslint-plugin-sonarjs remain Oxlint JavaScript plugins because they preserve enforced rules; pnpm still installs ESLint as their declared peer, but no repository configuration or workflow invokes it.
Verification
The executable lint contract drives a deliberately overlapping style violation through the repository runner and requires a successful exit plus exact final bytes. The same contract pins the complete Stylistic rule set, project-free TypeGraph fixture coverage, the package scripts, the staged hook command, the deleted formatter configuration, and the absence of direct ESLint parser and runner dependencies. Existing executable probes continue to cover the Stylistic and SonarJS compatibility plugins, project-free staged validation, and type-aware project discovery.
Alternatives considered
Keep the formatting-only ESLint pass. This preserves ESLint's built-in multipass behavior but retains a second runner, a duplicated formatting config, and direct dependencies after Oxlint can execute the same plugin fixes.
Run Oxlint once with --fix. This is simpler, but overlapping safe fixes can leave the command partially formatted and non-zero even though another identical pass completes it.
Adopt Oxfmt. A formatter migration changes the repository's output contract and would create an unrelated formatting diff. It remains a separate decision from removing the redundant ESLint execution path.
Remove the JavaScript compatibility plugins. This would eliminate their ESLint peer graph but would also drop the enforced Stylistic and SonarJS rules. Dependency-tree purity does not justify weakening the quality contract.
Consequences
Contributors, package scripts, hooks, and CI have one lint runner and one rule configuration. The staged profile repeats the root ignore list so it can re-include formatter-only fixtures without exposing vendor or generated files. A completed non-zero fix invocation always pays for one retry, including a stable unfixable error. The first pass buffers each output stream up to 64 MiB so obsolete diagnostics are not printed when the retry succeeds; process creation and capture failures, including that limit being exceeded, surface immediately without a retry.
The dependency lock can still contain ESLint through plugin peer resolution. Removing that transitive package requires native replacements or a formatter decision that also replaces the compatibility plugins; it is not part of the workflow simplification.
中文
问题
仓库 linter 迁移保留了一次仅用于格式化的 ESLint 调用,因为当时认为 Oxlint 的 JavaScript 插件桥接层只能用于校验。固定版本的 Oxlint 工具链能够执行 @stylistic/eslint-plugin 提供的安全修复,因此单独的格式化器重复引入了配置边界、命令启动过程,以及对 eslint 和 @typescript-eslint/parser 的直接依赖。
单次调用 Oxlint 并不能实现等价替代。相互重叠的插件修复可能先应用一项变更,却留下由此新暴露的诊断;仓库中包含 semi 和 object-curly-spacing 违规的 fixture(测试前置数据)需要运行第二轮才能完全修复。工作流必须重试这种情况,同时不得打印第一轮中已经失效的诊断。
决策
仓库的所有 lint 与修复工作流都通过 scripts/run-oxlint.ts 调用 Oxlint。普通校验仍由单个进程执行,并直接继承输出。包含 --fix、--fix-suggestions 或 --fix-dangerously 的调用会捕获第一次 Oxlint 的运行结果;如果成功,就通过原有通道输出其 stdout 和 stderr;如果进程正常结束但状态非零,则丢弃其中可能已经失效的诊断,随后以继承输出的方式再运行一次相同命令。子进程被信号终止时,运行器会重新触发该信号,而不会重试或将其转换为退出码;第二个进程的结束结果作为最终结果。
lint:fix 包脚本和处理暂存文件的 lefthook 作业直接使用该运行器。类型感知的根配置仍会忽略 oxlint-tsgolint 无法分析、需要保留原始形态的 TypeGraph fixture;不加载项目的暂存配置会重新纳入该目录,保留其中有意设置的 any 与引号规则例外,并在完整的类型感知修复轮次之前应用其样式修复。仓库中不存在仅用于格式化的 ESLint 配置,也不直接依赖 eslint 和 @typescript-eslint/parser 这两个开发依赖。@stylistic/eslint-plugin 和 eslint-plugin-sonarjs 仍作为 Oxlint 的 JavaScript 插件保留,因为它们承载了已强制执行的规则;pnpm 仍会将 ESLint 作为这些插件声明的对等依赖(peer dependency)进行安装,但仓库中的配置和工作流均不会调用它。
验证
可执行 lint 约定将一项特意构造、会触发重叠修复的样式违规交给仓库运行器处理,并要求运行器成功退出且最终字节完全一致。同一约定还会固定完整的 Stylistic 规则集、不加载项目时对 TypeGraph fixture 的覆盖、包脚本、暂存文件钩子命令、已删除的格式化器配置,以及 ESLint 解析器和运行器不存在直接依赖这一事实。现有的可执行探针继续覆盖 Stylistic 和 SonarJS 兼容插件、不加载项目的暂存文件校验,以及类型感知的项目发现。
考虑过的替代方案
保留仅用于格式化的 ESLint 轮次。 这会保留 ESLint 内置的多轮修复行为,但在 Oxlint 已能执行相同插件修复的情况下,仍需维护第二个运行器、重复的格式化配置和直接依赖。
仅运行一次带 --fix 的 Oxlint。 这样更简单,但相互重叠的安全修复可能使命令只完成部分格式化并以非零状态退出,即使再次运行同一命令就能完成修复。
采用 Oxfmt。 迁移格式化器会改变仓库的输出约定,并产生无关的格式化 diff。它与移除冗余 ESLint 执行路径是两项独立决策。
移除 JavaScript 兼容插件。 这会消除这些插件引入的 ESLint 对等依赖图,但也会移除强制执行的 Stylistic 和 SonarJS 规则。追求依赖树纯净不能成为削弱质量约定的理由。
结果
贡献者、包脚本、钩子和 CI 统一使用一个 lint 运行器和一份规则配置。暂存配置会重复根配置的忽略清单,以便重新纳入仅用于格式化的 fixture,同时避免将 vendor 或生成文件纳入检查。修复进程正常结束但状态非零时,始终会额外执行一次重试;稳定存在且无法修复的错误也不例外。第一轮的每条输出流最多缓冲 64 MiB,因此重试成功时不会打印已经失效的诊断;进程创建或输出捕获失败(包括超出该上限)时会立即报告错误,不会重试。
依赖锁中仍可能因插件的对等依赖解析而包含 ESLint。要移除这个传递依赖,需要使用原生替代方案,或另行决定采用能够同时取代兼容插件的格式化器;这不属于本次工作流简化的范围。