monorepos by lerna

lerna

最近在开发一个类似 create-react-app 工具。但面临一个问题,需要同时维护两个 packages,开发起来不是很方便,后期维护成本也高(如版本号维护)。于是查看了 create-react-app 源码,发现在其源码中有个 lerna.json 文件。好奇这个文件是做什么的,就了解一番。经查阅了解到 Lerna 可以用来管理项目中多个 packages。这正是自己所需要的,于是就有了这篇文章。本文主要对 Lerna 的使用做个简单介绍。

Lerna

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

Lerna 是一个基于 git 和 npm 管理多个 packages 来简化工作流程的工具。

安装 Lerna

1
2
3
4
5
# npm
npm install --global lerna || npm install -g lerna
# yarn
yarn global add lerna

如果不想安装,也可以使用 npx

初始化项目

创建一个名为 lerna-demo 项目。

1
git init lerna-demo && cd lerna-demo

使用 Lerna 初始化项目

1
lerna init

此时项目结构如下:

1
2
3
4
.
├── lerna.json
├── package.json
└── packages

创建 packages

singsong: 为了方便讲解,这里假设有三个 packages:banana、apple、grocery。其中 grocery 依赖于 banana、apple 两个 package。

packages/ 目录下创建 bananaapplegrocery 三个目录:

1
mkdir banana apple grocery

然后分别在 bananaapplegrocery 目录下执行如下命令初始化 package:

1
npm init -y

并分别创建一个index.js文件,增加如下代码:

1
2
// apple index.js
module.exports = 'apple package';

1
2
// banana index.js
module.exports = 'banana package';
1
2
3
4
5
6
7
// grocery index.js
const apple = require('apple');
const banana = require('banana');
console.log('all the dependencies of the grocery package:');
console.log(apple);
console.log(banana);

此时目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── lerna.json
├── package.json
└── packages
├── apple
│ ├── index.js
│ └── package.json
├── banana
│ ├── index.js
│ └── package.json
└── grocery
├── index.js
└── package.json

创建 packages 依赖关系

上一步骤已创建了 bananaapplegrocery 三个 packages,其中 grocery 依赖于 bananaapple。要建立此依赖只需执行如下命令:

1
2
3
4
// add apple to grocery as a dependency
lerna add apple packages/grocery
// add banana to grocery as a dependency
lerna add banana packages/grocery

lerna add 类似于 npm install

此时目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── lerna.json
├── package.json
└── packages
├── apple
│ ├── index.js
│ └── package.json
├── banana
│ ├── index.js
│ └── package.json
└── grocery
├── index.js
├── node_modules
│ ├── apple -> ../../apple
│ └── banana -> ../../banana
└── package.json

grocerynode_modules 下,Lerna 会分别为 bananaapple 创建一个链接到对应 package 的 symlink(符号链接或软连接,相当于 Windows 中快捷方式)。这样对 bananaapple 任何修改都能立刻生效。

1
2
3
├── node_modules
│ ├── apple -> ../../apple
│ └── banana -> ../../banana

运行

为了方便运行代码,对根目录下 package.json 文件增加如下代码:

1
2
3
"scripts": {
"start": "node packages/grocery/index.js"
}

执行如下命令,运行代码

1
npm start

输出

1
2
3
4
5
6
> root@ start /Users/singsong/github/lerna-demo
> node packages/grocery/index.js
all the dependencies of the grocery package:
apple package
banana package

添加第三方依赖

为所有的 packages 添加 eslint.

1
lerna add eslint --dev

这里只有三个 packages,如果存在很多 packages,每个 package 都单独安装 eslint 包,这会造成资源的浪费。Lerna 也考虑到这个问题,提供了如下命令来解决:

1
lerna bootstrap --hoist

lerna bootstrap 会根据每个 package 的 package.json 为其安装依赖。如果加上 --hoist 参数,Lerna 会把所有 packages 中共有的依赖包安装到根目录中,然后分别在各自的 node_modules/.bin 中创建软链接指向对应依赖包的实际路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── apple
│ ├── index.js
│ ├── node_modules
│ │ └── .bin
│ │ └── eslint -> ../../../../node_modules/eslint/bin/eslint.js
│ ├── package-lock.json
│ └── package.json
├── banana
│ ├── index.js
│ ├── node_modules
│ │ └── .bin
│ │ └── eslint -> ../../../../node_modules/eslint/bin/eslint.js
│ ├── package-lock.json
│ └── package.json
└── grocery
├── index.js
├── node_modules
│ └── .bin
│ └── eslint -> ../../../../node_modules/eslint/bin/eslint.js
├── package-lock.json
└── package.json

当然如果只是安装开发依赖包,可以直接安装在根目录下即可。

1
2
3
4
5
// npm
npm install -D eslint
// yarn
yarn add -D eslint

因为 node 在查找模块时,会从当前目录向上逐级查找。

当然,也许只想对特定 package 安装依赖包,可以通过如下方式:

1
lerna add lodash --scope=grocery

使用 --scope 参数来指定安装位置。

版本管理

Lerna 提供了两种版本管理模式:

  • Fixed/Locked mode (default)

    任何 package 更新发布,都统一由根目录下 lerna.json 中的 version 字段来记录跟踪。即这种模式会将所有 packages 版本号关联起来。但这样会存在一个问题:
    任何 package 版本号变化,都会导致其他所有 package 拥有一个新的版本号。

    开启方法:默认模式。

  • Independent mode (–independent)

    packages 发布新版时,会逐个询问每个 package 需要升级的版本号。即每个 package 都独立维护自己的 version。这样就可以有效地避免默认模式下版本号语义化的问题。

    开启方法:

    • lerna init --independent
    • lerna.json 中的 version 字段设置为 'independent'

发布

要发布新版时,只需执行如下命令即可。

1
lerna publish

另外,Lerna 还为 lerna publish 提供了一些选项:@lerna/publish

在执行该命令时,需要注意,至少要有个 commit,否则会得到如下提示:

Working tree has uncommitted changes, please commit or remove changes before continuing.

Current HEAD is already released, skipping change detection.

因为在发布之前,Lerna 会检查 packages 是否有更新。如果有更新才会以 一问一答 的方式获取发布相关信息:

singsong: 假设你已成功登录 NPM。如何注册及登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
info cli using local version of lerna
lerna notice cli v3.13.1
lerna info current version 0.0.5
lerna info Looking for changed packages since zhansingsong-apple@0.0.5
? Select a new version (currently 0.0.5) Patch (0.0.6)
Changes:
- zhansingsong-apple: 0.0.5 => 0.0.6
- zhansingsong-banana: 0.0.4 => 0.0.6
- zhansingsong-grocery: 0.0.4 => 0.0.6
? Are you sure you want to publish these packages? Yes
lerna info execute Skipping GitHub releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
....

singsong:为了能成功将 apple、banana、grocery 发布到 NPM,在包命名时都为每个 package 加了 zhansingsong- 前缀。

上述是默认模式下的输出信息。虽然只对 zhansingsong-apple 做了修改,然而在版本号更新时,会更新所有 packages 的版本号。而且如果执行发布,会把所有的 packages 都发布到 NPM。那如果换成Independent模式,会是怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
info cli using local version of lerna
lerna notice cli v3.13.1
lerna info versioning independent
lerna info Looking for changed packages since v0.0.7
? Select a new version for zhansingsong-apple (currently 0.0.7) Patch (0.0.8)
Changes:
- zhansingsong-apple: 0.0.7 => 0.0.8
? Are you sure you want to publish these packages? Yes
lerna info execute Skipping GitHub releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...

Independent 模式下,只会更新已更新 zhansingsong-apple 的版本号,并只将其发布到 NPM。

其他常用命令

  • lerna create <name> [loc]:在 loc 目录下创建一个 package。默认位置 packages/
  • lerna version:更新 package 的版本号。提供 Patch、Minor、Major、Prepatch、Preminor、Premajor、Custom Prerelease、Custom Version 选项。
  • lerna clean:删除所有 packages 的 node_modules 目录。PS:不会删除根目录的 node_modules。
  • lerna list | lerna ls | lerna ll | lerna la:列举 packages 目录下的所有本地 packages。

    1
    2
    3
    4
    5
    6
    7
    // 执行 lerna list 的输出:
    info cli using local version of lerna
    lerna notice cli v3.13.1
    apple
    banana
    grocery
    lerna success found 3 packages
  • lerna changed | lerna updated:查看本地 packages 是否发生变化。

  • lerna link:根据依赖关系为本地所有 packages 创建软链接。
  • lerna run <script>:运行每个 package 中包含 npm run <script> 的脚本。