store 位置
pnpm store path # macOS/Linux: ~/.local/share/pnpm/store/v3 # Windows: %LOCALAPPDATA%\pnpm\store\v3
每个磁盘盘符/分区独立一个 store——这很重要,后面讲硬链接的限制。
CAS:按内容哈希存储
store/v3/files/ ├── 00/ │ ├── 1234abcd...ef ← react index.js 的内容 │ └── ... ├── 01/ │ └── ff00aa11... ← lodash debounce.js ├── ... └── ff/
文件名就是它的 SHA-512 哈希。两个包里有两份相同的 LICENSE 文件(内容一样),在 store 里只占一份——不区分来自哪个包,只看内容。
CAS 是 Git 的发明
Git 对象库就是 content-addressable:文件按 SHA-1 哈希存 blob,commit/tree 是 blob 的引用。pnpm 把这套搬给 node_modules,收益:高度去重、完整性校验、可并行访问。
Git 对象库就是 content-addressable:文件按 SHA-1 哈希存 blob,commit/tree 是 blob 的引用。pnpm 把这套搬给 node_modules,收益:高度去重、完整性校验、可并行访问。
硬链接:0 拷贝引用
store/v3/files/ab/cd1234... ← 真实磁盘块(8KB) ↑ (inode 被多处引用) ├── 项目 A/node_modules/.pnpm/react@18.3.1/node_modules/react/index.js ├── 项目 B/node_modules/.pnpm/react@18.3.1/node_modules/react/index.js └── 项目 C/node_modules/.pnpm/react@18.3.1/node_modules/react/index.js
硬链接不是拷贝:系统只记录「多了一个引用」,实际数据还是那一块。你在项目 A 打开 index.js 和在 B 打开,操作的是同一个物理文件。
硬链接 vs symlink
| 硬链接 | 符号链接(symlink) | |
|---|---|---|
| 本质 | 同一个 inode 的多个 dentry | 一个指向路径的"快捷方式"文件 |
| 跨文件系统 | ❌ 不行 | ✅ 可以 |
| 删除原文件 | 数据仍在(其它链接仍有效) | symlink 变死链 |
| 工具识别 | 几乎无感(看起来就是文件) | 某些工具要 follow |
| pnpm 用来 | store → node_modules 文件层复用 | node_modules 目录层组织 |
为什么跨盘符不行
硬链接不能跨 partition/volume——数据块必须在同一个文件系统内。
# 场景:项目在 D 盘,store 在 C 盘 # pnpm 发现跨盘,退回到"复制"(仍然比 npm 快,不过没有去重) # 解决:把 store 移到和项目同盘 pnpm config set store-dir D:\pnpm-store
# .npmrc store-dir=D:\pnpm-store
store 内部结构
~/.local/share/pnpm/store/v3/ ├── index/ ← 包的索引(metadata) │ └── react@18.3.1-<hash>.json ├── files/ ← 内容寻址的实际文件 │ ├── 00/ │ ├── 01/ │ └── ... ├── tmp/ ← 下载中转 └── v3/.storage ← 访问时间等
// index/react@18.3.1-xxx.json 示例 { "name": "react", "version": "18.3.1", "files": { "index.js": { "checkedAt": 1732..., "integrity": "sha512-xxx", "mode": 420, "size": 3421 }, "package.json": { ... } } }
完整性校验
每次安装,pnpm 会根据 lockfile 的 integrity 字段校验 store 里文件的哈希。被篡改/损坏 → 报错并重下。这是供应链安全的第一道门。
# pnpm-lock.yaml 片段 packages: react@18.3.1: resolution: integrity: sha512-xxx... // SRI 哈希 tarball: https://registry.npmjs.org/react/-/react-18.3.1.tgz
store prune:清理没引用的包
pnpm store prune # 扫 store,找到没有被任何项目引用的文件,删掉 # 日常不需要跑,几个月一次
删了也没事,下次 install 会重新下。
store status:检查一致性
pnpm store status # 对比 store 里文件和 index 里记录的哈希,找损坏
CI 缓存 store
CI 最大的瓶颈是每次 install 重下所有包。pnpm store 是个目录,只要把它缓存下来,二次 CI 几乎零下载:
# GitHub Actions - uses: actions/cache@v4 with: path: ~/.local/share/pnpm/store key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: pnpm-store- - run: pnpm install --frozen-lockfile
首次 CI:2 分钟;有缓存时:20 秒。再配合 Turborepo 的任务缓存,端到端 30 秒出构建产物。
Docker 多阶段利用 store
FROM node:20-alpine AS base RUN corepack enable WORKDIR /app FROM base AS deps COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile FROM base AS runtime COPY --from=deps /app/node_modules ./node_modules COPY . . CMD ["pnpm", "start"]
--mount=type=cache 让 BuildKit 把 store 挂成 layer-less 缓存,跨构建持久——CI 构建速度从 3 分钟降到 30 秒。
真实节省
实测一台 Mac 上装 20 个 Next.js 项目:
| 总大小 | 平均 | |
|---|---|---|
| npm(20 项目) | 11.8 GB | 590 MB/项目 |
| pnpm store + 20 项目 | 1.9 GB | 95 MB/项目 |
| 节省 | 9.9 GB | 83% |
CAS 的副作用
修改 node_modules 里的文件 = 改 store
因为是同一个 inode。你手动改了
node_modules/react/index.js,所有用 react@18.3.1 的项目都会看到改动。永远别手动改,改了也别期望保留——用 pnpm patch 正规做。(第 8 章)node_modules 只读
好的 IDE 插件会识别 node_modules 只读,防止误编辑。
某些工具不认 symlink
极少,主要是老 bundler。遇到可以
node-linker=hoisted 临时降级。本章小结
- store 位置
~/.local/share/pnpm/store/v3,按文件 SHA-512 哈希分桶存储 - 硬链接把 store → node_modules,0 拷贝、0 额外空间;跨盘退回复制
- integrity 字段在 lockfile,install 时校验,防止篡改
- CI 缓存 store 目录,重装秒级完成;Docker 用
--mount=type=cache - 别手动改 node_modules 里的文件——改 store 影响所有项目,用 pnpm patch