⤴Top⤴

pnpm & Monorepo

博客分类: 前端
修改内容:add 扁平化如何处理不同版本的依赖

pnpm & Monorepo

pnpm & Monorepo

什么是 pnpm

pnpm 就是包管理器,类似的有 npm 和 yarn。那么 pnpm 为什么而存在,摘自官方的解释:

Fast, disk space efficient package manager
  1. Fast. Up to 2x faster than the alternatives (see benchmark).
  2. Efficient. Files inside node_modules are linked from a single content-addressable storage.
  3. Great for monorepos.
  4. Strict. A package can access only dependencies that are specified in its package.json.
  5. Deterministic. Has a lockfile called pnpm-lock.yaml.
  6. Works everywhere. Supports Windows, Linux, and macOS.
  7. Battle-tested. Used in production by teams of all sizes since 2016.

pnpm 带来了两个重要的提升:

一、 节约磁盘空间并提升安装速度

在使用 npm 或 Yarn 时,如果你有 100 个项目使用了依赖,你就会在硬盘上存储 100 个依赖的副本。在使用 pnpm 时,依赖会被存储在内容可寻址的地方,所以:

  1. 如果你依赖于不同版本的依赖,那么只需将差异的文件添加到存储中心。例如,如果它有 100 个文件,而新版本只改变了其中 1 个文件。那么 pnpm update 只会向存储中心添加 1 个新文件,不会仅仅因为单一的改变而克隆整个依赖。
  2. 所有文件都会存储在硬盘上的同一位置。当多个包被安装时,所有文件都会从同一位置创建硬链接,不会占用额外的磁盘空间。这允许你跨项目地共享同一版本的依赖。最终你节省了大量与项目和依赖成比例的硬盘空间,并且拥有更快的安装速度!

saving disk space

二、 创建非扁平化的 node_modules 文件夹

当使用 pnpm 安装依赖时,所有的包会被提升或打平到模块的根目录。因此,项目还是可以访问到未被直接添加进来的依赖。pnpm 使用软链接(symlink)的方式将项目的直接依赖添加进模块文件夹的根目录,提高了安全性。具体后面会讨论,可以参考此文章

flat node_modules

pnpm 安装与使用

pnpm 的安装和使用也比较简单,具体命令可以查看官方文档:

# 安装及更新
npm install -g pnpm
# 一旦你安装了 pnpm,就无需再使用其他软件包管理器进行升级。 你可以使用 pnpm 升级自己
pnpm add -g pnpm

## 安装依赖 add
pnpm install
pnpm add express
pnpm add -D express # devDependencies

## 更新依赖 update
pnpm up
pnpm up --latest
pnpm up foo@2
pnpm up "@babel/*" # Updates all dependencies under the @babel scope

# 类似于 npx
pnpx create-react-app my-project

Benchmark 大乱斗

操作 cache lockfile node_modules npm pnpm yarn yarnPnP
install       51s 14.4s 39.1s 29.1s
install 5.4s 1.3s 707ms n/a
install   10.9s 3.9s 11s 1.8s
install     33.4s 6.5s 26.5s 17.2s
install     28.3s 11.8s 23.3s 14.2s
install   4.6s 1.7s 22.1s n/a
install   6.5s 1.3s 713ms n/a
install     6.1s 5.4s 41.1s n/a
update n/a n/a n/a 5.1s 10.7s 35.4s 28.3s

参与实验的包包们传送门 👈

一些操作的描述:

  1. clean install - How long it takes to run a totally fresh install: no lockfile present, no packages in the cache, no node_modules folder.
  2. with cache, with lockfile, with node_modules - After the first install is done, the install command is run again.
  3. with cache, with lockfile - When a repo is fetched by a developer and installation is first run.
  4. with cache - Same as the one above, but the package manager doesn’t have a lockfile to work from.
  5. with lockfile - When an installation runs on a CI server.
  6. with cache, with node_modules - The lockfile is deleted and the install command is run again.
  7. with node_modules, with lockfile - The package cache is deleted and the install command is run again.
  8. with node_modules - The package cache and the lockfile is deleted and the install command is run again.
  9. update - Updating your dependencies by changing the version in the package.json and running the install command again.

benchmark

yarn PnP

对于 npm 和 yarn 安装依赖而言,一般会遵循以下几个步骤:

  1. 将依赖包的版本区间解析为某个具体的版本号
  2. 下载对应版本依赖的 tar 包到本地离线镜像
  3. 将依赖从离线镜像解压到本地缓存
  4. 将依赖从缓存拷贝到当前目录的 node_modules 目录

其中第 4 步涉及大量的文件 I/O,导致安装依赖时效率不高。然后 Node 按照它的模块查找规则在 node_modules 目录中查找。但实际上 Node 并不知道这个模块是什么, 它在 node_modules 查找, 没找到就在父目录的 node_modules 查找, 以此类推,这个效率是非常低下的。

但是 Yarn 作为一个包管理器, 它知道你的项目的依赖树,那能不能让 Yarn 告诉 Node? 让它直接到某个目录去加载模块。这样即可以提高 Node 模块的查找效率, 也可以减少 node_modules 文件的拷贝,这就是 Plug’n’Play 即插即用的基本原理。Yarn 会维护一张静态映射表,该表中包含了以下信息:

  1. 当前依赖树中包含了哪些依赖包的哪些版本
  2. 这些依赖包是如何互相关联的
  3. 这些依赖包在文件系统中的具体位置

这个映射表的实现则对应项目目录中的 .pnp.js 文件,在安装依赖时,在第 3 步完成之后,Yarn 并不会拷贝依赖到 node_modules 目录,而是会在 .pnp.js 中记录下该依赖在缓存中的具体位置。这样就避免了大量的 I/O 操作同时项目目录也不会有 node_modules 目录生成。同时 .pnp.js 还包含了一个特殊的 resolver,Yarn 会利用这个特殊的 resolver 来处理 require() 请求,该 resolver 会根据 .pnp.js 文件中包含的静态映射表直接确定依赖在文件系统中的具体位置。

需要留意的是 .pnp.js 要添加到 .gitignore

这里再介绍一下修改 node_modules 目录下依赖来进行调试的场景,很显然在 pnp 模式下是没有 node_modules 的,但是 Yarn 提供了 yarn unplug packageName 来将某个指定依赖拷贝到项目中的 .pnp/unplugged 目录下。之后 .pnp.js 中的 resolver 就会自动加载这个 unplug 的版本。调试完毕后,再执行 yarn unplug --clear packageName 即可移除本地的对应依赖。

目前 pnp 在使用上还有一定风险,需要有良好的集成,无非就是重新实现现有工具的模块查找机制。比如 Webpack 使用的模块查找器是 enhanced-resolve, 可以通过 pnp-webpack-plugin 插件来进行扩展:

const PnpWebpackPlugin = require(`pnp-webpack-plugin`)

module.exports = {
  resolve: {
    plugins: [
      PnpWebpackPlugin,
    ],
  },
  resolveLoader: {
    plugins: [
      PnpWebpackPlugin.moduleLoader(module),
    ],
  },
}

开启 pnp 的方式有两种,在 package.json 中配置或者直接使用命令行:

// package.json
// 之后运行 yarn install 即可生成 .pnp.js
{
  "installConfig": {
    "pnp": true
  }
}
# 也会自动生成上述 installConfig 配置
yarn --pnp

为什么创建非扁平化的 node_modules

node_modules

首先我们看下 npm 和 yarn 是怎么管理依赖的,在 npm@3 之前,node_modules 的结构是很清晰的:

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json

但是这样会存在两个严重的问题:

  1. 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下。大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个 baz,两者都依赖于同一个版本的 lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
  2. 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

为了解决这些问题,从 npm3 开始,包括 yarn,都着手来通过扁平化依赖的方式来解决这个问题:

node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json

这样看来不再有很深层次的嵌套关系。安装新的包时,根据 node require 机制,会不停往上级的 node_modules 当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。之前的问题是解决了,但仔细想想这种扁平化的处理方式,它真的就是无懈可击吗?并不是。它照样存在诸多问题,梳理一下:

  1. 依赖结构的不确定性
  2. 扁平化算法本身的复杂性很高,耗时较长
  3. 项目中仍然可以非法访问没有声明过依赖的包

比如 foo 和 bar 都依赖于 a,但是两者版本不一致,前者为 a@1.0.0,而后者为 a@1.1.0。那么在打平的过程中,依赖的结构并不是确定的,和 package.json 中依赖申请的先后顺序也有关,这也是锁文件诞生的原因,即只要你目录下有 lock 文件,那么你每次执行 npm install 后生成的 node_modules 目录结构一定是完全相同的。

package-lock.json 中同时已经缓存了每个包的具体版本和下载链接,不需要再去远程仓库进行查询,然后直接进入文件完整性校验环节,即校验 hash 值,减少了大量网络请求

pnpm 则是在 npm@2 的基础上尝试去做改变,所有的包会把他们的依赖组合到一起,但是层级不会像 npm 一样太深,因为 pnpm 会通过 symlinks 软链接来进行组合,案例可以参考这里:

-> - a symlink (or junction on Windows)

node_modules
├─ foo -> .pnpm/foo/1.0.0/node_modules/foo
└─ .pnpm
   ├─ foo/1.0.0/node_modules
   |  ├─ bar -> ../../bar/2.0.0/node_modules/bar
   |  └─ foo
   |     ├─ index.js
   |     └─ package.json
   └─ bar/2.0.0/node_modules
      └─ bar
         ├─ index.js

通过查看案例,我们可能觉得结构会比较复杂,但对于大型项目而言,其结构看起来比 npm 创建的结构更好。首先,node_modules 根目录中的软件包只是一个 symlink 符号链接,Node.js 会忽略它并执行真正路径下的文件。 因此 require(’foo’) 将执行 node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/foo/index.js 中的文件,而不是 node_modules/foo/index.js 中的文件。

其次,所有已安装的软件包在其目录内都没有其自己的 node_modules 文件夹,那么 foo 如何引入 bar 的呢?让我们看一下包含 foo 包的文件夹结构:

node_modules/.pnpm/foo/1.0.0/node_modules
├─ bar -> ../../bar/2.0.0/node_modules/bar
└─ foo
   ├─ index.js
   └─ package.json

我们可以看到 foo 的依赖项 bar 已安装,但目录结构中的依赖项是上一级的。这两个软件包都在一个名为 node_modules 的文件夹中,那么依然根据 node require 机制,会不停往上级的 node_modules 当中去找,当然可以找到 bar。好家伙,.pnpm 目录这里其实还是做了扁平化处理,取其精华,这样子之后,在整个 node_modules 文件结构来看就十分清晰了。

我们最终不妨还是以安装 express 为例,node_modules 的结构对比如下:

<!-- npm -->
.bin
accepts
array-flatten
body-parser
bytes
content-disposition
cookie-signature
cookie
debug
depd
destroy
ee-first
encodeurl
escape-html
etag
express
<!-- pnpm -->
.pnpm
.modules.yaml
express

扁平化怎么处理不同版本的依赖

规则:当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。。以下面的 base64-js 为例子:

pnpm-node-modules

如果存在多个不同版本的子依赖,哪个版本打平在根目录则可能取决于 package.json 的放置顺序。先放置的会提取到根目录下,后放置的则只能在当前模块下安装。

一些安全问题

pnpm 采用了 npm@2 那一套依赖管理机制,其实也规避了非法访问依赖的问题。不妨来举一些例子:

场景一:如果 A 依赖 B, B 依赖 C,那么 A 就算没有声明 C 的依赖,但是 C 被打平到了 A 的 node_modules 里面,那我在 A 里面还是可以引用 C,那这样会有啥安全问题呢?

  1. 你要知道 B 的版本是可能随时变化的,假如之前依赖的是 C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了
  2. 如果 B 更新之后,可能不需要 C 了,那么安装依赖的时候,C 都不会装到 node_modules 里面,A 当中引用 C 的代码直接报错

场景二:在 monorepo 项目中,如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。

这些都是依赖提升潜在的 bug。如果是自己的业务代码还好,试想一下如果是给很多开发者用的工具包,那危害就非常严重了。npm 也有想过去解决这个问题,指定 --global-style 参数即可禁止变量提升,但这样做相当于回到了当年嵌套依赖的时代,一夜回到解放前,前面提到的嵌套依赖的缺点仍然暴露无遗。

npm/yarn 本身去解决依赖提升的问题貌似很难完成,不过社区针对这个问题也已经有特定的解决方案: dependency-check,但不可否认的是,pnpm 做的更加彻底,独创的一套依赖管理方式不仅解决了依赖提升的安全问题,还大大优化了时间和空间上的性能。

Monorepo vs Multirepo

multirepo 是我们目前采用最频繁的项目结构,针对于不同业务模块或功能会拆分成多个项目进行管理。但是对于一些开源项目或者工具库,monorepo 显然是一种比较好的项目管理方式,即把所有的相关项目都放在一个仓库中。业界中有很多优秀案例可以参考,比如 react-routerbabelJest等:

package.json
packages
├─ react-router-config
|  ├─ index.js
|  └─ package.json
├─ react-router-dom
|  ├─ index.js
|  └─ package.json
├─ react-router-native
|  ├─ index.js
|  └─ package.json
└─ react-router
   ├─ index.js
   └─ package.json

要是用 monorepo 仍然要面临一些问题,比如:

  1. 项目越来越庞大,对于版本控制技术会有很大的挑战。因为 Git 社区建议的是使用更多更小的代码库,Git 本身并不适合单个巨大的代码库
  2. 因为所有的代码都放在一起,所以你需要时刻保持警惕,以保持良好的项目结构和提交测试
  3. 不适合项目权限的分配

lerna init

在管理 monorepo 项目时,这里推荐使用 lerna。它是一种工具,可以优化使用 git 和 npm 管理多包存储库的工作流程;还可以减少开发和构建环境中大量软件包副本的时间和空间要求,这通常是将项目分成许多单独的 npm 软件包的缺点。我们先看看在项目中如何使用:

# 首先我们创建一个空文件夹
mkdir monorepo-test && cd monorepo-test
# 初始化
lerna init

初始化时会生成一个 lerna.json 文件,结构解读如下:

// lerna.json
{
  "version": "1.1.3",
  "npmClient": "npm",
  "command": {
    "publish": {
      "ignoreChanges": ["ignored-file", "*.md"],
      "message": "chore(release): publish",
      "registry": "https://npm.pkg.github.com"
    },
    "bootstrap": {
      "ignore": "component-*",
      "npmClientArgs": ["--no-package-lock"]
    }
  },
  "packages": ["packages/*"]
}
// package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}

lerna init 初始化时有两种模式可以选择:

还有一点我们需要注意的是 package.json 的 private 属性被设置为了 private: true,这是因为 monorepo 本身的这个 Git 仓库并不是一个项目,他是多个项目,所以他自己不能直接发布,发布的应该是 packages/ 下面的各个子项目。

接下来我们需要创建 package,可以手动也可以通过 lerna create 创建:

lerna create packageA

创建完后,我们可以看到默认的目录如下:

└── packageA
   ├── __tests__
   ├── lib
   ├── package.json
   └── README.md

每个子项目的 package.json 里的 name 推荐设置为 @scope/项目名 的格式,比如 @monorepo-test/packageA

lerna bootstrap

我们再通过 create-react-app 创建另外两个子项目,分别为 my-app 和 my-app-2,我们可以看到一个问题,packages/ 下面的每个子项目有自己的 node_modules,如果将它打开,会发现很多重复的依赖包,这会占用我们大量的硬盘空间。lerna 提供了另一个强大的功能:将子项目的依赖包都提取到最顶层:

# 删除已经安装的子项目 node_modules。可以手动删,也可以用 clean 命令
lerna clean

lerna bootstrap --hoist

lerna bootstrap 实际执行的操作如下,实现机制可以参考这里:

  1. npm install 安装每个软件包的所有外部依赖项
  2. 将所有相互依赖的 Lerna 子项目软链接(symlink)在一起
  3. 在所有引导程序包中运行 npm run prepublish(除非传递 –ignore-prepublish)
  4. 在所有引导程序包中运行 npm run prepare

该命令可以支持传递参数到 npm 命令中,或者在 lerna.json 中的 command.bootstrap.npmClientArgs 进行配置:

lerna bootstrap -- --production --no-optional

这里再推荐一些好用的 monorepo 工具集合 👈

yarn workspace vs pnpm workspace

上述提到,lerna bootstrap --hoist 虽然可以将子项目的依赖提升到顶层,但是他的方式比较粗暴:先在每个子项目运行 npm install,等所有依赖都安装好后,将他们移动到顶层的 node_modules。这会导致一个问题,如果多个子项目依赖同一个第三方库,但是版本不同怎么办?官方的解释是:

If packages depend on different versions of an external dependency, the most commonly used version will be hoisted, and a warning will be emitted.

但是这样的做法很明显还存在问题,虽然统一了第三方库的版本,但是某个子项目就是使用的旧版本 api 咋办,肯定会报错的。这时候可以使用 yarn workspace 功能,按照工作区间来决定引用第三方库的版本。我们需要修改 package.json 和 lerna.json:

// package.json
{
  "workspaces": [
    "packages/*"
  ]
}
// lerna.json
{
  "npmClient": "yarn",
  "useWorkspaces": true
}

使用了 yarn workspace 后,我们就不用 lerna bootstrap 来安装依赖了,而是像以前一样 yarn install 就行了,他会自动帮我们提升依赖,无论在顶层运行还是在任意一个子项目运行效果都是一样的。但是随之而来的还是一些安全问题,详细的可见上一节

好消息是 pnpm workspace 已经内置实现对 monorepo 的支持,我们不妨举个例子,假设:

  1. 有个 monorepo 项目,存在两个子项目,分别是 foo 和 bar
  2. foo 依赖 is-negative@1.0.0
  3. bar 依赖 is-positive@1.0.0

首先我们按照上述步骤配置 yarn workspace,生成的文件结构如下,可以参考这里:

├─ foo
├─ bar
├─ node_modules
|  ├─ is-negative
|  └─ is-positive
└─  package.json

显而易见,foo 和 bar 都能访问 is-negative 和 is-positive。接下来我们看 pnpm workspace 的配置。首先我们要在根目录下创建一个空的 pnpm-workspace.yaml 和一个 .npmrc 文件:

<!-- .npmrc -->
shared-workspace-shrinkwrap = true
link-workspace-packages = true

接下来输入命令行 pnpm multi install 进行工作区依赖的安装。生成的文件结构如下,可以参考这里:

├─ foo
|  ├─ node_modules
|  | └─  is-negative(symlink)
├─ bar
|  ├─ node_modules
|  | └─  is-positive(symlink)
├─ bar
├─ node_modules
|  ├─ .registry.npmjs.org
|  | ├─ is-negative/1.0.0/node_modules/is-negative
|  | └─ is-positive/1.0.0/node_modules/is-positive
|  ├─ .modules.yaml
|  └─  .shrinkwrap.yaml
├─ .npmrc
├─ pnpm-workspace.yaml
└─  shrinkwrap.yaml

pnpm 这里和 yarn 一样都生成了 node_modules 文件,但是不一样的是,这里的依赖都隐藏在 .registry.npmjs.org 中,node 的查询机制将无法找到。而 foo 的 node_modules 下有个 is-negative 的软链接,指向 ../../node_modules/.registry.npmjs.org/is-negative/1.0.0/node_modules/is-negative,同理 bar 也是。这样子也解决了 yarn workspace 带来的安全问题。

lerna run/add

接下来我们要启动子项目,可以去那个目录下运行 yarn start,但是频繁切换文件夹实在是太麻烦了。我们可以直接通过 lerna 进行启动:

# 相当于去每个子项目下面都去执行 yarn run start
lerna run [script]

# 通过 --scope 指定子项目
lerna run --scope @monorepo-test/packageA test

如果我们要为某个子项目添加依赖,我们直接在对应 package.json 手动改,也可以通过命令行实现:

lerna add @a/dependence --scope @monorepo-test/packageA

lerna publish

我们执行发布的时候,如果模式选择的是默认,则会给你选择一个统一的版本号;如果是 independent 模式则会分别选择子项目的版本:

lerna publish
# info cli using local version of lerna
# lerna notice cli v4.0.0
# lerna info versioning independent
# lerna info Assuming all packages changed
# ? Select a new version for my-app-2 (currently 0.1.0) Patch (0.1.1)
# ? Select a new version for my-app (currently 0.1.0) Major (1.0.0)

当然前提是我们的子项目已经监测到了修改,我们也可以通过命令来查看:

lerna changed                                                                                                                           master 
# info cli using local version of lerna
# lerna notice cli v4.0.0
# lerna info versioning independent
# lerna info Looking for changed packages since my-app-2@0.1.3
# my-app-2
# lerna success found 1 package ready to publish

另外需要注意的是,子项目 package.json 里 private 设置为 true 的话,是没办法发布到对应私服的,会提示:

npm ERR! This package has been marked as private
npm ERR! Remove the 'private' field from the package.json to publish it.

完整的打印如下:

[my-app-2] lerna publish                                                                                                                           master 
# info cli using local version of lerna
# lerna notice cli v4.0.0
# lerna info versioning independent
# lerna info Looking for changed packages since my-app-2@0.1.2
# ? Select a new version for my-app-2 (currently 0.1.2) Patch (0.1.3)
# 
# Changes:
#  - my-app-2: 0.1.2 => 0.1.3
# 
# ? Are you sure you want to publish these packages? Yes
# lerna info execute Skipping releases
# lerna info git Pushing tags...
# lerna info publish Publishing packages to npm...
# lerna notice Skipping all user and access validation due to third-party registry
# lerna notice Make sure you're authenticated properly ¯\_(ツ)_/¯
# lerna WARN ENOLICENSE Package my-app-2 is missing a license.
# lerna WARN ENOLICENSE One way to fix this is to add a LICENSE.md file to the root of this repository.
# lerna WARN ENOLICENSE See https://choosealicense.com for additional guidance.
# lerna http fetch PUT 201 http://localhost:4873/my-app-2 68ms
# lerna success published my-app-2 0.1.3
# lerna notice 
# lerna notice 📦  my-app-2@0.1.3
# lerna notice === Tarball Contents === 
# lerna notice 564B  src/App.css           
# lerna notice 366B  src/index.css         
# lerna notice 1.7kB public/index.html     
# lerna notice 3.9kB public/favicon.ico    
# lerna notice 532B  src/App.js            
# lerna notice 246B  src/App.test.js       
# lerna notice 500B  src/index.js          
# lerna notice 362B  src/reportWebVitals.js
# lerna notice 241B  src/setupTests.js     
# lerna notice 492B  public/manifest.json  
# lerna notice 916B  package.json          
# lerna notice 3.4kB README.md             
# lerna notice 5.3kB public/logo192.png    
# lerna notice 9.7kB public/logo512.png    
# lerna notice 2.6kB src/logo.svg          
# lerna notice 67B   public/robots.txt     
# lerna notice === Tarball Details === 
# lerna notice name:          my-app-2                                
# lerna notice version:       0.1.3                                   
# lerna notice filename:      my-app-2-0.1.3.tgz                      
# lerna notice package size:  25.1 kB                                 
# lerna notice unpacked size: 30.9 kB                                 
# lerna notice shasum:        4723436ec58197c91534699763d02ef35aa3930c
# lerna notice integrity:     sha512-losmxDXqWBxgA[...]bAud2srazQltg==
# lerna notice total files:   16                                      
# lerna notice 
# Successfully published:
#  - my-app-2@0.1.3
# lerna success published 1 package

完整 demo 可以参考下这里 👈

lerna version

lerna version 主要的工作为标识出在上一个 tag 版本以来更新的 monorepo package,然后为这些包 prompt 出版本,在用户完成选择之后修改相关包的版本信息并且将相关的变动 commit 然后打上 tag 推送到 git remote:

  1. Identifies packages that have been updated since the previous tagged release.
  2. Prompts for a new version.
  3. Modifies package metadata to reflect new release, running appropriate lifecycle scripts in root and 1. per-package.
  4. Commits those changes and tags the commit.
  5. Pushes to the git remote.
lerna version 1.0.1 # explicit
lerna version [major | minor | patch | premajor | preminor | prepatch | prerelease] # semver keyword
lerna version       # select from prompt(s)

同时也支持诸多 options,具体查看官方文档

# --conventional-commits - will use the Conventional Commits Specification to determine the version bump and generate CHANGELOG.md files.
# --yes - will skip all confirmation prompts. 
lerna version --conventional-commits  --yes

lifecycle scripts

Lerna will run npm lifecycle scripts during lerna version in the following order:

  1. Detect changed packages, choose version bump(s)
  2. Run preversion lifecycle in root
  3. For each changed package, in topological order (all dependencies before dependents):
    1. Run preversion lifecycle
    2. Update version in package.json
    3. Run version lifecycle
  4. Run version lifecycle in root
  5. Add changed files to index, if enabled
  6. Create commit and tag(s), if enabled
  7. For each changed package, in lexical order (alphabetical according to directory structure):
    1. Run postversion lifecycle
  8. Run postversion lifecycle in root
  9. Push commit and tag(s) to remote, if enabled
  10. Create release, if enabled

其他科普

硬链接、软链接、复制

  1. 硬链接 - 硬链接实际上是为文件建一个别名,链接文件和原文件实际上是同一个文件。通过 ls -i 来查看的话,这两个文件的 inode 号是同一个,即属于同一个文件
  2. 软链接 - 相当于原文件的快捷方式。具体理解的话,链接文件内存储的是原文件的 inode,也就是说是用来指向原文件文件,这两个文件的 inode 是不一样的
  3. 复制 - 相当于将原文件进行一个拷贝,为另一个全新的文件。修改任何一个都不会影响另一个
# 硬链接
ln source source1 
# 软链接
ln -s source source1 
# 复制
cp source source1

参考链接

  1. Flat node_modules is not the only way - pnpm by Zoltan Kochan
  2. 关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn? by 神三元
  3. npm install 原理分析 by ConardLi
  4. Yarn 的 Plug’n’Play 特性 by loveky
  5. Yarn Plug’n’Play可否助你脱离node_modules苦海? by 荒山
  6. 使用 mono-repo 实现跨项目组件共享 by _蒋鹏飞
  7. 深入 lerna 发包机制 —— lerna version by zoomdong