如何在 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 中非常关键。典型的忽略规则:
shellnode_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 规则:
bashpnpm add -wD eslint-config-prettier
javascript// .eslintrc.js module.exports = { extends: [ "eslint:recommended", // 其他配置... "prettier" // 必须放最后,覆盖前面的格式化规则 ] };
第二步(可选):如果想在 ESLint 中实时报告格式问题,安装 eslint-plugin-prettier:
bashpnpm add -wD eslint-plugin-prettier
javascriptmodule.exports = { plugins: ["prettier"], rules: { "prettier/prettier": "error" } };
不过在 monorepo 中,更推荐的做法是分离职责:ESLint 只管代码质量,Prettier 只管格式,不要把 Prettier 嵌入 ESLint。这样运行更快,调试也更清晰。
Husky + lint-staged 自动格式化
提交时自动格式化是 monorepo 的标配实践:
bashpnpm 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 的优势在于受影响项目检测——只格式化当前提交影响到的子包:
bashnx format:check --projects=tag:scope:ui nx format:write --projects=tag:scope:ui
这在大型 monorepo 中比 prettier --write . 高效很多。
Lerna 集成
Lerna 的 --scope 选项可以针对特定子包执行格式化:
bashlerna 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 暂存区中的变更文件:
bashgit diff --name-only --diff-filter=ACM HEAD | grep -E '\.(js|ts|tsx)$' | xargs prettier --write
并行处理——多核同时跑,适合项目文件数过万的场景:
bashfind . -name "*.ts" -not -path "*/node_modules/*" | parallel -j 4 prettier --write
缓存机制——Prettier 3.0 原生支持缓存:
bashprettier --write --cache --cache-strategy content "src/**/*.ts"
--cache-strategy content 基于文件内容哈希判断是否需要重新格式化,比默认的 metadata 策略更准确。首次运行生成缓存,后续未修改的文件直接跳过。
真实场景:在一个 200+ 子包的 monorepo 中,全量 prettier --check . 需要 45 秒。加上缓存后,二次运行降至 3 秒以内。配合 lint-staged 只处理暂存文件,提交时的格式化检查几乎无感。
CI/CD 集成
在 GitHub Actions 中配置格式化检查:
yamlname: 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 配置则允许在配置文件中动态导入其他模块,比如根据环境变量切换规则。