基于lerna和yarn workspace的monorepo工作流

基于lerna和yarn workspace的monorepo工作流

先插播一段广告

字节跳动校招提前批开启,欢迎小伙伴们投递简历,提前进入面试环节,提前锁定 offer!,详细的jd可查看 字节跳动校招提前批开始

内推邮箱: yangjian.fe@bytedance.com

标题格式: 校招 /实习-姓名-意向岗位-城市

monorepo管理代码有众多好处,但是也带来了很多的技术上的挑战,github上的很多的项目都是使用lerna管理monorepo项目,我们探讨下在gitlab上也通过lerna和yarn workspace结合来管理项目。由于yarn和lerna在功能上有较多的重叠,我们采用yarn官方推荐的做法,用yarn来处理依赖问题,用lerna来处理发布问题。能用yarn做的就用yarn做吧

一个非monorepo的普通项目,常见的开发流程如下,对于monorepo项目却可能存在各个问题

搭建环境

主要指安装依赖

  • 普通项目:clone下来后通过​yarn install​,即可搭建完项目,有时需要配合​postinstall​ hooks,来进行自动编译,或者其他设置。
  • monorepo: 各个库之间存在依赖,如A依赖于B,因此我们通常需要将B link到A的node_module里,一旦仓库很多的话,手动的管理这些link操作负担很大,因此需要自动化的link操作,按照拓扑排序将各个依赖进行link
  • 解决方式:

通过使用workspace,​yarn install​会自动的帮忙解决安装和link问题( github.com/lerna/lerna/

$ yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces

清理环境

在依赖乱掉或者工程混乱的情况下,清理依赖

  • 普通项目: 直接删除node_modules以及编译后的产物。
  • monorepo: 不仅需要删除root的node_modules的编译产物还需要删除各个package里的node_modules以及编译产物
  • 解决方式:使用​lerna clean​来删除所有的node_modules,使用​yarn workspaces run clean​来执行所有package的清理工作
$ lerna clean # 清理所有的node_modules
$ yarn workspaces run clean # 执行所有package的clean操作

安装|删除依赖

普通项目: 通过​yarn add​和​yarn remove​即可简单姐解决依赖库的安装和删除问题

monorepo: 一般分为三种场景

  • 给某个package安装依赖:yarn workspace packageB add packageA 将packageA作为packageB的依赖进行安装
  • 给所有的package安装依赖: 使用​yarn workspaces add lodash​ 给所有的package安装依赖
  • 给root 安装依赖:一般的公用的开发工具都是安装在root里,如​typescript​,我们使用​yarn add -W -D typescript​来给root安装依赖

对应的三种场景删除依赖如下

yarn workspace packageB remove packageA
yarn workspaces remove lodash
yarn remove -W -D typescript
对于安装local dependency,yarn的实现暂时有bug,第一次安装需要指明版本号,否则会安装失败如下
如果ui-button没有发布到npm则
yarn workspace ui-form add ui-button 会安装失败,但是
yarn workspace ui-form add ui-button@1.0.0会成功 ,详情见 github.com/yarnpkg/yarn

项目构建

普通项目:建立一个​build​的npm script,使用​yarn build​即可完成项目构建

monorepo:区别于普通项目之处在于各个package之间存在相互依赖,如packageB只有在packageA构建完之后才能进行构建,否则就会出错,这实际上要求我们以一种拓扑排序的规则进行构建。

我们可以自己构建拓扑排序规则,很不幸的是yarn的workspace暂时并未支持按照拓扑排序规则执行命令,虽然该 rfc 已经被accepted,但是尚未实现

幸运的是lerna支持按照拓扑排序规则执行命令, ​--sort​参数可以控制以拓扑排序规则执行命令

lerna run --stream --sort build

项目测试

普通项目: 建立一个​test​的npm script即可

monorepo项目:有两种方式

  • 使用统一的jest测试配置这样方便全局的跑jest即可,好处是可以方便统计所有代码的测试覆盖率,坏处是如果package比较异构(如小程序,前端,node 服务端等),统一的测试配置不太好编写
  • 每个package单独支持test命令,使用​yarn workspace run test​,坏处是不好统一收集所有代码的测试覆盖率

版本升级及发包

项目测试完成后,就涉及到版本发布,版本发布一般涉及到如下一些步骤

条件验证

如验证测试是否通过,是否存在未提交的代码,是否在主分支上进行版本发布操作,以及其他条件

更加严苛的一些验证操作可以通过danger.js,如rxjs github.com/ReactiveX/rx

version_bump

发版的时候需要更新版本号,这时候如何更新版本号就是个问题,一般大家都会遵循 semVer语义 ,如果版本之间的提交记录较少,能够较为容易的手动更新版本好,但这样也存在人为失误的可能,更好的办法是根据git的提交记录自动更新版本号,实际上只要我们的git commit message符合 Conventional commit规范 ,即可通过工具根据git提交记录,更新版本号,简单的规则如下

  • 存在feat提交: 需要更新minor版本
  • 存在fix提交: 需要更新patch版本
  • 存在 BREAKING CHANGE 提交: 需要更新大版本

生成changelog

为了方便查看每个package每个版本解决了哪些功能,我们需要给每个package都生成一份changelog方便用户查看各个版本的功能变化。同理只要我们的commit记录符合 conventional commit规范 ,即可通过工具为每个package生成changelog文件

生成git tag:

为了方便后续回滚问题及问题排查通常需要给每个版本创建一个git tag

git 发布版本:

每次发版我们都需要单独生成一个commit记录来标记milestone

发布npm包:

发布完git后我们还需要将更新的版本发布到npm上,以便外部用户使用

我们发现手动的执行这些操作是很麻烦的且及其容易出错,幸运的是lerna可以帮助我们解决这些问题

yarn官方并不打算支持发布流程,只是想做好包管理工具,因此这部分还是需要通过lerna支持

lerna提供了​publish​和​version​来支持版本的升级和发布

publish的功能可以即包含version的工作,也可以单纯的只做发布操作。

lerna version

lerna version的作用是进行version bump,支持手动和自动两种模式

只发布某个package

不支持,lerna官方不支持仅发布某个package,见 github.com/lerna/lerna/ ,如果需要,只能自己手动的进入package进行发布,这样lerna自带的各种功能就需要手动完成且可能和lerna的功能相互冲突

由于lerna会自动的监测git提交记录里是否包含指定package的文件修改记录,来确定版本更新,这要求设置好合理的ignore规则(否则会造成频繁的,无意义的某个版本更新),好处是其可以自动的帮助package之间更新版本

例如如果ui-form依赖了ui-button,如果ui-button发生了版本变动,会自动的将ui-form的对ui-button版本依赖更新为ui-button的最新版本。 如果ui-form发生了版本变动,对ui-button并不会造成影响。

自动选择发布版本

使用--conventional-commits 参数会自动的根据conventional commit规范和git commit message记录帮忙确定更新的版本号。

lerna version --conventional-commits

自动确立了版本更新

经测试version_bump是依赖于文件检测和subject结合,并不依赖于scope,scope的作用是用来生成changelog的吧,即如果是修改了ui-form的文件,但是commit记录写的是fix(ui-button),lerna是会生成ui-form的版本更新,并不会去更新ui-button的版本

手动选择发布版本

如果git commit message发现不太靠谱,且无法修改的话,那么需要手动的确认新版本,version默认是手动选择版本

lerna version 
version成功后会自动的推送到主分支,我一般是关闭主分支的推送权限的,这样就会导致推送失败,但是暂时没找到如何禁止推送主分支的好办法,使用​--no-push​会把tag推送一起禁止掉,好在禁止推送主分支只会报错,但不影响整个流程
lerna version自动生成的提交格式为“ publish xxx",并不符合conventional-commit规范,因此需要加以修改,我们通过message参数可以修改自动生成的提交记录
// lerna.json
 "packages": [
 "packages/*"
 "version": "independent",
 "npmClient": "yarn",
 "command": {
 "publish": {
 "ignoreChanges": ["*.md"],
 "verifyAccess": false,
 "verifyRegistry": false,
 "message":"chore: publish" // 自定义version生成的message记录
}

​changelog.md​

version完成后会自动生成 changelog.md ,但是由于lerna是根据什么规则来生成changelog的规则尚不清楚,现在发现A库的changlog里可能包含B的commit记录,具体原因待查

lerna publish

git vesion_bump完成后,就可以根据version生成的tag进行npm发包了

lerna publish from-git
这里没使用from-package是因为每次用from-package都会在package.json里生成个gitHead字段,来关联package和git记录,造成文件被修改,需要手动的checkout或者提交掉,暂时没找到方法禁掉这个修改

示例


完整的demo地址 github.com/hardfist/mon

我们通过一个简单的项目来演示上述操作

创建项目

新建项目&&安装lerna&& 初始化lerna

mkdir monorepo-template && cd monorepo-template && yarn init -y && yarn add -D lerna && lerna init && mkdir packages


lerna配置使用yarn workspaces, 使用independent模式(根据需求选择是否使用independent)

// lerna.json
  "packages": ["packages/*"], // 配置package目录
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true // 使用yarn workspaces
}

配置package.json使用yarn workspacess

// package.json
  "name": "monorepo-template",
  "private": true, // root禁止发布
  "workspaces": [ // 配置package目录
     "packages/*"
}

创建package

创建ui-lib 模块

初始化ui-button模块

cd packages && mkdir ui-lib && yarn init -y 

配置

// package.json
 "name": "ui-button",
 "version": "1.0.0",
 "main": "index.js",
 "publishConfig": {
   "access": "publish" // 如果该模块需要发布,对于scope模块,需要设置为publish,否则需要权限验证
}

创建ui-app 模块

同上,或者使用lerna create快速创建package

lerna create ui-app -y

将ui-lib作为ui-app的依赖

yarn workspace ui-app add ui-lib/1.0.0 # 这里必须加上版本号,否则报错

将lodash添加为所有package的依赖(不包含root)

yarn workspaces run add lodash

将typescript设置为root的开发依赖

一般root只包含一些开发工具依赖如webpack,babel,typescript等

yarn add -W -D typescript jest

构建和测试

为ui-lib和ui-app添加build和test和clean脚本

  • ui-lib
packages/ui-lib/package.json
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "clean": "rimraf lib"
}
  • ui-app
packages/ui-app/package.json
    "scripts": {
    "test": "echo success", // 如果对应脚本无事可做,可以直接echo success
    "build": "tsc",
    "clean": "rimraf lib"
}

在root里添加build、test和clean脚本

"scripts": {
    "build": "lerna run --stream --sort build", // 按照拓扑依赖进行构建
    "clean": "yarn workspaces run clean", // 彼此独立,可以并行执行
    "test": "yarn workspaces run test" // 彼此独立可以并行执行
  },

添加conventional-commit支持

lerna的version_bump和changelog生成都依赖于conventional-commit,因此需要保证commit-msg符合规范。

添加@commitlint/cli和@commitlint/config-conventional以及husky

yarn add -W -D @commitlint/cli @commitlint/conventional-commit lint-staged husky

配置commitlint

// commmitlint.config.js
module.exports = {
  extends: [
    "@commitlint/config-conventional"
};

配置commit-msg的hooks

  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }

添加git-cz,支持commit-msg提示

$ yarn add -W -D commitizen cz-conventional-changelog

配置commitizen并添加commit为npm script

// package.json
"scripts": {
   commit: "git-cz"
 "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
  }

这样后续commit,就可以使用yarn commit进行commit,其会自动做出如下提示

其自动生成的commit-msg如下


发版

开发测试通过后,每隔一段时间即可发版,我们使用lerna version来做发版

配置发版的message

// lerna.json
  "packages": ["packages/*"],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "publish": {
      "ignoreChanges": ["*.md"], // md文件更新,不触发版本变动
      "verifyAccess": false, // 内网发包需开启
      "verifyRegistry": false, // 内网发包需开启
      "message": "chore: publish" // 修改默认的publish的commit msg
}

``

配置发版的策略,我们积极convention-commit来发版

// package.json
  "scripts":
    version: "lerna version --conventional-commits" ## 生成changelog文件以及根据commit来进行版本变动
}

发版

$ yarn run version # 不要使用 yarn version,yarn version 是yarn自动的命令不是npm script

这个会提示用户输入版本,如果不想这个提示可以关闭

// package.json