61DH61DH
Home
Diary
English
Tools
Photography
Health
Tech
Home
Diary
English
Tools
Photography
Health
Tech
  • 日记

    • 前言
    • pnpm 10 也可以管理项目指定的 pnpm 版本
    • Node.js、nvm、Corepack 与 pnpm 环境说明
    • 装修故事 3|厨房
    • 装修的故事二 · 卫生间
    • 装修的故事一 · 选柜子
    • 讨好 Google,不是一件容易的事
    • VS Code 中 Markdown 粘贴图片工作流(VuePress)
    • Best Buy 与我的电器店记忆:从 2001 到今天的一次“退货又下单”
    • 中国流量异常日记
    • 我的 iPhone 17 Pro 首日开箱记
    • Perplexity 是流氓软件吗?– 一个技术站长的实录与更正
    • 给 VuePress 添加评论系统:Waline 实战指南
    • 从“骨瘤?”到“脂肪瘤”:我的第一次手术日记
    • Coffee Sleeve 是什麼?
    • 一次 Maker 升级到 SKY 的实操:从 Coinbase 到 MetaMask,再谈钱包安全
    • Skyoo「脑力素」广告很火,但靠谱吗?
    • 回国礼物推荐:清单与避坑指南
    • 厨房装修小记:岛台移除后的地板方案
    • 苹果 2025 发布会小记
    • VuePress v2 index.scss 覆盖问题笔记
    • 康师傅酸梅汤:异乡小记
    • 升级笔记:VuePress 从 2.0.0-beta.51 到 ^2.0.0-rc.24
    • 旧日记
2026/08/23

Node.js、nvm、Corepack 与 pnpm 环境说明

这份文档记录这次遇到的问题,以及 nvm、Corepack、pnpm、npm、yarn 之间到底是什么关系,方便以后排查。

1. 先理解整体关系

大致可以理解成:

nvm
└── Node.js v22.11.0
    ├── node
    ├── npm
    └── Corepack
        ├── pnpm shim
        └── yarn shim

其中:

  • nvm:管理不同 Node.js 版本
  • Node.js:JavaScript runtime
  • npm:Node 自带的 package manager
  • Corepack:Node 某些版本附带的 package-manager 管理工具
  • pnpm / Yarn:可以由 Corepack 管理,也可以自己独立安装

Corepack 主要管理 pnpm 和 Yarn,不管理 npm。


2. Corepack 是干什么的?

假设项目的 package.json 有:

{
  "packageManager": "pnpm@10.4.1"
}

或者实际看到的是:

{
  "packageManager": "pnpm@10.4.1+sha512.c753b6c3..."
}

主要信息就是:

pnpm 10.4.1

后面的 sha512 是 integrity hash,用来验证 package manager 的内容。

如果启用了 Corepack:

corepack enable

以后执行:

pnpm install

实际上不是直接调用普通的 pnpm,而是:

pnpm
 ↓
Corepack shim
 ↓
读取 package.json
 ↓
发现 packageManager = pnpm@10.4.1
 ↓
Corepack 获取/使用 pnpm 10.4.1
 ↓
pnpm 10.4.1 install

因此最大的好处是:

不同项目可以自动使用不同版本的 pnpm。

例如:

Project A
packageManager = pnpm@9.15.0
        ↓
pnpm → Corepack → pnpm 9.15.0
Project B
packageManager = pnpm@10.4.1
        ↓
pnpm → Corepack → pnpm 10.4.1

不用自己不停执行:

npm install -g pnpm@某个版本

3. Yarn 也是一样吗?

基本一样。

如果:

{
  "packageManager": "yarn@4.9.2"
}

并且 Corepack enabled,那么:

yarn install

基本流程就是:

yarn
 ↓
Corepack shim
 ↓
读取 package.json
 ↓
发现 yarn@4.9.2
 ↓
使用对应 Yarn

所以:

Corepack
├── pnpm
└── yarn

是同样的设计思想。


4. Corepack 不管理 npm

这一点很重要。

即使:

corepack enable

下面这些仍然是普通 npm:

npm install
npm run dev
npm --version

Corepack 不会把 npm 命令替换成 Corepack shim。

可以简单理解:

npm  ─────────────→ npm
pnpm ─→ Corepack ─→ pnpm
yarn ─→ Corepack ─→ yarn

5. corepack enable 到底 enable 在哪里?

corepack enable 不是针对当前 project。

它会给当前 Node installation建立 pnpm/yarn shim。

我的机器使用 nvm,所以:

nvm use 22.11.0

对应的 Node 在:

~/.nvm/versions/node/v22.11.0/

因此执行:

corepack enable

以后会看到类似:

~/.nvm/versions/node/v22.11.0/bin/pnpm
    ↓
../lib/node_modules/corepack/dist/pnpm.js

所以准确地说:

Corepack 是针对当前 nvm 管理的这个 Node installation 启用的。

它不是整个 Mac 所有 Node 版本共享一个 Corepack 状态。

例如:

nvm
├── Node 20
│   └── Corepack state A
│
├── Node 22.7
│   └── Corepack state B
│
└── Node 22.11
    └── Corepack state C

6. 怎么知道 Corepack 是否正在控制 pnpm?

最简单的方法:

which pnpm

我的实际情况非常清楚。

Corepack disabled

执行:

corepack disable
which pnpm

得到:

/opt/homebrew/bin/pnpm

进一步:

ls -l "$(which pnpm)"

得到:

/opt/homebrew/bin/pnpm
→ ../lib/node_modules/pnpm/bin/pnpm.cjs

这说明:

pnpm ↓ 真正独立安装的 pnpm

没有经过 Corepack。


Corepack enabled

执行:

corepack enable
which pnpm

得到:

~/.nvm/versions/node/v22.11.0/bin/pnpm

然后:

ls -l "$(which pnpm)"

看到:

.../bin/pnpm
→ ../lib/node_modules/corepack/dist/pnpm.js

这说明:

pnpm ↓ Corepack shim ↓ 真正的 pnpm

所以以后判断 Corepack 是否控制 pnpm,最实用的方法就是:

which pnpm ls -l "$(which pnpm)"

对于 Yarn 也是同样:

which yarn ls -l "$(which yarn)"

如果指向 Corepack,就是 Corepack 在管理。


7. Corepack enabled 后,没有 packageManager 的项目怎么办?

假设:

Project A packageManager = pnpm@10.4.1 Project B 没有 packageManager

Corepack enabled 后,两边执行的:

pnpm

都会先经过 Corepack。

Project A 很明确:

pnpm ↓ Corepack ↓ packageManager says 10.4.1 ↓ pnpm 10.4.1

但是 Project B 没有指定版本。

Corepack 就会使用它自己的默认 / Last Known Good pnpm,而不是自动使用 /opt/homebrew/bin/pnpm。

所以 Corepack 的设计并不是:

有 packageManager → Corepack 没有 packageManager → 系统 global pnpm

而是:

只要 pnpm 命令已经被 Corepack shim 接管 pnpm ↓ Corepack /
packageManager有 没有 ↓ ↓ 指定版本 Corepack默认版本

这也是为什么 corepack enable 有可能影响原来没有 packageManager 的 pnpm 项目。


8. Corepack 和 global pnpm 可以同时存在

我的机器本来已经有:

/opt/homebrew/bin/pnpm

版本:

pnpm --version
# 10.28.0

同时 Node 22.11.0 可以有:

~/.nvm/versions/node/v22.11.0/bin/pnpm

作为 Corepack shim。

真正执行哪一个完全取决于:

PATH 顺序

例如:

~/.nvm/versions/node/v22.11.0/bin
/opt/homebrew/bin

那么:

which pnpm

找到的是:

~/.nvm/versions/node/v22.11.0/bin/pnpm

所以 Corepack 赢。

反过来:

/opt/homebrew/bin
~/.nvm/versions/node/v22.11.0/bin

那么:

/opt/homebrew/bin/pnpm

赢。


9. 为什么 Node 22.11.0 的 Corepack 出错?

当时执行:

pnpm --version

出现:

Cannot find matching keyid

这不是项目的问题,也不是 pnpm 本身坏了。

Node 22.11.0 发布的时候,它附带了当时版本的 Corepack。

后来 npm registry 更换了 package signing key。

于是出现:

旧 Corepack
 ↓
只认识旧 signing key
新的 pnpm package
 ↓
使用新的 signing key
旧 Corepack验证
 ↓
不认识新的 key
 ↓
Cannot find matching keyid

所以 Node 22.11.0 发布的时候并不是坏的。

是后来外部的 signing key 发生变化,而旧 Corepack 没有新的 key。

解决方式是更新 Corepack:

npm install -g corepack@latest

这里使用 npm 只是因为:

npm 被用来安装 Corepack package。

不代表 Corepack 属于 npm。

Corepack 报 Cannot find matching keyid 时应该升级谁?

如果目标是继续使用 Corepack,出现该错误时,需要升级的是 Corepack,不是 pnpm。旧版 Corepack 的 trusted signing keys 早于 pnpm registry 使用的新 signing key;pnpm 本身没有坏,也不需要先重新安装。

正确的针对性修复是在当前 nvm Node 环境下升级 Corepack:

nvm use 22.11.0
npm install -g corepack@latest
corepack enable

corepack --version
pnpm --version

这里的 -g 会安装到当前 Node 环境,而不是所有 Node 环境:

~/.nvm/versions/node/v22.11.0/

可用下面的命令验证 pnpm 已经由新版 Corepack 接管:

which pnpm
ls -l "$(which pnpm)"

预期会看到类似:

~/.nvm/versions/node/v22.11.0/bin/pnpm
    -> ../lib/node_modules/corepack/dist/pnpm.js

然后在包含 package.json 与 pnpm-lock.yaml 的 project root 运行:

pnpm install
pnpm dev

如果 package.json 有以下字段,Corepack 会选择项目要求的 pnpm 版本:

{
  "packageManager": "pnpm@10.4.1+sha512..."
}

不需要做什么

不需要为了这个错误执行:

npm install -g pnpm

如果决定使用 Corepack,pnpm 应由 Corepack 管理:

旧 Corepack
    ↓
无法验证新的 pnpm signing key
    ↓
升级 Corepack
    ↓
Corepack 根据 packageManager 选择/获取正确 pnpm

升级 Node 到较新的 patch/minor release 有时也会带来更新的 bundled Corepack;但当项目需要继续使用 Node 22.11.0 时,只升级 Corepack 更有针对性。

最终原则是二选一:使用 Corepack 时,让它负责选择和启动 pnpm;不使用时执行 corepack disable,再使用独立安装的 pnpm。不要把 Corepack-managed pnpm 与独立安装的 global pnpm 混在一起。


10. 不使用 Corepack 可以吗?

完全可以。

例如项目写:

{
  "packageManager": "pnpm@10.4.1"
}

但是 Corepack disabled。

如果系统安装:

pnpm 10.28.0

那么执行:

pnpm install

实际使用的仍然可以是:

pnpm 10.28.0

packageManager 本身不会神奇地把 pnpm 10.28.0 变成 10.4.1。

Corepack disabled 后,这个字段主要相当于:

这个项目声明自己期望 pnpm 10.4.1。

但你自己负责确保实际使用的 pnpm 版本正确。


11. pnpm 项目可以执行 npm install 吗?

技术上可以,但不推荐。

如果项目已经有:

pnpm-lock.yaml

说明项目使用 pnpm。

应该:

pnpm install

而不是:

npm install

因为 npm 可能创建:

package-lock.json

并且 dependency resolution、node_modules layout 等可能和 pnpm 不一样。

所以看到:

pnpm-lock.yaml

最好坚持使用 pnpm。


12. 这次真正发现的 nvm 问题

这次另外发现一个与 Corepack 不同的问题:

VS Code Terminal 和 macOS Terminal 使用了不同的 Node。

VS Code:

nvm list

显示:

-> v22.11.0

而普通 Terminal:

-> system

而且:

which node

普通 Terminal 得到:

/opt/homebrew/bin/node

VS Code 得到:

~/.nvm/versions/node/v22.11.0/bin/node

这说明问题在:

PATH / nvm initialization

而不是 project directory。

即使两个 Terminal 都在:

~/Projects/swimstandards

也完全可能使用不同 Node。


13. 为什么同一个目录,VS Code 和 Terminal 可以不同?

Shell 找 node 的规则就是从 $PATH 左到右。

VS Code 的 PATH 中:

~/.nvm/versions/node/v22.11.0/bin

在:

/opt/homebrew/bin

前面。

所以:

which node

得到:

~/.nvm/versions/node/v22.11.0/bin/node

普通 Terminal 当时相反:

/opt/homebrew/bin

在 nvm Node 前面。

所以:

which node

得到:

/opt/homebrew/bin/node

因此:

当前目录不会决定 Node 版本,shell 的环境和 PATH 才决定实际执行哪个 node。


14. 最终发现的真正冲突

我的 ~/.zprofile 原来有:

export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \
  . "/opt/homebrew/opt/nvm/nvm.sh"
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \
  . "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"

也就是说 .zprofile 在加载:

Homebrew nvm /opt/homebrew/opt/nvm/nvm.sh

但是 ~/.zshrc 又有:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use default >/dev/null 2>&1

这里加载的是:

~/.nvm/nvm.sh

也就是说:

.zprofile ↓ Homebrew nvm .zshrc ↓ ~/.nvm nvm

两个不同来源的 nvm initialization 混在一起了。

这导致 Terminal 和 VS Code 的 PATH/nvm 状态不一致。


15. 最终修复

保留 ~/.zshrc 中的 nvm:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
# Select the nvm default
nvm use default >/dev/null 2>&1

然后把 .zprofile 中 Homebrew nvm 的初始化注释掉:

# 8/24/2026 - Disabled: nvm is initialized in ~/.zshrc instead.
# Loading Homebrew's nvm here conflicts with the ~/.nvm installation used by ~/.zshrc.
# export NVM_DIR="$HOME/.nvm"
# [ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && . "/opt/homebrew/opt/nvm/nvm.sh"
# [ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && . "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"

只做这个修改后,问题解决。

现在新的 Terminal 应该正确使用:

nvm ↓ Node 22.11.0

而不是:

Homebrew Node


16. 我的环境最终应该怎么理解

现在整个结构可以理解为:

macOS │ ├── Homebrew │ ├── /opt/homebrew/bin/node │ └── /opt/homebrew/bin/pnpm → pnpm 10.28.0 │ └── nvm └── Node 22.11.0 ├── node ├── npm 10.9.2 └── Corepack ├── pnpm shim └── yarn shim

正常情况下,我希望:

node ↓ nvm ↓ Node 22.11.0

至于 pnpm,则取决于是否 enable Corepack。

Corepack disabled

pnpm ↓ /opt/homebrew/bin/pnpm ↓ pnpm 10.28.0

Corepack enabled

pnpm ↓ ~/.nvm/versions/node/v22.11.0/bin/pnpm ↓ Corepack ↓ 读取 packageManager ↓ 对应 pnpm


17. 日常排查最有用的命令

以后如果 Node/pnpm 行为奇怪,先不要猜,直接看实际执行的是谁:

which node
node -v
which npm
npm -v
which pnpm
pnpm --version
nvm current

检查 pnpm 是真正的 pnpm 还是 Corepack shim:

ls -l "$(which pnpm)"

如果看到:

corepack/dist/pnpm.js

就是:

Corepack enabled,并且当前 pnpm 命令由 Corepack 接管。

如果看到:

node_modules/pnpm/bin/pnpm.cjs

就是:

直接使用独立安装的 pnpm。

对于 Yarn:

which yarn
ls -l "$(which yarn)"
yarn --version

判断方法完全一样。


18. 最重要的几个结论

  1. Corepack 不是 pnpm,也不是 npm。 它是 pnpm/Yarn 的 package-manager dispatcher/version manager。
  2. corepack enable 不是针对当前 project。 在 nvm 环境中,它影响当前 nvm Node installation 的 pnpm/yarn command shim。
  3. packageManager 才是 project-level 的版本声明。
  4. Corepack disabled 时,packageManager 不会强制实际 pnpm 版本。
  5. Corepack enabled 后,即使项目没有 packageManager,pnpm 仍然可能经过 Corepack。
  6. which pnpm 是判断到底使用哪个 pnpm 最直接的方法。
  7. which node 比“我以为自己用了 nvm”更可靠。 永远以 shell 实际解析出来的 executable 为准。
  8. nvm 应该只初始化一次。 不要同时从 Homebrew nvm 和 ~/.nvm/nvm.sh 加载。
  9. 同一个目录不代表同一个 Node 环境。 VS Code Terminal 和 Terminal.app 可以因为 PATH/startup environment 不同而使用不同 Node。
  10. 这次最终的问题不是 nvm 本身坏了。 是 .zprofile 和 .zshrc 从两个不同位置初始化 nvm,造成 PATH 和 Node selection 不一致。
Prev
pnpm 10 也可以管理项目指定的 pnpm 版本
Next
装修故事 3|厨房