5月28日 07:25

如何在 Monorepo 项目中配置和使用 Prettier?

核心答案

在 Monorepo 中配置 Prettier,关键是统一配置 + 分包覆盖 + 工具链集成三步走:根目录放一份基础 .prettierrc 作为全局基准,通过共享配置包 @org/prettier-config 让各子项目继承,再用 overrides 按包定制差异规则,最后配合 Husky + lint-staged 在提交时自动格式化、Turborepo/Nx 在 CI 层做缓存检查。

根目录统一配置

最简单的方式是在 monorepo 根目录创建 .prettierrc

json
{ "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80 }

Prettier 会从文件所在目录向上查找配置,子包如果自己没有 .prettierrc,就自动继承根目录的规则。这意味着只要根目录配置到位,大部分子包无需额外配置。

需要注意的是,如果子包自己也有 .prettierrc,它会完全覆盖根配置而不是合并。所以除非有必要,不要在子包里单独放配置文件。

共享配置包

当团队规模较大或 monorepo 包含多个独立发布的库时,推荐把 Prettier 配置抽成 npm 包:

javascript
// packages/prettier-config/index.js module.exports = { semi: true, singleQuote: true, tabWidth: 2, trailingComma: "es5", printWidth: 80, bracketSpacing: true, arrowParens: "always", };
json
// packages/prettier-config/package.json { "name": "@my-org/prettier-config", "version": "1.0.0", "main": "index.js" }

在各子包中引用:

json
{ "prettier": "@my-org/prettier-config" }

共享配置包的优势在于版本可控——改一处发布新版本,所有依赖它的子包 npm update 即可同步。对于使用 pnpm workspace 的项目,直接用 workspace:* 协议引用,无需发布到外部 registry。

分包差异化配置(overrides)

有些子包需要不同的格式化规则,比如 UI 库希望更宽的 printWidth,而后端服务保持 80 列。用 overrides 字段实现:

json
{ "semi": true, "singleQuote": true, "printWidth": 80, "overrides": [ { "files": "packages/ui/**/*", "options": { "printWidth": 100 } }, { "files": "packages/server/**/*", "options": { "printWidth": 80 } }, { "files": "packages/docs/**/*.md", "options": { "proseWrap": "always", "printWidth": 90 } } ] }

overrides 是在根配置基础上增量覆盖,不会丢失未显式指定的规则。这比在每个子包单独放 .prettierrc 更容易维护。

.prettierignore 配置

很多教程忽略了 .prettierignore,但它在 monorepo 中非常关键。典型的忽略规则:

shell
node_modules dist build coverage .next .turbo *.min.js *.min.css pnpm-lock.yaml package-lock.json

不配 .prettierignore 会导致 prettier --write 扫描 node_modules 和构建产物,既浪费时间又可能报错。尤其在 monorepo 中,子包的 dist 目录层级深,手动排除不现实,需要用通配符一次搞定。

与 ESLint 的冲突解决

Prettier 和 ESLint 同时存在时,格式化规则会冲突。比如 ESLint 要求尾逗号,Prettier 又删掉尾逗号,来回打架。解决方案分两步:

第一步:安装 eslint-config-prettier,它关闭所有与 Prettier 冲突的 ESLint 规则:

bash
pnpm add -wD eslint-config-prettier
javascript
// .eslintrc.js module.exports = { extends: [ "eslint:recommended", // 其他配置... "prettier" // 必须放最后,覆盖前面的格式化规则 ] };

第二步(可选):如果想在 ESLint 中实时报告格式问题,安装 eslint-plugin-prettier

bash
pnpm add -wD eslint-plugin-prettier
javascript
module.exports = { plugins: ["prettier"], rules: { "prettier/prettier": "error" } };

不过在 monorepo 中,更推荐的做法是分离职责:ESLint 只管代码质量,Prettier 只管格式,不要把 Prettier 嵌入 ESLint。这样运行更快,调试也更清晰。

Husky + lint-staged 自动格式化

提交时自动格式化是 monorepo 的标配实践:

bash
pnpm add -wD husky lint-staged pnpm exec husky init
json
// package.json { "lint-staged": { "*.{js,jsx,ts,tsx}": ["prettier --write"], "*.{json,css,md}": ["prettier --write"] } }
bash
# .husky/pre-commit pnpm exec lint-staged

这样每次 git commit 只会格式化暂存区的文件,而不是整个项目。对于 monorepo 来说,增量处理比全量扫描快得多。

如果使用 pnpm workspace,可以把 lint-staged 配置放在根目录,它会自动根据修改文件的路径匹配对应规则。

Turborepo 集成

Turborepo 的缓存机制能避免重复格式化检查:

json
// turbo.json { "pipeline": { "format": { "outputs": [] }, "format:check": { "outputs": [] } } }
json
// 根 package.json { "scripts": { "format": "prettier --write .", "format:check": "prettier --check ." } }

outputs 设为空数组是因为格式化不产生构建产物,Turborepo 只需要根据输入文件的变化判断是否需要重新执行。

实际项目中,format:check 通常放在 CI 里,而 format 在本地开发时使用。Turborepo 会缓存未变更文件的结果,二次运行几乎零耗时。

Nx 集成

Nx 对 Prettier 有专门的 executor 支持:

json
{ "targets": { "format": { "executor": "@nx/vite:format", "options": { "write": true } } } }

Nx 的优势在于受影响项目检测——只格式化当前提交影响到的子包:

bash
nx format:check --projects=tag:scope:ui nx format:write --projects=tag:scope:ui

这在大型 monorepo 中比 prettier --write . 高效很多。

Lerna 集成

Lerna 的 --scope 选项可以针对特定子包执行格式化:

bash
lerna exec --scope @my-org/ui -- prettier --write "src/**/*.js" lerna exec --scope @my-org/core -- prettier --check "src/**/*.{ts,tsx}"

Lerna 7 之后去除了内置的 lerna run 对 Prettier 的特殊处理,推荐直接在子包的 package.json 里加 format 脚本,然后用 lerna run format 批量执行。

性能优化

增量格式化——只处理 Git 暂存区中的变更文件:

bash
git diff --name-only --diff-filter=ACM HEAD | grep -E '\.(js|ts|tsx)$' | xargs prettier --write

并行处理——多核同时跑,适合项目文件数过万的场景:

bash
find . -name "*.ts" -not -path "*/node_modules/*" | parallel -j 4 prettier --write

缓存机制——Prettier 3.0 原生支持缓存:

bash
prettier --write --cache --cache-strategy content "src/**/*.ts"

--cache-strategy content 基于文件内容哈希判断是否需要重新格式化,比默认的 metadata 策略更准确。首次运行生成缓存,后续未修改的文件直接跳过。

真实场景:在一个 200+ 子包的 monorepo 中,全量 prettier --check . 需要 45 秒。加上缓存后,二次运行降至 3 秒以内。配合 lint-staged 只处理暂存文件,提交时的格式化检查几乎无感。

CI/CD 集成

在 GitHub Actions 中配置格式化检查:

yaml
name: Format Check on: [push, pull_request] jobs: format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm run format:check

关键点:CI 中必须用 --check(只检查不修改),而不是 --write。如果格式不合格,CI 直接报错,开发者本地 format 后重新提交。

如果用 Turborepo,可以配合缓存进一步加速:

yaml
- uses: actions/cache@v4 with: path: .turbo key: turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} - run: npx turbo format:check

常见问题排查

问题1:子包格式化规则不生效 检查子包目录下是否有自己的 .prettierrc。如果存在,它会完全覆盖根配置。删除子包的 .prettierrc,改用根目录的 overrides 来定制规则。

问题2:Prettier 和 ESLint 反复修改同一行 确认 eslint-config-prettier 放在了 extends 数组的最后一位。如果放在前面,后续配置会重新开启被关闭的规则。

问题3:CI 中格式检查通过但本地不通过(或反过来) 通常是 Prettier 版本不一致导致的。在 monorepo 根目录统一安装 Prettier,子包不要单独安装。用 pnpm ls prettier 检查是否有多个版本。

问题4:格式化速度过慢 按优先级排查:1) 检查 .prettierignore 是否正确排除了 node_modules 和构建产物;2) 启用 --cache;3) 用 lint-staged 只处理变更文件;4) 考虑并行处理。

追问

为什么推荐共享配置包而不是根目录 .prettierrc? 根目录配置对纯内部 monorepo 足够。但如果某些子包会独立发布到 npm,它们脱离 monorepo 上下文后就失去了根配置。共享配置包作为 npm 依赖,无论在不在 monorepo 中都能生效。

Prettier 3.0 有哪些影响 monorepo 的变化? 最大的变化是原生缓存支持(--cache)和 ESM 配置文件支持(prettier.config.mjs)。缓存对大型 monorepo 的性能提升显著。ESM 配置则允许在配置文件中动态导入其他模块,比如根据环境变量切换规则。

标签:Prettier