3 条命令搞定闭环 Monorepo:Lerna 版本管理 + 拓扑构建 + 自定义分发

🚀 省流助手

  • 场景 :闭环 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 生命周期钩子里挂自定义上传

思路是把上传逻辑写到 prepublishOnlypostpublish 里,跑 lerna publish 时顺带触发。问题有两层:

  1. prepublishOnly / postpublish npm publish 这个动作的钩子,不真发包就不会触发
  2. 即使能触发,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,默认按拓扑顺序

三个关键事实:

  1. "升版本"和"发包"是两个命令 ------lerna version 完整覆盖前者,跟 npm registry 一点关系都没有
  2. "拓扑顺序"是 lerna run 的能力,不是 lerna publish 独占的------闭环场景根本不需要借 publish 的壳
  3. 任何能写成 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 runnx 至少两条路
  • 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 时代的可选替代品

如果只是要"拓扑构建 + 缓存",可以直接上 nxturborepo------两者都比 lerna run 更快(带任务级缓存 + 远程缓存)。Lerna 自 2022 年起由 Nrwl/Nx 团队接管维护,Lerna 8 本身现在也是 Nx 生态的一部分,新项目可以直接用 Nx。

但如果团队已经在用 Lerna,且只是想做"版本 + Changelog + 拓扑构建 + 自定义上传"这件具体事------3 条命令就够了,不必换栈

八、一句话总结

闭环 Monorepo 不需要"hack" Lerna 来跳过 npm publish------Lerna 3 之后它本来就把这件事拆成了 lerna versionlerna run,按需取用即可。publish 命令是发包的,不发包根本用不上它。

相关推荐
IT_陈寒1 小时前
Vue的这个响应式陷阱让我熬到凌晨三点
前端·人工智能·后端
爱勇宝10 小时前
大多数人不是在使用 AI 赚钱,而是在帮 AI 公司赚钱
前端·后端·程序员
冬奇Lab11 小时前
每日一个开源项目(第143篇):page-agent - 纯 JS 的网页 GUI Agent,无需截图、无需插件、无需后端
前端·人工智能·agent
IT_陈寒15 小时前
React的这个渲染问题连官方文档都没说清楚
前端·人工智能·后端
追逐时光者16 小时前
别再满网找零散工具了,腾讯 QQ 浏览器这个“帮小忙”工具箱真能省时间
前端·后端
Asmewill18 小时前
grep&curl命令学习笔记
前端
stringwu18 小时前
Flutter 开发必备:MVI 架构的高效实现指南
前端·flutter
用户21366100357219 小时前
Vue2组件化开发与父子通信
前端·vue.js
Momo__20 小时前
TypeScript satisfies 操作符——比 as 更安全的类型守门员
前端·typescript