Monorepo 里有 app 也有共享包,lerna 真的还需要吗?

🚀 省流助手

  • 困惑: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-uishared-utils),但这些包不发布到任何 registry,只在 monorepo 内部被 app 引用。

这就引出几个问题:

  1. app 怎么引用一个不发布的本地包?
  2. 包改了,app 该怎么知道要重新构建发布?
  3. 包是不是要先于 app 构建?谁来保证顺序?
  4. lerna 在这套里还扮演什么角色?

我之前只搞过纯 npm 包协同(lerna 的经典场景),app + 包混合协调没经验,于是开始挨个假设验证。


二、三个假设的验证过程

假设 1:lerna 不是 monorepo 万能管家吗,让它接管这一切

第一反应自然是 lerna------毕竟它最早就是 monorepo 的代名词。

为什么这么猜 :lerna 有 lerna changedlerna runlerna versionlerna 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 + 私有包混合场景的特征完全不同:

  1. 包不发布 (或只发到内部 registry,频率极低)→ lerna 最强的 version/publish 在此场景下用不上
  2. 真正的"产出"是部署后的 app ,而不是 npm 包 → lerna 视野里只有 npm publish 这一个终点,部署不在它的世界里
  3. "包变 → 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.jsonlerna 依赖即可。

未来方案(包要发布外部时)

引入 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 部署"的完整闭环。


六、预防建议

工具选型不是站队,是对齐场景。问三个问题就能避免"用错地方":

  1. 它的"独特价值"在你的场景里用得上吗? lerna 的独特价值是 npm publish 联动,纯多包发布场景仍合适;私有包场景这部分价值就用不上
  2. 它的其他能力是否在你的工具栈里已经被覆盖? 包链接 → 已被 pnpm workspace 覆盖、任务调度 → turbo 更强,重复装载没意义
  3. 它的设计预设和你的场景匹配吗? 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 仍然能干得很好,没必要为了"现代"而切换。

相关推荐
神所夸赞的夏天2 小时前
如何获取多层json数据,存成dictionary,并取最大最小值
java·前端·json
红色的小鳄鱼2 小时前
前端面试js手写
开发语言·前端·javascript
焦糖玛奇朵婷2 小时前
健身房预约小程序开发、设计
java·大数据·服务器·前端·小程序
上海云盾王帅2 小时前
WEB业务如何接入安全防护:从零到一的实战指南
前端·安全
用户059540174462 小时前
AI Agent记忆丢失踩坑实录:这个问题让我排查了3天
前端·css
web行路人2 小时前
前端对Commands(斜杠命令)一些常用
前端·javascript·vue.js·vue
当时只道寻常2 小时前
从零到一打造企业级全栈后台管理系统 —— 技术选型、工程化实践与深度思考
前端·全栈·前端工程化
竹林8182 小时前
用 ethers.js 连 MetaMask 做钱包登录,我踩了三个坑才搞定跨页面状态同步
前端·javascript
饺子不吃醋2 小时前
深入理解 Vue 3 的 setup(含 Composition API)
前端·vue.js
阿星做前端2 小时前
重度 AI 编程用户的一天:我怎么把 Claude Code / Codex 工作流搬进浏览器工作台
前端·javascript·后端