lerna是一个用于管理包含多个package结构的代码仓库的工具,优化工作流。新版的vue-clinuxtbabel均使用lerna进行自身的package管理。

package可理解为功能模块或子项目。

本文使用的lerna版本: version 3.8.0

场景

  • 当存在一个含有多个package的monorepo
  • 管理这些package的版本与发布时
  • 管理package共用的代码规范等配置时
  • 管理package共用的依赖时

介绍

lerna的主要功能可以分为:版本控制发布,需要与npm(或yarn)和git一同使用。

模式

  • fixed/locked(默认)

    固定模式。该模式为单版本号,在根目录中的lerna.json中设置。当使用lerna publish时,如果自从上次发布后有模块改动,那么将会更新到新发布的版本。

    这也是目前Babel用的模式,当你想要自动整合不同包的版本时使用这个模式。它的特点是任何package的major change均会导致所有包都会进行major version的更新。

  • independent

    lerna init --independent
    

    独立模式。该模式中允许开发者独立管理多个包的版本更新。每次发布时,会得到针对每个包改动(patch, minor, major custom change)的提示。lerna会配合git,检查文件变动,只发布有改动的package。

    独立模式允许开发者更新指定package的版本。将lerna.json中的version键设为independent来启用独立模式。

配置

在项目根目录的lerna.json中设置lerna的相关配置。

{
  "version": "1.0.0",
  "npmClient": "yarn",
  "command": {
    "publish": {
      "ignoreChanges": ["ignored-file", "*.md"]
    }
  },
  "packages": [
    "packages/*"
  ]
}

常用的字段:

key value
version 当前仓库版本,当设为independent时开启独立模式
npmClient 执行命令的client,默认为npm,可以设为yarn
command.publish.ignoreChanges 设置不会包含进lerna change/publish操作的文件路径,使用它来避免一些非重要改动时的版本更新,比如更新README.md中的拼写错误
packages 用于定位package的文件路径

yarn workspace

lerna与yarn的workspace特性很好的融合在了一起,前者负责版本管理与发布,后者负责依赖管理

workspace的特点:在所有workspaces所匹配的项目路径下会执行统一的yarn命令,包含测试、安装依赖或执行脚本。

在lerna中启用workspace:

lerna.json中lerna的设置

{
    ...
    "npmClient": "yarn",
    "useWorkspaces": true,
    ...
}

lerna与yarn workspace有很好的相性,设置useWorkspaces等价于使用bootstrap命令的--use-workspaces选项,详情见bootstrap

根目录下的package.json

{
    ...
    "private": true,
    "workspaces": [
    "packages/*"
    ],
    ...
}

"private": true是必须的,workspaces为工作空间中所包含的项目路径,详见workspace

注意事项

需要注意的是,若开启了workspace功能,则lerna会将package.jsonworkspaces中所设置的项目路径作为lerna packages的路径,而不会使用lerna.json中的packages值。相关源码:

get packageConfigs() {
    if (this.config.useWorkspaces) {
      const workspaces = this.manifest.get("workspaces");
        ...
      return workspaces.packages || workspaces;
    }
    return this.config.packages || [Project.PACKAGE_GLOB];
  }

也就是说,如果使用workspace时未开启useWorkspaces,则yarnlerna会分别管理对应的项目路径。

vue-cli为例,它的lerna.json配置:

{
  "npmClient": "yarn",
  "useWorkspaces": false,
  "version": "3.2.1",
  "packages": [
    "packages/@vue/babel-preset-app",
    "packages/@vue/cli*"
  ]
}

根目录下的package.json:

{
  "private": true,
  "workspaces": [
    "packages/@vue/*",
    "packages/test/*",
    "packages/vue-cli-version-marker"
  ],
  ...
}

它将useWorkspaces设为了false,那么意味着使用yarn管理的是package.jsonworkspaces所对应的项目路径下的依赖,有@vue下的所有项目,test中的测试文件和vue-cli-version-marker。而leran管理的是lerna.jsonpackages所对应的@vue/babel-preset-app@vue/cli*的版本与发布。

而在nuxt中则是lernayarn workspace均采用了相同的package路径。

依赖管理与npm script

下面所操作的lerna项目默认开启了useWorkspaces

初始化

安装lerna与初始化lerna项目

```shell
yarn global add lerna
mkdir monorepo && cd monorepo
lerna init
```

创建package

```shell
cd packages
mkdir module-a && cd module-a
yarn init
```

将package的`name`设置成统一的`@repo/module`的格式,在这里就是`@monorepo/module-a`

依赖的安装与移除

添加所有package中的依赖

lerna add dep-name

会将dep-name包安装到packages所包含的package中。

移除所有package中的依赖

lerna exec -- yarn remove dep-name

移除packages所包含的package中的dep-name包。

给指定package中添加依赖

lerna add dep-name --scope module-a

module-apackage中添加的dep-name包,使用--scope命令限定目标package范围。

移除指定package中的依赖

lerna目前并没有remove这种命令,需要在对应package的package.json中删除对应依赖,然后执行lerna bootstrap即可。

在package中引入相邻依赖

目前的项目结构如下:

monorepo/
    packages/
        module-a/
        module-b/

如果想在module-b中引入module-a,执行如下命令即可

lerna add @monorepo/module-a --scope @monorepo/module-b

执行npm script

执行所有package中的scripts命令

使用lerna的run命令就可以在每个package中执行所包含的对应脚本,前提是需要先在package中写好公共的scripts

比如,若每个package均有testscript

"name": "@monorepo/module-a",
"scripts": {
    "test": "jest"
}

则使用如下命令即可在每个package内执行测试:

lerna run test --stream

执行指定package中的scripts命令

需要使用--scope过滤器来限定作用范围

比如,在project-alpha的package.json中:

{
    "name": "project-alpha",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
    "dev": "node index.js"
    }
}

运行它的dev命令需用下面的语句:

lerna exec --scope project-alpha -- yarn run dev

采用统一的规范配置

以husky和prettier为例

yarn add --dev husky prettier lint-staged -W

使用-W选项会将依赖安装到workspace的根目录下。

在根目录下正常设置相关配置文件

// .prettierrc
{
    "singleQuote": true,
    "jsxBracketSameLine": true,
    "bracketSpacing": true,
    "semi": true,
    "arrowParens": "always",
    "printWidth": 120
}
// package.json
{
    ...
    "scripts": {
        ...
        "precommit": "lint-staged",
        ...
    },
    "lint-staged": {
        "packages/**/*.{js,jsx}": [
            "prettier --write",
            "git add"
        ]
    },
    ...
    "devDependencies": {
        "husky": "^1.2.1",
        "lerna": "^3.8.0",
        "lint-staged": "^8.1.0",
        "prettier": "^1.15.3"
    }
}

测试

git commit -m "lint test"

这样,每次在根目录下执行git命令时会对里面的所有package进行lint

共用的devDependencies

多数package中共用的devDependencies类型的库都可以提升到项目根目录中,这样做的好处有:

  1. 所有包使用相同版本的依赖,统一管理
  2. 可使用自动化工具让根目录下的依赖保持更新
  3. 减少依赖的安装时间,一次安装,多处使用
  4. 节省存储空间,安装在根目录的node_module

提交与发布

lerna中版本控制及发布相关的概念与工具:

  • Conventional Commits

    约定式提交。一种源于AngularJS commit rules的提交规则。

  • Conventional Changelog

    用于从git元数据中生成CHANGELOG.md的工具,该工具仅当遵循Conventional Commits规则时起作用。

  • Semantic Release

    lerna中内置的一个工具,用于生成版本号、git标签、Conventional Changelog、发布的提交信息以及修改记录。可以在lerna.json中将conventionalCommits标记设为true开启,该工具仅当遵循Conventional Commits规则时起作用。

  • Commitlint

    一个遵循Conventional Commits的commit信息格式化工具

具体的操作与demo可查看这篇文章

命令

command value options
lerna init 创建一个新的lerna项目或将已存在项目改造为lerna项目 --independent/-i
lerna bootstrap 当使用yarn并开启了workspace时等价于在根目录执行yarn install
lerna import <pathToRepo> 将本地路径<pathToRepo>中的包导入到packages/<directory-name>,并提交操作记录
lerna publish 对更新后的包发布新版本;使用新版本号标记;升级所有npm和git中的库 --npm-tag [tagname], --canary/-c, --skip-git, --force-publish [packages]
lerna change 检查自上次发布以来改动的包
lerna diff [package?] 比较自上次发布以来的所有或指定的包
lerna run [script] 在每个包中执行一个npm script
lerna ls 列出当前lerna项目中的public包

过滤器

用来过滤命令执行时的范围,详见@lerna/filter-options

filter description
--scope <glob> 仅包含glob所匹配到的package
--ignore <glob> 排除glob匹配到的package
--no-private 排除私有package,默认是包含的

注意,如果package想要使用npm script执行本地的可执行文件需要自己单独设置依赖。并且,在package的package.json中,一般还需设置在runtime需要的依赖和一些公共的scripts

其他

  • 跨项目本地开发
  • TypeScript项目

参考