pnpm 工程手册:内容寻址依赖、Workspace 与可复现构建
pnpm 最容易被简化成一句“更快、更省磁盘的 npm 替代品”。这句话只描述了结果,没有解释代价。它之所以节省磁盘,是因为包内容进入共享的内容寻址 store,项目再通过虚拟 store 和链接结构构造依赖视图;它之所以更容易暴露幽灵依赖,是因为默认的隔离布局不会把所有间接依赖平铺给项目源码。
这套模型改变了排障方式。看到 node_modules/.pnpm 不能当成目录异常,遇到工具不兼容也不能立刻打开 shamefully-hoist。工程上需要先判断问题发生在版本入口、解析、store、虚拟布局、workspace 协议、生命周期脚本还是 registry 信任边界,然后做最小修复。
先把链接布局问题与应用故障分开
pnpm 负责 CLI 版本、依赖解析、内容寻址 store、虚拟 store、nodeLinker、pnpm-lock.yaml、workspace 协议、catalog、脚本执行和 registry 访问。JavaScript/TypeScript 语法、框架运行时、包发布流程,以及 Nx、Turborepo、Rush、Bazel 的任务图与远端缓存,各有独立调用链。pnpm workspace 解决多包依赖和基础命令作用域,不提供 affected 计算或分布式构建缓存。
出现 MODULE_NOT_FOUND 时,先确认依赖是否在实际使用它的成员 manifest 中声明,再观察链接层;出现任务未按预期增量执行时,应检查任务编排工具,而不是调整 nodeLinker。这个分流能避免用 hoist 掩盖幽灵依赖,也能避免把应用代码错误误判成 store 损坏。
在练习仓库采集 store 与 linker 现场
准备一个可删除的练习目录、一套项目批准的 Node.js 环境,以及能访问官方或企业 registry 的网络。进入目录后先记录 CLI、源、store 和链接策略:
node --version
pnpm --version
pnpm config get registry
pnpm store path
pnpm config get node-linker把输出与根 package.json 的 packageManager、CI 激活步骤和 pnpm-workspace.yaml 放在一起比较。动态兼容边界通过当前 node --version、pnpm --version 与 pnpm 安装兼容表确认,不能从 Node 主版本猜测随附工具,也不能因为 pnpm 能管理 Node 就忽略 CLI 自身所需的运行时。
安装、启用与退出
pnpm 官方提供多种入口,选择依据是运行时、平台和组织可控性。
通过 npm 安装
已有兼容 Node.js 时,Windows 官方当前推荐 npm 或 Corepack 路线,避免独立可执行文件被 Defender 阻断:
npm install --global pnpm@<approved-version>
pnpm --version通过 Corepack 管理
Corepack 是独立工具边界,不应假设所有 Node 版本永久内置。pnpm 官方当前建议先更新 Corepack,再启用 pnpm:
npm install --global corepack@latest
corepack enable pnpm
corepack use pnpm@<approved-version>
pnpm --versioncorepack use 会把精确 packageManager 声明写入项目。Corepack 的签名、随 Node 分发变化和退出策略由同家族 Corepack 专篇展开。
独立脚本或 @pnpm/exe
独立脚本和 @pnpm/exe 可在没有 Node.js 的机器上安装自带运行时的 pnpm,但平台边界必须核对:当前独立 POSIX 安装不支持 Intel macOS;Linux glibc 构建要求 glibc 2.27 及 libatomic.so.1,最小容器镜像可能缺失。不要在生产构建机直接执行未经固定和校验的 curl | sh。
普通 pnpm 包、独立安装脚本和 @pnpm/exe 的运行时边界并不相同。选择入口前要在兼容表中核对操作系统、CPU、Node.js 与 pnpm 主版本;升级后重新执行版本取证和最小安装,不能把另一种安装渠道的支持结论直接套用过来。
版本升级使用明确的批准版本,记录升级前后:
pnpm --version
pnpm self-update <approved-version>
pnpm --version卸载时按原安装渠道反向处理。若 PATH 仍解析到旧 shim,Windows 使用 where.exe pnpm.*,POSIX 使用 which -a pnpm,确认并只删除已识别的 pnpm 文件。不要递归清理整个 Node 或用户 bin 目录。
固定工具版本
根 package.json 应留下精确工具声明:
{
"name": "pnpm-workspace-lab",
"private": true,
"packageManager": "pnpm@11.8.0",
"engines": {
"node": ">=22 <27"
}
}版本号只示范固定形态,正式项目应采用当天批准版本。packageManager 负责告诉 Corepack和人类“该仓库使用哪一个 pnpm”,CI 仍要显式安装/激活同一版本并打印版本证据。只写 pnpm@11 会让不同时间得到不同 CLI,无法解释 lockfile 格式和默认行为变化。
pnpm 还可以在 pnpm-workspace.yaml 中用 nodeVersion 与 engineStrict 限制依赖解析所依据的 Node 版本。它解决“成员用较新 Node 意外加入不兼容依赖”,但不替代真实运行时矩阵:
nodeVersion: 22.22.0
engineStrict: true核心模型:三层存储,不是一份平铺目录
pnpm 默认安装可以理解为三层:
内容寻址 store:按内容保存包文件,可被同一信任域内多个项目复用。虚拟 store:默认位于项目 node_modules/.pnpm,按解析出的包版本和 peer 组合组织依赖视图。项目可见层:根和各包的 node_modules 通过链接暴露直接依赖和命令 shim。
默认 nodeLinker: isolated 从虚拟 store 链接依赖,让包只能稳定访问自己声明的依赖。nodeLinker: hoisted 会生成类似 npm/Yarn Classic 的平坦布局,适用于确实不兼容链接的工具、部分 React Native 或不支持链接的 serverless 打包场景;pnp 则不生成常规 node_modules。三者不是性能档位,而是兼容模型选择。
store 最好与项目位于同一磁盘。跨文件系统无法建立硬链接时会退化为复制,磁盘与安装性能收益下降。更重要的是,store 和 cache 都是信任域:只有相互信任的用户、CI job 和进程可以共享写权限。把所有 runner 指向一个任何人可写的 store,会让缓存设施变成代码注入设施。
配置分层
pnpm 11 把大量团队设置集中在根 pnpm-workspace.yaml。一个偏保守的示例是:
packages:
- "packages/*"
nodeLinker: isolated
engineStrict: true
strictPeerDependencies: true
minimumReleaseAge: 1440
minimumReleaseAgeStrict: true
blockExoticSubdeps: true
allowBuilds:
esbuild: true
core-js: false
registries:
default: https://registry.npmjs.org/
"@example": https://packages.example.invalid/npm/这些值不能不加分析地复制到现有仓库。minimumReleaseAgeStrict 可能阻断刚发布的紧急安全修复;allowBuilds 需要根据实际原生依赖逐版本审批;私有 registry 可能缺少发布时间元数据。先在代表仓库记录受影响包,再决定例外及到期日。
最终配置可通过以下命令取证:
pnpm config list
pnpm config get registry
pnpm config get store-dir
pnpm config get node-linker
pnpm config get strict-ssl第一次跑通:观察 store、lockfile 与链接
在空目录创建:
{
"name": "pnpm-verify-lab",
"version": "1.0.0",
"private": true,
"scripts": {
"verify": "node -e \"const v=require('lodash/package.json').version; console.log('lodash='+v)\"",
"test": "pnpm run verify"
},
"dependencies": {
"lodash": "4.17.21"
}
}执行:
pnpm install
pnpm test
pnpm why lodash
pnpm store path预期测试输出 lodash=4.17.21,根目录出现 pnpm-lock.yaml 和 node_modules,pnpm why 显示根项目直接依赖,pnpm store path 返回当前 store。再查看而不修改:
Get-ChildItem -LiteralPath .\node_modules
Get-ChildItem -LiteralPath .\node_modules\.pnpm | Select-Object -First 10POSIX 可使用 ls -la node_modules node_modules/.pnpm | head。这里看到链接和 .pnpm 是预期模型。
模拟干净恢复:
Remove-Item -LiteralPath .\node_modules -Recurse -Force
pnpm install --frozen-lockfile
pnpm test预期 lockfile 不被改写且测试结果一致。随后只改 package.json 中 lodash 版本,不更新 lockfile,pnpm install --frozen-lockfile 应失败。实验结束后删除整个练习目录。
lockfile、store、cache 和离线模式
pnpm-lock.yaml 记录整个项目或 workspace 的解析结果,应提交。CI 检测到 lockfile 时默认使用 frozen 模式;manifest 不一致、lockfile 缺失或格式来自更高 pnpm 主版本都会失败。解决方式是统一 pnpm 版本并在开发变更中更新 lockfile,不能让 CI 关闭 frozen 后静默改写。
store 保存包内容,cache 还包括 registry 元数据、dlx cache 和部分安装验证结果。它们职责不同。常用诊断:
pnpm store path
pnpm store status
pnpm install --prefer-offline
pnpm install --offline--prefer-offline 可以复用本地内容但仍允许网络补齐;--offline 只允许 store 中已有内容,缺失即失败。要把离线能力当验收项,应先在联网受控环境预热,再断网执行 frozen + offline,不能把一次温缓存成功误写成所有平台可离线。
CI 默认是否启用 frozen lockfile、不同主版本如何处理不兼容 lockfile,都应按当前 pnpm --version 查 pnpm install。团队规则应显式执行 pnpm install --frozen-lockfile,避免依赖 CI 环境探测的隐式默认值;CLI 主版本变化时,用同一提交在干净目录比较退出码、lockfile diff 与测试结果。
pnpm store prune 删除未被当前系统项目引用的历史包。官方说明它不会破坏项目,但会让切换旧分支时重新下载,因此不应每次 CI 都运行。共享 store 上执行 prune 还要确认 job 并发和所有权。
pnpm v11.4 起对 tarball 完整性不匹配默认硬失败。看到 ERR_PNPM_TARBALL_INTEGRITY 时先比较 lockfile integrity、registry/代理来源和上游内容,禁止用 --force 猜测修复。只有确认合法 registry 确实重写了字节并完成安全审查后,才使用 --update-checksums 生成可审计 diff。
Workspace:依赖边界优先于批量命令
pnpm workspace 根必须有 pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"内部包建议使用 workspace: 协议:
{
"name": "@example/api",
"dependencies": {
"@example/shared": "workspace:^"
}
}它的关键能力不是“自动链接”,而是禁止静默回退到 registry。如果 workspace 中不存在满足范围的 @example/shared,安装失败;普通 semver 在某些配置下可能转去 registry,产生本地包与远程同名包混淆。
常用范围操作:
pnpm --filter @example/api install
pnpm --filter @example/api test
pnpm --filter "./packages/*" test
pnpm -r run testworkspace 存在循环依赖时,pnpm 不能保证脚本按拓扑顺序执行,并会发出循环警告。不要用调整并发掩盖依赖环;先把运行时依赖、类型依赖和构建产物关系拆清。大仓任务图和 affected 执行交给 31 家族。
Catalog:统一版本意图,不替代 lockfile
catalog 把多个 workspace 重复使用的版本范围集中到 pnpm-workspace.yaml:
catalog:
typescript: "5.9.3"
eslint: "^9.0.0"
catalogs:
react18:
react: "^18.3.0"成员 manifest 使用:
{
"devDependencies": {
"typescript": "catalog:"
},
"dependencies": {
"react": "catalog:react18"
}
}catalog 表达“团队想用哪个范围”,lockfile 表达“本次实际解析出什么”。catalogMode: strict 会阻止添加 catalog 之外的版本,但可能增加紧急修复摩擦;prefer 有回退,治理强度较弱;默认 manual 不自动纳管。选择要写进团队决策,不能只为了减少重复字符串。
项目脚本与跨平台入口
pnpm run 会把项目 node_modules/.bin 加入 PATH,脚本仍由 shell 执行。Windows 与 POSIX 的变量、引号和管道差异不会因为换成 pnpm 自动消失。shellEmulator 可以为部分 bash 风格脚本提供跨平台执行,但不是完整 shell,也不影响直接用 node --run 执行脚本。
团队公开入口应保持少而稳定:
{
"scripts": {
"check": "pnpm run lint && pnpm run test",
"lint": "eslint .",
"test": "node --test",
"clean": "node scripts/clean.mjs"
}
}复杂跨平台逻辑放到受测试的 Node 脚本或专门任务工具,不把密钥拼进 scripts,也不要依赖开发者全局安装的 CLI。
生命周期脚本与供应链
依赖安装脚本拥有当前进程的文件、网络和环境变量权限。pnpm 从 v10 起不再默认自动执行依赖的 postinstall/build 脚本;pnpm 11 使用 allowBuilds 统一允许和拒绝策略,未列出的构建脚本默认视为未审查,strictDepBuilds 默认让它成为错误。实际可用配置以当前 CLI 对应的 Settings 为准;从 onlyBuiltDependencies、neverBuiltDependencies 等旧配置迁移时,要逐包比较策略,不能只改键名。
allowBuilds:
"esbuild@0.25.0": true
"telemetry-package": false实际版本选择应来自当前 lockfile,示例名称不构成批准。可以使用:
pnpm approve-builds
pnpm ignored-builds不要设置 dangerouslyAllowAllBuilds: true 作为长期兼容方案。ignoreScripts 也不是完整沙箱:官方明确指出它不阻止 .pnpmfile.mjs 执行。仓库中的 .pnpmfile.mjs 可以改写依赖 manifest,必须像构建插件一样评审。
pnpm 11 还提供三类解析防护:blockExoticSubdeps 阻止间接依赖使用 Git/直链 tarball 等异常来源;minimumReleaseAge 延迟采用刚发布版本;trustPolicy 可以阻止发布信任等级下降。它们降低窗口风险,不证明包无漏洞,也不能替代锁文件审查、许可证治理和测试。
registry、代理、CA 与凭证
pnpm 11 可在 pnpm-workspace.yaml 声明非敏感 registry 路由:
registries:
default: https://registry.npmjs.org/
"@example": https://packages.example.invalid/npm/凭证属于用户/CI 信任域。pnpm 11 的认证优先读取 workspace .npmrc、用户级 auth.ini、再回退到用户 ~/.npmrc;但 workspace .npmrc 应加入 .gitignore,不能提交真值。认证文件的优先级和环境变量展开规则会随主版本变化,先根据当前 CLI 查 pnpm 认证设置,再让 pnpm login 或 pnpm config set 把 token 写入受信用户配置:
pnpm config set //registry.npmjs.org/:_authToken "$NPM_TOKEN"这是示例,真实 CI 应使用只读、短期、最小 scope token。v11.5.3 起,项目 .npmrc 中 ${NPM_TOKEN} 等敏感位置不再展开,以阻止仓库重定向凭证。旧 CI 迁移可以使用受信用户配置、NPM_CONFIG_USERCONFIG,或按官方 v11.6+ 的 URL 绑定环境配置;不能看到变量失效就把 token 明文写回仓库。
代理与 TLS 示例:
pnpm config set https-proxy http://proxy.example.invalid:8080 --global
pnpm config set cafile /path/to/corporate-ca.pem --globalHTTPS_PROXY、HTTP_PROXY 和 NO_PROXY 也会影响请求。证书错误应补齐受信 CA,不能长期设置 strict-ssl=false。调试输出、pnpm config list 和 CI artifact 必须脱敏 registry、代理账号、token、证书私钥和内网路径。
常见失败:现象、判断、修复、再验证
ERR_PNPM_OUTDATED_LOCKFILE 或 CI 拒绝 lockfile
本机安装可继续,CI frozen install 失败;或提示 lockfile 来自更高 pnpm 主版本。
比较 packageManager、本机/CI pnpm --version、manifest 与 lockfile diff。
CLI 主版本不一致,或依赖声明变化后未更新 lockfile。
使用仓库批准版本重新安装并评审 lockfile;不要关闭 CI frozen 模式。
删除 node_modules,执行 pnpm install --frozen-lockfile && pnpm test,工作区无 diff。
包在 npm 下可引用,迁移 pnpm 后 MODULE_NOT_FOUND
源码引用了未写入自己 manifest 的包,npm 平坦布局碰巧可见,pnpm isolated 布局不可见。
定位报错模块,在实际使用它的 workspace manifest 中查依赖声明,执行 pnpm why <package>。
幽灵依赖,或工具错误假设所有间接依赖都提升到根。
补齐直接依赖;若第三方工具确实不兼容,再评估局部 hoist/hoisted linker 和替代工具。
恢复 isolated 布局,干净安装与测试通过。
ERR_PNPM_UNEXPECTED_STORE 或跨盘复制导致性能下降
项目期望的 store 与当前配置不同,或磁盘占用/安装时间没有预期改善。
执行 pnpm store path,比较项目磁盘、store 所在文件系统和配置来源。
切换 pnpm 配置/用户、移动项目、跨文件系统无法硬链接,或把旧 node_modules 带到新环境。
删除项目派生的 node_modules 后用批准 store 重装;不要移动或手改 store 内容。
pnpm store status 返回成功,frozen install 和测试通过。
未审查 build script 阻断安装
安装报告 ignored/unreviewed builds,原生工具缺少二进制。
列出被阻断包,检查 lockfile 版本、来源、安装脚本及其网络/文件行为。
pnpm 的依赖脚本默认安全策略生效,或从 v10 旧配置迁移到 v11 allowBuilds 不完整。
逐包逐版本批准或拒绝,必要时替换依赖;不启用全局放开。
干净安装,批准列表无未解释新增项,关键原生产物存在并通过测试。
私有源 401,项目 .npmrc 变量被忽略
升级 pnpm 11 后 ${NPM_TOKEN} 配置出现警告,私有包 401。
确认占位位于项目 .npmrc 还是用户 auth 文件,核对 pnpm 小版本和 registry host。
v11.5.3+ 的仓库凭证防外泄策略生效。
把 token 注入受信用户级 auth 文件或 CI 的受信配置,绑定准确 registry,撤销可能暴露的旧凭证。
安装私有测试包成功,仓库和日志中没有 token,恶意改 registry 的测试不能带走凭证。
完整性错误
ERR_PNPM_TARBALL_INTEGRITY,重复下载仍失败。
比对 lockfile integrity、实际 registry、企业代理缓存和上游变更记录。
缓存损坏、镜像重写 tarball、同版本重新发布或供应链篡改。
隔离源并由 registry/security owner 判断;只有确认合法字节变化后才更新 checksum。
空 store/干净 runner 也能按新审查结果恢复,lockfile diff 有审批记录。
CI 等价入口
CI 的最小链路是“同版本 CLI + 同 lockfile + 受信源 + 可丢弃 cache + 同 scripts”:
steps:
- name: versions
run: |
node --version
pnpm --version
- name: restore
run: pnpm install --frozen-lockfile
- name: verify
run: pnpm test激活 pnpm 的步骤必须解析到 packageManager 指定版本。缓存通常以 pnpm store path 为目标,键至少包含 OS、CPU/libc、Node/pnpm 版本和 pnpm-lock.yaml 摘要。共享 store 仅允许相互信任的 job 写入;来自 fork 或低信任分支的 job 使用隔离、只读或空 store。
CI 不应缓存整个 node_modules 作为正确性来源。pnpm 的链接布局包含项目路径、peer 组合和平台产物,跨 runner 直接复用可能制造难以解释的命中。缓存删除后仍能从批准 registry 完成 frozen install,才算契约成立。
仓库只保留 pnpm-lock.yaml,出现 npm/Yarn lockfile 时阻断合并。packageManager 固定精确 pnpm 版本,开发机和 CI 都打印并校验版本。pnpm-workspace.yaml 的 registry、catalog、脚本准入、Node/linker 策略由 owner 评审。
内部依赖优先使用 workspace:,禁止同名包静默回退 registry。store/cache 按信任域隔离,低信任 CI 无权写入高信任共享缓存。私有源凭证不进仓库;只读安装 token 与发布凭证分离并定期轮换。
pnpm 主版本升级单独演练 lockfile、脚本策略、认证文件、全局 bin 路径与回滚。
store 是信任边界,不只是空间优化
包文件可能从 store 硬链接到多个项目。攻击者若能写共享 store,就可能影响多个构建。共享策略必须同时约束文件权限、runner 身份、并发、完整性校验和清理 owner;“同一台机器”不是足够的信任判断。
side-effects cache 会复用安装脚本产物
pnpm 默认可缓存 pre/postinstall 修改后的包内容。若脚本写包目录之外、依赖未纳入键的环境变量,或产物与 CPU/libc 强绑定,缓存会制造错误复用。原生依赖要在平台矩阵验证;不满足可重现条件时关闭该依赖路径的副作用缓存,而不是接受偶发成功。
hoist 是兼容债务
nodeLinker: hoisted 或 shamefully-hoist 可以快速让旧工具工作,却会重新暴露幽灵依赖并扩大包可见面。每条例外需要对应故障、受影响工具、退出版本和验证;没有退出条件的 hoist 最终会成为迁移障碍。
发布时间策略会与私有镜像能力冲突
minimumReleaseAge 依赖 registry 元数据中的发布时间。某些私服/镜像缺少 time 字段,v11 默认可能跳过检查;设置 minimumReleaseAgeIgnoreMissingTime: false 又可能阻断全部私有包。团队应实测元数据、定义公共/私有 scope 差异和紧急升级例外。
认证加固会打破旧 CI,但回退方式决定风险
pnpm 11.5.3+ 不展开项目 .npmrc 的敏感变量,是为了防止恶意仓库把 token 导向外部 host。修复方向是把凭证和目的 host 一起移入受信配置,不是恢复仓库对 secret 的解释权。兼容开关只用于完全受信仓库,并有迁移截止时间。
清理、回滚与迁移
依赖变更回滚应同时恢复 manifest、pnpm-lock.yaml 和 pnpm-workspace.yaml:
git diff -- package.json pnpm-lock.yaml pnpm-workspace.yaml
pnpm install --frozen-lockfile
pnpm test清理项目派生状态只删除已确认的 node_modules;store 先用 pnpm store status 取证,历史内容用 pnpm store prune 受控回收。不要手工删除 store 内部散列目录。
从 npm/Yarn 迁移 pnpm 时,先冻结原工具版本与测试基线,再在单独分支生成 pnpm-lock.yaml、修复幽灵依赖、审查脚本准入,最后一次性切换 CI 和文档并删除旧 lockfile。回滚则恢复原 lockfile 与入口。不能长期保留多 lockfile 让成员自行选择。
已记录 Node、pnpm、registry、store、cache 与 linker 的实际来源。packageManager 固定精确版本,开发机和 CI 解析一致。pnpm-lock.yaml 已提交,frozen install 后工作区无 diff。
已理解 store、虚拟 store 和项目链接层,没有手改内部目录。workspace 内部包使用 workspace:,不存在意外 registry 回退。catalog 模式、例外和 owner 清楚,catalog 与 lockfile 职责没有混淆。
依赖 build scripts 已按包和版本审批,未全局放开。store/cache 只在相互信任的用户和 CI job 之间共享。私有源 token 位于受信用户/CI 配置,未写入仓库或日志。
企业代理使用受信 CA,未关闭 TLS 校验。pnpm 主版本升级已验证 lockfile、认证、脚本策略、全局 PATH 和回滚。
改动 pnpm 基线时去哪里确认行为
workspace 包错误回退 registry:查 Workspace 与 workspace: 协议,确认成员范围、版本匹配和发布转换行为。配置键在升级后失效:按实际 CLI 主版本查 Settings,重点比较 linker、catalog、脚本准入和供应链策略的默认值。
store 异常、跨盘链接或清理风险:查 Store 管理,先辨认 store 路径和状态,再决定 prune 或重建。CI 与本机安装结果不同:查 Continuous Integration,核对激活方式、frozen install、缓存路径和 runner 平台。
准备启用发布时间、来源或信任策略:查 供应链攻击缓解,同时验证私有 registry 是否提供策略依赖的元数据。
