我以前对代码格式化工具没有太强的感觉。编辑器保存时整理一下,ESLint 再管一部分,代码照样能写。
真正让我觉得该认真处理这件事,是一个 TypeScript API 项目开始堆积路由、Schema 和 OpenAPI 描述以后。为了少占几行,我会顺手把多个对象和回调挤在一起。改的时候还看得懂,隔几天再回来,一段路由里哪里是验证器、哪里是中间件、响应结构在哪结束,已经要靠括号计数。
我想要的其实很像 gofmt:不讨论每一处该怎么换行,给项目一个命令,让结果稳定下来。
Oxfmt 正好适合这个位置。它是独立的格式化器,不负责替代 TypeScript、ESLint 或测试。第一次跑完以后,最直接的变化不是“代码更漂亮”,而是我不用再手工维护排版了。
先把命令放进项目
Oxfmt 可以通过 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 会从待格式化文件所在目录向上查找配置,离文件最近的配置优先。可以先判断哪些目录确实共享同一套规则:风格一致就共用根配置,差异已经存在则在对应目录放置更近的配置。配置文件的位置应该反映规则的适用范围,而不是机械地追求全仓库只有一份。
下面是一份适合 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 上的 Git 使用 core.autocrlf=true,而 Oxfmt 配置为 LF,就会出现来回转换:Oxfmt 写入 LF,Git 检出时转成 CRLF,下一次格式检查又失败。反复出现的提示通常是:
LF will be replaced by CRLF the next time Git touches it如果代码会在 Windows 和 Linux 之间流转,并且仓库中包含 Shell 脚本,可以明确统一为 LF。在根目录增加 .gitattributes:
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf本仓库关闭自动转换:
git config --local core.autocrlf false同时在 Oxfmt 配置中固定:
{
"endOfLine": "lf",
"insertFinalNewline": true
}这样 Git、格式化器和 Linux 构建使用同一个结果,Windows 批处理文件仍保留 CRLF。
如果仓库以前混用了换行符,可以在干净工作区执行一次规范化:
git add --renormalize .
git status --short
git diff --cached --check不要在一堆业务改动尚未提交时运行 git add --renormalize .。换行规范化最好单独审查、单独提交,否则真正的代码变化很容易被淹没。
格式化应该排在检查之前
接入完成后,我把项目规则写成了固定顺序:
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 最让我满意的地方。工具本身安装只要一条命令,真正需要想清楚的是项目边界:哪些风格要保留,哪些文件不归它管,换行策略由谁决定。边界定好以后,日常使用就只剩 npm run format。