pnpm install / pnpm run dev / pnpm run build 底层原理
讲清楚
pnpm i、pnpm run dev、pnpm run build在底层做了什么:执行步骤、数据流、以及它们如何与 lockfile、store、node_modules、package.jsonscripts 配合。附流程图方便对照。
一、总览:三条命令分别干啥
| 命令 | 缩写 | 主要职责 |
|---|---|---|
pnpm install |
pnpm i |
解析依赖 → 拉包/复用 store → 算目录结构 → 链接到 node_modules |
pnpm run dev |
--- | 执行 package.json 里 dev 脚本(及 predev / postdev),通常跑开发服务器 |
pnpm run build |
--- | 执行 build 脚本(及 prebuild / postbuild),通常做生产构建 |
pnpm run 的底层逻辑对 dev、build 等完全一致 ,只是脚本名不同;差异来自你在 scripts 里写的具体命令(如 vite、next build)。
二、pnpm install(pnpm i)底层原理
2.1 核心目标
- 根据
package.json(及 workspace 的pnpm-workspace.yaml)确定要装哪些包、哪些版本。 - 用
pnpm-lock.yaml锁定解析结果,保证可复现。 - 把包实体 放进全局 store ,再通过硬链接 + 符号链接 挂到项目的
node_modules,避免重复拷贝。
2.2 整体执行流程(高层)
否
是
否且非 frozen
是或 frozen 通过
pnpm install
存在 pnpm-lock.yaml?
依赖解析,读 package.json 等
解析 lockfile
从 registry 拉元数据与 tarball
构建依赖树
写入/更新 pnpm-lock.yaml
锁内容与 package.json 一致?
计算 node_modules 目录结构
包入 store 或复用已有
硬链接到 .pnpm, 符号链接到 node_modules
安装完成
2.3 分阶段说明
阶段一:Lockfile 与依赖解析
-
读 lockfile
- 若存在
pnpm-lock.yaml,先解析;得到「包名 → 解析后版本、integrity、resolved」等映射。
- 若存在
-
与
package.json对齐- 对比
package.json(及 workspace 子包)的dependencies、devDependencies与 lockfile。 --frozen-lockfile:若不一致直接失败,不改 lockfile、不写node_modules。- 未 frozen:若不一致则重新解析,再更新 lockfile。
- 对比
-
依赖解析(无 lockfile 或需要更新时)
- 从 registry (默认 npm)拉取元数据,按 semver 解析版本;workspace 内
workspace:*等解析为本地包。 - 递归处理传递依赖 ,得到整棵依赖树。
- 若有 overrides 、catalog 等,在此阶段应用。
- 从 registry (默认 npm)拉取元数据,按 semver 解析版本;workspace 内
-
写回 lockfile
- 将解析结果写回
pnpm-lock.yaml(--lockfile-only时只做这一步,不进行后续链接)。
- 将解析结果写回
阶段二:目录结构计算
- 计算
node_modules布局- 确定哪些包放在 根
node_modules(直接依赖)、哪些只在.pnpm下、以及 符号链接 的指向。 - 满足 非扁平、严格依赖:未声明的包不会出现在项目可访问路径下。
- 确定哪些包放在 根
阶段三:Store 与链接
-
Store 存取
- 包实体 存到 全局 store (默认
~/.local/share/pnpm/store等,可store-dir配置)。 - 内容寻址:同版本、同 integrity 只存一份;缺少则从 registry 下载 tarball 写入 store。
- 包实体 存到 全局 store (默认
-
硬链接到
.pnpm- 在
node_modules/.pnpm下按package@version建目录,包内文件以硬链接从 store 链出。 - 每个
package@version有自己的node_modules,里面只放它自己的依赖的符号链接。
- 在
-
符号链接到「使用方」的
node_modules- 根
node_modules:项目直接依赖 的包,符号链接到.pnpm/<pkg>@<version>/node_modules/<pkg>。 - workspace 包 :
workspace:*解析出的本地包,链接到源码目录,不占 store。
- 根
2.4 pnpm install 流程简图(按阶段)
阶段三 存储与链接
store 取/存包
硬链接到 .pnpm
符号链接到 node_modules
阶段二 结构
算依赖树
算 node_modules 布局
阶段一 解析
否
是
读 package.json
读/解析 lockfile
一致?
解析 + 拉 registry
写 lockfile
2.5 小结
pnpm i= 解析(含 lockfile)→ 算结构 → store + 硬链接 + 符号链接。- Workspace 下会多一步:解析
pnpm-workspace.yaml、处理workspace:*,再统一算布局、链接。
三、node_modules 目录结构与执行相关文件
本节把 pnpm install 完成后 node_modules 里有哪些目录和文件、pnpm run dev / pnpm run build 执行时又会用到其中哪些,按「目录 → 文件 → 执行逻辑」列清楚,并详细列出与执行相关的文件清单。
3.1 node_modules 顶层目录一览
以单包项目、依赖了 vite 和 lodash 为例,项目根目录下的 node_modules 大致长这样:
<项目根>/node_modules/
├── .bin/ # 可执行命令的入口(见 3.3)
│ ├── vite # Unix 下执行 vite 时实际跑的文件
│ ├── vite.cmd # Windows CMD
│ ├── vite.ps1 # Windows PowerShell
│ ├── tsc
│ ├── tsc.cmd
│ └── ...
├── .modules.yaml # pnpm 元数据(store 路径、layout 版本等)
├── .pnpm/ # 所有包实体所在处(硬链接到 store,见 3.2)
├── vite # 符号链接 → .pnpm/vite@x.x.x/node_modules/vite
├── lodash # 符号链接 → .pnpm/lodash@x.x.x/node_modules/lodash
└── ...
- 直接依赖 (如
vite、lodash):在顶层以包名 出现,实际是符号链接 ,指向.pnpm/<包名>@<版本>/node_modules/<包名>。 .bin:下面是对应各包bin字段的可执行入口 (脚本或符号链接),pnpm run dev/pnpm run build时 PATH 里会带上这个目录。.modules.yaml:pnpm 自己用的元数据,记录 store 路径、layout 版本等,run 不读它,install 会写。
3.1.1 与「执行」相关的 node_modules 内文件详细清单
下表按路径 列出 pnpm run dev / pnpm run build 执行链路中会读、会执行的 node_modules 内文件与目录;「执行逻辑」一列说明该文件在运行时的作用。
| 路径 | 类型 | 谁创建 | 执行时作用 / 执行逻辑 |
|---|---|---|---|
node_modules/.bin/ |
目录 | pnpm install | 被加入 PATH 前面;shell 解析 vite、tsc 等命令时在此目录查找可执行文件。 |
node_modules/.bin/vite |
文件(脚本或符号链接) | pnpm install(根据 vite 的 bin 字段) | Unix/macOS :被 shell 执行。若为脚本,首行 shebang 调 node,正文调包内入口;若为符号链接,指向 .pnpm/vite@x.x.x/node_modules/vite/dist/node/cli.js 等,由 node 执行。 |
node_modules/.bin/vite.cmd |
文件(批处理) | pnpm install | Windows CMD :执行时用 node "%~dp0\..\vite\dist\node\cli.js" 等形式调包内入口(%~dp0 为 .cmd 所在目录)。 |
node_modules/.bin/vite.ps1 |
文件(PowerShell) | pnpm install | Windows PowerShell :脚本内用 node $PSScriptRoot\..\vite\dist\node\cli.js 等调包内入口。 |
node_modules/.bin/tsc |
文件 | pnpm install(typescript 的 bin) | 同上逻辑,最终执行 node .../typescript/bin/tsc 或 tsc.js。 |
node_modules/.bin/tsc.cmd / .ps1 |
文件 | pnpm install | Windows 下执行 tsc 时命中的 wrapper。 |
node_modules/.pnpm/ |
目录 | pnpm install | 存所有包实体;run 不直接遍历,而是通过 .bin 里的 wrapper 间接执行到其下某包的 bin 入口文件。 |
node_modules/.pnpm/vite@5.4.0/node_modules/vite/ |
目录(硬链接到 store) | pnpm install | 包本体;.bin/vite 的 wrapper 最终会 node 这个目录下 package.json#bin 指定的入口(如 dist/node/cli.js)。 |
node_modules/.pnpm/vite@5.4.0/node_modules/vite/package.json |
文件 | 包自带 | 定义 bin 入口路径;pnpm 安装时据此在 .bin 下生成 wrapper;运行时由 wrapper 或 node 间接读到入口路径。 |
node_modules/.pnpm/vite@5.4.0/node_modules/vite/dist/node/cli.js |
文件 | 包自带 | vite 的 CLI 入口;.bin/vite (或 .cmd/.ps1)最终执行 node .../cli.js,即此文件。 |
node_modules/.pnpm/typescript@x.x.x/node_modules/typescript/bin/tsc 或 tsc.js |
文件 | 包自带 | tsc 命令的真实入口;.bin/tsc 最终执行此文件。 |
node_modules/vite |
符号链接 | pnpm install | 指向 .pnpm/vite@x.x.x/node_modules/vite;run 时若脚本里用 node 的 require/import 解析 vite,会走到此链接再到 .pnpm 下包本体。 |
node_modules/.modules.yaml |
文件 | pnpm install | 记录 store 路径、layout 版本;仅 install 使用 ,run 不读。 |
目录小结:
- 执行 dev/build 直接用到 :
package.json(scripts)、node_modules/.bin/*(wrapper)、.pnpm/<包>@<版本>/node_modules/<包>/下 bin 入口文件。 - 间接用到 :顶层
node_modules/<包名>符号链接(Node 解析require('vite')等时)、.pnpm 下各依赖的 node_modules(运行时模块解析)。
3.2 .pnpm 目录结构(包实体与依赖链)
.pnpm 里才是「包的真实内容」所在位置(内容来自 store 的硬链接)。每个 package@version 一个目录,且每个包有自己的 node_modules ,只放自己声明的依赖的符号链接。
示例(项目依赖 vite,vite 又依赖 esbuild 等):
node_modules/.pnpm/
├── vite@5.4.0
│ └── node_modules/
│ ├── vite # 指向 store 的硬链接(包本体)
│ ├── esbuild # 符号链接 → ../../esbuild@x.x.x/node_modules/esbuild
│ ├── rollup # 符号链接 → ...
│ └── ...
├── esbuild@0.19.x
│ └── node_modules/
│ └── esbuild # 指向 store
├── lodash@4.17.21
│ └── node_modules/
│ └── lodash
└── ...
<包名>@<版本>/node_modules/<包名>:包本体(目录或硬链接到 store 的目录)。<包名>@<版本>/node_modules/<依赖名>:该包的依赖,以符号链接 指到../../<依赖名>@<版本>/node_modules/<依赖名>。- 根目录的
node_modules/vite:符号链接到.pnpm/vite@5.4.0/node_modules/vite,所以你在代码里import 'vite'时,Node 解析到的就是.pnpm里这一份。
执行逻辑:
pnpm install只写 store 和node_modules(含.pnpm与顶层符号链接、.bin);不执行任何业务脚本。pnpm run dev/pnpm run build不会去「遍历 .pnpm」;它们只是执行package.json里配置的命令 ,命令里若写vite,就会通过 PATH 找到node_modules/.bin/vite,再由该文件间接执行到.pnpm里对应包的入口。
3.3 .bin 目录:有哪些文件、怎么被执行
.bin 下的文件来自各依赖包 package.json 的 bin 字段。pnpm 在 install 阶段会为每个 bin 项在 node_modules/.bin 下生成可执行入口 ,名字即 bin 的 key(如 vite、tsc)。
| 平台 / 类型 | 文件名示例 | 说明 |
|---|---|---|
| Unix / Linux / macOS | vite、tsc(无后缀) |
一般为脚本 (shebang 调用 node)或符号链接到包内 bin 文件。 |
| Windows CMD | vite.cmd、tsc.cmd |
批处理,内部通常用 node "%~dp0\..\vite\dist\cli.js" 等形式调包内入口。 |
| Windows PowerShell | vite.ps1、tsc.ps1 |
PowerShell 脚本,同样会去调包内对应 js。 |
.bin 下典型文件内容示例(执行逻辑):
-
Unix:
node_modules/.bin/vite(脚本形式时)内容通常类似:
bash#!/usr/bin/env node require('../vite/dist/node/cli.js')或直接为符号链接 ,指向
.pnpm/vite@5.4.0/node_modules/vite/dist/node/cli.js。执行时:shell 调起该文件 → 若为脚本则#!/usr/bin/env node导致用 node 执行本文件,进而 require 包内 cli.js ;若为符号链接则 node 执行链接目标(即 cli.js)。 -
Windows CMD:
node_modules/.bin/vite.cmd内容通常类似:
bat@echo off node "%~dp0..\vite\dist\node\cli.js" %*执行逻辑 :
%~dp0为当前 .cmd 所在目录(即node_modules/.bin),..\vite为顶层符号链接 vite (在 pnpm 下会解析到 .pnpm 里对应包),最终用 node 执行 cli.js ,%*把命令行参数原样传给 cli.js。 -
Windows PowerShell:
node_modules/.bin/vite.ps1逻辑类似,用
$PSScriptRoot定位到 .bin,再 node 执行上一级 vite 下的入口 js。
执行逻辑(以 pnpm run dev 且 scripts.dev 为 vite 为例):
- pnpm 在当前包 的
package.json里读到scripts.dev = "vite"。 - pnpm 把
<包目录>/node_modules/.bin(及 workspace 根同路径)加到 PATH 前面,再在子 shell 里执行vite。 - 系统在 PATH 里找到第一个 名为
vite的可执行文件:- Unix :即
node_modules/.bin/vite(无后缀),可能是脚本或符号链接; - Windows CMD :会找
vite.cmd;PowerShell 可能用vite.ps1。
- Unix :即
- 执行该文件:
- 若是脚本 ,内容通常类似
#!/usr/bin/env node+ 调node <包内入口>,或直接node path/to/vite/dist/node/cli.js; - 若是符号链接 ,会指向
.pnpm/vite@x.x.x/node_modules/vite下的 bin 入口 (如dist/node/cli.js),再由 node 执行该 js。
- 若是脚本 ,内容通常类似
- 最终实际运行的是
node+.pnpm/vite@x.x.x/node_modules/vite里声明的 bin 入口文件。
因此:执行链路 = package.json#scripts.dev → shell 执行 vite → PATH 解析到 node_modules/.bin/vite (或 .cmd/.ps1)→ 该文件内部执行 node + .pnpm 里 vite 的 bin 入口。
执行时文件与目录关系(示意):
渲染错误: Mermaid 渲染失败: Parse error on line 5: ...bin/vite] D --> E[.pnpm/vite@x.x.x/n ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'
3.4 执行 dev / build 时涉及的文件与目录(按顺序)
| 步骤 | 类型 | 路径 / 文件 | 作用 |
|---|---|---|---|
| 1 | 读 | <包目录>/package.json |
确定当前包、查 scripts.dev / scripts.build 等。 |
| 2 | 读 | scripts.predev / scripts.dev / scripts.build 等 |
得到要执行的命令字符串 (如 vite、vite build)。 |
| 3 | 环境 | <包目录>/node_modules/.bin 、<workspace根>/node_modules/.bin |
被 pnpm 追加到 PATH 前面。 |
| 4 | 执行 | node_modules/.bin/vite (或 vite.cmd / vite.ps1) |
shell 解析 vite 时命中的可执行文件。 |
| 5 | 执行 | node_modules/.pnpm/vite@x.x.x/node_modules/vite/dist/node/cli.js(以 vite 为例) |
.bin 里的 wrapper 最终用 node 执行的真实入口。 |
| 6 | 读 | 该包及其依赖下的 package.json 、node_modules/... |
由 Node / Vite 等运行时按模块解析规则继续读,与 pnpm 无直接关系。 |
3.4.1 按「是否参与执行」区分的 node_modules 目录一览
下面用一棵更完整的目录树,标出执行 dev/build 时直接参与 的目录/文件(✅)与仅 install 使用、run 不读的(○):
<项目根>/node_modules/
├── .bin/ ✅ run 时 PATH 包含此目录,执行 vite/tsc 等命中此处
│ ├── vite ✅ Unix 下执行 vite 时运行
│ ├── vite.cmd ✅ Windows CMD 下执行 vite 时运行
│ ├── vite.ps1 ✅ Windows PowerShell 下执行 vite 时运行
│ ├── tsc / tsc.cmd / tsc.ps1 ✅ 同上,tsc 命令
│ └── ...
├── .modules.yaml ○ 仅 pnpm install 读写,run 不读
├── .pnpm/ ✅ run 时通过 .bin wrapper 间接执行到其下包的 bin 入口
│ ├── vite@5.4.0/
│ │ └── node_modules/
│ │ ├── vite/ ✅ 包本体,.bin/vite 最终 node 其下 bin 入口
│ │ │ ├── package.json ✅ 定义 bin 入口路径
│ │ │ └── dist/node/cli.js ✅ vite 命令的真实执行文件
│ │ ├── esbuild ○ run 时由 vite 等按 require 解析
│ │ └── ...
│ ├── typescript@5.x.x/
│ │ └── node_modules/
│ │ └── typescript/bin/tsc ✅ tsc 命令的真实执行文件
│ └── ...
├── vite ✅ 符号链接;Node require('vite') 等会解析到此
├── lodash ✅ 同上
└── ...
执行链路小结 :
scripts.dev 字符串(如 vite)→ shell 在 PATH 里找到 node_modules/.bin/vite (或 .cmd/.ps1)→ 该文件内部执行 node + .pnpm 下 vite 的 bin 入口 (如 cli.js)→ 之后由 Vite/Node 按模块解析规则读 .pnpm 下各依赖,与 pnpm 无直接关系。
目录小结:
- install 生成并维护:
node_modules/、node_modules/.pnpm/、node_modules/.bin/、node_modules/.modules.yaml,以及顶层包名符号链接。 - run 直接用到的是:
package.json(读 scripts)、node_modules/.bin/*(执行入口),间接用到.pnpm里对应包的 bin 入口文件。
3.5 小结
node_modules顶层:.bin(可执行入口)、.pnpm(包实体与依赖链)、.modules.yaml(pnpm 元数据)、以及直接依赖的符号链接。.pnpm:按包名@版本存包本体(硬链接到 store),每个包有自己的node_modules,里面是该包依赖的符号链接。.bin:由 install 根据各包 bin 生成;run 时 PATH 包含.bin,执行vite等会先走到.bin再转到.pnpm里对应包的入口文件。- 执行 dev/build :先读
package.json的 scripts,再在子 shell 里执行命令字符串,通过.bin找到并执行对应包的 bin 入口。
四、pnpm run(含 dev / build)底层原理
4.1 核心目标
- 在当前包 的
package.json的scripts里找到对应脚本(如dev、build)。 - 按 pre / 本体 / post 顺序执行(若有)。
- 执行时把
node_modules/.bin(及 workspace 根node_modules/.bin)加入 PATH,以便直接跑本地安装的 CLI。
4.2 整体执行流程
否
是
是
否
是
否
pnpm run scriptName
解析当前包,读 package.json
scripts.scriptName 存在?
报错 Missing script
存在 pre scriptName?
执行 pre scriptName
执行 scriptName
存在 post scriptName?
执行 post scriptName
结束
4.3 分步骤说明
1. 确定「当前包」与 script
- 当前目录 若不是 workspace 根,pnpm 会向上找 包含
package.json的目录,当作当前包。 - Workspace :若在子包目录执行
pnpm run dev,则用该子包 的package.json;在根目录则用根包的。
2. 查找 script
- 在
package.json→scripts里找scriptName(如dev、build)。 - 没有则报
Missing script: "dev"等错误。
3. Pre / 本体 / Post 顺序
- 若存在
prescriptName,先执行pnpm run prescriptName(递归,同样有 pre/post)。 - 再执行
scriptName对应的命令。 - 若存在
postscriptName,再执行pnpm run postscriptName。 - 例如
pnpm run build→ 有prebuild则先prebuild,再build,再postbuild(若有)。
4. 准备执行环境(PATH 等)
- 将
<包目录>/node_modules/.bin加入 PATH 前端。 - Workspace :还会把
<workspace 根>/node_modules/.bin加入 PATH,因此根目录装的 CLI(如vite、tsc)在子包里也能直接调用。
5. 执行命令
- 在子 shell 中执行
scripts[scriptName]里的字符串(如vite、next dev)。 - 通常通过
node跑node_modules/.bin下对应平台的 wrapper (如vite→vite.js或vite.cmd),再node vite.js;具体由 npm lifecycles / run-script 等底层处理。
PATH 与 .bin 的关系 :
pnpm 先把 node_modules/.bin (及 workspace 根同路径)塞进 PATH 前面,再启子进程跑脚本。因此脚本里写的 vite、tsc 等会解析到 node_modules/.bin 里的 wrapper,而 .bin 里的文件是 pnpm install 阶段根据各包 bin 字段创建的符号链接或脚本。流程关系如下:
pnpm run dev 或 build
pnpm install
install 完成后 .bin 就绪
解析依赖
链接包到 node_modules
根据 bin 字段生成 .bin 下可执行文件
查找 scripts.dev 或 scripts.build
PATH 前追加 node_modules/.bin
子 shell 执行脚本命令
解析 vite 等到 .bin 对应 wrapper
4.4 pnpm run 流程简图(环境 + 生命周期)
生命周期
pre scriptName
scriptName
post scriptName
环境准备
确定当前包
找 scripts.scriptName
PATH += node_modules/.bin
Workspace 时 PATH += 根 node_modules/.bin
在子 shell 中执行命令
退出码决定 pnpm run 成功/失败
4.5 dev 与 build 在「run」层面的区别
- 执行机制完全相同 :都是
pnpm run <script>,只是<script>名字不同。 - 差异 来自你在
scripts里写的命令,例如:dev:常为vite、next dev、webpack serve等长期进程;build:常为vite build、next build、tsc等一次性构建。
- Pre/post :若你配置了
predev/postdev、prebuild/postbuild,会按顺序跑;没配则只跑本体。
4.6 小结
pnpm run dev/pnpm run build= 找 script → 可能 pre → 本体 → 可能 post;执行前把node_modules/.bin等加入 PATH,在子 shell 中跑对应命令。node_modules/.bin里的可执行文件由 依赖包 的bin字段生成,pnpm 在 install 阶段已经链好;run 只负责 查 script、改 PATH、调起这些 bin。
五、三者之间的关系
pnpm run dev / build
pnpm install
解析依赖
store 与链接
node_modules 就绪
.bin 可执行文件就绪
读 scripts
PATH += .bin
pre / 本体 / post
执行 vite / next 等
pnpm install准备好node_modules和node_modules/.bin;pnpm run dev/pnpm run build才能正确找到vite、next等命令。- 未安装就 run ,通常会报 找不到命令 或 Cannot find module。
六、常用 flag 与行为
6.1 pnpm install
| Flag | 作用 |
|---|---|
--frozen-lockfile |
不更新 lockfile;若与 package.json 不一致则失败 |
--lockfile-only |
只更新 pnpm-lock.yaml ,不写 node_modules |
--prefer-offline |
尽量用 store,缺的再拉 |
--offline |
只用 store,不访问 registry |
6.2 pnpm run
| Flag | 作用 |
|---|---|
--silent |
少打日志 |
--prefix <path> |
以指定目录为包根(找 package.json) |
dev / build 本身没有专属 flag;传参会透传给脚本,例如:
bash
pnpm run build -- --mode production
-- 后面的 --mode production 会交给 scripts.build 对应的命令。
七、参考
- pnpm install
- pnpm run
- Symlinked node_modules structure
- npm scripts(pre/post 等与 pnpm 兼容)