我以前对代码格式化工具没有太强的感觉。编辑器保存时整理一下,ESLint 再管一部分,代码照样能写。
真正让我觉得该认真处理这件事,是一个 TypeScript API 项目开始堆积路由、Schema 和 OpenAPI 描述以后。为了少占几行,我会顺手把多个对象和回调挤在一起。改的时候还看得懂,隔几天再回来,一段路由里哪里是验证器、哪里是中间件、响应结构在哪结束,已经要靠括号计数。
我想要的其实很像 gofmt:不讨论每一处该怎么换行,给项目一个命令,让结果稳定下来。
Oxfmt 官方文档把它定位为独立格式化器,不负责替代 TypeScript、ESLint 或测试。第一次跑完以后,最直接的变化不是“代码更漂亮”,而是我不用再手工维护排版了。
先把命令放进项目
Oxfmt Quickstart列出了 npm、pnpm、Yarn 和 Bun 的安装方式。使用 npm 的项目可以将它安装为开发依赖:
npm add -D oxfmt然后在各自的 package.json 里增加两个命令:
{
"scripts": {
"format": "oxfmt .",
"format:check": "oxfmt --check ."
}
}npm run format 会直接改写文件,npm run format:check 只检查,不写入。后者适合 CI,也适合提交前确认没有漏掉刚改的文件。
Bun 本身没有对应的源码格式化命令,但可以按照 bunx 文档运行 npm CLI。临时试用时可以这样执行:
bunx --bun oxfmt --check .正式项目还是应该把 Oxfmt 放进 devDependencies 并提交 lockfile。否则不同时间下载到不同版本,格式化结果发生变化时很难追查。
配置范围不要顺手变成风格迁移
如果仓库里的代码原本就使用同一种风格,在根目录保留一份配置最简单。问题通常出现在已有项目:不同 package 可能来自不同阶段,分号、尾随逗号和 TSX 的使用情况并不一致。
这时直接增加根配置,第一次格式化就可能夹带大范围风格迁移。原本只想统一缩进和换行,结果 diff 里全是分号与逗号,真正的代码改动反而难以审查。
Oxfmt 配置说明指出,它会从待格式化文件所在目录向上查找配置,离文件最近的配置优先。可以先判断哪些目录确实共享同一套规则:风格一致就共用根配置,差异已经存在则在对应目录放置更近的配置。配置文件的位置应该反映规则的适用范围,而不是机械地追求全仓库只有一份。
Oxfmt 配置项参考列出了可用字段。下面是一份适合 TSX 项目的配置:
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 140,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "none",
"endOfLine": "lf",
"insertFinalNewline": true,
"sortImports": false,
"sortPackageJson": false,
"ignorePatterns": [
"dist/**",
".next/**",
".open-next/**",
".wrangler/**",
"node_modules/**",
"src/generated/**"
]
}如果项目原本使用分号,只需把 semi 改为 true;如果原本保留尾随逗号,再把 trailingComma 改成 all。格式化工具的任务是终止排版争论,不是趁机重写所有团队习惯。
我也关闭了 sortImports 和 sortPackageJson。排序可能有用,但它已经超出单纯的空白与换行调整。尤其是副作用 import,顺序变化有机会改变运行结果。刚接入时先让格式化器只做格式化,审查范围会清楚很多。
singleQuote 不会控制 JSX 属性
这是我第一次配置时马上踩到的坑。
我已经写了:
"singleQuote": true普通 TypeScript 字符串和 import 确实变成了单引号,但 TSX 里的属性还是双引号:
<View className="page-placeholder">原因是 JSX 属性由另一个选项控制。要统一成单引号,还要显式设置:
"jsxSingleQuote": true再次格式化后才会得到:
<View className='page-placeholder'>JSON 不在这个规则里。JSON 语法要求键名和字符串使用双引号,.oxfmtrc.json 自己也不可能改成单引号。
生成文件不要跟着格式化
第一次在服务端目录执行 oxfmt . 时,源码变得清楚了,Drizzle 的迁移快照也一起变了。数组被压成单行,数据完全相同,diff 却多出了几百行。
这种变化没有维护价值。更麻烦的是,下次由生成器重建快照时,它可能再按自己的格式写回来。两个工具轮流改同一份生成文件,提交记录里会一直出现噪声。
我后来把生成目录当成接入格式化器时必须检查的一项:
{
"ignorePatterns": [
"dist/**",
"deploy_versions/**",
"src/generated/**",
"drizzle/meta/**",
".next/**",
".open-next/**",
".wrangler/**",
"next-env.d.ts",
"cloudflare-env.d.ts",
"tsconfig.tsbuildinfo"
]
}具体列表要跟着项目走。常见的例子包括 dist、框架构建目录、RPC 生成的类型声明和数据库迁移工具生成的快照。它们的来源不同,但处理原则一样:源头不在这里,就不要让格式化器接管它。
可以先用下面的命令查看哪些文件会发生变化:
npx oxfmt --list-different .先看列表,再运行全量格式化,比格式化以后面对一大片陌生 diff 省事。
Windows 换行符要和 Git 一起处理
我原本希望 Oxfmt 不碰现有换行符,后来发现它没有 auto 或 preserve。endOfLine 只能明确选择 lf、crlf 或 cr。
只改这一个选项还不够。在 Windows 上,编辑器、Oxfmt 和 Git 都可能接触换行符。它们各自的配置单看都说得通,放到一起却可能互相改写。
我最后统一成了 LF,并把约束分别写进 EditorConfig、Oxfmt 和 Git。EditorConfig 先这样设置:
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2end_of_line 约束支持 EditorConfig 的编辑器,insert_final_newline 则要和 Oxfmt 的同名行为保持一致。如果这里写成 false,Oxfmt 却配置成 true,保存文件和运行格式化就可能轮流改动文件末尾。
Oxfmt 仍然显式固定这两个值:
{
"endOfLine": "lf",
"insertFinalNewline": true
}Oxfmt 会读取 EditorConfig,但 .oxfmtrc.json 中已经写明的选项优先。两边保持一致不是为了让 Oxfmt 能运行,而是避免编辑器或其他工具写出另一种结果。
Git 负责的是仓库中的规范化和检出结果。按照 Git 的 .gitattributes 文档,可以在根目录增加:
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlfeol=lf 表示这些文本文件在索引中以 LF 规范化,Git 下次检出或重写工作区文件时也使用 LF。Windows 批处理文件单独保留 CRLF。
我还在当前仓库关闭了自动转换:
git config --local core.autocrlf false这里的 --local 很重要。Git 配置分为 system、global 和 local 等层级,仓库级配置会覆盖前两级。因此,看到全局仍然是 core.autocrlf=true,不等于当前仓库还在使用它。可以直接查看配置来自哪里:
git config --show-origin --show-scope --get-all core.autocrlf
git config --get core.autocrlf第一条会列出各层配置,第二条返回当前仓库最终采用的值。
Git 的换行警告在说什么
加入 .gitattributes 后,第一次执行 Git 命令仍可能看到这样的提示:
warning: in the working copy of '.editorconfig', CRLF will be replaced by LF the next time Git touches it这不是提交失败,也不表示文件已经损坏。它只是在说明:工作区里的这个文件目前是 CRLF,但该路径的 Git 属性要求 LF;Git 下次处理它时会按规则转换。
一次性出现这种提示很正常。真正的问题是它反复出现,例如编辑器每次保存都写回 CRLF,Oxfmt 每次运行又改成 LF。遇到这种情况,不要继续猜 core.autocrlf,直接检查具体文件:
git check-attr text eol -- .editorconfig
git ls-files --eol -- .editorconfiggit check-attr 显示这个路径实际命中了哪些属性。git ls-files --eol 会同时显示索引和工作区的换行状态,例如:
i/lf w/crlf attr/text=auto eol=lf .editorconfig这里的 i/lf 是索引中的状态,w/crlf 是工作区中的状态。两者不同并不一定会让 git status 报告修改,因为 Git 比较文本文件时会先按照属性规范化内容。这也解释了一个看起来很奇怪的现象:工作区显示干净,文件的实际换行符却还没有变成 LF。
第一次统一已有文件
如果仓库以前混用了换行符,先提交业务改动,或者把它们另行妥善保存,再在干净工作区执行一次规范化:
git add --renormalize .
git status --short
git diff --cached --stat
git diff --cached --check如果 git status 一下出现很多文件,先别急着还原,也不要只凭文件数量判断发生了大规模代码修改。查看暂存区的统计和差异,确认它们是否只是换行规范化。
git diff --cached这一步会更新暂存区,所以不要在一堆尚未提交的业务改动中间执行。换行规范化最好单独审查、单独提交。处理完以后,EditorConfig 负责编辑器写入,Oxfmt 负责格式化结果,.gitattributes 负责 Git 中的文本策略,各自的边界就清楚了。
格式化应该排在检查之前
接入完成后,我把项目规则写成了固定顺序:
npm run format
npm run check
npm run test先格式化再检查有一个很实际的好处:检查通过以后,工作区就是准备提交的最终代码,不会发生“测试刚通过,又运行格式化改了一遍文件”的情况。
format:check 仍然值得保留。它适合 CI 或只读审查:
npm run format:checkOxfmt 不会证明代码正确。TypeScript 仍然负责类型,ESLint 仍然检查代码规则,测试仍然验证行为。它只是把排版从这些工作里拿走,让每个工具做自己的事。
第一次格式化要单独看
在一个已有项目里首次执行 Oxfmt,diff 很大是正常的。我遇到的一次全量格式化处理了 50 个文件,用时不到一秒;真正花时间的是审查这些差异。
我会先确认生成文件已经排除,再看 git diff --stat 和几个变化最大的文件:
git diff --stat
git diff --check
npm run check
npm run test如果格式化和功能修改混在同一批改动里,git diff -w 可以暂时帮助观察非空白变化,但它不能代替正常审查。更稳妥的做法仍然是先完成一次纯格式化提交,以后的功能提交自然会安静下来。
这也是 Oxfmt 最让我满意的地方。工具本身安装只要一条命令,麻烦的是第一次把边界理顺:哪些风格要保留,哪些文件不归它管,编辑器和 Git 又该听谁的。处理完这些,日常使用确实只剩 npm run format。