🚀 省流助手
- 困惑:pnpm + lerna 的 monorepo 里,将要新增"不发布到 npm 的共享包",怎么协调"包变 → app 重新构建发布"?
- 真相 :lerna 的核心独特价值是「多 npm 包发布到 registry 时的版本号联动」,对 app + 私有包混合场景几乎没用
- 解法 :
pnpm workspace管依赖 +turbo管构建调度;lerna可逐步退场;将来某个包要发外部时用changesets而非回头找 lerna
一、困惑现场
我手里有个 monorepo:
arduino
your-monorepo/
├── apps/
│ └── admin-portal/ # 后台管理 app(private: true,不发布到 npm)
└── pnpm-workspace.yaml
技术栈是 pnpm workspace + lerna,目前只有一个 app 子包。
接下来的需求是:
想拆几个共享 npm 包出来放进
packages/(比如shared-ui、shared-utils),但这些包不发布到任何 registry,只在 monorepo 内部被 app 引用。
这就引出几个问题:
- app 怎么引用一个不发布的本地包?
- 包改了,app 该怎么知道要重新构建发布?
- 包是不是要先于 app 构建?谁来保证顺序?
- lerna 在这套里还扮演什么角色?
我之前只搞过纯 npm 包协同(lerna 的经典场景),app + 包混合协调没经验,于是开始挨个假设验证。
二、三个假设的验证过程
假设 1:lerna 不是 monorepo 万能管家吗,让它接管这一切
第一反应自然是 lerna------毕竟它最早就是 monorepo 的代名词。
为什么这么猜 :lerna 有 lerna changed、lerna run、lerna version、lerna publish 一整套命令,听起来什么都能干。
怎么验证:把 lerna 的能力一项项摆出来,看每一项在我的场景里能不能用上:
| lerna 能力 | 我的场景 | 是否用得上 |
|---|---|---|
| 包链接(lerna bootstrap) | pnpm workspace 已经做了 | ❌ 重复 |
| 命令编排(lerna run) | pnpm -r / --filter 也能做 |
❌ 重复 |
| 任务调度+缓存 | 需要,但 lerna 这块很弱 | ⚠️ 不如专用工具 |
| 变更检测(lerna changed) | 需要"包变 → app 重 build" | ⚠️ 它只输出"哪些包变了",不告诉"哪些 app 需要重 build" |
| version + publish | 包不发布到 npm | ❌ 完全用不上 |
| 自动 changelog | 看起来有用 | ⚠️ 但绑定在 publish 流程上 |
结果 :lerna 的"独特价值"------version + publish + changelog 联动------在我的场景里全部失效,因为包不发布到 npm。其他能力都被 pnpm workspace 覆盖了。
第一个假设破产:lerna 不是不能用,是它最值钱的部分用不上。
假设 2:那直接换 turbo,turbo 全包了
社区现在主流推 turborepo 替代 lerna,那干脆换成 turbo?
为什么这么猜:看到很多文章写「turbo 是 lerna 的现代替代」。
怎么验证:拉 turbo 的能力清单对比 lerna:
| 能力 | lerna | turbo |
|---|---|---|
| 包链接 | ❌(早被 workspace 替代) | ❌(不管这个) |
| 命令编排 | ✅ | ✅ |
| 拓扑构建调度 | ⚠️ 弱 | ✅ 强(dependsOn: ["^build"] ) |
| 任务级缓存 | ❌ | ✅ 本地 + 远程 |
| 变更检测 | lerna changed |
--filter='...[ref]'(基于 task graph) |
| version 自动 bump | ✅ | ❌ |
| 生成 CHANGELOG | ✅ | ❌ |
| publish 到 npm | ✅ | ❌ |
turbo 完全不管版本号、changelog、publish------它只管 task 调度和缓存。
结果 :turbo 把 lerna 的"任务编排"那部分大幅度增强了,但把"version + publish + changelog"那部分完全砍了。
第二个假设破产:turbo 不能完整替代 lerna,它只替代 lerna 的一半。
假设 3:那剩下那一半(version + changelog)怎么办?
如果未来某个 lib 要发到外部 npm,version + changelog 这套总是要的。这部分被 turbo 砍掉了,谁来补?
怎么验证:去看 turbo + 谁的搭配是社区主流。
答案是 changesets (@changesets/cli)。
把三家放一起:
| 能力 | lerna | turbo | changesets |
|---|---|---|---|
| 检测变更影响哪些包 | ✅ git diff | ✅ task graph | ✅ 显式声明 |
| 自动 bump 版本号 | ✅ 按 commit 类型猜 | ❌ | ✅ 按 changeset 文件声明 |
| 生成 CHANGELOG | ✅ 按 commit message | ❌ | ✅ 按 changeset 描述 |
| 依赖包级联 bump | ✅ | ❌ | ✅ |
| publish 到 npm | ✅ | ❌ | ✅ |
lerna vs changesets 的本质区别:
- lerna 是"猜" :靠 commit message 前缀(
feat:→ minor、fix:→ patch)自动推断版本号 + 拼 changelog。开发者无感知,但 commit message 写错了版本号就错了。 - changesets 是"显式声明" :开发者写代码时跑
pnpm changeset,交互式选择"这次改动影响哪些包、什么级别、changelog 写什么",生成一个.changeset/*.md文件随 PR 走 review。合并后 CI 跑changeset version真正 bump。
三、真相浮现
把三个假设拼起来看,结论自动浮现:
lerna 在 2017 年是 monorepo 唯一选择,它的能力是"打包合集"。十年过去,每一项能力都被一个更专的工具替代了------而我恰好用不上它没被替代的那一项。
| lerna 原能力 | 现代替代 | 在我的场景适用 |
|---|---|---|
| 包链接 bootstrap | pnpm workspace |
✅ 已用 |
| 命令编排 | pnpm -r / --filter |
✅ 已用 |
| 任务调度 + 缓存 | turborepo | ✅ 即将引入 |
| version + publish + changelog | changesets | ⏸️ 包发布到 npm 时再启用 |
剩下属于 lerna 的"独特领域"是零。
四、根因
为什么 lerna 不适合我的场景?因为它的设计预设是:
所有包都发布到 npm registry,发布频率高,依赖关系复杂。
这是 2017 年 React/Babel/Jest 这些大型 OSS 项目的真实需求------一次提交可能让 20 个 npm 包同时升 patch 版本。
但 app + 私有包混合场景的特征完全不同:
- 包不发布 (或只发到内部 registry,频率极低)→ lerna 最强的
version/publish在此场景下用不上 - 真正的"产出"是部署后的 app ,而不是 npm 包 → lerna 视野里只有 npm publish 这一个终点,部署不在它的世界里
- "包变 → app 重 build & 部署"的链路 → 你需要的是
lerna changed输出 + 手写脚本反向映射到 app 列表,远不如 turbo 的--filter='...[HEAD^1]'一行直给
澄清一下 :lerna 在 2022 年由 nrwl(nx 母公司)接手维护,至今仍在更新。在"纯多 npm 包发布到 registry"的经典场景里,lerna 仍然是合理选项,babel 这种老牌 OSS 也还在用。本文说的"不合适",只针对"app + 私有包混合"这个具体场景------不是说 lerna 在哪里都过时了。
五、解决方案
临时方案(不破坏现状)
保留 lerna 不动,加 turbo 进来管 build/dev 拓扑------两者井水不犯河水:
bash
pnpm add -Dw turbo
根目录加 turbo.json:
json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
新建共享包时,app 用 workspace:* 协议引用:
json
// apps/admin-portal/package.json
{
"dependencies": {
"@your-org/shared-ui": "workspace:*"
}
}
构建走 turbo(自动按拓扑先 build 所有依赖的 lib):
bash
turbo run build --filter=admin-portal
中期方案(lerna 退场)
观察一两个月,如果 lerna 真的没人调用,删掉 lerna.json 和 lerna 依赖即可。
未来方案(包要发布外部时)
引入 changesets,不要回头用 lerna:
bash
pnpm add -Dw @changesets/cli
pnpm changeset init
开发流程变成:
bash
# 改完代码后
pnpm changeset
# 选受影响的包 + bump 类型 + 写 changelog 描述
# 生成 .changeset/xxx.md,随 PR review
# CI 在主分支:
pnpm changeset version # 真正 bump 版本号 + 拼 CHANGELOG.md
pnpm changeset publish # 发到 npm
CI 联动:"包变 → app 重新部署"的一行命令
整个方案里最实用的一行:
bash
# 检测受 git 变更影响的所有 app
turbo run build --filter='...[HEAD^1]' --filter='./apps/*'
含义拆解:
...[HEAD^1]:从上一个 commit 到 HEAD 之间,所有变更的包 + 所有依赖它们的包./apps/*:再过滤出apps/目录下的包(即 app)- 两个
--filter是交集
CI 拿到这个交集后,对每个 app 跑部署流程即可------这就是"包变 → 反向追溯 → 触发 app 部署"的完整闭环。
六、预防建议
工具选型不是站队,是对齐场景。问三个问题就能避免"用错地方":
- 它的"独特价值"在你的场景里用得上吗? lerna 的独特价值是 npm publish 联动,纯多包发布场景仍合适;私有包场景这部分价值就用不上
- 它的其他能力是否在你的工具栈里已经被覆盖? 包链接 → 已被 pnpm workspace 覆盖、任务调度 → turbo 更强,重复装载没意义
- 它的设计预设和你的场景匹配吗? lerna 预设"所有包都发 npm";如果你的产出是部署的 app 而非 npm 包,预设不匹配
按这三个问题筛一遍,结论自然浮现------lerna 在 monorepo 里的位置已经从「事实标准」收敛到「特定场景下的版本管理工具」。不是它变差了,是 monorepo 的形态比 2017 年丰富了,需要更细的工具分工。
七、知识点提炼
1. monorepo 工具的"四象限"分工
现代 monorepo 不再有"全能管家",而是四件工具各管一摊:
| 关注点 | 工具 |
|---|---|
| 依赖链接 + 安装 | pnpm workspace / yarn workspace / npm workspace |
| 任务调度 + 缓存 | turborepo / nx / moon |
| 版本管理 + changelog | changesets / lerna(仅 version 部分)/ release-please |
| 部署 pipeline | 你的 CI(GitHub Actions / GitLab CI 等) |
挑工具时按象限选,不要迷信"all-in-one"。
2. workspace:* 协议是关键拼图
workspace:* 协议是 pnpm/yarn/bun 都支持的依赖标识,意思是"从本地 workspace 找这个包,而不是去 registry"。
json
"@your-org/shared-ui": "workspace:*"
publish 时这个协议会被自动替换成具体版本号------所以同一份 package.json 既能本地开发也能发布到 npm,是 monorepo 内外协同的关键拼图。
3. "包变 → app 重 build"的本质是 task graph 反向追溯
这件事的核心是:
css
包 A 变更 ─→ 包 B(依赖 A)也算变更 ─→ app C(依赖 B)也算变更 ─→ app C 需要重 build
turbo 的 --filter='...[ref]' 就是在做这个反向追溯------它读 task graph,从变更包出发往下游遍历,找出所有受影响的 task。
lerna 的 lerna changed 只能告诉你"哪些包变了",下游传播得自己写脚本。这就是工具代差。
写在最后:lerna 不是坏工具,它定义了 monorepo 这个范畴,今天在合适的场景里依然是合理选择。本文不是劝你卸载 lerna,而是想说------工具选型要对齐场景而不是跟风。我的场景里 lerna 不合适,所以我换了;如果你的场景是纯多 npm 包发布,lerna 仍然能干得很好,没必要为了"现代"而切换。