全局软件包

全局软件包是使用 pnpm add -g 安装到系统范围内的 CLI 工具和实用程序。在 pnpm v11 中,全局包管理被重新设计,以提升隔离性和可靠性。

安装全局软件包#

pnpm add -g <pkg>

例如:

pnpm add -g typescript prettier eslint

注意

请勿通过 sudo 运行这些命令。 pnpm 将全局包与配置保存在调用者的主目录下,因此在 sudo 下它们会静默地作用于 root 用户的主目录,而非你的主目录。自 v11.21.0 起,以 root 身份运行 pnpm setup、pnpm self-update 以及任何修改全局安装的命令都会打印警告,而在 pnpm v12 中会以 ERR_PNPM_SUDO_NOT_SUPPORTED 失败。像 pnpm bin -g 这样的只读命令不受影响。

独立安装#

每个全局安装的软件包(或一起安装的一组软件包)都有其自己的独立安装目录,其中包含自己的 package.json、node_modules/ 和锁文件。这样可以阻止全局包之间因对等依赖冲突、提升更改或版本错位而相互干扰。

隔离安装存储在 {pnpmHomeDir}/global/v11/{hash}/,其中哈希值是从一起安装的软件包集合中派生出来的。

例如,运行以下两个命令:

pnpm add -g typescript
pnpm add -g prettier

创建两个独立的隔离安装——typescript 和 prettier 各自拥有自己的 node_modules 树,并且不会影响彼此的依赖项解析。

在单个命令中安装多个空格分隔 软件包也会为每个软件包单独创建一个单独的安装:

pnpm add -g eslint prettier

eslint和prettier都会得到它们自己的 node_modules 树和锁pnpm文件并可以被独立删除 — — pnpm remove -g eslint 保持 prettier 不变。

若要将多个包捆绑到同一个隔离安装环境中——从而使它们共享 node_modules 树和锁文件、相互解析对等依赖项,并可被一并移除——请将它们作为逗号分隔的列表进行传递:

pnpm add -g eslint,prettier

这里的 eslint 和 prettier 形成了一个安装组。使用 pnpm remove -g 删除任一项都会删除整个组。

这两种形式可以混合使用。例如:

pnpm add -g eslint,prettier typescript

将 eslint 和 prettier 打包为一次孤立安装,同时单独安装 typescript。

目录布局#

{pnpmHomeDir}/global/v11/ 的内容如下所示:

{pnpmHomeDir}/global/v11/
├── {hash-A}              → 符号链接 → ./{hash-A-target}/
├── {hash-A-target}/ ← 隔离的安装目录
│ ├── package.json ← 列出一起安装的软件包
│ ├── pnpm-lock.yaml ← 此安装组的锁定文件
│ └── node_modules/
│      ├── <pkg>/ ← 顶级依赖项,符号链接到全局虚拟存储
│     └── .pnpm/
├── {hash-B}              → 符号链接 → ./{hash-B-target}/
├── {hash-B-target}/ ← 另一个隔离的安装目录
└── store/ ← 共享的全局虚拟存储
    └── ...
  • {hash} 条目是符号链接;pnpm 会扫描它们以枚举活动的安装。
  • 目标是真实的目录,它们就像普通的 pnpm 项目一样——每个项目都有自己的 package.json 和 lockfile。
  • 共享的 store/ 目录保存了 全局虚拟存储。每个安装组的直接依赖项(即其 node_modules/ 根目录下的条目)都是指向该存储的符号链接,因此实际的包内容是共享的,而不是按组复制的。
  • 二进制 shims 位于 {pnpmHomeDir}/bin/ 中,并指向相应的安装组的 node_modules。

当一个软件包被删除或其安装组被替换时,哈希符号链接会被更新,孤立的目标目录最终会被 pnpm store prune 清理掉。

列出全局软件包#

pnpm list -g
pnpm list -g --json # 机器可读
pnpm list -g --parseable # 仅路径

由于每个安装组都有自己的锁文件,因此跨多个组列出只能可靠地聚合它们一起安装的顶级软件包——来自不同组的传递依赖关系树无法一致地合并。因此:

  • pnpm list -g(默认 --depth=0)始终有效,并显示所有全局安装的软件包。
  • pnpm list -g --depth=<n>(其中 n > 0)仅在以下情况下显示完整的依赖关系树:
    • 只有一个全局安装组,或者
    • 位置参数将请求范围缩小到单个安装组,例如 pnpm list -g eslint --depth=1。

如果请求 --depth>0 但无法将请求缩小到单个安装组,pnpm 将报错 ERR_PNPM_GLOBAL_LS_DEPTH_NOT_SUPPORTED。

管理全局软件包#

命令描述
pnpm add -g <pkg>全局安装软件包
pnpm remove -g <pkg>移除一个全局安装的软件包(如果该软件包被打包在某个安装组中,则整个组将被移除)
pnpm update -g [pkg]更新全局软件包(重新安装到新的独立目录中)
pnpm list -g列出所有全局安装的软件包

注意

不再支持 pnpm install -g (无参数)。使用 pnpm add -g <pkg> 安装特定软件包。

二进制文件位置#

全局安装的二进制文件存储在 PNPM_HOME 的 bin 子目录中(即 $PNPM_HOME/bin/)。这样可以保持 PNPM_HOME 目录的清洁——当 PNPM_HOME 在 PATH 中时,像 global/ 和 store/ 这样的内部目录不会污染 shell 自动补全。

升级到 pnpm v11 后,运行 pnpm setup 来更新你的 shell 配置,以便 $PNPM_HOME/bin 在你的 PATH 中。

你可以通过以下方式检查当前的全局 bin 目录:

pnpm bin -g

项目感知的全局二进制#

添加于:v12.0.0-rc.2(仅 pnpm v12)

全局命令可以跟随你当前所在的项目。当你在项目中运行全局命令,而该项目要求同一个工具的另一版本时,pnpm 会运行该项目所要求的版本。

最实用的场景是 Node.js 本身。假定某个项目固定了运行时版本:

package.json
{
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "^22.0.0",
      "onFail": "download"
    }
  }
}

在该目录内执行裸 node 就会运行 Node.js 22,即便你全局的 Node.js 属于另一个主版本:

cd ~/projects/legacy-app
node --version   # v22.x.x — 该版本由项目固定
cd ~
node --version   # 你全局安装的版本

无需任何 shell 钩子、.bashrc 修改或 use 式命令 — 调度由 pnpm 写入全局 bin 目录的填充程序自行完成。

版本的选定方式#

pnpm 会自当前工作目录向上寻找最近的、提供该命令的项目,随后:

  • 对于运行时(node、deno、bun),只考虑清单中固定的版本 — 即 devEngines.runtime,其次是 engines.runtime。固定的版本会按需下载至全局虚拟存储并直接执行。对于运行时,绝不会查询项目的 node_modules/.bin,因此依赖不可能提供你运行的 node。
  • 对于**包管理器**(npm、yarn、bun),该支持于 v12.0.0-rc.6 加入:以项目的 packageManager 或 devEngines.packageManager 固定版本为准,pnpm 会按需准备该版本。该锁定版本优先级高于全局安装的对应包管理器副本,因为它是项目自身指定的安装版本。
  • 对于其他任何包,将使用项目的 node_modules/.bin/<name>。

pnpm 主目录内部的目录会被跳过,因为全局安装不属于项目。

如果项目未提供任何内容,或该查找被拒绝,则运行全局安装的版本 — 命令绝不会仅仅因为未应用调度而失败。

信任#

你 cd 进入的项目,并不会自动获准用它自己的二进制文件替代你全局的二进制文件。

本节描述默认的 auto 策略。在 auto 下:

  • 稳定的 Node.js 发布会在运行前依据 Node.js 发布团队的签名进行验证,因此无需询问即可切换。(在基于 musl 的系统上,对应构建未签名,因此会归入需询问的情形。)

  • 其余一切 — Deno、Bun、Node.js 预发布版本,以及你启用的任何普通包 — 都会按项目与候选项各询问一次:

    The project at "/home/user/projects/app" provides its own "tsc", which will be used
    instead of the globally installed one.
    Do you trust this project? [y/N]
    

回答会保存在机器本地的注册表中,以项目目录以及该确切二进制文件的指纹作为键。若二进制文件或其提供者包发生变化,pnpm 会再次询问,而不会复用原来的批准。

在 CI 以及任何非交互式会话中,无法发起询问,因此会运行全局版本,且不会记录任何内容。

另外两个策略会影响哪些候选项会进入该询问。 prompt 会使每个候选项都经过该询问,包括已通过签名验证的稳定版 Node.js 发布;回答依旧会被记住,因此按项目与候选项各询问一次,而不是每次运行都询问。 always 会完全跳过询问并直接切换 — 这正是它能在 CI 中使用的原因,因为否则提示会回退到全局版本。

还有一道与策略无关的防护:项目的命令必须来自与全局命令相同的包。若某个项目提供的是来自其他包的仿冒 tsc,则不匹配,此时运行的是你的全局 tsc。

参与其中的包#

默认只有运行时会参与。自 v12.0.0-rc.6 起,全局安装包管理器(pnpm add -g yarn)也会为其添加一个条目,因此它会像全局安装的 Node.js 已经遵循 devEngines.runtime 那样遵循项目的固定版本。由你自行设置的条目(包括 false)会保持你设定的状态。

通过 globalShims 可启用其他包,键为提供命令的包名:

~/.config/pnpm/config.yaml
globalShims:
  typescript: true

因为 pnpm 在安装时决定为哪些可执行文件写入调度填充程序,所以在启用某个已全局安装的包后,需要重新安装它:

pnpm add -g typescript

将某个包关闭无需重新安装 — 该设置在每次调度时都会重新读取。同一设置可以按包禁用调度,或用 globalShims: false 整体禁用。若只针对单条命令,可设置 PNPM_SHIM_BYPASS=1:

PNPM_SHIM_BYPASS=1 node --version

完全没有全局安装的包,也能借助 pnpm shim add 得到调度命令 — 这正是让只有 pnpm 的机器在 Yarn 项目中使用 yarn 的关键。

注意

globalShims 刻意不从项目的 pnpm-workspace.yaml 读取 — 只读取全局配置文件、pnpm 主目录自身的 pnpm-workspace.yaml 以及环境变量。具体细节见该设置的文档。

全局虚拟存储#

全局安装使用全局虚拟存储。软件包存储在 {storeDir}/links 中,并在全局安装之间共享。这样可以避免多个全局包依赖于相同库时进行冗余获取。

全局注册本地软件包#

要使本地软件包的二进制文件在系统范围内可用,请在软件包目录中使用 pnpm add -g . 命令:

cd ~/projects/my-tool
pnpm add -g .

这样就注册了软件包的 bin 条目,以便可以从任何地方调用它们。请参阅 pnpm link 了解更多详情。

构建脚本批准#

包含构建脚本(例如 postinstall)的全局软件包需要获得批准。当你安装需要运行构建脚本的全局软件包时,pnpm 会提示你以交互方式批准或拒绝构建。

你还可以使用 --allow-build 标志预先批准构建:

pnpm add -g --allow-build=esbuild esbuild