【pnpm 】pnpm 执行 xxx 的 底层原理

pnpm install / pnpm run dev / pnpm run build 底层原理

讲清楚 pnpm ipnpm run devpnpm run build 在底层做了什么:执行步骤、数据流、以及它们如何与 lockfile、store、node_modulespackage.json scripts 配合。附流程图方便对照。


一、总览:三条命令分别干啥

命令 缩写 主要职责
pnpm install pnpm i 解析依赖 → 拉包/复用 store → 算目录结构 → 链接到 node_modules
pnpm run dev --- 执行 package.jsondev 脚本(及 predev / postdev),通常跑开发服务器
pnpm run build --- 执行 build 脚本(及 prebuild / postbuild),通常做生产构建

pnpm run 的底层逻辑对 devbuild完全一致 ,只是脚本名不同;差异来自你在 scripts 里写的具体命令(如 vitenext build)。


二、pnpm installpnpm 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 与依赖解析
  1. 读 lockfile

    • 若存在 pnpm-lock.yaml,先解析;得到「包名 → 解析后版本、integrity、resolved」等映射。
  2. package.json 对齐

    • 对比 package.json (及 workspace 子包)的 dependenciesdevDependencies 与 lockfile。
    • --frozen-lockfile :若不一致直接失败,不改 lockfile、不写 node_modules
    • 未 frozen:若不一致则重新解析,再更新 lockfile。
  3. 依赖解析(无 lockfile 或需要更新时)

    • registry (默认 npm)拉取元数据,按 semver 解析版本;workspace 内 workspace:* 等解析为本地包。
    • 递归处理传递依赖 ,得到整棵依赖树
    • 若有 overridescatalog 等,在此阶段应用。
  4. 写回 lockfile

    • 将解析结果写回 pnpm-lock.yaml--lockfile-only 时只做这一步,不进行后续链接)。
阶段二:目录结构计算
  1. 计算 node_modules 布局
    • 确定哪些包放在 node_modules (直接依赖)、哪些只在 .pnpm 下、以及 符号链接 的指向。
    • 满足 非扁平、严格依赖:未声明的包不会出现在项目可访问路径下。
阶段三:Store 与链接
  1. Store 存取

    • 实体 存到 全局 store (默认 ~/.local/share/pnpm/store 等,可 store-dir 配置)。
    • 内容寻址:同版本、同 integrity 只存一份;缺少则从 registry 下载 tarball 写入 store。
  2. 硬链接到 .pnpm

    • node_modules/.pnpm 下按 package@version 建目录,包内文件以硬链接从 store 链出。
    • 每个 package@version 有自己的 node_modules ,里面只放它自己的依赖的符号链接。
  3. 符号链接到「使用方」的 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 顶层目录一览

以单包项目、依赖了 vitelodash 为例,项目根目录下的 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
└── ...
  • 直接依赖 (如 vitelodash):在顶层以包名 出现,实际是符号链接 ,指向 .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 解析 vitetsc 等命令时在此目录查找可执行文件。
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/tsctsc.js 文件 包自带 tsc 命令的真实入口;.bin/tsc 最终执行此文件。
node_modules/vite 符号链接 pnpm install 指向 .pnpm/vite@x.x.x/node_modules/viterun 时若脚本里用 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 ,只放自己声明的依赖的符号链接。

示例(项目依赖 vitevite 又依赖 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 只写 storenode_modules (含 .pnpm 与顶层符号链接、.bin);不执行任何业务脚本。
  • pnpm run dev / pnpm run build 不会去「遍历 .pnpm」;它们只是执行 package.json 里配置的命令 ,命令里若写 vite,就会通过 PATH 找到 node_modules/.bin/vite ,再由该文件间接执行到 .pnpm 里对应包的入口

3.3 .bin 目录:有哪些文件、怎么被执行

.bin 下的文件来自各依赖包 package.jsonbin 字段。pnpm 在 install 阶段会为每个 bin 项在 node_modules/.bin 下生成可执行入口 ,名字即 bin 的 key(如 vitetsc)。

平台 / 类型 文件名示例 说明
Unix / Linux / macOS vitetsc(无后缀) 一般为脚本 (shebang 调用 node)或符号链接到包内 bin 文件。
Windows CMD vite.cmdtsc.cmd 批处理,内部通常用 node "%~dp0\..\vite\dist\cli.js" 等形式调包内入口。
Windows PowerShell vite.ps1tsc.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 为例)

  1. pnpm 在当前包package.json 里读到 scripts.dev = "vite"
  2. pnpm 把 <包目录>/node_modules/.bin (及 workspace 根同路径)加到 PATH 前面,再在子 shell 里执行 vite
  3. 系统在 PATH 里找到第一个 名为 vite 的可执行文件:
    • Unix :即 node_modules/.bin/vite(无后缀),可能是脚本或符号链接;
    • Windows CMD :会找 vite.cmd ;PowerShell 可能用 vite.ps1
  4. 执行该文件:
    • 若是脚本 ,内容通常类似 #!/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。
  5. 最终实际运行的是 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 得到要执行的命令字符串 (如 vitevite 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.jsonnode_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.jsonscripts 里找到对应脚本(如 devbuild)。
  • 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.jsonscripts 里找 scriptName (如 devbuild)。
  • 没有则报 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(如 vitetsc)在子包里也能直接调用。
5. 执行命令
  • 子 shell 中执行 scripts[scriptName] 里的字符串(如 vitenext dev)。
  • 通常通过 nodenode_modules/.bin 下对应平台的 wrapper (如 vitevite.jsvite.cmd),再 node vite.js;具体由 npm lifecycles / run-script 等底层处理。

PATH 与 .bin 的关系

pnpm 先把 node_modules/.bin (及 workspace 根同路径)塞进 PATH 前面,再启子进程跑脚本。因此脚本里写的 vitetsc 等会解析到 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 devbuild 在「run」层面的区别

  • 执行机制完全相同 :都是 pnpm run <script> ,只是 <script> 名字不同。
  • 差异 来自你在 scripts 里写的命令,例如:
    • dev :常为 vitenext devwebpack serve长期进程
    • build :常为 vite buildnext buildtsc一次性构建
  • Pre/post :若你配置了 predev / postdevprebuild / 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_modulesnode_modules/.binpnpm run dev / pnpm run build 才能正确找到 vitenext 等命令。
  • 未安装就 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 对应的命令。


七、参考

相关推荐
中草药z2 小时前
【Vibe Coding】初步认识LangChain&LangGraph
前端·langchain·html·agent·cursor·langgraph·vibe
弹简特2 小时前
【JavaEE03-前端部分】JavaScript入门:给网页注入灵魂,从基础到实战玩转交互!
前端·javascript·交互
天人合一peng2 小时前
unity获得和修改button的text(TMP)
java·前端·unity
jiayong232 小时前
Vue 3 面试题 - 状态管理与数据流
前端·javascript·vue.js
摇滚侠4 小时前
npm 设置了阿里云镜像,然后全局安装了 pnpm,pnpm 还需要设置阿里云镜像吗
前端·阿里云·npm
程序员清洒10 小时前
Flutter for OpenHarmony:GridView — 网格布局实现
android·前端·学习·flutter·华为
VX:Fegn089510 小时前
计算机毕业设计|基于ssm + vue超市管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
0思必得010 小时前
[Web自动化] 反爬虫
前端·爬虫·python·selenium·自动化
LawrenceLan10 小时前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart