R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。

为 PNPM peer 添加 NPM alias 支持(PNPM 与 VSCode 动态调试入门)

以下语言可用: 简体中文 正體中文 English
注意:本文发布于 369 天前,文章中的一些内容可能已经过时。

在之前做组件库的多版本共存时,我遇到了这样一个问题:PNPM 如何安装多个版本的包?似乎唯一的解决方法是使用 alias(下文的 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-1foo-parent-2 都在 dependencies 中依赖了 foobar,但是 bar 的版本不同;
  • foo 也通过 peerDependencies 表明自己需要 bar

PNPM 可以保证:如果我在项目中同时安装了 foo-parent-1foo-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-1node_modules 下,将 [email protected]([email protected]) 软链接到 foo-parent-2node_modules 下。这样foo-parent-1foo-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@3dependencies 明确添加一项,就可以让 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 里面指定的全部依赖,包括 dependenciesdevDependenciesoptionalDependencies
  • 本次命令中新安装的依赖,以及通过 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-a1.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 能够有更多的人参与进来,那么它的发展会更加迅速。

Disqus 加载中……如未能加载,请将 disqus.com 和 disquscdn.com 加入白名单。

这是我们共同度过的

第 3050 天