主流的 monorepo 做法是只有一个 git 仓库,然后建立一个 packages 目录,每个子目录都是一个 npm package,有自己的 package.json。
如果我们不用这个主流做法,而是每个 npm package 都有一个自己的 git 仓库,该如何维护 package.json 文件?
问题
假设我们有 pkg-a,pkg-b,pkg-c 三个包,彼此有依赖关系。
列举可能遇到的问题:
- 本地开发的时候如何让 pkg-a,pkg-b,pkg-c 依赖的都是本地正在开发的版本?
- 本地开发能够选择 pkg-a,pkg-b,pkg-c 的指定版本进行集成开发测试?
- 在 CI 服务器上跑测试的时候,pkg-a 如何找到 pkg-b,pkg-c,并使用正确的版本?
- 如何频繁手工维护 package.json 升版本号?一旦忘记升版本,就导致一个包安装出两个版本来?
分析
拆分成多个 git 仓库之后,所有问题的实质都是单个 git 仓库的代码都是不完整的,必须组合了其依赖之后,才能跑单元测试
每个包,都有两个形态
- git 仓库形态:代码从 git 服务器 clone 下来
- npm 包形态:代码从 npm registry 下载下来
yarn 具有 workspaces 的功能。可以写一个 project 级别的大 package.json,把一堆小的 workspaces 给整合起来,选择每个包是从 git 拉,还是从 npm registry 来拉。
下面我们来看具体的解法。
解法
pkg-a 的 package.json 这么写
{
"name": "pkg-a",
"version": "0.1.0",
"peerDependencies": {
"pkg-b": "*",
"pkg-c": "*"
}
}
这么写的道理是:
- version 填 0.1.0 是你当前开发所基于的上一个版本的版本号。发布的时候产生新的 git commit,修改这个 version 字段,并打 git tag
- peerDependencies 指定对 pkg-b,pkg-c 的依赖,表示 pkg-a 本身并不明确依赖一个固定的版本号,或者版本范围,这样就不需要频繁维护 package.json 文件来升级 dependencies 字段指定的版本号
然后建立这样的本地开发目录结构
project ->
package.json
packages ->
pkg-a (git submodule)
pkg-b (git submodule)
pkg-c (git submodule)
在 project/package.json 这么写
{
"private": true,
"name": "project",
"workspaces": ["packages/*"]
}
workspaces 是 yarn 的功能。当在 project 目录下执行 yarn install 的时候。会让 pkg-a,pkg-b,pkg-c 互相都依赖本地 workspaces 里指定的文件夹。
packages 目录下的 pkg-a 和 pkg-b 这些都是 git submodule,需要独立 git commit 和 git push。
如果不希望使用本地的 pkg-c,而是指定一个 npm registry 上的版本,可以不建立 packages/pkg-c 这个 submodule,然后修改一下 package.json
{
"private": true,
"name": "project",
"workspaces": ["packages/*"],
"dependencies": {
"pkg-c": "0.1.0"
}
}
这样我们就可以避免 submodule 越来越多的问题。同时也避免了单 git 仓库的 monorepo 越来越大的问题。可以自由选择把多少 pkg 用 git submodule 拉到本地来协同修改。
那么在 CI 服务器上,pkg-a 如何跑自己的单元测试呢?只有 peerDependencies 无法 yarn install 自己的依赖啊。
可以在 pkg-a 的目录下,新建一个 pkg-a/ci/package.json 文件
{
"private": true,
"workspaces": [".."],
"scripts": {
"postinstall": "cd .. && rm -rf node_modules && ln -s ci/node_modules"
},
"dependencies": {
"pkg-b": "git://github.com/xxx/pkg-b.git",
"pkg-c": "git://github.com/xxx/pkg-c.git"
},
"resolutions": {
"pkg-b": "git://github.com/xxx/pkg-b.git",
"pkg-c": "git://github.com/xxx/pkg-c.git"
}
}
在 ci 脚本中,切换到 pkg-a/ci 子目录去做 yarn install,而不是在 pkg-a 自己这层目录做 yarn install
name: Continuous Integration
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 15.5.0
- run: cd ci && yarn
- run: yarn ci
如果不加 resolutions 也可以,就是会报 warning。
其中 postinstall 这个 script 是为了修复 pkg-a/node_modules 里的内容不正确的问题。一般 workspaces 是不会写 ".." 这样的路径的。属于 yarn 没有考虑过的使用方式。
结论
- 使用 peerDependencies 和 * 放宽对依赖的版本约束,免得天天改 package.json
- 使用 yarn workspaces 链接本地的多个开发中的 package 目录
- 给 CI 跑测试单独建一个 ci/package.json 指定 CI 时的依赖版本
- 抄作业:github.com/rotcare/rx-…