一个仓库里的「环境约束文件」考古
随便打开一个稍有年头的 Node.js 仓库,根目录大概率有这几个文件:
.nvmrc--- 告诉 nvm / fnm / volta 用哪个 Node.js 版本.npmrc--- registry、auth token、pnpm 行为、auto-install-peers、shamefully-hoist这些杂物的栖息地package.json的engines.node--- 给包管理器装依赖时做兜底校验package.json的packageManager--- Corepack 用来固定 pnpm/yarn 版本
四份地方各管一摊,每次升级 Node 要改三处,新人入职得讲三遍「先 nvm use,再 corepack enable,registry 看一眼 .npmrc......」。在 monorepo 里更糟糕:子项目要不要带自己的 .npmrc?根目录的版本要不要往下复制? 一不留神,CI 用 18 跑过,本地 20 又装出另一份 lockfile。
pnpm 11 把这套东西收得相当彻底------只要愿意把 pnpm 升上去,本仓库里的 .nvmrc 和 .npmrc 都可以删掉。这篇笔记记录我在 bolt 仓库迁移过来的过程和踩到的几个点。
pnpm 11 提供了什么
1. packageManager 字段:脱离 Corepack 自己工作
json
{
"packageManager": "pnpm@11.1.0"
}
packageManager 是 Node.js 官方定义的字段,本来是给 Corepack 看的------Corepack 看到这一行,就会自动下载并启用对应版本的 pnpm。
但 Corepack 自己有点麻烦:Homebrew 装的 Node 默认禁用 Corepack,得手动 corepack enable;企业网络下首次激活经常被代理卡住;COREPACK_ENABLE_AUTO_PIN 在某些版本会自动改写 package.json,反而搅乱 git diff。
pnpm 11 给出了一个更优雅的回路:pnpm 自己读 packageManager 字段 ,发现当前运行的版本对不上时,按 pmOnFail 设置来处理。新版默认 pmOnFail: download------直接下载对的版本接管这次调用,不动 package.json,不依赖 Corepack。
旧版那一坨 managePackageManagerVersions / packageManagerStrict / packageManagerStrictVersion 配置全部被 pmOnFail 一个键吃掉了:
| 旧设置 | 替代值 |
|---|---|
managePackageManagerVersions: true |
pmOnFail: download (默认) |
managePackageManagerVersions: false |
pmOnFail: ignore |
packageManagerStrict: false |
pmOnFail: warn |
packageManagerStrictVersion: true |
pmOnFail: error |
pnpm 11 还新增了 devEngines.packageManager,支持版本 range (packageManager 字段只能写精确版本),并把解析后的版本写进 pnpm-lock.yaml:
json
{
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": ">=11.0.0",
"onFail": "download"
}
}
}
两个字段如何取舍?
- 想跟 npm/yarn 生态共用一套机制 → 用
packageManager,pnpm 11 默认会下载缺失版本,不再需要 Corepack - 想配合 lockfile 把版本最终固化、且接受版本范围 → 用
devEngines.packageManager
2. engines.runtime / devEngines.runtime:pnpm 来管 Node、Bun、Deno
这是真正干掉 .nvmrc 的关键。pnpm 10.14+ 引入了 devEngines.runtime,10.21+ 又补了 engines.runtime:
json
{
"engines": {
"runtime": {
"name": "bun",
"version": "1.3.13",
"onFail": "download"
}
}
}
两者的区别:
engines.runtime--- 发布约束。声明此包对外发布时依赖哪个运行时(CLI 包尤其重要,pnpm 会把可执行文件绑死到指定 Node 版本)。devEngines.runtime--- 项目约束 。声明本地开发用的运行时,pnpm install时自动按版本下载到 pnpm-managed runtime store,写进 lockfile。
pnpm install 一跑,正确版本的 Node/Bun/Deno 就到位了,路径由 pnpm 注入到当前 shell。等价于 nvm use + 「保证装过这个版本」,而且不需要每个开发者本地都装 nvm。
CLI 触发同一行为的命令是 pnpm runtime set node 24.4.0(也支持 pnpm runtime set bun ...)------它会写入 devEngines.runtime 并把版本固定下来。
顺带一提:旧的
pnpm env use已被pnpm runtime set替代,两者效果相同,前者保留为别名。如果你之前用useNodeVersion配置,pnpm 11 已经移除,必须迁到devEngines.runtime。
3. .npmrc 被「削权」:非 auth 配置一律走 YAML
pnpm 11 的破坏性变更里最显眼的一条:.npmrc 只保留 auth / registry 两类配置 ,其余 pnpm 行为必须搬家到 pnpm-workspace.yaml(monorepo)或全局 ~/.config/pnpm/config.yaml。
典型迁移:
yaml
# pnpm-workspace.yaml
registries:
default: https://registry.npmmirror.com/
"@my-org": https://private.example.com/
allowBuilds:
bun: true
electron: true
core-js: false
packageConfigs:
"apps/web":
saveExact: true
"packages/sdk":
savePrefix: "~"
minimumReleaseAge: 1440 # 新发布的包静默期,11 起默认值
strictDepBuilds: true
注意几个细节:
package.json里的pnpm字段也不再被读取了,所以以前写在那里的onlyBuiltDependencies、overrides 都得迁出来。- 网络相关项(
httpProxy、strictSsl等)目前仍兼容从.npmrc读取,但官方建议挪到config.yaml。 - 环境变量从
NPM_CONFIG_*改成PNPM_CONFIG_*,CI 脚本里那一长串NPM_CONFIG_REGISTRY=...要顺手换掉。
本仓库的最终形态
bolt 仓库现在的根 package.json 大致长这样:
json
{
"name": "bolt",
"private": true,
"packageManager": "pnpm@11.1.0",
"engines": {
"runtime": {
"name": "bun",
"version": "1.3.13",
"onFail": "download"
}
},
"devDependencies": {
"@typescript/native-preview": "7.0.0-dev.20260414.1"
}
}
.nvmrc不存在 :本仓库实际跑在 Bun 上,engines.runtime已经把 Bun 版本钉死,pnpm install时按需下载。.npmrc不存在 :本仓库没有私有 registry 也没有镜像需求;如果要换 npm 镜像,再单写一个.npmrc就够了,毕竟它现在只剩 registry/auth 一个职责。packageManager锁 pnpm :任何人 clone 下来跑pnpm install,pnpm 自己会校对版本并按需下载。- 配合 5 月 8 日那则笔记 :Bun 不是装在
node_modules/.bin里的 devDependency,而是 pnpm runtime;调用直接bun ...,PATH 由 pnpm 接管。
四个文件压缩成了一个 package.json 的几个字段。
什么时候还需要 .npmrc
.npmrc 没死透,下面这些场景仍然要留:
- 私有 registry / scope 映射 ------虽然 pnpm 11 提供了
registries配置,但 npm 自身、CI 缓存层、其它工具链仍读.npmrc,多写一份能省麻烦。 - 企业镜像或代理 token ------auth token 一般会塞
.npmrc或~/.config/pnpm/auth.ini。 - 被其它工具读取 ------某些独立工具(
npm publish直接、IDE 插件、Snyk/Renovate 等)仍按 npm 规范读.npmrc。
我的判断:只要不出现 auth token、私有 registry,就不必为了 pnpm 行为单独建 .npmrc。
给团队的迁移清单
- 把 CI 和开发环境的 Node 升到 22+(pnpm 11 硬性要求)。
- 在根
package.json写"packageManager": "pnpm@11.x.y",删掉.nvmrc。 - 用
pnpm runtime set node <version>(或bun/deno)填好devEngines.runtime,必要时同步engines.runtime。 - 跑
pnpm-v10-to-v11codemod 自动迁.npmrc里的非 auth 配置到pnpm-workspace.yaml。 - 把
package.json里的pnpm字段配置(onlyBuiltDependencies/overrides等)挪到pnpm-workspace.yaml,注意onlyBuiltDependencies已被allowBuildsmap 取代。 - CI 把
nvm use这类命令换成corepack enable && pnpm install(或直接pnpm install,pnpm 11 自己会处理版本)。 - 第一次
pnpm install后检查pnpm-lock.yaml是否记录了 runtime 版本和packageManagerDependencies,确认锁定生效。 - 删
.nvmrc,做一次完整冷启动验证(先rm -rf node_modules再pnpm install再启动应用)。
小结
packageManager+devEngines.packageManager接手「锁包管理器版本」,pnpm 11 自己负责下载,不再需要 Corepack。engines.runtime/devEngines.runtime接手「锁 Node / Bun / Deno 版本」,pnpm install时自动下载,.nvmrc可以删。.npmrc被砍到只剩 registry/auth;pnpm 行为搬到pnpm-workspace.yaml。- 本仓库实测下来,根目录不需要
.nvmrc也不需要.npmrc,所有约束都集中在package.json一处。
升级 pnpm 11 的本质收益不只是性能,更是把「四份散文件维护的项目环境」收敛回一个 package.json 单点定义------一次配置,团队、CI、未来的自己都受益。