Monorepo 的诉求
- 多个互相依赖的包,想在一个 PR 里改完
- 版本升级一次全仓生效
- CI 只跑受影响的包(靠依赖图)
- 共享 config(tsconfig、eslint、prettier)
- 统一发版流程
最小布局
my-mono/
├── pnpm-workspace.yaml ← 声明哪些目录是包
├── package.json ← 根 package.json(私有)
├── pnpm-lock.yaml ← 全仓唯一 lockfile
├── packages/
│ ├── ui/
│ │ ├── package.json ← @myorg/ui
│ │ └── src/
│ ├── utils/
│ │ ├── package.json ← @myorg/utils
│ │ └── src/
│ └── config/
│ ├── package.json ← @myorg/config
│ └── eslint.config.js
└── apps/
├── web/
│ ├── package.json ← @myorg/web(Next.js)
│ └── src/
└── mobile/
├── package.json ← @myorg/mobile(Expo)
└── app/
pnpm-workspace.yaml
packages: - 'packages/*' - 'apps/*' - '!**/test/**' # 排除测试目录
只支持 glob 模式,不支持 JS 配置——简单但够用。
根 package.json
{
"name": "my-mono",
"private": true,
"packageManager": "pnpm@9.12.0",
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint"
},
"devDependencies": {
"typescript": "^5.6.0",
"prettier": "^3.3.3"
}
}
private: true 很重要
根 package.json 是"伞",不应该发布到 npm。
根 package.json 是"伞",不应该发布到 npm。
private: true 告诉 pnpm 和 npm 发不出去(pnpm publish 会拒绝)。子包视需要设 public/private。
子包 package.json
// packages/ui/package.json { "name": "@myorg/ui", "version": "0.0.1", "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { "@myorg/utils": "workspace:*", "react": "^18.3.1" } }
workspace 协议
workspace:* ← 用 workspace 里的任意版本(最灵活) workspace:^ ← 用 workspace 版本 + 发版时替换成 ^x.y.z workspace:~ ← 类似,替换成 ~x.y.z workspace:1.2.3 ← 必须精确版本
workspace: 协议强制告诉 pnpm「这个依赖必须从 workspace 里找」——如果写 "^0.0.1" 也能匹配到 workspace 里的包,但不够严格,容易意外从 npm 下载。
pnpm install 自动 link
pnpm install # 在根目录运行
apps/web/node_modules/
├── react → .pnpm/react@18.3.1/...
└── @myorg/
└── ui → ../../../packages/ui (symlink 指向源码!)
跨 workspace 的引用,pnpm 直接 symlink 到源码目录——改代码实时生效,无需 build。
包间脚本协调
# 所有 workspace 运行 build pnpm -r build # 所有 workspace 并行跑 dev pnpm -r --parallel dev # 只跑某个 workspace pnpm --filter @myorg/web dev # 上面简写 pnpm -F @myorg/web dev
-r(recursive)
-r 对每个 workspace 执行一次命令,按依赖拓扑排序:被依赖的先跑。
pnpm -r build # 1. @myorg/utils build # 2. @myorg/config build # 3. @myorg/ui build(依赖 utils、config,等它们完) # 4. @myorg/web build(依赖 ui,等它完)
加 --parallel 忽略拓扑,全并发,适合 dev 启动多个进程(例如 Next dev + Storybook)。
在根目录给子包加依赖
pnpm add --filter @myorg/web axios # 只在 apps/web 加 axios pnpm add -Dw typescript # -w 表示加到根 workspace(monorepo 共享 devDep)
root-workspace-package vs filter
| 场景 | 加在哪 |
|---|---|
| TS、Prettier、ESLint 配置包 | 根 devDependencies(-Dw) |
| React(被多个 app 用) | 各自的 package.json,用 catalog 统一版本(第 7 章) |
| 某个 app 私有的 | 那个 app 的 package.json(-F app) |
包间拓扑感知安装
# 只装 @myorg/web 和它依赖的所有 workspace pnpm install --filter "@myorg/web..." # 只装 @myorg/ui 和反向依赖它的 workspace pnpm install --filter "...@myorg/ui"
共享 tsconfig
// packages/tsconfig/base.json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "jsx": "react-jsx" } }
// apps/web/tsconfig.json { "extends": "@myorg/tsconfig/base.json", "include": ["src"], "compilerOptions": { "outDir": "dist" } }
Prettier、ESLint 同理——每个 app 的 config 文件 require('@myorg/config/eslint')。
TS 包引用(project references)
// packages/ui/tsconfig.json { "references": [{ "path": "../utils" }], "compilerOptions": { "composite": true } }
TS 能「增量编译」关联的包,不用每次全量 type-check。对大 monorepo 是性能关键。
发布:多包发一次
Changesets 是 pnpm monorepo 标配的版本/发布工具:
pnpm add -Dw @changesets/cli pnpm changeset init pnpm changeset # 交互式记录本次改动 pnpm changeset version # 按记录的 changeset 升版本 pnpm publish -r # 发到 npm(只发 public 的)
pnpm publish 自动展开 workspace 协议
"dep": "workspace:^" 在发版那一刻被替换成真实版本号 "^1.2.3"——对外是标准 npm 包,对内保持 workspace 链接。这是 pnpm 的关键胶水。
Changesets 的工作流
- 写代码,
pnpm changeset描述改动 + 选版本升级类型(patch/minor/major) - PR merge,CI 运行
changeset version更新 package.json 版本 + 生成 CHANGELOG - CI 下一步
pnpm publish -r发包
小心:workspace:^ 不等于 workspace:*
workspace:* 发版时替换为 dep 的当前版本(精确) workspace:^ 替换为 ^x.y.z(允许兼容更新) workspace:~ 替换为 ~x.y.z(只允许 patch)
内部库推荐 workspace:^,对外用户拿到 ^x.y.z 灵活升级。如果是只在公司内部的私包,workspace:* 没问题。
真实案例:Vercel 的 Turborepo 官方模板
turborepo-starter/
├── pnpm-workspace.yaml
├── turbo.json ← 任务缓存配置
├── apps/
│ ├── web/ (Next.js)
│ └── docs/ (Next.js)
└── packages/
├── ui/ (React 组件)
├── eslint-config/
└── typescript-config/
数万个 Vercel 用户在这个模板上跑,验证过 pnpm + Turborepo 的生产表现。下一章继续讲 filter 和依赖图。
本章小结
pnpm-workspace.yaml+ 根 private package.json 开启 workspaces- 包间用
workspace:*/workspace:^协议引用,pnpm symlink 到源码目录 pnpm -r遍历执行,按拓扑排序;--parallel忽略顺序- 根装公共 devDep 用
-Dw,某包装依赖用-F <pkg> - 发版用 Changesets,
pnpm publish -r自动展开 workspace 协议