昌维的博客

为什么 D 盘上的 pnpm 项目会在盘符根目录创建 `.pnpm-store`?

2026年6月22日 12:00 · Chang Wei (昌维) <[email protected]> · zh-Hans-CN

如果你把前端仓库放在 D 盘,第一次运行 pnpm install 后打开资源管理器,很可能会在 D:\ 根目录看到一个名为 .pnpm-store 的隐藏文件夹。它体积不小,名字也令人困惑——这到底是病毒、误操作,还是 pnpm 的正常行为?本文从文件系统底层出发,解释 pnpm 内容寻址 store 的选址逻辑、hard link(硬链接)原理,以及 Windows、Linux、macOS 上的实现差异。

先描述一下你看到的现象

典型的 Windows 开发环境是这样的:

  • 系统盘 C: 上有用户目录,pnpm 的「默认全局 store」位于 C:\Users\<用户名>\AppData\Local\pnpm\store
  • 代码仓库放在 D: 盘,例如 D:\GitHub\cw1997\blog.changwei.me
  • 在 D 盘项目里执行 pnpm install 之后,D 盘根目录出现了 D:\.pnpm-store

以本仓库为例,在 D 盘项目目录下执行:

Bash
pnpm store path

输出为:

Plain Text
D:\.pnpm-store\v11

同时 pnpm config get store-dir 返回 undefined,说明你没有手动配置 store 路径——这是 pnpm 根据项目所在磁盘 自动选择 的结果。

D 盘根目录下的 .pnpm-store 文件夹
D 盘根目录下的 .pnpm-store 文件夹

pnpm store path 命令的终端输出
pnpm store path 命令的终端输出

需要立刻澄清的一点:D:\.pnpm-store 不是 某个项目的 node_modules,而是 pnpm 在该磁盘上的 内容寻址存储(content-addressable store),相当于全局包缓存。项目里你日常接触到的依赖目录仍然是项目内的 node_modulesnode_modules/.pnpm

pnpm 的三层存储模型

要理解 .pnpm-store 为何存在、为何在 D 盘根目录,先要搞清楚 pnpm 安装依赖时数据流经的三个层级。pnpm 官方文档 Symlinked node_modules structure 对此有完整说明。

flowchart TB registry[npm registry] --> fetch[下载 tarball 并校验 hash] fetch --> store["Content-addressable store\n例如 D:\\.pnpm-store\\v11"] store -->|hard link / clone / copy| virtualStore["node_modules/.pnpm\n虚拟 store"] virtualStore -->|symlink / junction| nodeModules["node_modules\n依赖树"] nodeModules --> nodeResolve[Node.js 模块解析]
层级典型路径使用的链接机制作用
Store(全局缓存)D:\.pnpm-store\v11内容寻址;文件以 hash 命名跨项目共享同一份包文件,节省磁盘与下载
Virtual store(虚拟 store)node_modules/.pnpmhard link、clone 或 copy为当前项目准备可挂载依赖图的实体目录
依赖树node_modules/reactsymlink(符号链接)或 junction(联接点)满足 Node.js 模块解析算法所需的嵌套结构

打开本项目的 node_modules/.pnpm,可以看到每个依赖对应一个形如 @[email protected] 的目录——这是 pnpm 虚拟 store 的实体部分:

项目 node_modules/.pnpm 目录结构
项目 node_modules/.pnpm 目录结构

官方文档用 [email protected] 依赖 [email protected] 的例子说明:node_modules真正占磁盘的文件 只有 hard link 到 store 的那份;其余结构靠 symlink 拼装:

Plain Text
node_modules
└── .pnpm
    ├── [email protected]
    │   └── node_modules
    │       └── bar
    │           ├── index.js     -> <store>/001
    │           └── package.json -> <store>/002
    └── [email protected]
        └── node_modules
            ├── foo -> <store>
            └── bar -> ../../[email protected]/node_modules/bar

Node.js 在解析模块时会 忽略 symlink 的层级(follow symlinks),因此这种看起来「绕」的结构与 CommonJS / ESM 的解析算法完全兼容。pnpm 还刻意避免循环 symlink:依赖与被依赖包放在同一层级的 node_modules 目录中,而不是 foo/node_modules/bar 这种无限嵌套。

为什么 store 会落在 D 盘根目录?

这是本文的核心问题。答案可以概括为:pnpm 必须让 store 与项目处于同一磁盘/文件系统,才能使用 hard link;而 D 盘通常没有用户 home 目录,于是 store 退而求其次落在该卷根目录 D:\.pnpm-store

决策逻辑

pnpm 官方在 FAQ — Does pnpm work across multiple drives or filesystems?storeDir 设置 中写得很清楚:

  1. Hard link 不能跨文件系统。 一个文件在 C: 上、项目在 D: 上时,操作系统无法为两者建立 hard link,pnpm 只能 复制(copy) 文件,失去 dedup 与速度优势。
  2. 默认 store 在用户目录。 Windows 上为 ~/AppData/Local/pnpm/store(通常在 C:);macOS 为 ~/Library/pnpm/store;Linux 为 ~/.local/share/pnpm/store
  3. 每个磁盘/文件系统各有一个 store。 若未手动指定 store-dir,在 D: 盘安装时 pnpm 必须在 D: 上创建本地 store。
  4. 该磁盘上没有 home 目录时,store 建在文件系统根目录。 例如 Linux 上挂载在 /mnt/data 的外置盘会得到 /mnt/data/.pnpm-store;Windows D: 盘同理,得到 D:\.pnpm-store
flowchart TD start["pnpm install\n在项目目录执行"] --> locateProject["确定项目所在卷\n例如 D:"] locateProject --> defaultStore["默认 store 候选:\n~/AppData/Local/pnpm/store\n通常在 C:"] defaultStore --> sameVol{"候选 store 与项目\n在同一卷?"} sameVol -->|是,C 盘项目| useDefault["使用 C 盘用户目录 store"] sameVol -->|否,项目在 D 盘| needLocal["必须在 D 盘创建 store"] needLocal --> hasHome{"D 盘存在\n用户 home?"} hasHome -->|有| homeStore["D:\\Users\\...\\AppData\\Local\\pnpm\\store"] hasHome -->|无,常见情况| rootStore["D:\\.pnpm-store"]

与「C 盘已有 store」的关系

很多开发者会困惑:C 盘明明已经有 AppData\Local\pnpm\store,为什么 D 盘还要再来一份?

因为 C 盘的 store 无法 hard link 到 D 盘的项目。FAQ 明确说明:

The package store should be on the same drive and filesystem as installations, otherwise packages will be copied, not linked.

因此:

  • C 盘项目 → 使用 C:\Users\...\AppData\Local\pnpm\store
  • D 盘项目 → 使用 D:\.pnpm-store(或 D 盘上你手动指定的路径)
  • 两个 store 各自独立,可能存储部分相同的包,但在各自磁盘上通过 hard link 复用

若你 强制store-dir 设到 C 盘,却在 D 盘项目里 install,pnpm 会对每个文件执行 copy,安装变慢、磁盘占用翻倍——FAQ 将这种情况称为严重削弱 pnpm 优势。

Linux / macOS 上的类比

逻辑完全一致,只是路径不同:

场景store 位置
项目在 home 分区(如 /home/user/proj~/.local/share/pnpm/store
项目在外置盘 /mnt/ssd/proj,该盘无 home/mnt/ssd/.pnpm-store
macOS 项目在外置 APFS 卷卷根目录 .pnpm-store~/Library/pnpm/store(同卷时)

D 盘根目录的 .pnpm-store 并非 Windows 独有行为,而是 「项目所在卷无 home → store 落卷根」 这一规则在 NTFS 上的体现。

pnpm 的性能与省空间能力,根基在于 hard link(硬链接)。理解它,才能理解 store 选址为何如此「固执」。

文件在磁盘上到底是什么

在 Unix 语义下(Linux、macOS 以及 Windows NTFS 的类似抽象),一个普通文件可以拆成两部分:

  1. 目录项(directory entry):你在资源管理器里看到的「名字 + 路径」,例如 D:\.pnpm-store\v11\files\00\abc123...
  2. inode / 文件记录(file record):存储元数据(权限、大小、时间戳)以及数据块位置的底层结构。Windows NTFS 上对应 MFT(Master File Table) 中的 FILE 记录,其中 $DATA 属性指向实际簇。

Hard link 的含义是:多个目录项(多个路径名)指向 同一个 inode / 同一个 FILE 记录。文件记录里有一个 link count(链接计数);每增加一个 hard link,计数加一;删除一个路径名,计数减一;计数归零时,数据块才被回收。

Soft link(软链接 / 符号链接 symlink) 则不同:它本身是一个独立的小文件,内容是一段路径字符串。访问 symlink 时,操作系统多一次路径解析;symlink 可以跨磁盘、跨文件系统。

用一张简图对比:

Plain Text
Hard link(同 inode,共享数据块):
 
  path A: node_modules/.pnpm/[email protected]/.../index.js  ──┐
                                                         ├──> inode #42 ──> 数据块
  path B: .pnpm-store/v11/files/00/abc...              ──┘
 
Soft link(独立 inode,存目标路径):
 
  path: node_modules/foo  ──> symlink inode ──> 文本 ".pnpm/[email protected]/node_modules/foo"

                                                      └──> 解析后再找到真实 inode

pnpm FAQ — Why have hard links at all? 给出了关键理由:

One package can have different sets of dependencies on one machine. In project A [email protected] can have a dependency resolved to [email protected], but in project B the same dependency of foo might resolve to [email protected].

同一个 [email protected] 在不同项目里,其 依赖子图 可能不同。pnpm 需要为每个项目 hard link 一份 可以独立挂载 node_modules 的包目录,而不是让所有项目 symlink 到 store 里同一份全局目录。

若直接 symlink 到全局 store,再配合 Node 的 --preserve-symlinks,会引入大量工具链兼容问题;pnpm 团队经过权衡,选择了 store → 项目 virtual store 用 hard link,virtual store 内部用 symlink 搭结构 的方案。

这是 FAQ 里最高频的误解之一。pnpm FAQ 的解释:

pnpm creates hard links from the global store to the project's node_modules folders. Hard links point to the same place on the disk where the original files are. So, for example, if you have foo in your project as a dependency and it occupies 1MB of space, then it will look like it occupies 1MB of space in the project's node_modules folder and the same amount of space in the global store. However, that 1MB is the same space on the disk addressed from two different locations. So in total foo occupies 1MB, not 2MB.

Windows 资源管理器、macOS Finder、du 等工具在统计目录大小时,往往 按路径分别累加,因此 store 与 node_modules 看起来各占了 1MB,合计显示 2MB;物理磁盘上实际只有 1MB 数据块。只有 copy 才会真正占用双倍空间。

pnpm 通过 packageImportMethod 控制从 store 导入到 node_modules/.pnpm 的策略(见 settings — packageImportMethod):

行为
auto(默认)优先 clone(写时复制);不支持则 hard link;再不行则 copy
hardlink始终 hard link
clone始终 clone(CoW)
clone-or-copyclone 或 copy
copy始终复制

Clone 在支持 copy-on-write(写时复制)的文件系统上最优:导入速度与 hard link 相当,且你在 node_modules 里修改文件 不会 污染 central store。官方建议在 Linux 上尽量使用 Btrfs 等 CoW 文件系统以获得最佳体验。

Hard link 则有一个实用限制:不能跨文件系统。只要 store 与项目不在同一卷,pnpm 自动降级为 copy——这正是 D 盘必须自建 store 的根本原因。

各操作系统的底层实现差异

Hard link、symlink、junction、reflink、clonefile——这些概念在不同 OS 上的支持程度直接决定 pnpm 的行为。

Windows(NTFS / ReFS)

  • API:CreateHardLinkW(Win32)/ NtSetInformationFile(内核)
  • 限制:仅同一 NTFS 卷内;不能对目录创建 hard link(目录只有 symlink/junction);单文件 hard link 数量上限约 1024(NTFS 实现细节,一般 npm 包远达不到)
  • pnpm 从 store 到 node_modules/.pnpm 的文件级链接依赖此机制

pnpm 在 node_modules 里搭建依赖树时需要 目录级 链接。Windows 上目有两条路:

机制类型跨卷权限要求
Symlink(符号链接)目录或文件可以(相对路径常同卷)创建目录 symlink 需要 SeCreateSymbolicLinkPrivilege;普通用户需 开发者模式 或管理员
Junction(联接点)仅目录否,必须同卷普通用户可用,mklink /J

pnpm FAQ — Does it work on Windows? 说明:

Using symbolic linking on Windows can sometimes be problematic, however, pnpm has a workaround. For Windows, if the Developer Mode is off, we use junctions instead.

因此:

  • 开发者模式关闭 → pnpm 对 node_modules 结构使用 junction
  • 开发者模式开启 → 可使用 symlink

Windows 开发者模式与 pnpm symlink/junction 的关系
Windows 开发者模式与 pnpm symlink/junction 的关系

Junction 与 symlink 对 Node.js 模块解析通常透明,但 junction 不能跨卷——pnpm 的 virtual store 设计本就把实体文件放在项目同卷,junction 在同卷内搭建依赖树足够用。

跨盘行为

C: store + D: 项目 → copy。D: 根目录 .pnpm-store 的存在,正是为了避免这一降级。

ReFS 支持部分类似 CoW 的 block clone,pnpm 在 packageImportMethod=auto 时可能尝试利用;日常开发环境仍以 NTFS 为主。

Linux

ext4 / XFS 等传统文件系统

  • Hard link:同一文件系统内,多个目录项共享 inode;link(2) 系统调用
  • 跨设备 hard link 返回 EXDEV(Cross-device link not permitted)→ pnpm 降级 copy
  • Symlink:独立 inode,内容为路径;可跨 mount point
  • 默认 store:~/.local/share/pnpm/store;外置盘项目 → 卷根 .pnpm-store

Btrfs

pnpm FAQ — Btrfs subvolumes 特别说明:

While Btrfs does not allow cross-device hardlinks between different subvolumes in a single partition, it does permit reflinks. As a result, pnpm utilizes reflinks to share data between these subvolumes.

Reflink(reference link) 是 Btrfs 的 copy-on-write 共享机制:两个文件初始共享数据 extent,写入时才分裂。语义上类似 macOS APFS 的 clonefile,pnpm 的 clone 导入路径会优先使用它。

macOS

  • 默认 store:~/Library/pnpm/store
  • APFS 提供 clonefile(2):CoW 克隆,pnpm packageImportMethod=auto 时优先选择
  • Hard link、symlink 均完整支持;HFS+ 卷上无 reflink,回退 hard link 或 copy
  • 外置 APFS 卷上的 per-volume store 规则与 Windows D: 盘相同

跨平台对比

特性Windows NTFSLinux ext4Linux BtrfsmacOS APFS
同卷 hard link支持支持支持(同 subvolume)支持
跨卷 / 跨 subvolume不支持 → copyEXDEV → copyhard link 不支持;reflink 可跨 subvolume跨卷 → copy
CoW cloneReFS block clone(有限)不支持reflinkclonefile
pnpm 依赖树目录链接junction 或 symlinksymlinksymlinksymlink
无 home 的外置卷 storeD:\.pnpm-store/mnt/xxx/.pnpm-store同左/Volumes/xxx/.pnpm-store

与 npm、Yarn 的对比

npm / Yarn Classicpnpm
node_modules 结构扁平 hoist,多 copy严格隔离 + symlink/junction 树
磁盘复用项目间重复存储多同卷 store + hard link 去重
幽灵依赖易出现(依赖的依赖被 hoist 到顶层)默认仅声明依赖可访问,减少「误用未声明包」
跨盘无 hard link 约束,但空间浪费必须 per-drive store 或接受 copy

pnpm 的 strictness 有意为之:许多历史包未在 package.json 声明全部依赖,在 flat node_modules 下「碰巧能跑」。pnpm 通过 hoist 到 node_modules/.pnpm/node_modules 等方式兼容,但核心设计仍是 显式依赖图 + 链接而非复制

实践建议

确认当前 store 位置

Bash
pnpm store path
pnpm config get store-dir

本仓库在 D 盘,输出为 D:\.pnpm-store\v11,且 store-dir 未配置——符合预期。

能否删除 D:\.pnpm-store

不要在不清楚影响时手动删整个目录。它是 D 盘所有 pnpm 项目的共享缓存;删除后下次 install 需重新下载并填充。

安全清理方式:

Bash
pnpm store prune

prune 会移除 没有任何项目引用 的包文件,不会破坏正在使用的依赖。

想换 store 位置?

若觉得根目录 .pnpm-store 碍眼,可移到 D 盘其他路径,只要仍在 D 盘同一卷 即可保持 hard link:

Bash
pnpm config set store-dir "D:/Dev/pnpm-store"

修改后在新位置重新 install;旧 store 可用 pnpm store prune 逐步清理。

切忌 把 store 设到 C 盘而项目留在 D 盘——除非你愿意接受 copy 带来的空间与性能损失。

多盘开发怎么规划

策略说明
接受 per-drive store最简单;D 盘项目共享 D:\.pnpm-store,符合 pnpm 设计
项目与 store 同放 C 盘C 盘项目统一用 AppData\Local\pnpm\store
外置 SSD 仅放代码store 仍会在该 SSD 卷上生成 .pnpm-store;无法「只用 C 盘 store」除非 copy
统一大容量 D 盘 storepnpm config set store-dir D:/pnpm-store,所有 D 盘项目共用

安全提示

storeDir 文档 强调:pnpm store 属于 trust domain——包文件可能通过 hard link 进入项目,index.db 记录校验 hash。若多用户共用机器,应对 store 目录设置适当文件系统权限,避免不可信用户写入。

总结

  1. 同卷约束:pnpm 依赖 hard link(或 CoW clone)在 store 与 node_modules/.pnpm 之间共享文件数据;hard link 无法跨 Windows 盘符或 Linux mount point。
  2. per-drive store:项目不在默认 store 所在盘时,pnpm 必须在项目所在盘再建 store;未手动配置 store-dir 时,每个磁盘各一份。
  3. D 盘根目录:D 盘通常没有 Windows 用户 home,store 按规则落在卷根 D:\.pnpm-store——这是正常行为,不是异常。

理解 hard link 与 symlink/junction 的分工,就能理解 pnpm 为何在「省空间」与「依赖隔离」之间走出了一条与 npm 截然不同的路。延伸阅读推荐官方 FAQstoreDir / packageImportMethod 设置