【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog
【Agent】【OpenCode】项目配置(Monorepo)
分析了 Monorepo 面临的挑战与局限:构建与部署压力(庞大的仓库体积导致 Git 操作变慢),协作边界模糊(容易产生代码冲突,且难以实现精细化的权限控制)以及技术栈锁定,所以 Monorepo 并非万能架构,其最适合中小型团队开发的多模块应用,对 UI 和交互有强一致性要求的中后台产品,以及迭代周期短,频繁共享代码的项目,接着强调了在 Monorepo 架构下,所有代码都放在同一个 Git 仓库里,在物理层面共用一个 Git 仓库,而在逻辑层面则包含多个独立应用,它们可以拥有各自独立的技术栈,可以独立打包,独立测试,独立部署上线,下面继续分析
OpenCode
再回到 package.json,其中的绝大多数字段都是 npm/Node.js 官方定义的标准规范,下面来看其中的 version 字段
这里的 version 指的是子包版本,而非整个项目版本,在 Monorepo 中,每一个子包都有自己独立的 package.json,其中
- 根目录的
package.json:其version通常代表整个 Monorepo 脚手架或开发环境的版本,业务代码一般不依赖它,不过 OpenCode 这里并没有在这里定义项目的版本号

- 子包的
package.json:其version严格代表该子包自身的语义化版本,比如这里定义的1.2.27

在 Monorepo 中,虽然版本号是独立的,但现代 Monorepo 工具提供了统一版本模式,可以配置让所有子包共享同一个版本号,或者在发布时自动根据 Git 提交记录批量更新相关子包的版本,不过这些都是工具层面的自动化,verision 字段本身的含义依然是当前这个包的版本
在终端输入
bash
find . -name "package.json" -not -path "*/node_modules/*" -not -path "*/dist/*" -exec grep -H '"version"' {} \;
可以找到所有子包 package.json 中的版本号

可以看到所有的子包都是统一的 1.2.27,这种统一的版本号,并不是 Monorepo 的强制规定,而是一种主动选择的版本管理策略,在 Monorepo 中,子包的版本管理通常有两种模式
- 锁定版本模式(Fixed/Locked Mode) :这就是 OpenCode 里的现象,所有子包的
version都是1.2.27,永远保持一致,这是因为很多团队的内部包是强耦合的,比如@my-org/ui-button和@my-org/ui-input都属于同一套设计系统,它们必须同时发布,同时升级,如果版本不一致,使用者就会感到困惑,比如装了2.0的button,能不能匹配1.5的input?
而虽然 Git 仓库里所有子包的 package.json 写的都是 1.2.27,但在执行 pnpm publish 时,发布工具会动态计算,如果某个子包在本次发布中没有改动,就不会被发到 npm 上,如果有改动要发布,工具会在内存中把版本号 bump 递增到 1.2.28 再发布 ,而不会修改 Git 仓库里的源码文件(版本号),所以在代码库里看到的永远是整齐统一的版本号,而 npm 上的实际版本可能是按需递增的
- 独立版本模式(Independent Mode) :并非所有 Monorepo 都锁版本,像 Babel,Next.js 等这种大型的开源项目,子包版本是完全独立的,比如
@my-org/utils可能一年才更新一次(v1.2.0),而@my-org/web-app每天都在迭代(v3.45.0),强行锁版本会导致utils产生大量无意义的版本跳跃,所以每个子包的package.json里的version各不相同,发布时也各自独立递增
这里得理解 Monorepo 版本管理中关键的一点:Git 里的版本 ≠ npm 上的版本 ,开发者在 Git 仓库里看到的 package.json 中的 version,往往只是一个基准版本,或占位符,而不是最终发布到 npm 的真实版本,现代 Monorepo 发布工具的流程如下
- 开发者日常提交代码,不会修改
version字段 - 每次有变更时,开发者创建一个 changeset 文件,描述改了什么,当前的
version是什么 - 执行发布命令时,工具读取所有的 changesets,自动计算出要发布的包应该 bump 提升到什么版本
- 工具在临时目录中修改
package.json的版本号,打包,发布到 npm,并打 Tag,而 Git 主分支上的package.json版本号则可以始终保持不变,或者只在发布后由脚本统一回写

所以最后总结一下
- 所有子包版本号一样,是团队选择的锁定版本策略,不是 Monorepo 的强制要求
package.json中的version永远是子包自己的版本,只是当前多个子包被统一设定为相同数值- npm 版本号由 Changesets / Nx Release 等工具根据 Git 提交记录自动计算
- npm 版本 ≠ Git 里的版本,Git 里往往是基准版本,而 npm 是工具动态计算后的结果
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog