用 Oxfmt 统一 TypeScript 项目的代码格式

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处
OxfmtTypeScript代码格式化工程化

我以前对代码格式化工具没有太强的感觉。编辑器保存时整理一下,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。格式化工具的任务是终止排版争论,不是趁机重写所有团队习惯。

我也关闭了 sortImportssortPackageJson。排序可能有用,但它已经超出单纯的空白与换行调整。尤其是副作用 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 不碰现有换行符,后来发现它没有 autopreserveendOfLine 只能明确选择 lfcrlfcr

这时如果 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:check

Oxfmt 不会证明代码正确。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

参考资料