在之前做组件库的多版本共存时,我遇到了这样一个问题:PNPM 如何安装多个版本的包?似乎唯一的解决方法是使用 alias(下文的 components
和 business-components
都是示例包名,不是 NPM 上面实际的包):
pnpm i components@3
pnpm i components-next@npm:components@4
import xxxV3 from 'components';
import xxxV4 from 'components-next';
但其实我的项目中还有另外一个组件库,也就是基于 components
封装的 business-components
:
// 项目的 package.json
{
"dependencies": {
"components": "3",
"components-next": "npm:components@4",
"business-components": "3",
"business-components-next": "npm:business-components@4"
}
}
// business-components@3 的 package.json
{
"peerDependencies": {
"components": "3"
}
}
// business-components@4 的 package.json
{
"peerDependencies": {
"components": "4"
}
}
看似很合理,但由于 PNPM 生成 node_modules
的算法没有考虑到 alias(有人提了个 issue),会导致 business-components@4
对应的 peers 成了 components@3
,从而无法达到目的。我当时使用了 hooks 以及在安装后执行额外的 shell 代码来解决它。不过这样的修改太大了,而且当依赖过多的时候,shell 代码也会变得很长。因此最优雅的办法还是让 PNPM 在解析 peers 的时候支持 alias。
PNPM 的 peers 解析规则
官方的文档 How peers are resolved 简单讲了一下 PNPM 的 peers 解析规则。它的目标是:尽可能保证在树形结构下,按照 Node.js 的查找规则,每个包可以找到已安装的、版本正确的 peers。例如有如下的一个依赖树:
// foo-parent-1 的 package.json
{
"dependencies": {
"foo": "1.0.0",
"bar": "1.0.0"
}
}
// foo-parent-2 的 package.json
{
"dependencies": {
"foo": "1.0.0",
"bar": "1.1.0"
}
}
// foo 的 package.json
{
"peerDependencies": {
"bar": "^1.0.0"
}
}
可以看到:
foo-parent-1
和foo-parent-2
都在dependencies
中依赖了foo
和bar
,但是bar
的版本不同;foo
也通过peerDependencies
表明自己需要bar
。
PNPM 可以保证:如果我在项目中同时安装了 foo-parent-1
和 foo-parent-2
,那么 foo-parent-1
会使用 [email protected]
,foo-parent-2
会使用 [email protected]
,这符合它们两个包自身的预期。要做到这一点,PNPM 会在 node_modules
中生成如下的结构,将 foo
-bar
的版本绑定起来:
# 此处使用 PNPM lockfile v6 格式的目录名
.pnpm
├── [email protected]([email protected])
│ └── node_modules
│ ├── foo
│ └── bar -> ../../[email protected]/node_modules/bar
├── [email protected]([email protected])
│ └── node_modules
│ ├── foo
│ └── bar -> ../../[email protected]/node_modules/bar
├── [email protected]
│ └── node_modules
│ └── bar
└── [email protected]
└── node_modules
└── bar
然后将 [email protected]([email protected])
软链接到 foo-parent-1
的 node_modules
下,将 [email protected]([email protected])
软链接到 foo-parent-2
的 node_modules
下。这样foo-parent-1
和 foo-parent-2
都可以使用正确版本的 bar
了。
NPM(v3+)和 Yarn(使用 linker: node-modules
)使用了扁平化 node_modules
,它们也可以保证这一点,方式是:先把第一个安装的 [email protected]
hoist 到 node_modules
根部,然后发现了第二个包 [email protected]
,因为无法再次 hoist,于是就保留在了 foo-parent-2/node_modules
中。但这样会有一个问题——可能会造成大量的重复安装。以前端著名的库 core-js
为例,如果先被安装的是一个比较旧的包,它依赖了 core-js@2
,随后装的一系列新包都依赖了 core-js@3
,那么 core-js@3
就会被重复安装到每个新包的 node_modules
下。
在解析的过程中,PNPM 会报两类错误:
- 缺失 peers:如果一个包的 peers 未安装,就会报这个错(可以开启
auto-install-peers
选项来自动安装缺失的 peers); - 版本不匹配:如果一个包的 peers 版本不匹配,如上面例子中的
foo-parent-2
如果依赖了[email protected]
,不满足[email protected]
的peerDependencies
约束^1.0.0
,就会报这个错(可以开启strict-peer-dependencies
选项来忽略版本不匹配的 peers)。PNPM 此时会尽可能地使用已安装的包来满足需求,最终会生成[email protected]([email protected])
目录。
但是 PNPM 在解析的时候,没有考虑到 alias 的情况,所以在我们组件库的场景下它找不到 components@4
,会报“版本不匹配”,同时生成一个 business-components@4(components@3)
的目录,从而导致 business-components@4
错误地使用了 components@3
。
改进思路、PR,以及后续的 issues
通过追代码发现,peers 的解析规则是在 pkg-manager/resolve-dependencies/src/resolvePeers.ts 文件中:
// 对于当前 package 的 peerDependencies 中的每个 peer
for (const peerName in ctx.resolvedPackage.peerDependencies) {
// 从一个全局的列表中查找是否已经安装了这个 peer
const resolved = ctx.parentPkgs[peerName]
// 如果没有找到,则为“缺失 peers”
if (!resolved) {
ctx.peerDependencyIssues.missing[peerName].push({/* ... */})
// ...
// 并且跳过后续的步骤,不再检查这个 peer
continue
}
// 如果 semver 不满足要求,则为“版本不匹配”
if (!semverUtils.satisfiesWithPrereleases(resolved.version, peerVersionRange, true)) {
// ...
ctx.peerDependencyIssues.bad[peerName].push({/* ... */})
}
// ...
// 不管版本是否匹配,都会记录下这个 peer 的版本
resolvedPeers[peerName] = resolved.nodeId
}
最一开始我的思路是:扩展一下 ctx.parentPkgs
记录每一个出现过的版本,这样就可以在每次都逐一匹配 semver 是否满足要求了。这样 peers 的解析问题会比以前减少很多。但是 Zoltan(PNPM 的作者)提出了几个疑惑:
- 如果有多个版本,那么应该使用哪个版本?
- 如果有一个包被 alias 成了别的包名(例如把
evil.js
alias 成了lodash
,并且项目也依赖了lodash
),那么应该使用哪个包?
经过与 Zoltan 的一番讨论, Zoltan 认为 peer dependencies are meant to be singletons,也就是我不能这样做。仔细一想也有道理,这样做会导致不同的包跟不同的 peers 绑定:
.pnpm
├── [email protected]([email protected])
│ └── node_modules
│ ├── react-dom
│ └── react -> ../../[email protected]/node_modules/react
└── [email protected]([email protected])
└── node_modules
├── react-router
└── react -> ../../[email protected]/node_modules/react
项目里面出现两个 react
会有致命的问题,例如 useContext
由于不是同一个 React 实例,会导致无法获取到正确的 context。
此外,PNPM 也需要照顾到 NPM 和 Yarn 的解析算法:
- NPM 和 Yarn 的扁平化
node_modules
结构无法保证 hoist 的包每次都是固定的,这取决于在依赖图中哪个版本更靠前。还是以core-js
为例,如果core-js@2
被 hoist 到根部,那么其它通过peerDependencies
依赖core-js
的包就会使用core-js@2
。 - PNPM 的兼容策略是在
parentPkgs
里面只追加新的 package,而不覆盖已有的 package,这样在解析 peers 的时候就会拿第一次遇到的 package 版本了。
所以 PNPM 宁可报“版本不匹配”错误,也不允许在 parentPkgs
中记录多个不同版本的 peers。
最终定下来的思路是(参考 PR 的修改 pkg-manager/resolve-dependencies/src/resolvePeers.ts):
- 依旧维护全局的
parentPkgs
,不过将其重命名为parentRefs
,表示保存的不仅仅是 package,而是名字到 package 的引用关系; 新增一个
updateParentRefs
函数,以替代之前直接对parentPkgs
的赋值操作:- 如果
parentRefs
中不存在此引用,就将其加入到parentRefs
中; 否则:
- 如果已存在的引用不是 alias,就什么也不做(确保了 alias 不会覆盖 non-alias);
- 如果已存在的引用是 alias,就保留 semver 更高的包(确保了之前的行为不变);
- 如果
- 在调用
updateParentRefs
时,如果发现这个包是个 alias,那么对于其原始的包名也要调用一次updateParentRefs
(确保原始名字也能被正确地记录)。
PNPM 也不会试图解决所有的 peers 问题,如果项目中确实有这种需求,应当由项目开发者通过 pnpm.packageExtensions
来实现。例如在这个 PR 合并后,我的场景可以这样配置:
{
"dependencies": {
"components": "3",
"components-next": "npm:components@4",
"business-components": "3",
"business-components-next": "npm:business-components@4"
},
"pnpm": {
"packageExtensions": {
// 因为 peers 会取最新的版本,因此需要约束旧库使用旧的 peers
"business-components@3": {
"dependencies": {
"components-peer": "npm:components@3"
}
}
}
}
}
通过给 business-components@3
的 dependencies
明确添加一项,就可以让 PNPM 安装两个版本的 components
,同时在解析 peers 的时候可以正确找到 components-next
对应的 components@4
了。注意必须是添加 components-peer
(或其它合理的 alias),而不能是 components
,因为按照 PNPM 的规则,packageExtensions
不能覆盖已有的字段。
PR 链接:#6220 feat: support resolving peers with npm aliases
在合并之后,PNPM 发了 v7.30.1
版本,但马上有人反馈:在安装的时候无法删除 peers(issue #6271)、执行 pnpm update -r
时错误地删除了 link 的包(issue #6272)。这两个问题我都没有考虑到,但幸亏 Zoltan 及时找出原因并修复了问题(PR #6275),原因是在 getTopParents
的参数中,传进来的参数是两个集合之差:
package.json
里面指定的全部依赖,包括dependencies
、devDependencies
、optionalDependencies
;- 本次命令中新安装的依赖,以及通过
pnpm link
引入的依赖。
Issue #6271 的问题是,当我们添加了 alias 的支持后,getTopParents
会将 alias 也当作一个依赖,但实际上 alias 不应该被当作一个依赖,所以在 getTopParents
的参数中,应当将 alias 过滤掉。Issue #6272 的问题不是这个 PR 引入的,是因为之前的行为是要在 getTopParents
中先过滤掉 pnpm link
引入的包,因此在执行 pnpm update -r
命令的时候,link 的包被错误地删除了。
在 PR 的过程中,Zoltan 的回复非常及时,并且在我遇到困难的时候会提示我如何解决(甚至帮我改代码、添加测试用例)。在讨论的过程中以及后续的两个 issue 里我也意识到包管理器需要考虑的问题非常多,一个小小的修改可能会造成意料之外的影响。
PNPM 项目概况,以及如何调试
随着 PNPM 7.30.3
的发布,我的需求结束了。仔细总结一下发现 PNPM 有很多值得研究的地方,但网上可用的资料非常少,PNPM 自身也没有一个详细的开发文档,因此在这里记录一下我的心得,也希望可以帮助到所有想给 PNPM 做出贡献、但苦于不知道如何入手的开发者。
PNPM 的代码结构
本节参考自奇舞团的文章 《pnpm 源码结构及调试指南》。文章写于一年前,其中的一些内容已经有所改变,因此我会在此基础上做一些补充。
PNPM 自身也使用了 PNPM 作为包管理器,得益于其强大的 workspace 功能,PNPM 将项目拆分成了很多子包,通过两级目录可以方便地按功能查找。我没有做特别细致的研究,但是通过命名、README、测试代码、入口文件的导出内容,已经足够了解这些包大概是用来做什么的了,也得以一窥 PNPM 众多的命令、支持的协议和特性:
cli
├── cli-meta # 获取当前运行的 PNPM 的元数据
├── cli-utils # PNPM 指令使用的工具函数
├── command # PNPM 指令的类型定义
├── command-cli-options-help # 输出 cli 的帮助信息
├── default-reporter # 输出日志和报错
└── parse-cli-args # 解析命令行参数
config
├── config # 从命令行参数、npmrc 等位置获取配置
├── matcher # 简单的匹配规则,可用于 list、--hoist-pattern 等地方
├── normalize-registries # 标准化 registry 的地址以便后续拼接
├── package-is-installable # 判断包能否在当前平台、Node 版本等限制中安装
├── parse-overrides # 用于 resolution 等覆盖版本号的场景
├── pick-registry-for-package # 支持不同的包使用不同的 registry
└── plugin-commands-config # 入口:pnpm config 命令
env
├── node.fetcher # 获取 Node.js 的安装包(与 nvm 类似,PNPM 可以管理 Node.js 版本:pnpm env use --global 18)
├── node.resolver # 根据限制(如 rc、nightly)计算出正确的 Node.js 的版本号
└── plugin-commands-config # 入口:pnpm env 命令
exec
├── build-modules # 按照拓扑序,把包添加到 node_modules 中,并执行对应的 link bin、post install 等操作
├── lifecycle # 执行包的生命周期脚本,如 preinstall、install、postinstall 等
├── plugin-commands-rebuild # 入口:pnpm rebuild 命令
├── plugin-commands-script-runners # 入口:pnpm create/dlx/exec/restart/run 命令
├── prepare-package # 处理 pnpm prepare 相关的事项
└── run-npm # 执行 npm 脚本,例如在 pnpm config get 时也需要获取 npm config
fetching
├── directory-fetcher # 从本地目录获取包的文件
├── fetcher-base # fetcher 的类型定义
├── git-fetcher # 从 git 仓库获取包的文件
├── pick-fetcher # 根据包的来源,选择合适的 fetcher
└── tarball-fetcher # 从 tarball 中获取包的文件
fs
├── find-packages # 从指定目录中找到所有包
├── graceful-fs # graceful-fs 包的 Promise 版本
├── hard-link-dir # 硬链接相关的操作
├── indexed-pkg-importer # 确定要通过硬链接还是复制来安装包
├── read-modules-dir # 读取 node_modules 下面一级包(包括 @xxx/yyy 这类包)的信息
└── symlink-dependency # 创建软链接
hooks
├── pnpmfile # 与 .pnpmfile.js 相关的操作
└── read-package-hook # Hook:readPackage
lockfile
├── audit # 读取 lockfile,检查是否有安全问题
├── filter-lockfile # 通过已安装的包、引擎版本等信息,过滤 lockfile
├── lockfile-file # 读写 pnpm-lock.yaml,包括自动解决 Git 冲突的逻辑
├── lockfile-to-pnp # 将 lockfile 转换为 Yarn 2+ 的 .pnp.cjs 文件,以便在 Yarn 2+ 中使用 PNPM 安装的包
├── lockfile-types # lockfile 的类型定义
├── lockfile-utils # 一系列工具函数
├── lockfile-walker # 读取 lockfile
├── merge-lockfile-changes # 合并 lockfile 的变更,解决 Git 冲突所需的底层函数
├── plugin-commands-audit # 入口:pnpm audit 命令
└── prune-lockfile # 从 lockfile 中移除不再需要的包
network
├── auth-header # 生成不同类型的 Authorization 头
├── fetch # node-fetch 的封装,支持重试
└── fetching-types # fetching 的类型定义
packages
└── ... # 一些没有具体分类的包
patching
├── apply-patch # 从 patch 包中获取文件,然后应用到 node_modules 的包中
└── plugin-commands-patching # 入口:pnpm patch/patch-commit 命令
pkg-manager
├── client # 与 registry 通信的客户端,在 resolve package 阶段调用
├── core # PNPM 的核心功能,大量调用了其它包来完成工作,这里的 test 代码多为完整的 E2E 测试
├── direct-dep-linker # 在有 lockfile 的时候快速执行 link 阶段
├── get-context # 获取当前 PNPM 运行的一切所需信息,以 context 的形式传递到其它包中
├── headless # headless 模式,用于 prefer-frozen-lockfile 的场景,这会跳过解析依赖的步骤,且不会修改 lockfile
├── hoist # 将包提升到 node_modules 的一级目录(不推荐)
├── link-bins # 将包的 bin 链接到 node_modules/.bin 中
├── modules-cleaner # 一些用于清理 node_modules 的工具函数
├── modules-yaml # 读写 node_modules/.modules.yaml(PNPM 所需的一些信息)
├── package-bins # 获取包的 bin 信息
├── package-requester # 从 registry 下载包(并发下载)
├── plugin-commands-installation # 入口:pnpm add/dedupe/i/fetch/link/prune/remove/unlink/update 命令
├── read-projects-context # 读取项目的信息
├── real-hoist # 与 hoist 包类似(但我没看懂它们的区别)
├── remove-bins # 移除包的 bin
└── resolve-dependencies # 解析依赖,生成依赖树,包括了 peers、links 的解析
pkg-manifest
├── exportable-manifest # 在发布前根据 publishConfig 等配置来修改 package.json
├── manifest-utils # 一些工具函数
├── read-package-json # 读取 package.json 的内容
├── read-project-manifest # 读取项目的 manifest,包括 package.json、package.json5、package.yaml
└── write-project-manifest # 写入项目的 manifest
pnpm
└── ... # PNPM 的主包
releasing
├── plugin-commands-deploy # 入口:pnpm deploy 命令
└── plugin-commands-publishing # 入口:pnpm pack/publish 命令
resolving
├── default-resolver # 默认的 resolver,会依次尝试 NPM、tarball、Git、本地源
├── git-resolver # 从 Git 仓库中获取文件地址
├── local-resolver # 从本地源中获取文件地址
├── npm-resolver # 从 NPM 源中获取文件地址
├── resolver-base # resolver 的类型定义
└── tarball-resolver # 从 tarball 中获取文件地址
reviewing
├── dependencies-hierarchy # 生成依赖树
├── license-scanner # 扫描包的 license
├── list # 打印已安装的包列表
├── outdated # 打印已安装的包中,有更新的包列表
├── plugin-commands-licenses # 入口:pnpm licenses 命令
├── plugin-commands-listing # 入口:pnpm list/ll/why 命令
└── plugin-commands-outdated # 入口:pnpm outdated 命令
store
├── cafs # store 的基础包(cafs:content-addressable filesystem)
├── cafs-types # cafs 的类型定义
├── create-cafs-store # 创建 cafs store
├── package-store # store controller 相关的内容
├── plugin-commands-server # 入口:pnpm server 命令
├── plugin-commands-store # 入口:pnpm store 命令
├── server # 一个简单的 HTTP 服务器,通过 store controller 提供 store 的管理操作
├── store-connection-manager # store 的连接管理器
├── store-controller-types # store controller 的类型定义
└── store-path # 获取 PNPM store 的路径
text
└── comments-parser # 添加/删除注释,用于 lockfile,由 issue #2008 引入
workspace
├── filter-workspace-packages # 过滤出 workspace 中的包
├── find-workspace-dir # 找出 workspace 的根目录
├── find-workspace-packages # 找出 workspace 中的包
├── pkgs-graph # 通过扁平化的包列表生成依赖图
├── resolve-workspace-range # 解析 workspace 中的包的版本号
└── sort-packages # 对包进行拓扑排序
可以发现如下几个规律:
- 所有以
plugin-commands-
开头的包,都对应着 PNPM 的一个或多个命令(如plugin-command-env
对应了pnpm env
); - PNPM 有专门用来解决 lockfile 合并冲突的方法(可以放心使用
pnpm i
来解决冲突了); - PNPM 在下载包的时候是并发的;
- PNPM 做了许多标准化的工作,兼容了不同类型的 NPM 源、Git 源、本地源
- PNPM 会试图兼容其它包管理器(从其它包管理器的 lockfile 中读取信息,或生成 Yarn 的
.pnp.cjs
); - PNPM 支持 JSON5 和 YAML 版本的
package.json
,这两种文件类型对注释更加友好;
通过本小节,我们就可以了解到具体的需求大概要改哪些文件了。例如我的 peerDependencies
的需求应该是与依赖解析相关,就需要改动 resolve-dependencies
包中的文件。
如何验证修改结果
如果你需要在真实的项目中验证你的修改,有两个步骤:
第一步:编译。在一切开始之前,你应该在 PNPM 的源码根目录执行 pnpm compile
,这会编译所有的包(一次性的操作),然后你可以在每次修改后,在对应的包下面执行 pnpm compile
来编译对应的包。当然一个更方便的做法是:直接在这个包下面运行 pnpm start
,这会启动一个监听器,当你修改文件后会自动编译。
第二步:执行。在确保编译完成后,可以通过这样的命令在你的真实项目中执行:
# PATH_TO_PNPM 请替换成你本机的 PNPM 的源码路径
# 使用 --store-dir 是为了避免污染本地的 store
node $PATH_TO_PNPM/pnpm/lib/pnpm.js i --store-dir 一个临时目录
node $PATH_TO_PNPM/pnpm/lib/pnpm.js add xxx --store-dir 一个临时目录
node $PATH_TO_PNPM/pnpm/lib/pnpm.js license --store-dir 一个临时目录
如何使用 VSCode 动态调试
本节内容参考 Curly_Brackets 的文章 VSCode 启动 Node.js 调试的几种方式,里面还包含了 VSCode 调试 Node.js 相关的其它操作,可以在你实际调试的过程中帮上大忙。
首先,如果要让 VSCode 支持断点,需要先在 PNPM 的源码根目录下添加 .vscode/launch.json
文件,内容如下:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/pnpm/lib/pnpm.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
否则你打的断点在调试的时候会变成灰色,且不会生效。
VSCode 在调试 Node.js 方面非常有优势,它提供了 auto attach 功能,可以直接 attach 到新运行的 Node.js 进程上。按下 Ctrl/Cmd + Shift + P
,输入 Debug: Toggle Auto Attach
并回车,然后选择 Only With Flag
,这样你只需要在运行的时候添加上 --inspect
参数,就可以直接在 VSCode 中调试了:
node --inspect $PATH_TO_PNPM/pnpm/lib/pnpm.js i --store-dir 一个临时目录
注意,配置的 auto attach 只对在其后新打开的交互式终端有效,如果你在 VSCode 以外的终端运行,那么就需要手动 attach 到对应的进程上了。
如何编写/执行单元测试
PNPM 使用 Jest 来执行单元测试,它的每个子包都有自己的测试代码,你可以在对应的子包下面执行 pnpm test
来执行测试。有时在你进行完修改后,除了你修改的那个包以外,可能还需要为 pkg/manager/core
包编写 E2E 测试,以确保你真的解决了问题。E2E 的常见操作如下:
- 使用
prepareEmpty()
生成一个空的临时目录; - 使用
addDistTag({ package, version, distTag })
添加一个 tag,例如给package-a
的1.0.0
版本添加latest
tag,那么pnpm i package-a
就会安装1.0.0
版本,这会影响到包的解析; - 使用
addDependenciesToPackage(manifest, [packages...], opts)
添加依赖,并修改 manifest; - 使用
install(manifest, opts)
根据 manifest 安装依赖; - 使用
readLockfile()
读取 lockfile,可以 expect 其中的一些内容; - 当然,你也可以直接 expect store 中的内容。
为了加快 E2E 的速度,PNPM 提供了一个 registry-mock
项目,这里面的 packages 在测试的时候可以认为存在于 registry 中。可以仔细观察 test
脚本:
pnpm run compile && pnpm run _test
先编译,然后执行 _test
脚本:
cross-env PNPM_REGISTRY_MOCK_PORT=4873 pnpm run test:e2e
而 test:e2e
则是这样的:
registry-mock prepare && run-p -r registry-mock test:jest
终于到头了……这里的 registry-mock
是一个简易的 registry 代理,它会优先返回本地的包,在本地没有时再去指定的 uplinks 找。那么它究竟运行了什么东西呢:
registry-mock prepare
用于创建缓存目录;run-p
是同时运行两个程序:启动 registry 代理、执行 jest 命令。
虽然看起来很复杂,但其实完全手动执行 test 脚本也没有太难(假定这是在 Linux/Mac/CygWin 等支持 POSIX shell 的环境):
# 创建缓存目录
pnpm registry-mock prepare
# 这里会卡住,因为这是在 4873 端口启动了一个 registry 服务
PNPM_REGISTRY_MOCK_PORT=4873 pnpm registry-mock
# 新建一个终端
pnpm jest
这样如果你想对命令做一些微调,例如修改一下端口、只执行特定的 test 文件(pnpm jest -- xxx.js
),都会比较方便。甚至如果你想创建一个真实的项目来做测试,也完全没问题:
pnpm registry-mock prepare
PNPM_REGISTRY_MOCK_PORT=4873 pnpm registry-mock
# 切换到一个空文件夹下,这就是要测试的“真实的项目”
# cd 某目录 && mkdir 新目录
# 先创建好 package.json
pnpm init
# 运行时指定 --registry 参数即可
node --inspect $PATH_TO_PNPM/pnpm/lib/pnpm.js add [email protected] --registry http://localhost:4873 --store-dir 一个临时目录
写在最后
自从我提了 PR 之后,我就一直在关注 PNPM 的邮件。我发现这个项目绝大部分需求都是 Zoltan 一个人负责——解决 issues、代码审查、回复 discussions……经常看到他在当地时间凌晨四点多还有动态,可以说是很忙了。
PNPM 项目已经非常庞大,加上包管理器的逻辑本身就复杂,一个人不一定有足够的精力满足越来越多的需求。我这次提 PR 的契机,其实是之前他为我们公司做线上分享时,最后提到的“欢迎大家多贡献”。我想,如果 PNPM 能够有更多的人参与进来,那么它的发展会更加迅速。