Node.js 工程化的三个常见问题:Corepack 报错、版本号符号与 ESM 下运行 TypeScript
这篇把三个用 Node.js 时反复遇到、每次都得重新查一遍的小问题整理到一起:Corepack 启用 pnpm 后的签名报错、package.json 里版本号符号的含义,以及在原生 ESM 项目里怎么直接跑 .ts 文件。它们没什么关联,按需查阅即可。
一、Corepack 启用 pnpm 后报 keyid 错误
用 corepack enable pnpm 启用 pnpm 之后,执行任何 pnpm 命令都报这个错:
Error: Cannot find matching keyid: {"signatures":[{"sig":"MEQCIHGq...","keyid":"SHA256:DhQ8wR5..."}],"keys":[{"keyid":"SHA256:jl3bwswu..."}]}
原因
Corepack 在下载 pnpm 包之后会校验它的签名:它内置了一份 npm 官方的公钥列表,用来验证下载到的包确实是 npm 签发的。报这个错,是因为本地 Corepack 里存的公钥列表过期了——pnpm 发布方更换或新增了签名密钥,而你这个版本的 Corepack 还不知道新密钥,于是「下载包用的 keyid」在「本地已知的 keys」里找不到匹配项,校验失败。
这本质上是 Corepack 版本太旧导致的,和 pnpm 本身、和你的项目都没关系。
解决方法
更新 Corepack 到最新版,让它带上最新的密钥列表:
npm i -g corepack@latest
更新完再跑 pnpm,它会提示要下载最新版本,确认即可:
pnpm -v
# ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-10.1.0.tgz
# ? Do you want to continue? [Y/n] y
# 10.1.0
能正常打印版本号,就说明签名校验通过、问题解决了。
二、package.json 里的版本号符号
每次看 package.json 里的依赖版本,^ 和 ~ 总要想一下才能确认区别。一次性记清楚。
先看 SemVer 格式
版本号格式是 主版本.次版本.补丁版本,比如 18.2.0:
- 主版本(major):有破坏性变更(不向后兼容)时递增
- 次版本(minor):新增功能但向后兼容时递增
- 补丁版本(patch):只做 bug 修复时递增
理解了这三段各代表什么,下面的符号就好记了——它们本质上是在说「允许自动升级到哪一段为止」。
常用符号
| 写法 | 含义 | 例子 | 实际范围 |
|---|---|---|---|
18.2.0 | 精确版本 | "react": "18.2.0" | 只装 18.2.0 |
^18.2.0 | 锁主版本 | "react": "^18.2.0" | >=18.2.0 <19.0.0 |
~18.2.0 | 锁次版本 | "react": "~18.2.0" | >=18.2.0 <18.3.0 |
* | 任意版本 | "react": "*" | 最新版 |
>=18.2.0 | 大于等于 | — | 18.2.0 及以上 |
^ 是最常见的,pnpm add / npm install 默认加的就是它。它允许次版本和补丁版本自动升级,但不会跨主版本(不会升到有破坏性变更的新大版本),适合绝大多数依赖。
~ 更保守,只允许补丁版本升级,适合对稳定性要求很高、不想随便引入新功能代码的场景。
一个细节:
^0.x.y的行为和直觉不同。当主版本是0时,^0.2.3只允许升到<0.3.0——因为 SemVer 规定0.x阶段每个次版本都可能有破坏性变更,所以^此时退化成类似~的行为。
范围写法
可以用空格组合多个条件:
"react": ">=16.0.0 <18.0.0"
这样只会安装 16.x.x 和 17.x.x,不会装到 18。
锁文件才是最终决定者
这里有个常被误解的点:^ 和 ~ 只是声明「允许的范围」,实际装哪个版本由锁文件决定(pnpm-lock.yaml 或 package-lock.json)。锁文件记录的是精确版本,保证团队里每个人 install 出来的依赖完全一致。
所以:
- 日常
pnpm install是严格按锁文件来的,^/~不会让你今天装 18.2.0、明天就变成 18.3.0。 package.json里的版本符号,主要影响pnpm update时能把依赖升到哪个版本,以及锁文件不存在时首次安装解析出的版本。
三、在 ESM 项目里直接运行 TypeScript 文件
Node.js 原生不认识 .ts 文件。在原生 ESM 项目(package.json 里 "type": "module")里这个问题更明显,因为连 require 那套 hack 也用不了。常见有三种方式。
方法一:tsx(推荐)
tsx 是目前最省事的方案,直接运行 .ts 文件,ESM 和 CJS 都支持,几乎零配置。
pnpm add -D tsx
package.json:
{
"scripts": {
"dev": "tsx src/index.ts"
}
}
pnpm dev
就这样,没有其他配置。它底层用 esbuild 做即时转译,启动速度也很快,开发阶段首选。
方法二:ts-node + ESM loader
ts-node 是老牌工具,但在 ESM 项目里需要额外配置:
pnpm add -D ts-node
package.json:
{
"scripts": {
"dev": "node --loader ts-node/esm src/index.ts"
}
}
同时 tsconfig.json 里 module 要设成 ESNext 或 NodeNext。需要注意的是,在较新的 Node.js 版本里 --loader 标志会有 deprecation 警告(官方改推 --import + register),所以新项目不太推荐再用这个组合。
方法三:先编译再运行
最稳妥的方式,也是生产环境部署该用的方式——先用 tsc 编译成 JS,再用 Node 跑编译产物:
pnpm add -D typescript
npx tsc # 编译到 dist/
node dist/index.js
package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
缺点是开发阶段每改一次都要重新编译,一般配合 tsc --watch 用。
怎么选
- 开发阶段用
tsx,启动快、零配置。 - 生产部署用
tsc编译后跑纯 JS,运行时不带任何转译开销,也最接近线上真实产物。
这套「开发 tsx、部署 tsc」的组合是目前比较主流的搭配。