Babel 是一个比较庞大的项目,其子工程就有至少 140 个,产出的子工具已经是前端开发的基础设施,对开发效率、代码质量等有非常高的要求。
本章,我们将了解 Babel 是怎样进行项目管理的。
本书从工程管理、代码管理、文档管理、质量管理四个方面对 Babel 项目管理进行拆解分析。
工程管理
Babel 是典型的 monorepo 项目,即单仓库项目,所有的子模块在同一个仓库里。Babel 目前有 140+ 个子模块,在工程管理部分,需要解决以下问题:
- 模块间如何方便地互相关联进行本地开发
- 整个项目的版本控制
- 操作自动化
工程管理部分主要使用 lerna、yarn 等工具。
代码风格
Babel 是多人协作开发的开源项目,如何保证代码风格一致,Babel 使用的是社区常见的解决方案。
该模块主要使用 eslint、prettier 等工具。
文档
Babel 的迭代速度很快、涉及的模块很多,该模块解决版本发布后如何自动更新相关文档等问题。
该模块主要使用 lerna 等工具。
质量控制
Babel 的产品是前端开发的基础设施,该模块主要保证 Babel 的产出是高质量的。
该模块主要使用 jest、git blame 等工具。
# 13.1 monorepo
Babel 使用 monorepo 模式进行工程管理。
# 13.1.1 什么是 monorepo
monorepo(monolithic repository),指的是单仓库代码,将多个项目代码存储在一个仓库里。另外有一种模式是 multirepo,指的是多仓库代码(one-repository-per-module),不同的项目分布在不同的仓库里。React、Babel、Jest、Vue、Angular 均采用 monorepo 进行项目管理。
典型的 monorepo 结构是:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
2
3
4
5
6
这是 Babel 源码的目录结构:
├─ lerna.json
├─ package.json
└─ packages/ # 这里将存放所有子项目目录
├─ README.md
├─ babel-cli
├─ babel-code-frame
├─ babel-compat-data
├─ babel-core
├─ babel-generator
├─ babel-helper-annotate-as-pure
├─ babel-helper-builder-binary-assignment-operator-visitor
├─ babel-helper-builder-react-jsx
├─ ...
2
3
4
5
6
7
8
9
10
11
12
13
而 rollup 则采取了 multirepo 的模式:
├─ rollup
├─ package.json
├─ ...
├─ plugins
├─ package.json
├─ ...
├─ awesome
├─ package.json
├─ ...
├─ rollup-starter-lib
├─ package.json
├─ ...
├─ rollup-plugin-babel
├─ package.json
├─ ...
├─ rollup-plugin-commonjs
├─ package.json
├─ ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 13.1.2 monorepo 的优缺点
# 优点
便捷的代码复用与依赖管理
当所有项目代码都在一个工程里,抽离可复用的代码就十分容易了。并且抽离后,如果复用的代码有改动,可以通过一些工具,快速定位受影响的子工程,进而做到子工程的版本控制。
便捷的代码重构
通过一些工具,monorepo 项目中的代码改动可以快速地定位出代码变更的影响范围,对整个工程进行快速的整体测试。而如果子工程分散在不同的工程分支里的话,通用代码的重构将难以触达各个子工程。
倡导开放、共享
monorepo 项目中,开发者可以方便地看到所有子工程,这样响应了"开放、共享"的组织文化。可以激发开发者对工程质量等维护的热情(毕竟别人看不到自己的代码,乱不乱就看自己心情了),有助于团队建立良好的技术氛围。
# 缺点
复杂的权限管理
因为所有子工程集中在一个工程里,某些子工程如果不希望对外展示的话,monorepo 的权限管理就比较难以实现了,难以锁定目标工程做独立的代码权限管理。
较高的熟悉成本
相对于 multirepo,monorepo 涉及各种子工程、通用依赖等,新的开发者在理解整个项目时,可能需要了解较多的信息才能入手,如通用依赖代码、各子工程功能。
较大的工程体积
很明显,所有子工程集成在一个工程里,代码体积会非常大,对文件存储系统等提出了较高的要求。
较高的质量风险
成也萧何败萧何,monorepo 提供了便捷的代码复用能力,同时,一个公用模块的某个版本有 bug 的话,很容易影响所有用到它的子工程。此时,做好高覆盖率的单元测试就比较重要了。
# 选择
multirepo 和 monorepo 是两种不同的理念。
multirepo 允许多元化发展,每个模块独立实现自己的构建、单元测试、依赖管理等。monorepo 抹平了模块间的很多差异,通过集中管理和高度集成化的工具,减少开发和沟通时的成本。monorepo 最大的问题可能就是不能管理占用空间太大的项目了。
所以,还是要根据项目的实际需求出发选择用哪种项目管理模式。
# 13.2 lerna
http://www.febeacon.com/lerna-docs-zh-cn/routes/commands/bootstrap.html
lerna 是基于 git/npm/yarn 等的工作流提效工具,用于维护 monorepo。它是 Babel 开发过程中提升开发效率产出的工具。
lerna 本身也是一个 monorepo 的项目,并且,lerna 为 monorepo 项目提供了如下支持:
项目管理
lerna 提供了一系列命令用于 monorepo 项目初始化、添加子项目、查看项目信息等。
依赖管理
lerna 支持为 monorepo 项目统一管理公共依赖、自动安装各个子项目的依赖、自动创建子模块符号链接等。
版本管理
lerna 可以根据项目代码的变动情况,发现影响的子项目范围,在发布时提供语义化版本推荐等,极大提升了 monorepo 项目的版本管理效率。
# 13.2.1 lerna 命令集
# 命令行列表
lerna 官网有对各种命令各种用法的详细介绍,这些命令可以分为: 项目管理、依赖管理、版本管理三大类。
分类 | 命令 | 作用 |
---|---|---|
项目管理 | lerna init | 创建一个新的 monorepo 项目,或者将当前项目升级到支持 monorepo 的状态 |
项目管理 | lerna create | 创建一个新的 lerna 管理的包 |
项目管理 | lerna import | 将一个包导入到带有提交历史记录的 monorepo 项目中 |
项目管理 | lerna list | 列出本地包 |
项目管理 | lerna link | 将所有相互依赖的包符号链接在一起 |
项目管理 | lerna exec | 在每个包中执行任意命令 |
项目管理 | lerna run | 在包含该脚本中的每个包中运行 npm 脚本 |
依赖管理 | lerna add | 向匹配的包添加依赖关系,添加公共依赖 |
依赖管理 | lerna clean | 删除子项目的 node_modules |
依赖管理 | lerna bootstrap | 将本地包链接在一起并安装剩余的包依赖项。 |
版本管理 | lerna publish | 发布子项目 |
版本管理 | lerna version | 确认发布的版本号 |
版本管理 | lerna diff | 获取上次发布后所有包或某个包的代码变更情况 |
版本管理 | lerna changed | 列出自上次标记发布以来发生变化的本地包 |
版本管理 | lerna info | 打印本地环境信息 |
# 全局配置项
lerna 有一批通用参数,所有子命令均可以使用。
--concurrency
当 lerna 将任务并行执行时,需要使用多少线程(默认为逻辑 CPU 内核数)。
lerna publish --concurrency 1
1--loglevel <silent|error|warn|success|info|verbose|silly>
要报告什么级别的日志。如果失败,所有日志都写到当前工作目录中的 lerna-debug.log 中。
任何高于该设置的日志都会显示出来。默认值是
"info"
。--max-buffer <bytes>
为每个底层进程调用设置的最大缓冲区长度。例如,当有人希望在运行
lerna import
的同时导入包含大量提交的仓库时,就是它出场的时候了。在这种情况下,内置的缓冲区长度可能不够。--no-progress
禁用进度条。在CI环境中总是这样。
--no-sort
默认情况下,所有任务都按照拓扑排序的顺序在包上执行,以尊重所讨论的包的依赖关系。在不保证
lerna
调用一致的情况下,以最努力的方式打破循环。如果只有少量的包邮许多依赖项,或者某些包执行的时间长得不成比例,拓扑排序可能会导致并发瓶颈。
--no-sort
配置项禁用排序,而是以最大并发的任意顺序执行任务。如果您运行多个
watch
命令,该配置项也会有所帮助。因为lerna run
将按照拓扑排序的顺序执行命令,所以在继续执行之前可能会等待某个命令。当您运行 "watch
" 命令时会阻塞执行,因为他们通常不会结束。--reject-cycles
如果(在
bootstrap
、exec
、publish
或run
中)发现循环,则立即失败。lerna bootstrap --reject-cycles
1
# 过滤器参数
--scope <glob>
只包含名称与给定通配符匹配的包。
lerna exec --scope my-component -- ls -la lerna run --scope toolbar-* test lerna run --scope package-1 --scope *-2 lint
1
2
3--ignore <glob>
排除名称与给定通配符匹配的包。
lerna exec --ignore package-{1,2,5} -- ls -la lerna run --ignore package-1 test lerna run --ignore package-@(1|2) --ignore package-3 lint
1
2
3--no-private
排除私有的包。默认情况下是包含它们的。
--since [ref]
只包含自指定
ref
以来已经改变的包。如果没有传递ref
,它默认为最近的标记。# 列出自最新标记以来发生变化的包的内容 $ lerna exec --since -- ls -la # 为自“master”以来所有发生更改的包运行测试 $ lerna run test --since master # 列出自“某个分支”以来发生变化的所有包 $ lerna ls --since some-branch
1
2
3
4
5
6
7
8在
CI
中使用时, 如果您可以获得PR
将要进入的目标分支,那么它将特别有用,因为您可以将其作为--since
配置项的ref
。这对于进入master
和feature
分支的PR
来说很有效。--exclude-dependents
当使用
--since
运行命令时,排除所有传递的被依赖项,覆盖默认的"changed"
算法。如果没有
--since
该参数时无效的,会抛出错误。--include-dependents
在运行命令时包括所有传递的被依赖项,无视
--scope
、--ignore
或--since
。--include-dependencies
在运行命令时包括所有传递依赖项,无视
--scope
、--ignore
或--since
。与接受
--scope(bootstrap、clean、ls、run、exec)
的任何命令组合使用。确保对任何作用域的包(通过--scope
或--ignore
)的所有依赖项(和dev
依赖项)也进行操作。注意 这将会覆盖
--scope
和--ignore
。 例如,如果一个匹配了--ignore
的包被另一个正在引导的包所以来,那么它仍会照常工作。当您想要"设置"一个依赖于其他正在设置的包其中的一个包时,这是非常有用的。
lerna bootstrap --scope my-component --include-dependencies # my-component 及其所有依赖项将被引导
1
2lerna bootstrap --scope "package-*" --ignore "package-util-*" --include-dependencies # 所有匹配 "package-util-*" 的包将被忽略,除非它们依赖于名称匹配 "package-*" 的包
1
2--include-merged-tags
lerna exec --since --include-merged-tags -- ls -la
1在使用
--since
命令时,它包含来自合并分支的标记。这只有在从feature
分支进行大量发布时才有用,通常情况下不推荐这样做。
# 命令行详解
# lerna init
作用
创建一个新的 monorepo 项目,或者将当前项目升级到支持 monorepo 的状态。
创建一个新的 monorepo 项目
mkdir lerna-project cd lerna-project lerna init
1
2
3会创建如下文件和文件夹:
packages/ package.json lerna.json
1
2
3将已有项目升级到支持 monorepo 的状态
一个项目已经执行过
git init
并已有一些文件时:src/ package.json
1
2执行
lerna init
,会有如下动作:- 如果
package.json
中缺失lerna
的依赖声明,则优先添加到devDependencies
- 创建
lerna.json
用于存储version
等配置字段
- 如果
配置项
lerna init --independent
参数
--independent
,表示当前项目是否是独立模式,默认为非独立模式。非独立模式下,lerna.json 的内容为:
{ "packages": [ "packages/*" ], "version": "0.0.0" }
1
2
3
4
5
6子工程的版本号统一保持一致。
独立模式下,即用
lerna init --independent
初始化项目,lerna.json 的内容为:{ "packages": [ "packages/*" ], "version": "independent" }
1
2
3
4
5
6子工程的版本号独立维护。
lerna init --exact
默认情况下,当添加或更新 lerna 的本地版本时,
lerna init
将使用插入符号范围,如npm install --save-dev lerna
。为了保留
lerna 1.x
的"精确"安装行为,可以传递该参数。也可以在lerna.json
中配置以强制后续所有都执行精确匹配:{ "command": { "init": { "exact": true } }, "version": "0.0.0" }
1
2
3
4
5
6
7
8
原理
TODO
# lerna create <name> [loc]
作用
创建一个新的 lerna 管理的包。
比如,在项目目录执行:
lerna create module1 lerna create module2 lerna create module3
1
2
3项目目录会包含:
packages/ module1/ module2/ module3/ package.json lerna.json
1
2
3
4
5
6参数
<name>
[loc]
--access
--bin
--description
--dependencies
--es-module
--homepage
--keywords
--license
--private
--registry
--tag
--yes
# lerna import
作用
将一个包导入到带有提交历史记录的 monorepo 项目中。
lerna import <path-to-external-repository>
1将位于
<path-to-external-repository>
处的带有提交历史记录的包导入到packages/<directory-name>
中。原始提交作者、日期和消息保存了下来。提交应用于当前 monorepo 分支。这对于将预先存在的独立的包收集到
lerna
仓库非常有用。每次提交都修改为相对于包目录进行更改。例如,添加package.json
的提交将改为添加packages/<directory-name>/package.json
。配置项
lerna import --flatten
当导入带有冲突合并提交的存储库时,
import
命令在尝试应用所有提交时将失败。用户可以使用这个标志来请求"抹平(flat
)"历史的导入,也就是说,在每次合并提交时,合并就会被引入。lerna import ~/Product --flatten
1lerna import --dest
在导入存储库时,可以根据
lerna.json
中列出的目录指定目标目录。lerna import ~/Product --dest=utilities
1--preserve-commit
每次 git 提交都有一位作者和一位提交者(每人都有一个单独的日期)。通常他们是同一个人(和日期),但是因为
lerna import
从外部存储库重新创建每个提交,提交者就变成了当前的 git 用户(和日期)。这在技术上是正确的,但可能并不可取,例如,在 Github 上,如果作者和提交者是不同的人,它就会同时显示他们,这可能会导致导入提交时的历史/职责出现混乱。启用该配置项可以保留原始提交者(和提交日期),以避免此类问题。
lerna import ~/Product --preserve-commit
1
# lerna list
作用
列出本地包。
使用
lerna list
有很多简写方式:lerna ls
: 和lerna list
相同lerna ll
: 和lerna list -l
相同lerna la
: 和lerna list -la
相同,显示所有的包(包括子包)
lerna ls package-1 package-2
1
2
3
4在
shell
中运行这些命令时,您可能会注意到 lerna 提供了额外的日志记录。请放心,它们不会污染您的命令,因为所有的日志都是按照strerr
发送的而非stdout
。在任何情况下,你可以随时通过
--loglevel silent
恢复原始的shell
显示。配置项
lerna list
命令支持过滤器参数,lerna 的@lerna/filter-options
工程描述了 lerna 支持的过滤器参数列表:--json
用 JSON 数组的形式展示子工程。
[ { "name": "package-1", "version": "1.0.0", "private": false, "location": "/path/to/packages/pkg-1" }, { "name": "package-2", "version": "1.0.0", "private": false, "location": "/path/to/packages/pkg-2" } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14--ndjson
用换行分隔(
ndjson
)的方式展示子工程。{"name":"package-1","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-1"} {"name":"package-2","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-2"}
1
2--all, -a
展示默认隐藏的私有包。
package-1 package-2 package-3 (private)
1
2
3--long, -l
显示扩展信息。
lerna ls --long package-1 v1.0.1 packages/pkg-1 package-2 v1.0.2 packages/pkg-2 lerna ls -la package-1 v1.0.1 packages/pkg-1 package-2 v1.0.2 packages/pkg-2 package-3 v1.0.3 packages/pkg-3 (private)
1
2
3
4
5
6
7
8--parseable, -p
显示可解析的输出,而不是竖向排列的显示。
默认情况下,每一行都是包的绝对路径。
如果新增了
--long
参数,每一行的格式都是以:
分隔的<fullpath>:<name>:<version>[:flags..]
。lerna ls --parseable /path/to/packages/pkg-1 /path/to/packages/pkg-2 lerna ls -pl /path/to/packages/pkg-1:package-1:1.0.1 /path/to/packages/pkg-2:package-2:1.0.2 lerna ls -pla /path/to/packages/pkg-1:package-1:1.0.1 /path/to/packages/pkg-2:package-2:1.0.2 /path/to/packages/pkg-3:package-3:1.0.3:PRIVATE
1
2
3
4
5
6
7
8
9
10
11
12--toposort
按照拓扑顺序对包进行排序,而不是按目录对包进行词法排序。
json dependencies <packages/pkg-1/package.json { "pkg-2": "file:../pkg-2" } lerna ls --toposort package-2 package-1
1
2
3
4
5
6
7
8--graph
将依赖图显示为 json 格式的邻接表。
lerna ls --graph { "pkg-1": [ "pkg-2" ], "pkg-2": [] } lerna ls --graph --all { "pkg-1": [ "pkg-2" ], "pkg-2": [ "pkg-3" ], "pkg-3": [ "pkg-2" ] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# lerna link
作用
将所有相互依赖的包符号链接在一起。
使用
lerna link
1将当前 lerna 仓库中相互依赖的所有 lerna packages 符号链接在一起。
配置项
--force-local
lerna link --force-local
1当使用该参数时,此标志将导致
link
命令始终符号链接本地依赖项,而不考虑匹配的版本范围。publishConfig.directory
这是一个非标准字段,它将允许您定制作为符号链接的源目录的符号链接子目录,就像使用已发布的包一样。
"publishConfig": { "directory": "dist" }
1
2
3在这个例子中,当该包连接时,
dist
目录将成为源目录(例如,package-1/dist => node_modules/package-1
)。
# lerna exec
作用
在每个包中执行任意命令。
使用
lerna exec -- <command> [..args] # 在所有包中运行命令 lerna exec -- rm -rf ./node_modules lerna exec -- protractor conf.js
1
2
3在每个包中执行任意命令。必须使用双横线--来将虚线参数传递给派生的命令,但当所有参数都是位置时,就不是必需的了。
当前包的名称可以通过环境变量
LERNA_PACKAGE_NAME
获取:lerna exec -- npm view \$LERNA_PACKAGE_NAME
1您也可以通过环境变量
LERNA_ROOT_PATH
在一个复杂的目录结构中运行一个位于根目录下的脚本:lerna exec -- node \$LERNA_ROOT_PATH/scripts/some-script.js
1配置项
lerna exec
接受所有的过滤器参数lerna exec --scope my-component -- ls -la
1使用给定的并发(除了
--parallel
)并行生成命令。输出是以管道形式传输的,所以并不确定。如果您想一个接一个包运行这个命令,可以这样使用:lerna exec --concurrency 1 -- ls -la
1--stream
立即从子进程输出流,它的前缀为原始的包名。这让不同的包交叉输出成为可能。
lerna exec --stream -- babel src -d lib
1--parallel
和
--stream
相类似,但完全忽略并发性和拓扑顺序。它会立即在带有前缀的流输出的所有匹配包中运行给定的命令或脚本。这是在许多包上运行的长时间运行的进程(如babel src -d lib -w
)的首选参数。lerna exec --parallel -- babel src -d lib -w
1注意
在使用
--parallel
时建议限制该命令的作用域,因为生成几十个子进程可能会损害 shell 的稳定性(例如,最大文件描述符限制)。这个因人而异。--no-bail
# 运行一个命令,忽略非零(错误)退出代码 lerna exec --no-bail <command>
1
2默认情况下,如果任何执行过程返回一个非零的退出代码,
lerna exec
将退出并报错。——no-bail
可以禁用此行为,让其所有包中执行,而无视退出代码。--no-prefix
当输出为流(
--stream
或--parallel
)时禁用包名前缀。该配置项应用在将结果传输到其他进程时很有用,比如编辑器插件。--profile
对命令执行进行分析,并生成性能分析文件,可以在基于 chrome 的浏览器中使用 DevTools 进行分析(URL 为:
devtools://devtools/bundled/devtools_app.html
)。该分析文件显示了命令执行的时间线,其中每次执行都会分配给一个打开的槽。槽的数量由--concurrency
决定,开放槽的数量由--concurrency
减去正在进行的操作的数量决定。最终结果是对并行执行命令的可视化展示。性能分析文件输出的默认位置是项目的根目录。
lerna exec --profile -- <command>
1Lerna 只会在启用拓扑排序(即不使用
--parallel
和--no-sort
)时分析性能。--profile-location <location>
您可以提供一个自定义位置用于性能分析文件的输出。提供的路径是相对于当前工作目录进行解析的。
lerna exec --profile --profile-location=logs/profile/ -- <command>
1
# lerna run
作用
在包含该脚本中的每个包中运行 npm 脚本。
使用
lerna run <script> -- [..args] # 在所有包含 my-script 的包中运行 npm run lerna run test lerna run build # 观察所有包和 transpile 的变化,使用流前缀输出 lerna run --parallel watch
1
2
3
4
5
6在包含该脚本中的每个包中运行 npm 脚本,必须使用双横线
--
来将虚线参数传递给给执行的脚本。配置项
TODO 好像和 exec 一样。
# lerna add
作用
向匹配的包添加依赖关系,添加公共依赖。
使用
lerna add <package>[@version] [--dev] [--exact] [--peer]
1将本地或远程 package 作为依赖项添加到当前
lerna
仓库中的包。注意,与yarn add
或npm install
相比,一次只能添加一个包。运行时,该命令将:
- 向每个适用的包添加
package
。适用是指在作用域内且不是package
的包 - 对
manifest
文件(package.json
)进行更改的引导包 - 如果没有提供
version
指示符,其默认值为latestdist-tag
,和npm install
一样
- 向每个适用的包添加
配置项
lerna add
接受所有的过滤器参数。lerna add --dev
将新包添加到
devDependencies
而不是dependencies
。lerna add --exact
为新包添加一个确切的版本(例如,
1.0.1
),而不是默认的^语义化版本号范围(例如,^1.0.1
)。lerna add --peer
将新包添加到
peerDependencies
而不是dependencies
。lerna add --registry <url>
使用自定义注册表安装目标包。
lerna add --no-bootstrap
跳过链式的
lerna bootstrap
。
示例
# 将 module-1 的包添加到以“prefix-”为前缀文件夹中的包中 lerna add module-1 packages/prefix-* # 将 module-1 安装到 module-2 lerna add module-1 --scope=module-2 # 将 module-1 安装到 module-2 的 devDependencies lerna add module-1 --scope=module-2 --dev # 将 module-1 安装到 module-2 的 peerDependencies lerna add module-1 --scope=module-2 --peer # 将 module-1 安装到除了 module-1 的所有模块 lerna add module-1 # 在所有模块中安装 babel-core lerna add babel-core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18比如,执行
lerna add vue
的话,会默认在各个子项目中添加 vue,此时各个子项目均会安装一遍 vue。如果不希望子项目重复安装,可以在项目 lerna.json 添加字段:
"command": { "bootstrap": { "hoist": true } }
1
2
3
4
5此时,vue 作为依赖会被安装在项目根目录,而子项目中的 package.json 也会声明该依赖。
# lerna clean
作用
删除子项目的 node_modules。
参数
lerna clearn
接受所有的过滤器参数,以及--yes
。lerna clean
不会从根node_modules
目录中删除模块,即使您使用了--hoist
。
# lerna bootstrap
作用
将本地包链接在一起并安装剩余的包依赖项。
使用
引导当前
lerna
仓库中的包。安装其所有依赖项并连接所有的交叉依赖。在运行时,该命令:
npm install
每个包所有的外部依赖- 将所有相互依赖的
lerna
package
符号链接在一起(symlink) - 在所有引导包中运行
npm run prepublish
(除非传入了--ignore-prepublish
) - 在所有引导包中运行
npm run prepare
配置项
lerna bootstrap
接受所有过滤器属性通过将额外的参数放在
--
之后来传递给 npm 客户端lerna bootstrap -- --production --no-optional
也可以在 lerna.json 中这样配置:
{ "npmClient": "yarn", "npmClientArgs": ["--production", "--no-optional"] }
1
2
3
4--hoist [glob]
在仓库根目录安装与
glob
匹配的外部依赖,这样所有包都可以使用他们。这些依赖的任何二进制文件都将链接到依赖包的node_modules/.bin/
目录中,这样npm
脚本就可以使用它们了。如果传了该参数但却没有设置glob
,则默认为**
(提升所有)。该配置项仅影响bootstrap
命令。如果包依赖于不同版本的外部依赖项,则会将最常用的版本提升,并发出警告
--hoist
与file
: 标识符不兼容,请只使用一个--strict
当和
hoist
一起使用时,会抛出一个错误,并在发出版本警告后停止引导。如果没有hoist
或没有版本警告则无效果。--nohoist [glob]
不要在仓库根目录安装与
glob
匹配的外部依赖项。该参数可以用来选择不提升某些依赖项。lerna bootstrap --hoist --nohoist=babel-*
1--ignore
lerna bootstrap --ignore component-*
1该参数与
bootstrap
命令一起使用时,也可以在 lerna.json 的command.bootstrap.ignore
中设置。命令行中设置该参数优先于lerna.json
。{ "version": "0.0.0", "command": { "bootstrap": { "ignore": "component-*" } } }
1
2
3
4
5
6
7
8通配符匹配的是
package.json
中定义的包名,而不是与包所在的目录名。--ignore-prepublish
跳过在引导包中默认运行的预发布生命周期脚本。注意,这个生命周期是已废弃的,可能会在
lerna
的下一个主要版本中被删除。--ignore-scripts
跳过在引导包中正常运行的生命周期脚本(
prepare
等等)。--registry <url>
当使用该参数时,转发的 npm 命令将为您的包使用指定的注册表。
如果您不想在使用私有注册表时在所有
package.json
文件中分别设置注册表配置,那么这就是它出场的时候了。--npm-client <client>
它必须是一个知道如何安装
npm
包依赖的可执行文件。默认的--npm-client
是npm
。lerna bootstrap --npm-client=yarn
1也可在lerna.json中配置:
{ "npmClient": "yarn" }
1
2
3--use-workspaces
支持和 yarn 工作区 (从 yarn@0.27+ 版本开始)数组中的值是 lerna 将在其中委托操作给 Yarn 的命令(目前仅在
bootstrap
中可用)。如果--use-workspaces
为真,那么 packages 将被 package.json/workspaces 的值覆盖。也可在 lerna.json 中配置:
{ "npmClient": "yarn", "useWorkspaces": true }
1
2
3
4根级别的 package.json 还必须包含一个工作区数组:
{ "private": true, "devDependencies": { "lerna": "^2.2.0" }, "workspaces": ["packages/*"] }
1
2
3
4
5
6
7这个列表与 lerna 的 package 配置大体上类似(通过 package.json 用通配符匹配的目录列表),只是它不支持递归的通配符匹配(
**
,也叫"globstars"
)。--no-ci
当使用默认的
--npm-client
,lerna bootstrap
将调用npm ci
而非在 CI 环境中调用npm install
。若要禁用此行为,可以使用--no-ci
:lerna bootstrap --no-ci
1若要在本地安装期间强制执行(在本地安装中不会自动启用),请使用
--ci
:lerna bootstrap --ci
1这对于"干净地"重新安装或重新克隆后的初次安装非常有用。
--force-local
lerna bootstrap --force-local
1当使用该参数时,它会让
bootstrap
命令始终符号链接本地依赖项,而不考虑匹配的版本范围。publishConfig.directory
这个非标准字段允许您自定义符号链接子目录,它会作为符号链接的源目录,就像使用已发布的包一样。
"publishConfig": { "directory": "dist" }
1
2
3在本例中,当这个包被引导并链接时,dist 目录将是源目录(例如
package-1/dist => node_modules/package-1
)。
原理
TODO
# lerna publish
作用
在当前项目中发布包。
使用
lerna publish # 发布自上一个版本以来发生了变化的包 lerna publish from-git # 发布当前提交中标记的包 lerna publish from-package # 发布注册表中没有最新版本的包
1
2
3在运行时,该命令做了下面几件事中的一个:
- 发布自上一个版本以来更新的包(背后调用了
lerna version
)。这是 lerna 2.x 版本遗留下来的。 - 发布在当前提交中标记的包(
from-git
)。 - 发布在最新提交时注册表中没有版本的包(
from-package
)。 - 发布在前一次提交中更新的包(及其依赖项)的"金丝雀(
canary
)"版。
Lerna 永远不会发布标记为
private
的包(package.json 中的"private": true
)。在所有的发布过程中,都有生命周期在根目录和每个包中运行(除非使用了
--ignore-scripts
)。- 发布自上一个版本以来更新的包(背后调用了
位置
from-git
除了
lerna version
支持的语义化版本关键字外,lerna publish
也支持from-git
关键字。这将会识别lerna version
标记的包,并将它们发布到 npm。这在您希望手动增加版本的 CI 场景中非常有用,但要通过自动化过程一直地发布包内容本身。from-package
与 from-git 关键字类似,只是要发布的包列表是通过检查每个 package.json 确定的,并且要确定注册表中是否存在任意版本的包。注册表中没有的任何版本都将被发布。当前一个
lerna publish
未能将所有包发布到注册表时,就是他发挥的时候了。
生命周期
// prepublish: 在打包和发布包之前运行。 // prepare: 在打包和发布包之前运行,在 prepublish 之后,prepublishOnly 之前。 // prepublishOnly: 在打包和发布包之前运行,只在 npm publish 时运行。 // prepack: 只在源码压缩打包之前运行。 // postpack: 在源码压缩打包生成并移动到最终目的地后运行。 // publish: 在包发布后运行。 // postpublish: 在包发布后运行。
1
2
3
4
5
6
7Lerna 将在
lerna publish
期间运行 npm 生命周期脚本,顺序如下:- 如果采用没有指定版本,则运行所有版本生命周期脚本
- 如果可用,在根目录运行 prepublish 生命周期。
- 在根目录中运行 prepare 生命周期。
- 在根目录中运行 prepublishOnly 生命周期。
- 在根目录运行 prepack 生命周期。
- 对于每个更改的包,按照拓扑顺序(所有依赖项在依赖关系之前):
- 如果可用,运行 prepublish 生命周期。
- 运行 prepare 生命周期。
- 运行 prepublishOnly 生命周期。
- 运行 prepack 生命周期。
- 通过 JS API 在临时目录中创建源码压缩包。
- 运行 postpack 生命周期。
- 在根目录运行 postpack 生命周期。
- 对于每个更改的包,按照拓扑顺序(所有依赖项在依赖关系之前):
- 通过 JS API 发布包到配置的注册表。
- 运行 publish 生命周期。
- 运行 postpublish 生命周期。
- 在根目录中运行publish生命周期。
- 为了避免递归调用,不要使用这个根生命周期来运行 lerna publish。
- 在根目录中运行 postpublish 生命周期。
- 如果可用,将临时的 dist-tag 更新到最新
配置项
lerna publish
支持lerna version
提供的所有配置项,除了以下这些:--canary
lerna publish --canary # 1.0.0 => 1.0.1-alpha.0+${SHA} of packages changed since the previous commit # a subsequent canary publish will yield 1.0.1-alpha.1+${SHA}, etc lerna publish --canary --preid beta # 1.0.0 => 1.0.1-beta.0+${SHA} # The following are equivalent: lerna publish --canary minor lerna publish --canary preminor # 1.0.0 => 1.1.0-alpha.0+${SHA}
1
2
3
4
5
6
7
8
9
10
11当使用该标志运行时,
lerna publish
以更粒度的方式(每次提交)来发布包。在发布到 npm 之前,它会通过当前的 version 创建新的 version 标记,升级到下一个小版本(minor),添加传入的 meta 后缀(默认为 alpha)并且附加当前的 git sha 码(例如:1.0.0
变成1.1.0-alpha.0+81e3b443
)。如果您已经从
CI
中的多个活动开发分支发布了canary
版本,那么建议在每个分支的基础上定制--preid
和--dist-tag <tag>
以避免版本冲突。该参数是需要发布每次提交版或每日构建版时使用。
--contents <dir>
要发布的子目录。必定应用于所有包,且必须包含 package.json 文件。包的生命周期仍然在原来的叶子目录中运行。您应当使用其中的一个生命周期(
prepare
、prepublishOnly
或prepack
)来创建子目录等等。如果您不喜欢非必要的复杂发布,这将给您带来乐趣。
lerna publish --contents dist # 发布每个由 Lerna 管理的叶子包的 dist 文件夹
1
2注意 您应该等到
postpublish
生命周期阶段(根目录或叶目录)才清理这个生成的子目录,因为生成的 package.json 是在包上传期间(在postpack
之后)使用的。--dist-tag <tag>
lerna publish --dist-tag next
1当带有该参数时,
lerna publish
将使用给定的 npmdist-tag
(默认为latest
) 发布到 npm。该配置项可用于在非
latest
的dist-tag
下发布预发布或beta
版本,帮助用户免于自动升级到预发布质量的代码。注意
latest
标记是用户运行npm install my-package
时使用的标记。要安装不同的标记,用户可以运行npm install my-package@prerelease
。--git-head <sha>
在打包压缩时,显式地在 manifest 上设置为 gitHead,该操作只允许通过
from-package
位置进行。举个例子,当我们从 AWS CodeBuild (这里
git
用不了)发布时,您可以使用该配置项传递适当的环境变量来作为该包的元数据。lerna publish
时,只会更新发生变更的子项目的版本号,如果没有特殊制定,未变更的则不会更新版本号。lerna publish from-package --git-head ${CODEBUILD_RESOLVED_SOURCE_VERSION}
1在所有其他情况下,该值是从本地的 git 命令派生而成的。
--graph-type <all|dependencies>
设置在构建包依赖图时使用哪种依赖关系。默认值是
dependencies
,因此只包括包的 package.json 的dependencies
部分中列出的包。若设置为 all,则在构建包依赖图和决定拓扑顺序时会包括dependencies
和devDependencies
。在使用传统的 peer + dev 依赖对时,应该将此项配置为
all
,以便 peer 可以总在其依赖项之前发布。lerna publish --graph-type all
1通过 lerna.json 来配置:
{ "command": { "publish": { "graphType": "all" } } }
1
2
3
4
5
6
7--ignore-scripts
这个参数会让
lerna publish
在发布期间禁用运行的生命周期脚本。--ignore-prepublish
这个参数会让
lerna publish
在发布期间禁用已废弃的prepublish
脚本。--legacy-auth
当您发布需要身份验证但使用内部托管的NPM注册表时,该注册表只使用旧 Base64 版本的
username:password
。这与 NPM publish 的_auth
标志相同。lerna publish --legacy-auth aGk6bW9t
1--no-git-reset
默认情况下,
lerna publish
确保任何对工作树的更改都会被重置。为了避免这种情况,可以设置
——no-git-reset
。当作为 CI 流程的一部分与——canary
一起使用时,这一点特别有用。例如,已经被替换的 package.json 版本号可能需要在随后的 CI 流程步骤中使用(比如 Docker 构建)。lerna publish --no-git-reset
1--no-granular-pathspec
默认情况下,
lerna publish
将尝试(如果启用)只git checkout
在发布过程中临时修改的叶子包清单。这相当于git checkout -- packages/*/package.json
,但是精确地定制了变化。如果您确实知道您需要不同的行为,那么您就会理解:通过
--no-granular-pathspec
会让 git 命令执行的git checkout -- .
。通过选择这个路径规范,您必须有意忽略所有未版本化的内容。该项最好在 lerna.json 中配置,否则会原地去世的:
{ "version": "independent", "granularPathspec": false }
1
2
3
4根级配置是有意为之,因为它还包括了
lerna version
中的同名配置项。--no-verify-access
默认情况下,lerna 将严重登录的 npm 用户对即将发布的包的访问权限。设置该参数将禁用该验证。
如果您使用的是不支持
npm access ls-packages
的第三方注册表,则需要设置它(或在 lerna.json 中设置command.publish.verifyAccess
为false
)。请小心使用
--otp
当发布需要双重身份验证的包时,您可以使用
——otp
指定一次性密码:lerna publish --otp 123456
1请注意 一次性密码在生成后 30 秒内过期。如果它在发布操作期间到期,提示符将在继续之前请求更新后的值。
--preid
和同名的
lerna version
配置项不同,该配置项仅适用于--canary
版本计算。lerna publish --canary # 举例,使用下一个语义化预发布版本 # 1.0.0 => 1.0.1-alpha.0 lerna publish --canary --preid next # 举例,使用指定的预发布标识符来标识下一个语义化预发布版本 # 1.0.0 => 1.0.1-next.0
1
2
3
4
5
6
7当使用该参数运行时,
lerna publish --canary
将使用指定的prerelease
标识符递增premajor
、preminor
、prepatch
或prerelease
语义化版本。--pre-dist-tag <tag>
lerna publish --pre-dist-tag next
1除了只适用于与预发行版本一起发布的软件包外,它和
--dist-tag
并无二致。--registry <url>
当使用该参数运行时,转发的 npm 命令将为您的包使用指定 url 的注册表。
如果您不想在使用私有注册表时在所有 package.json 文件中分别设置注册表配置,那就是它出场的时候。
--tag-version-prefix
配置项项允许提供自定义前缀来替代默认的
v
:# 本地 lerna version --tag-version-prefix='' # CI lerna publish from-git --tag-version-prefix=''
1
2
3
4
5您也可以在 lerna.json 中进行配置,对这两个命令也同样适用:
{ "tagVersionPrefix": "", "packages": ["packages/*"], "version": "independent" }
1
2
3
4
5--temp-tag
当传递了该参数时,他将会改变默认的发布过程,首先将所有更改过的包发布到一个临时的
dist-tag
(lerna-temp
)中,然后将新版本移动到由--dist-tag
配置的dist-tag
中(默认值latest
)。这通常是没有必要的,因为 Lerna 在默认情况下会按照拓扑顺序(所有依赖项优先)来发布包。
--yes
lerna publish --canary --yes # 跳过“您确定要发布上述更改吗?”
1
2当使用此标志运行时,lerna publish 将跳过所有确认提示。在持续集成(CI)中用于自动回答发布确认提示。
每个包的配置
叶子包可以可以配置特殊的
publishConfig
,在某些情况下可以改变lerna publish
的行为。publishConfig.access
要发布具有作用域的包(如
@mycompany/rocks
),您必须设置access
:"publishConfig": { "access": "public" }
1
2
3- 如果把该字段设置到无作用域的包上,那么它会失败的。
- 如果您希望限定范围的包保持私有状态(即"
restricted
"),则无需设置该值。
注意:这与在叶子包中设置 "
private:true
" 不一样。如果设置了private
字段,那么在任何情况下都不会发布该包。publishConfig.registry
您可以通过设置 registry 在每个包上定制注册表。
"publishConfig": { "registry": "http://my-awesome-registry.com/" }
1
2
3传入
--registry
可以应用到全局,其实在某些情况下会起到反效果。publishConfig.tag
您可以通过设置tag来定制每个包的
dist-tag
:"publishConfig": { "tag": "flippin-sweet" }
1
2
3- 传入
--dist-tag
将会覆盖该值。 - 如果同时传入了
--canary
则该值会被忽略。
- 传入
publishConfig.directory
这个非标准字段允许您定制已发布的子目录,就像
--contents
一样,但它是以每个包为基础的。所有其他关于--contents
的内容仍然适用。"publishConfig": { "directory": "dist" }
1
2
3
# lerna version
作用
更改自上次发布以来的包版本号。
使用
lerna version 1.0.1 # 显式指定 lerna version patch # 语义化关键字 lerna version # 根据提示选择
1
2
3在运行时,该命令执行以下操作:
- 标识自上一个版本以来更新的包。
- 提示输入新版本。
- 修改包的元数据,在根目录和每个包当中运行适当的生命周期脚本。
- 提交这些更改并打上标记。
- 推动到 git 远程服务器。
位置
语义化版本号
lerna version [major | minor | patch | premajor | preminor | prepatch | prerelease] # 使用下一个语义化版本号,然后跳过“为…选择一个新版本”的提示。
1
2当传递位置参数时,
lerna version
将跳过版本选择的提示问题并根据关键字增加版本号。当然仍然需要使用--yes
来避免所有的问题。预发布
如果你有一个预发布版本号的软件包(例如
2.0.0-beta.3
),并且你运行了lerna version
和一个非预先发布的版本(major
、minor
或patch
),它将会发布那些之前发布的软件包以及自上次发布以来已经改变的软件包。对于使用常规提交的项目,使用以下标志进行预发行管理:
--conventional-prerelease
: 将当前更改作为预发行版本发布。--conventional-graduate
: 将预发布版本的包升级为稳定版本。
如果不使用上面的参数运行
lerna version --conventional-commits
,则只有在版本已经在prerelease
中时,才会将当前更改作为prerelease
释放。
生命周期
// preversion: 在设置版本号之前运行. // version: 在设置版本号之后,提交之前运行. // postversion: 在设置版本号之后,提交之后运行.
1
2
3lerna将在
lerna version
期间运行npm生命周期脚本:- 侦测更改的包,选择版本号进行覆盖。
- 在根目录运行 preversion。
- 对于每个更改的包,按照拓扑顺序(所有依赖项在依赖关系之前):
- 运行 preversion 生命周期。
- 更新 package.json 中的版本。
- 运行 version 生命周期。
- 在根目录运行 version 生命周期。
- 如果可用,将更改文件添加到索引。
- 如果可用创建提交和标记。
- 对于每个改变包,按照词法顺序(根据目录结构的字母顺序):
- 运行 postversion 生命周期。
- 在根目录运行 postversion。
- 如果可用推动提交和标记到远程服务器。
- 如果可用创建发布。
配置项
--allow-branch <glob>
匹配启用了
lerna version
的 git 分支的白名单。在 lerna.json 中配置它是最简单的(我们也这么推荐),但是也可以将它作为 CLI 配置项传入进去。{ "command": { "version": { "allowBranch": "master" } } }
1
2
3
4
5
6
7使用上面的配置,
lerna version
在除master
之外的任何分支运行时都将失败。最佳实践是将lerna version
限制到主分支。{ "command": { "version": { "allowBranch": ["master", "feature/*"] } } }
1
2
3
4
5
6
7在上面的配置中,
lerna version
将被允许出现在任何以feature/
为前缀的分支中。请注意,在 feature 分支中生成 git 标签会产生潜在的错误,因为这些分支会被合并到主分支中。如果标签和其原始上下文“分离”开来(可能通过squash merge
或conflicted merge
提交),那么未来的lerna version
执行将很难确定正确的"自上一个版本以来的差异"。lerna version --allow-branch hotfix/oops-fix-the-thing
1使用命令行会覆盖这个"持久"的配置,请谨慎使用。
--amend
lerna version --amend # 保留 commit 的消息,并跳过 git push
1
2当使用该参数运行时,
lerna version
将对当前提交执行所有更改,而不是添加一个新的。这在持续集成(CI)期间非常有用,可以减少项目历史记录中的提交数量。为了防止意外的覆盖,这个命令将跳过
git push
(也就是说--no-push
)。--changelog-preset
lerna version --conventional-commits --changelog-preset angular-bitbucket
1默认情况下,changelog 预设值是 angular。在某些情况下,您可能需要使用另一个预设值或自定义一个。
预设值是常规更改日志的内置或可安装配置的名称。预设值可以作为包的全名或自动扩展的后缀进行传递(举个例子,angular 扩展为
conventional-changelog-angular
)。--conventional-commits
lerna version --conventional-commits
1当您使用这个参数运行时,
lerna version
将使用传统的提交规范来确定版本并生成 CHANGELOG.md 文件。传入
--no-changelog
将阻止生成(或更新) CHANGELOG.md 文件。--conventional-graduate
lerna version --conventional-commits --conventional-graduate=package-2,package-4 # 强制分隔所有的预发行包 lerna version --conventional-commits --conventional-graduate
1
2
3
4当使用该参数时,
lerna version
将使用*
分隔指定的包(用逗号隔开的)或所有的包。无论当前的HEAD是否已释放,该命令都可以工作,它和--force-publish
相类似,除了忽略任何非预发布包。如果未指定的包(如果指定了包)或未预先发布的包发生了更改,那么这些包将按照它们通常使用的--conventional-commits
提交的方式进行版本控制。"分隔"一个软件包意味着一个预发布版本的非预发布版本变体。例如
package-1@1.0.0-alpha.0 => package-1@1.0.0
。注意:当指定包时,它的依赖项将被释放,但不会被分隔。
--conventional-prerelease
lerna version --conventional-commits --conventional-prerelease=package-2,package-4 # 强制所有发生改变的包变为预发布 lerna version --conventional-commits --conventional-prerelease
1
2
3
4当使用该参数时,
lerna version
将使用*
分隔指定的包(用逗号隔开的)或所有的包。通过在conventional-commits
的版本推荐之前加上pre
,可以将所有未发布的更改作为pre
(patch/minor/major/release
)版来发布。如果当前的更改包含了特性提交,那么推荐的版本将成为minor
,因此该参数会使其成为preminor
发布。如果未指定的包(如果指定了包)或未预先发布的包发生了更改,那么这些包将按照它们通常使用的--conventional-commits
提交的方式进行版本控制。--create-release <type>
当使用该参数时,
lerna version
将基于更改的包创建一个正式的 GitHub 或 GitLab 版本。需要传递conventional-commits
以便生成变更日志。要使用 Github 进行身份验证,可以定义以下环境变量。
GH_TOKEN
(必须)- 您的 GitHub 认证 token (在设置(Settings) > 开发人员设置(Developer Settings) > 个人访问令牌(Personal access tokens)下)。GHE_API_URL
- 当使用 GitHub Enterprise 时,API 的绝对 URL。GHE_VERSION
- 当使用 GitHub Enterprise 时,当前安装的 GHE 版本。
要使用 GitLab 进行身份验证,可以定义以下环境变量。
GL_TOKEN
(必须)- 您的 GitLab 认证 token (在用户设置(User Settings) > 访问令牌(Access Tokens)下)GL_API_URL
- API 的绝对URL,包括版本号。(默认值:https://gitlab.com/api/v4)
注意 当使用该配置项的时候,不要设置
--no-changelog
该配置项也可以在 lerna.json 中配置:
{ "changelogPreset": "angular" }
1
2
3如果预先导出一个构件函数(如:
conventional-changelog-conventionalcommits
),您也可以指定预设配置:{ "changelogPreset": { "name": "conventionalcommits", "issueUrlFormat": "{{host}}/{{owner}}/{{repository}}/issues/{{id}}" } }
1
2
3
4
5
6--exact
lerna version --exact
1当使用该参数时,
lerna version
将在更新的包中精确地指定更新过的依赖项(无标点符号),而不做语义化版本号兼容(使用^
)。--force-publish
lerna version --force-publish=package-2,package-4 # 强制所有的包标上版本 lerna version --force-publish
1
2
3
4当使用该参数时,
lerna version
将强制发布指定的包(逗号分隔)或使用*
发布所有包。注意 这将跳过以更改包的
lerna changed
检查,并强制更新没有git diff
更改的包。--git-remote <name>
lerna version --git-remote upstream
1当使用该参数时,
lerna version
将把git
更改推送到指定的远程服务器,而不是origin
。--ignore-changes
当检测到更改的包时,忽略由通配符匹配到的文件中的更改。
lerna version --ignore-changes '**/*.md' '**/__tests__/**'
1该配置项最好通过 lerna.json 指定,既避免过早的 shell 验证也能够和
lerna diff
及lerna changed
共享配置:{ "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**", "**/*.md"] }
1
2
3使用
--no-ignore-changes
禁用任何现有的持久配置。在下列情况下,无论该配置项如何设置,包都会发布:
- 该包的最新版本是 prerelease 版(即
1.0.0-alpha
,1.0.0-0.3.7
等等)。 - 包的一个或多个相关依赖项已发生更改。
- 该包的最新版本是 prerelease 版(即
--ignore-scripts
当使用该参数时,
lerna version
会在运行期间禁用生命周期脚本。--include-merged-tags
lerna version --include-merged-tags
1在检测更改的包时包含合并分支的标记。
--message <msg>
可简写为
-m
,用于git commit
。lerna version -m "chore(release): publish %s" # commit message = "chore(release): publish v1.0.0" lerna version -m "chore(release): publish %v" # commit message = "chore(release): publish 1.0.0" # 当单独对包进行版本控制时,不会替换占位符 lerna version -m "chore(release): publish" # commit message = "chore(release): publish # # - package-1@3.0.1 # - package-2@1.5.4"
1
2
3
4
5
6
7
8
9
10
11
12当使用该参数时,
lerna version
会在提交发布版本更新时使用所提供的消息。对于将 lerna 集成到期望提交消息遵守某些规则的项目中非常有用,例如使用commitizen
和/
或语义化版本发布的项目。如果消息包含
%s
,则将其替换为新的全局版本版本号,该版本号前缀为 "v
"。如果消息包含%v
,它将被替换为新的全局版本版本号,但没有前缀 "v
"。注意,这个占位符插值只在使用默认的"固定"版本模式时使用,因为在独立版本控制时没有"全局"版本可以进行插值。在 lerna.json 中这样配置:
{ "command": { "version": { "message": "chore(release): publish %s" } } }
1
2
3
4
5
6
7--no-changelog
lerna version --conventional-commits --no-changelog
1使用
--conventional-commits
时,不要生成任何 CHANGELOG.md 文件。注意 当使用该配置项的时候,不要设置
--create-release
--no-commit-hooks
默认情况下,
lerna version
将允许git commit
钩子在提交版本更改时运行。通过——no-commit-hook
来禁用此行为。该配置项类似于
npm version
的--commit-hooks
配置项,只是反过来了。--no-git-tag-version
默认情况下,
lerna version
将提交对 package.json 文件的更改,并标记发行版。通过——no-git-tag-version
可以禁用该行为。该配置项与
npm version
的配置项--git-tag-version
相类似,只是反过来了。--no-granular-pathspec
默认情况下,
lerna version
将在git add
时只添加在版本控制过程中更改过的叶子包 manifest (可能还有更改日志)。这相当于git add -- packages/*/package.json
,但是精确地定制了变化。如果您确定需要不同的行为,您就会理解:通过
——no-granular-pathspec
来使 git 命令执行git add -- .
。通过设置pathspec
,您必须将所有秘密和构建输出适当地忽略掉,否则它们会被提交并推到仓库的。通过lerna.json设置:
{ "version": "independent", "granularPathspec": false }
1
2
3
4采用根级配置是有意为之,因为它还包括了
lerna publish
中的同名配置项。--no-private
默认情况下,
lerna version
将在选择版本、提交和标记发布时包含私有包。我们可以通过--no-private
来禁用该行为。注意 该配置项并未将私有作用域的包排除在外,只会排除 package.json 文件中
private
字段设置为true
的包。--no-push
默认情况下,
lerna version
将提交和标记的更改推到已配置的 git 远程服务器。设置--no-push
来禁用此行为。--preid
lerna version prerelease # 使用下一个语义化预发布版本,如: # 1.0.0 => 1.0.1-alpha.0 lerna version prepatch --preid next # 通过制定的预发布标识符以使用下一个语义化预发布版本,如 # 1.0.0 => 1.0.1-next.0
1
2
3
4
5
6
7使用该参数时,
lerna version
将使用指定的prerelease
标识符递增premajor
、preminor
、prepatch
或prerelease
语义化版本号。--sign-git-commit
该配置项和
npm version
的同名配置项相类似。--sign-git-tag
该配置项和
npm version
的同名配置项相类似。--force-git-tag
该配置项将替换任意的现有标签,而非失败信息。
--tag-version-prefix
该配置项允许设置自定义前缀,默认的前缀是:
v
。目前您必须设置两次:分别对应 version 命令和 publish 命令。
# 本地 lerna version --tag-version-prefix='' # 在 CI 上 lerna publish from-git --tag-version-prefix=''
1
2
3
4--yes
lerna version --yes # 跳过 `Are you sure you want to publish these packages?`
1
2当使用该参数运行时,
lerna version
将将跳过所有确认提示。在持续集成(CI)中用于自动回答发布确认提示。
生成初始的更新日志
如果您在 monorepo 启动一段时间后才开始使用
--conventional-commits
配置项,您仍然可以使用conventional-changelog-cli
和lerna exec
为以前的版本生成更改日志。# Lerna does not actually use conventional-changelog-cli, so you need to install it temporarily npm i -D conventional-changelog-cli # Documentation: `npx conventional-changelog --help` # fixed versioning (default) # run in root, then leaves npx conventional-changelog --preset angular --release-count 0 --outfile ./CHANGELOG.md --verbose npx lerna exec --concurrency 1 --stream -- 'conventional-changelog --preset angular --release-count 0 --commit-path $PWD --pkg $PWD/package.json --outfile $PWD/CHANGELOG.md --verbose' # independent versioning # (no root changelog) npx lerna exec --concurrency 1 --stream -- 'conventional-changelog --preset angular --release-count 0 --commit-path $PWD --pkg $PWD/package.json --outfile $PWD/CHANGELOG.md --verbose --lerna-package $LERNA_PACKAGE_NAME'
1
2
3
4
5
6
7
8
9
10
11
12如果您使用
--changelog-preset
进行自定义,那么您应该相应地更改上面的示例中的--preset
值。
# lerna diff [package]
作用
获取上次发布后所有包或某个包的代码变更情况。
使用
lerna diff [package] lerna diff # 显示指定包的差异 lerna diff package-name
1
2
3
4
5底层依赖
git diff
实现。
# lerna changed
作用
列出自上次标记发布以来发生变化的本地包。
使用
lerna changed
输出的是下一个lerna version
或lerna publish
执行后的包列表。lerna publish
和lerna version
的 lerna.json 配置也会影响lerna changed
。比如command.publish.ignoreChanges
。配置项
lerna changed
支持lerna ls
的所有参数。--json --ndjson --a, --all -l, --long -p, --parseable --toposort --graph
1
2
3
4
5
6
7和
lerna ls
不同,lerna changed
并不支持过滤器配置项,因为其本身并不为lerna version
或lerna publish
所支持。lerna changed
支持下列lerna version
的配置项(其他的都无关紧要了):--conventional-graduate --force-publish --ignore-changes --include-merged-tags
1
2
3
4
# lerna info
作用
打印本地环境信息。
使用
lerna info
打印的本地环境信息在提交 bug 报告时尤其有用。lerna notice cli v4.0.0 Environment info: System: OS: macOS 11.2.3 CPU: (8) arm64 Apple M1 Binaries: Node: 16.4.2 - ~/.nvm/versions/node/v16.4.2/bin/node Yarn: 1.22.10 - ~/.nvm/versions/node/v16.4.2/bin/yarn npm: 7.18.1 - ~/.nvm/versions/node/v16.4.2/bin/npm Utilities: Git: 2.30.1 - /usr/bin/git
1
2
3
4
5
6
7
8
9
10
11
12
13
# 13.2.2 lerna原理解析
# 文件结构
以下是 lerna 的主要目录结构(省略了一些文件和文件夹):
lerna
├─ CHANGELOG.md -- 更新日志
├─ README.md -- 文档
├─ commands -- 核心子模块
├─ core -- 核心子模块
├─ utils -- 核心子模块
├─ lerna.json -- lerna 配置文件
├─ package-lock.json -- 依赖声明
├─ package.json -- 依赖声明
├─ scripts -- 内置脚本
└─ yarn.lock -- 依赖声明
2
3
4
5
6
7
8
9
10
11
有趣的是,lerna 本身也是用 lerna 进行开发管理的。它是一个 monorepo 项目,其各个子项目分布在 lerna/commands/*、core/*、utils/*
目 录下。
另外,在源码中,经常会看到名称为 @lerna/command
的子项目,如 lerna/core/lerna/index.js
的内容是:
const cli = require("@lerna/cli");
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这些子项目分布在 lerna/commands/*、core/*、utils/*
下,截至本书截稿时,lerna有 61 个子项目。
以下是各子项目分布:
相对路径 | package 名称 | 作用 |
---|---|---|
lerna/commands/add | @lerna/add | |
lerna/commands/bootstrap | @lerna/bootstrap | |
lerna/commands/changed | @lerna/changed | |
lerna/commands/clean | @lerna/clean | |
lerna/commands/create | @lerna/create | |
lerna/commands/diff | @lerna/diff | |
lerna/commands/exec | @lerna/exec | |
lerna/commands/import | @lerna/import | |
lerna/commands/info | @lerna/info | |
lerna/commands/init | @lerna/init | |
lerna/commands/link | @lerna/link | |
lerna/commands/list | @lerna/list | |
lerna/commands/publish | @lerna/publish | |
lerna/commands/run | @lerna/run | |
lerna/commands/version | @lerna/version | |
lerna/core/child-process | @lerna/child-process | |
lerna/core/cli | @lerna/cli | |
lerna/core/command | @lerna/command | |
lerna/core/conventional-commits | @lerna/conventional-commits | |
lerna/core/filter-options | @lerna/filter-options | |
lerna/core/global-options | @lerna/global-options | |
lerna/core/lerna | lerna | |
lerna/core/otplease | @lerna/otplease | |
lerna/core/package | @lerna/package | |
lerna/core/package-graph | @lerna/package-graph | |
lerna/core/project | @lerna/project | |
lerna/core/prompt | @lerna/prompt | |
lerna/core/validation-error | @lerna/validation-error | |
lerna/utils/check-working-tree | @lerna/check-working-tree | |
lerna/utils/check-uncommitted | @lerna/check-uncommitted | |
lerna/utils/check-updates | @lerna/check-updates | |
lerna/utils/check-symlink | @lerna/check-symlink | |
lerna/utils/describe-ref | @lerna/describe-ref | |
lerna/utils/filter-packages | @lerna/filter-packages | |
lerna/utils/get-npm-exec-opts | @lerna/get-npm-exec-opts | |
lerna/utils/get-packed | @lerna/get-packed | |
lerna/utils/github-client | @lerna/github-client | |
lerna/utils/gitlab-client | @lerna/gitlab-client | |
lerna/utils/has-npm-version | @lerna/has-npm-version | |
lerna/utils/listable | @lerna/listable | |
lerna/utils/log-packed | @lerna/log-packed | |
lerna/utils/map-to-registry | @lerna/map-to-registry | |
lerna/utils/npm-conf | @lerna/npm-conf | |
lerna/utils/npm-dist-tag | @lerna/npm-dist-tag | |
lerna/utils/npm-install | @lerna/npm-install | |
lerna/utils/npm-publish | @lerna/npm-publish | |
lerna/utils/npm-run-script | @lerna/npm-run-script | |
lerna/utils/output | @lerna/output | |
lerna/utils/pack-directory | @lerna/pack-directory | |
lerna/utils/prerelease-id-from-version | @lerna/prerelease-id-from-version | |
lerna/utils/profiler | @lerna/profiler | |
lerna/utils/pluse-till-done | @lerna/pluse-till-done | |
lerna/utils/query-graph | @lerna/query-graph | |
lerna/utils/resolve-symlink | @lerna/resolve-symlink | |
lerna/utils/rimraf-dir | @lerna/rimraf-dir | |
lerna/utils/run-lifecycle | @lerna/run-lifecycle | |
lerna/utils/run-topologically | @lerna/run-topologically | |
lerna/utils/symlink-binary | @lerna/symlink-binary | |
lerna/utils/symlink-dependencies | @lerna/symlink-dependencies | |
lerna/utils/timer | @lerna/timer | |
lerna/utils/write-log-file | @lerna/write-log-file |
# 命令行注册
lerna 命令注册工作集中在 lerna/core/lerna/*
路径下。
lerna/core/lerna/package.json
该文件的
bin
字段定义了lerna
命令:"bin": { "lerna": "cli.js" }
1
2
3lerna/core/lerna/cli.js
该文件描述了命令行的执行入口:
#!/usr/bin/env node "use strict"; /* eslint-disable import/no-dynamic-require, global-require */ const importLocal = require("import-local"); if (importLocal(__filename)) { require("npmlog").info("cli", "using local version of lerna"); } else { require(".")(process.argv.slice(2)); }
1
2
3
4
5
6
7
8
9
10
11
12lerna/core/lerna/index.js
该文件为命令行引入了所有 lerna 指令:
"use strict"; const cli = require("@lerna/cli"); const addCmd = require("@lerna/add/command"); const bootstrapCmd = require("@lerna/bootstrap/command"); const changedCmd = require("@lerna/changed/command"); const cleanCmd = require("@lerna/clean/command"); const createCmd = require("@lerna/create/command"); const diffCmd = require("@lerna/diff/command"); const execCmd = require("@lerna/exec/command"); const importCmd = require("@lerna/import/command"); const infoCmd = require("@lerna/info/command"); const initCmd = require("@lerna/init/command"); const linkCmd = require("@lerna/link/command"); const listCmd = require("@lerna/list/command"); const publishCmd = require("@lerna/publish/command"); const runCmd = require("@lerna/run/command"); const versionCmd = require("@lerna/version/command"); const pkg = require("./package.json"); module.exports = main; function main(argv) { const context = { lernaVersion: pkg.version, }; return cli() .command(addCmd) .command(bootstrapCmd) .command(changedCmd) .command(cleanCmd) .command(createCmd) .command(diffCmd) .command(execCmd) .command(importCmd) .command(infoCmd) .command(initCmd) .command(linkCmd) .command(listCmd) .command(publishCmd) .command(runCmd) .command(versionCmd) .parse(argv, context); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47Commander
类lerna 的子命令均继承自
Command
类,比如lerna init
命令定义为:const { Command } = require("@lerna/command"); class InitCommand extends Command { ... }
1
2
3
4
5Command
类定义在@lerna/command
,位于lerna/core/command
目录。有一些写法值得借鉴,比如个别方法需要
InitCommand
的实例自行定义,否则抛错,InitCommand
类的定义如下:class InitCommand extends Command { ... initialize() { throw new ValidationError(this.name, "initialize() needs to be implemented."); } execute() { throw new ValidationError(this.name, "execute() needs to be implemented."); } }
1
2
3
4
5
6
7
8
9
10
11
12import-local
上文提到,
lerna/core/lerna/cli.js
描述了命令行的执行入口:#!/usr/bin/env node "use strict"; /* eslint-disable import/no-dynamic-require, global-require */ const importLocal = require("import-local"); if (importLocal(__filename)) { require("npmlog").info("cli", "using local version of lerna"); } else { require(".")(process.argv.slice(2)); }
1
2
3
4
5
6
7
8
9
10
11
12其中,
import-local
的作用是,实现本地开发版本和生产版本的切换。import-local/index.js
的内容是:'use strict'; const path = require('path'); const resolveCwd = require('resolve-cwd'); const pkgDir = require('pkg-dir'); module.exports = filename => { const globalDir = pkgDir.sync(path.dirname(filename)); const relativePath = path.relative(globalDir, filename); const pkg = require(path.join(globalDir, 'package.json')); const localFile = resolveCwd.silent(path.join(pkg.name, relativePath)); const localNodeModules = path.join(process.cwd(), 'node_modules'); const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..'); // Use `path.relative()` to detect local package installation, // because __filename's case is inconsistent on Windows // Can use `===` when targeting Node.js 8 // See https://github.com/nodejs/node/issues/6624 return !filenameInLocalNodeModules && localFile && path.relative(localFile, filename) !== '' && require(localFile); };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19原理解析:TODO
# 依赖管理
Babel使用 lerna
进行依赖管理。其中,lerna
自己实现了一套依赖管理机制,也支持基于 yarn
的依赖管理。
yarn的依赖管理
pnp: https://loveky.github.io/2019/02/11/yarn-pnp/
官网: https://yarnpkg.com/features/pnp
https://www.yuque.com/allenstone/learn/deb1n1
对比: https://www.qiyuandi.com/zhanzhang/zonghe/12444.html
lerna 的
hoisting
子模块相同的依赖可以通过依赖提升(
hoisting
),将相同的依赖安装在根目录下,本地包之间用软连接实现。lerna bootstrap
该命令执行时,会在每个子项目下面,各自安装其中 package.json 声明的依赖。
这样会有一个问题,相同的依赖会被重复安装,除了占用更多空间外,依赖安装速度也受影响。
lerna bootstrap --hoist
--hoist
标记时,lerna bootstrap
会识别子项目下名称和版本号相同的依赖,并将其安装在根目录的node_modules
下,子项目的node_modules
会生成软连接。这样节省了空间,也减少了依赖安装的耗时。
yarn install
当在项目中声明
yarn
作为依赖安装的底层依赖,如:lerna.json
{ "npmClient": "yarn", "useWorkspaces": true, }
1
2
3
4package.json
{ "workspaces": [ "packages/*" ] }
1
2
3
4
5相对于
lerna
,yarn
提供了更强大的依赖分析能力、hoisting 算法。而且,默认情况下,yarn 会开启hoist
功能,也可以设置nohoist
关闭该功能:{ "workspaces": { "packages": [ "Packages/*", ], "nohoist": [ "**" ] } }
1
2
3
4
5
6
7
8
9
10
# Git相关
lerna 中广泛使用了 Git 命令用于内部工作,这里列举了 lerna 中使用的 Git 命令。
git init
git rev-parse
git describe
git rev-list
git tag
git log
git config
git diff-index
git --version
git show
git am
git reset
git ls-files
git diff-tree
git commit
git ls-remote
git checkout
git push
git add
git remote
git show-ref