pnpm & Monorepo
什么是 pnpm
pnpm 就是包管理器,类似的有 npm 和 yarn。那么 pnpm 为什么而存在,摘自官方的解释:
Fast, disk space efficient package manager
- Fast. Up to 2x faster than the alternatives (see benchmark).
- Efficient. Files inside node_modules are linked from a single content-addressable storage.
- Great for monorepos.
- Strict. A package can access only dependencies that are specified in its package.json.
- Deterministic. Has a lockfile called
pnpm-lock.yaml
. - Works everywhere. Supports Windows, Linux, and macOS.
- Battle-tested. Used in production by teams of all sizes since 2016.
pnpm 带来了两个重要的提升:
一、 节约磁盘空间并提升安装速度
在使用 npm 或 Yarn 时,如果你有 100 个项目使用了依赖,你就会在硬盘上存储 100 个依赖的副本。在使用 pnpm 时,依赖会被存储在内容可寻址的地方,所以:
- 如果你依赖于不同版本的依赖,那么只需将差异的文件添加到存储中心。例如,如果它有 100 个文件,而新版本只改变了其中 1 个文件。那么
pnpm update
只会向存储中心添加 1 个新文件,不会仅仅因为单一的改变而克隆整个依赖。 - 所有文件都会存储在硬盘上的同一位置。当多个包被安装时,所有文件都会从同一位置创建硬链接,不会占用额外的磁盘空间。这允许你跨项目地共享同一版本的依赖。最终你节省了大量与项目和依赖成比例的硬盘空间,并且拥有更快的安装速度!
二、 创建非扁平化的 node_modules 文件夹
当使用 pnpm 安装依赖时,所有的包会被提升或打平到模块的根目录。因此,项目还是可以访问到未被直接添加进来的依赖。pnpm 使用软链接(symlink)的方式将项目的直接依赖添加进模块文件夹的根目录,提高了安全性。具体后面会讨论,可以参考此文章
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 |
参与实验的包包们传送门 👈
一些操作的描述:
- clean install - How long it takes to run a totally fresh install: no lockfile present, no packages in the cache, no node_modules folder.
- with cache, with lockfile, with node_modules - After the first install is done, the install command is run again.
- with cache, with lockfile - When a repo is fetched by a developer and installation is first run.
- with cache - Same as the one above, but the package manager doesn’t have a lockfile to work from.
- with lockfile - When an installation runs on a CI server.
- with cache, with node_modules - The lockfile is deleted and the install command is run again.
- with node_modules, with lockfile - The package cache is deleted and the install command is run again.
- with node_modules - The package cache and the lockfile is deleted and the install command is run again.
- update - Updating your dependencies by changing the version in the package.json and running the install command again.
yarn PnP
对于 npm 和 yarn 安装依赖而言,一般会遵循以下几个步骤:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应版本依赖的 tar 包到本地离线镜像
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的 node_modules 目录
其中第 4 步涉及大量的文件 I/O,导致安装依赖时效率不高。然后 Node 按照它的模块查找规则在 node_modules 目录中查找。但实际上 Node 并不知道这个模块是什么, 它在 node_modules 查找, 没找到就在父目录的 node_modules 查找, 以此类推,这个效率是非常低下的。
但是 Yarn 作为一个包管理器, 它知道你的项目的依赖树,那能不能让 Yarn 告诉 Node? 让它直接到某个目录去加载模块。这样即可以提高 Node 模块的查找效率, 也可以减少 node_modules 文件的拷贝,这就是 Plug’n’Play 即插即用的基本原理。Yarn 会维护一张静态映射表,该表中包含了以下信息:
- 当前依赖树中包含了哪些依赖包的哪些版本
- 这些依赖包是如何互相关联的
- 这些依赖包在文件系统中的具体位置
这个映射表的实现则对应项目目录中的 .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
首先我们看下 npm 和 yarn 是怎么管理依赖的,在 npm@3 之前,node_modules 的结构是很清晰的:
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
但是这样会存在两个严重的问题:
- 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下。大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个 baz,两者都依赖于同一个版本的 lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
- 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。
为了解决这些问题,从 npm3 开始,包括 yarn,都着手来通过扁平化依赖的方式来解决这个问题:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
这样看来不再有很深层次的嵌套关系。安装新的包时,根据 node require 机制,会不停往上级的 node_modules 当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。之前的问题是解决了,但仔细想想这种扁平化的处理方式,它真的就是无懈可击吗?并不是。它照样存在诸多问题,梳理一下:
- 依赖结构的不确定性
- 扁平化算法本身的复杂性很高,耗时较长
- 项目中仍然可以非法访问没有声明过依赖的包
比如 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 为例子:
如果存在多个不同版本的子依赖,哪个版本打平在根目录则可能取决于 package.json 的放置顺序。先放置的会提取到根目录下,后放置的则只能在当前模块下安装。
一些安全问题
pnpm 采用了 npm@2 那一套依赖管理机制,其实也规避了非法访问依赖的问题。不妨来举一些例子:
场景一:如果 A 依赖 B, B 依赖 C,那么 A 就算没有声明 C 的依赖,但是 C 被打平到了 A 的 node_modules 里面,那我在 A 里面还是可以引用 C,那这样会有啥安全问题呢?
- 你要知道 B 的版本是可能随时变化的,假如之前依赖的是 C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了
- 如果 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-router、babel 、Jest等:
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 仍然要面临一些问题,比如:
- 项目越来越庞大,对于版本控制技术会有很大的挑战。因为 Git 社区建议的是使用更多更小的代码库,Git 本身并不适合单个巨大的代码库
- 因为所有的代码都放在一起,所以你需要时刻保持警惕,以保持良好的项目结构和提交测试
- 不适合项目权限的分配
lerna init
在管理 monorepo 项目时,这里推荐使用 lerna。它是一种工具,可以优化使用 git 和 npm 管理多包存储库的工作流程;还可以减少开发和构建环境中大量软件包副本的时间和空间要求,这通常是将项目分成许多单独的 npm 软件包的缺点。我们先看看在项目中如何使用:
# 首先我们创建一个空文件夹
mkdir monorepo-test && cd monorepo-test
# 初始化
lerna init
初始化时会生成一个 lerna.json
文件,结构解读如下:
- version - 项目当前版本号,独立(independent)模式下值为 independent
- npmClient - 用于指定要运行命令的特定客户端,比如 yarn
- command.publish.ignoreChanges - lerna changed/publish 忽略的文件改动,比如 README.md
- command.publish.message - 发布的提交描述
- command.publish.registry - 设置发布的 registry
- command.bootstrap.ignore - 执行
lerna bootstrap
启动时忽略的文件 - command.bootstrap.npmClientArgs - 在
lerna bootstrap
命令期间,将作为参数直接传递给npm install
的字符串数组 - command.bootstrap.scope - 用于限制在运行
lerna bootstrap
命令时将引导哪些软件包 - packages - 项目 packages 仓库存放的路径,推荐为 [“packages/*”]
// 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
初始化时有两种模式可以选择:
- Fixed/Locked mode - 即所有包的版本都是统一的,体现在
lerna.json
的 version 配置上。当运行lerna publish
时,如果某个模块自上次发布以来已被更新,则它将被更新为要发布的新版本 - independent mode - 独立模式 Lerna 项目允许维护者彼此独立地增加软件包的版本。每次发布时,都会提示已更改的每个软件包,以指定是 patch, minor, major 还是自定义更改
还有一点我们需要注意的是 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
实际执行的操作如下,实现机制可以参考这里:
- npm install 安装每个软件包的所有外部依赖项
- 将所有相互依赖的 Lerna 子项目软链接(symlink)在一起
- 在所有引导程序包中运行 npm run prepublish(除非传递 –ignore-prepublish)
- 在所有引导程序包中运行 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 的支持,我们不妨举个例子,假设:
- 有个 monorepo 项目,存在两个子项目,分别是 foo 和 bar
- foo 依赖 is-negative@1.0.0
- 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:
- Identifies packages that have been updated since the previous tagged release.
- Prompts for a new version.
- Modifies package metadata to reflect new release, running appropriate lifecycle scripts in root and 1. per-package.
- Commits those changes and tags the commit.
- 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:
- Detect changed packages, choose version bump(s)
- Run preversion lifecycle in root
- For each changed package, in topological order (all dependencies before dependents):
- Run preversion lifecycle
- Update version in package.json
- Run version lifecycle
- Run version lifecycle in root
- Add changed files to index, if enabled
- Create commit and tag(s), if enabled
- For each changed package, in lexical order (alphabetical according to directory structure):
- Run postversion lifecycle
- Run postversion lifecycle in root
- Push commit and tag(s) to remote, if enabled
- Create release, if enabled
其他科普
硬链接、软链接、复制
- 硬链接 - 硬链接实际上是为文件建一个别名,链接文件和原文件实际上是同一个文件。通过 ls -i 来查看的话,这两个文件的 inode 号是同一个,即属于同一个文件
- 软链接 - 相当于原文件的快捷方式。具体理解的话,链接文件内存储的是原文件的 inode,也就是说是用来指向原文件文件,这两个文件的 inode 是不一样的
- 复制 - 相当于将原文件进行一个拷贝,为另一个全新的文件。修改任何一个都不会影响另一个
# 硬链接
ln source source1
# 软链接
ln -s source source1
# 复制
cp source source1
参考链接
- Flat node_modules is not the only way - pnpm by Zoltan Kochan
- 关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn? by 神三元
- npm install 原理分析 by ConardLi
- Yarn 的 Plug’n’Play 特性 by loveky
- Yarn Plug’n’Play可否助你脱离node_modules苦海? by 荒山
- 使用 mono-repo 实现跨项目组件共享 by _蒋鹏飞
- 深入 lerna 发包机制 —— lerna version by zoomdong