2022-10-7 About 51 min

Babel 是一个比较庞大的项目,其子工程就有至少 140 个,产出的子工具已经是前端开发的基础设施,对开发效率、代码质量等有非常高的要求。

本章,我们将了解 Babel 是怎样进行项目管理的。

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
1
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
    ├─ ...
1
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
    ├─ ...
1
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

    如果(在 bootstrapexecpublishrun 中)发现循环,则立即失败。

    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。这对于进入 masterfeature 分支的 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
    2
    lerna 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
      
      1
    • lerna 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
    
    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>
      
      1

      Lerna 只会在启用拓扑排序(即不使用 --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 addnpm 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 命令。

      如果包依赖于不同版本的外部依赖项,则会将最常用的版本提升,并发出警告 --hoistfile: 标识符不兼容,请只使用一个

    • --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-clientnpm

      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-clientlerna 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
    7

    Lerna 将在 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 文件。包的生命周期仍然在原来的叶子目录中运行。您应当使用其中的一个生命周期(prepareprepublishOnlyprepack)来创建子目录等等。

      如果您不喜欢非必要的复杂发布,这将给您带来乐趣。

      lerna publish --contents dist
      # 发布每个由 Lerna 管理的叶子包的 dist 文件夹
      
      1
      2

      注意 您应该等到 postpublish 生命周期阶段(根目录或叶目录)才清理这个生成的子目录,因为生成的 package.json 是在包上传期间(在 postpack 之后)使用的。

    • --dist-tag <tag>

      lerna publish --dist-tag next
      
      1

      当带有该参数时,lerna publish 将使用给定的 npm dist-tag(默认为 latest) 发布到 npm。

      该配置项可用于在非 latestdist-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,则在构建包依赖图和决定拓扑顺序时会包括 dependenciesdevDependencies

      在使用传统的 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.verifyAccessfalse)。

      请小心使用

    • --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 标识符递增 premajorpreminorprepatchprerelease 语义化版本。

    • --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 和一个非预先发布的版本(majorminorpatch),它将会发布那些之前发布的软件包以及自上次发布以来已经改变的软件包。

      对于使用常规提交的项目,使用以下标志进行预发行管理:

      • --conventional-prerelease: 将当前更改作为预发行版本发布。
      • --conventional-graduate: 将预发布版本的包升级为稳定版本。

      如果不使用上面的参数运行 lerna version --conventional-commits,则只有在版本已经在 prerelease 中时,才会将当前更改作为 prerelease 释放。

  • 生命周期

    // preversion:  在设置版本号之前运行.
    // version:     在设置版本号之后,提交之前运行.
    // postversion: 在设置版本号之后,提交之后运行.
    
    1
    2
    3

    lerna将在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 mergeconflicted 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 difflerna changed 共享配置:

      {
          "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**", "**/*.md"]
      }
      
      1
      2
      3

      使用 --no-ignore-changes 禁用任何现有的持久配置。

      在下列情况下,无论该配置项如何设置,包都会发布:

      1. 该包的最新版本是 prerelease 版(即 1.0.0-alpha1.0.0-0.3.7 等等)。
      2. 包的一个或多个相关依赖项已发生更改。
    • --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 标识符递增 premajorpreminorprepatchprerelease 语义化版本号。

    • --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-clilerna 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 versionlerna publish 执行后的包列表。

    lerna publishlerna 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 versionlerna 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 -- 依赖声明
1
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");
1
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
    3
  • 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
  • lerna/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
    47
  • Commander

    lerna 的子命令均继承自 Command 类,比如 lerna init 命令定义为:

    const { Command } = require("@lerna/command");
    
    class InitCommand extends Command {
        ...
    }
    
    1
    2
    3
    4
    5

    Command 类定义在 @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
    12
  • import-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
      4

      package.json

      {
          "workspaces": [
              "packages/*"
          ]
      }
      
      1
      2
      3
      4
      5

      相对于 lernayarn 提供了更强大的依赖分析能力、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

# 13.3 TODO

Last update: October 10, 2022 17:58
Contributors: hoperyy