🚀 省流助手
- 场景 :闭环 Monorepo------子包用
workspace:*互引,永远不发 npm,但要版本号、CHANGELOG、按依赖拓扑顺序构建,最后还要上传到内网服务器。 - 常见误区 :网上一搜全是
lerna publish --skip-npm或挂prepublishOnly/postpublish钩子------前者参数早被删了,后者钩子根本不会被触发。 - 正确解 :3 条命令独立成链,整条流程跟
lerna publish没关系:
bash
lerna version --conventional-commits # 升版本 + Changelog + tag + push
lerna run build # 拓扑顺序构建
lerna run upload --concurrency 1 # 拓扑顺序自定义上传
一、需求场景:闭环 Monorepo 的四项刚需
国内挺多公司有一类很典型的 Monorepo 形态:
- 子包用
workspace:*互相引用,不对外发布到 npm 公网 - 业务上需要:
- 版本号自动升级(patch / minor / major)
- CHANGELOG 自动生成 + Git Tag
- 子包按依赖拓扑顺序自动构建
- 构建产物上传到内网服务器(自定义 SSH/FTP/对象存储等)
- 唯一明确"不要"的是:发到 npm registry
总结成四项刚需:
| 能力 | 是否需要 |
|---|---|
| 版本号自动管理 | ✅ |
| CHANGELOG 生成 + Git Tag | ✅ |
| 按依赖拓扑顺序构建 / 处理 | ✅ |
| 自定义上传 / 内网分发 | ✅ |
| 发到 npm registry | ❌ |
这种"既要 Lerna 的版本编排和拓扑调度能力,又不想真发包"的诉求,正好踩在一个文档稀薄、网上答案多半过期的盲区。
二、那些看着可行其实绕弯路的方案
整理一下网上能搜到的"答案",三条主流路径都是坑:
方案 A:lerna publish --skip-npm
bash
$ npx lerna publish --skip-npm
lerna ERR! Unknown argument: skip-npm
直接报错。这个参数是 Lerna 2.x 的旧 flag,Lerna 3.0(2018-08)就移除了------但因为 2015--2018 年中文博客铺天盖地都是这个写法,至今很多 AI 和老文章还在推荐它。
方案 B:在 npm 生命周期钩子里挂自定义上传
思路是把上传逻辑写到 prepublishOnly 或 postpublish 里,跑 lerna publish 时顺带触发。问题有两层:
prepublishOnly/postpublish是npm publish这个动作的钩子,不真发包就不会触发- 即使能触发,
postpublish失败时 npm 那边其实已经发出去了,状态半截在天上半截在地上,回滚很难
方案 C:只用 lerna version,不用 lerna run
只跑版本管理就停下来,构建和上传交给 pnpm -r 或自己写脚本。这条路其实能通,但损失了一个关键能力------按依赖拓扑顺序处理 。pnpm -r 默认是并发的,写成串行也不会按依赖图排序,自定义脚本要手写拓扑算法。
三条路都不够干净。问题出在哪?
三、关键证据:Lerna 3 早把"版本"和"发布"拆开了
看 Lerna 3 的命令矩阵:
| 命令 | 职责 |
|---|---|
lerna version |
bump 版本号 + 生成 CHANGELOG + git commit + git tag + git push(不发 npm) |
lerna publish |
包含 lerna version 所有动作 + 真正发到 npm registry |
lerna run <script> |
在所有子包里执行 npm script,默认按拓扑顺序 |
三个关键事实:
- "升版本"和"发包"是两个命令 ------
lerna version完整覆盖前者,跟 npm registry 一点关系都没有 - "拓扑顺序"是
lerna run的能力,不是lerna publish独占的------闭环场景根本不需要借 publish 的壳 - 任何能写成 npm script 的步骤都能交给
lerna run调度------build / test / upload / lint,统一一套语义
这意味着------四项刚需可以全部用"非 publish"的命令拼出来。
四、根因:版本管理 ≠ 发布动作
Lerna 3 的设计哲学其实很直白:
版本管理是仓库层面的事(git tag、changelog、版本号);发布是分发渠道层面的事(npm registry、私有 registry、内网服务器)。两者本就该解耦,用两个命令分别表达。
老版本 Lerna 把这两件事捆在一起做,所以才需要 --skip-npm 这种"假装跳过一半"的 flag。Lerna 3 拆开之后:
- 想做版本管理 → 用
lerna version - 想做 npm 发布 → 用
lerna publish - 想做自定义分发 → 用
lerna run <自定义 script>
闭环 Monorepo 的诉求本来就是"前两件事要做,第二件不要"------刚好对应"用前者 + 后者,不用中间那个"。
五、解决方案:3 步独立链路
5.1 核心三步
jsonc
// 根 package.json
{
"scripts": {
"release": "lerna version --conventional-commits && lerna run build && lerna run upload --concurrency 1",
"release:no-push": "lerna version --conventional-commits --no-push && lerna run build"
}
}
每一步做的事:
| 步骤 | 命令 | 干啥 |
|---|---|---|
| 1 | lerna version --conventional-commits |
按 conventional commit 决定 bump 类型 + 生成 CHANGELOG + 打 tag + push |
| 2 | lerna run build |
按子包依赖拓扑顺序执行 build(lerna run 默认拓扑) |
| 3 | lerna run upload --concurrency 1 |
按拓扑顺序串行执行各子包的 upload script |
5.2 自定义上传:作为 build 的兄弟步骤
每个子包加一个 upload script:
jsonc
// packages/foo/package.json
{
"scripts": {
"build": "...",
"upload": "node ../../scripts/upload.mjs" // 或直接 scp / rsync / aws s3 cp
}
}
scripts/upload.mjs 里读 process.cwd() 拿到当前子包路径,做你想做的上传逻辑------SSH 推到内网服务器、上传到 OSS / S3、推到私有 CDN,随你。
这种写法的好处是:
| 诉求 | 实现 |
|---|---|
| 拓扑顺序 | lerna run 默认就是 |
| 串行避免上传打架 | --concurrency 1 |
| 每个包独立的发布逻辑 | 每个包写自己的 upload script |
| 失败不污染版本/tag | lerna version 跑完才到 lerna run upload,时序天然隔离 |
| 失败可重跑 | lerna run upload --since 只跑改动过的包 |
整条链路完全不碰
lerna publish、不依赖任何 npm 生命周期钩子------状态机干净,每一步要么成要么败,没有中间态。
5.3 版本下限
上面这套组合的核心命令从 Lerna 3.0.0(2018-08) 起就完整可用,到 Lerna 8 一路向前兼容:
| 用到的能力 | 起始版本 |
|---|---|
lerna version 独立命令 |
3.0.0 |
lerna run 默认拓扑顺序 |
3.0.0 |
--since 增量执行 |
3.0.0 |
--conventional-commits |
2.x |
新建项目直接装最新的 Lerna 8 即可。
六、预防建议:怎么避开过期方案
闭环 Monorepo 的资料过期严重,搜出来多半是 2015--2018 年的内容。两个习惯能省掉一堆弯路:
- 看到具体参数先
--help验一遍 :30 秒能识破"--skip-npm""--no-publish"这种已删除的 flag - 涉及 Lerna / Webpack / Babel / TypeScript 这种版本断层大的工具,先确认讨论的是哪个大版本:Lerna 2 → 3 / Webpack 4 → 5 这种迁移点,参数和命令都可能完全不同
- 任何"唯一标准解""官方原生参数"的措辞都先打个问号 :工具链很少有真正的唯一解,Lerna 闭环这件事就有
lerna run和nx至少两条路 - AI 给出的方案问一句"哪个版本起支持的":能把它从"自信编造模式"拉回"按 changelog 说话"
七、知识点提炼
Lerna 3+ 的核心命令对照表(背下这张表能省掉一堆陈年博客带来的误解):
| 想做什么 | 命令 |
|---|---|
| 只升版本 + Changelog + Git Tag,不发 npm | lerna version |
| 升版本 + 发 npm | lerna publish |
| 拓扑顺序运行某个 script | lerna run <script>(默认拓扑) |
| 拓扑顺序串行执行(不并发) | lerna run <script> --concurrency 1 |
| 只在改动过的包里跑 | lerna run <script> --since |
| 只对依赖某个包的下游执行 | lerna run <script> --include-dependents |
Lerna 8 时代的可选替代品:
如果只是要"拓扑构建 + 缓存",可以直接上 nx 或 turborepo------两者都比 lerna run 更快(带任务级缓存 + 远程缓存)。Lerna 自 2022 年起由 Nrwl/Nx 团队接管维护,Lerna 8 本身现在也是 Nx 生态的一部分,新项目可以直接用 Nx。
但如果团队已经在用 Lerna,且只是想做"版本 + Changelog + 拓扑构建 + 自定义上传"这件具体事------3 条命令就够了,不必换栈。
八、一句话总结
闭环 Monorepo 不需要"hack" Lerna 来跳过 npm publish------Lerna 3 之后它本来就把这件事拆成了
lerna version和lerna run,按需取用即可。publish命令是发包的,不发包根本用不上它。