Node Package Managers · Part 5 — pnpm & the Content-Addressable Store (+ Bun)
How pnpm gets strictness, speed, and near-zero extra disk per project using a global content-addressable store — plus Bun's cache, linker choices, and security defaults.
Đây là Phần 5 của series 12 phần về package manager Node. Ta đã xong npm và cả hai Yarn. Giờ là pnpm — tool đã thành mặc định phổ biến của team năm 2026 — và một ghi chú cuối về installer của Bun.
Lời chào của pnpm: tính nghiêm ngặt của Yarn PnP, nhưng với node_modules thật mà tool hiểu được. Nó đạt được nhờ một ý tưởng thông minh.
1. Store địa chỉ-theo-nội-dung
npm và Yarn Classic copy package vào node_modules của từng project. Mười project dùng React = mười bản copy trên disk. pnpm từ chối làm vậy.
Thay vào đó, mỗi version package được lưu đúng một lần, toàn cục, đánh địa chỉ bằng hash nội dung:
~/.local/share/pnpm/store/v3/ ← the global content-addressable store
files/<hash-of-file-contents> ← each unique file stored ONCE, ever
{mỗi file duy nhất lưu một lần, vĩnh viễn}
Two packages share an identical file (e.g. a LICENSE)? Stored once.
{Hai package có file giống hệt? Lưu một lần.}
Same package version across 50 projects? Stored once.
{Cùng version package qua 50 project? Lưu một lần.}
Vì key là hash nội dung, file giống hệt tự động khử trùng — kể cả giữa các package khác nhau.
2. Hardlink + symlink = layout ma thuật
node_modules của project được lắp từ hai lớp:
1) The "real" packages live under a hidden folder, HARDLINKED from the store
(a hardlink = another name for the SAME bytes on disk, ~free):
node_modules/.pnpm/react@19.1.4/node_modules/react/... ← hardlink → store
node_modules/.pnpm/react-dom@19.1.4/node_modules/... ← hardlink → store
2) Your DIRECT deps are exposed at the top via SYMLINKS into .pnpm:
node_modules/react → symlink → .pnpm/react@19.1.4/node_modules/react
node_modules/react-dom → symlink → .pnpm/react-dom@19.1.4/...
(lodash is NOT symlinked at top → you can't import it = strict)
{lodash KHÔNG được symlink ở top → không import được = nghiêm ngặt}
Đây là toàn bộ thiết kế:
- Hardlink nghĩa byte không bao giờ bị copy — “cài mới” một package đã cache gần như miễn phí.
- Symlink chỉ phơi bày dependency bạn đã khai báo ở top level. Dependency transitive nằm dưới
.pnpm/và vô hình với code của bạn.
Layout symlink đó là thứ diệt phantom dependency — mà không bỏ thư mục node_modules thật.
3. Nghiêm ngặt, nhưng tương thích
pnpm trúng điểm cân bằng mà các tool khác bỏ lỡ:
Phantom deps Disk (10 projs) Real node_modules Tooling friction
npm ✗ allowed ~10x ✓ yes none
Yarn Classic ✗ allowed ~10x ✓ yes none
Yarn Berry PnP ✓ blocked small (zips) ✗ no HIGH
pnpm ✓ blocked ~1–2x ✓ yes LOW ← sweet spot
{điểm cân bằng}
Cái giá đánh đổi: vài tool hiếm không theo symlink đúng và cần config. Và migrate sang pnpm thường lộ ra phantom dep từng chạy âm thầm — bạn sửa một lần, rồi hưởng đúng đắn mãi mãi.
nodeLinker — chọn độ nghiêm ngặt
Khi một tool cứng đầu không chịu được symlink, pnpm cho cách dự phòng qua nodeLinker. Với pnpm 11, config project không liên quan auth/registry nằm trong pnpm-workspace.yaml:
# pnpm-workspace.yaml
nodeLinker: isolated # default: symlinked, strict (the design above)
# nodeLinker: hoisted # flat npm-style node_modules (lose strictness)
# nodeLinker: pnp # Plug'n'Play, like Yarn Berry (no node_modules)
shamefullyHoist: true # nuclear option: hoist EVERYTHING to top (max compat)
Tên shamefully-hoist là sự tự giễu cố ý của tác giả pnpm: nó vá tool hỏng nhưng tái xuất hiện phantom dependency, nên chỉ dùng như phương án cuối.
Vá dep & hook install
Như Berry, pnpm có luồng patch tích hợp và một file hook lập trình được:
pnpm patch left-pad # edit a copy → pnpm patch-commit writes patches/
// .pnpmfile.cjs — rewrite dependency metadata BEFORE resolution
// (e.g. strip a bad peer dep, or block a package entirely)
function readPackage(pkg) {
if (pkg.name === 'some-lib') delete pkg.dependencies['unwanted'];
return pkg;
}
module.exports = { hooks: { readPackage } };
.pnpmfile.cjs mạnh nhưng bản thân là code chạy khi resolution — coi nó là config tin cậy, đã review, không phải bãi rác.
4. Bộ lệnh pnpm
pnpm cố ý phản chiếu npm, nên trí nhớ cơ bắp chuyển được:
pnpm install # install using the global store
pnpm add react # add a dependency
pnpm add -D vite # add a devDependency
pnpm install --frozen-lockfile# CI: fail if pnpm-lock.yaml would change
pnpm dlx create-vite # npx equivalent
pnpm -r run build # run "build" in every workspace package (recursive)
pnpm --filter web dev # target a single workspace package
pnpm why lodash # explain the dependency
pnpm store prune # garbage-collect unreferenced store files
Hai thứ thiết yếu riêng của pnpm: --filter để nhắm monorepo (tốt nhất — Phần 11) và pnpm store prune để thu hồi disk từ store toàn cục.
clone (reflink) — còn rẻ hơn hardlink
Trên filesystem copy-on-write (APFS trên macOS, Btrfs/XFS trên Linux) pnpm có thể dùng reflink thay hardlink:
# pnpm-workspace.yaml
packageImportMethod: clone # reflink (CoW): a writable copy that shares
# the same blocks until modified — best of both
# fallback order if clone is unsupported: clone → hardlink → copy
Reflink hơn hardlink vì an toàn để sửa: một postinstall sửa file sẽ không làm hỏng store dùng chung, vì việc ghi kích hoạt copy-on-write tách. Với hardlink thuần, store và project trỏ tới đúng cùng byte.
dedupePeerDependents — ít peer trùng
pnpm đôi khi cài một dependency nhiều lần vì các package cần nó với ngữ cảnh peer khác nhau. Cài đặt dedupePeerDependents: true gộp chúng khi an toàn, thu nhỏ cây. Nếu bạn thấy bản trùng bất ngờ, đây là nút đầu tiên cần kiểm tra.
5. Các nút pnpm-workspace.yaml hữu ích
pnpm 11 chỉ đọc auth và registry từ .npmrc; các policy còn lại nằm trong pnpm-workspace.yaml. Vài cái quan trọng:
# pnpm-workspace.yaml
# How to materialize the store into node_modules:
# hardlink (default) | copy | clone (reflink, best on APFS/Btrfs)
packageImportMethod: auto
# Loosen strictness for a stubborn dependency by hoisting a pattern:
publicHoistPattern:
- "*eslint*"
# Dependency build scripts are denied unless explicitly allowed:
onlyBuiltDependencies:
- esbuild
- sharp
# pnpm 11 defaults to a 24-hour age gate; configure it explicitly when the
# policy must remain stable across manager upgrades:
minimumReleaseAge: 1440
minimumReleaseAgeStrict: true
pnpm 10 đổi mặc định bảo mật đáng chú ý: build script lifecycle của dependency không chạy tự động. pnpm 11 tiếp tục hướng này với age gate mặc định và các policy như
trustPolicy,blockExoticSubdeps. Ta sẽ xem lại ở Phần 7 và 10.
6. Installer của Bun nằm ở đâu
bun install là package manager độc lập viết bằng Zig, tối ưu mạnh cho install song song và cache toàn cục:
bun install
→ resolve → global cache (~/.bun/install/cache, hardlinked)
→ hoisted or isolated node_modules → bun.lock (text since 1.2)
Bun hỗ trợ cả linker hoisted lẫn isolated. Project thường kế thừa layout từ lockfile/config hiện có; workspace mới có thể dùng isolated. Vì vậy đừng suy luận độ nghiêm ngặt chỉ từ việc thấy bun.lock — hãy kiểm tra install.linker trong bunfig.toml.
bun install # blazing cold installs → bun.lock
bun add react # add a dependency
bun ci # CI; equivalent to --frozen-lockfile
bunx vite # fast npx equivalent
Khi nào dùng installer của Bun: bạn muốn install dev/CI nhanh nhất có thể, bạn đã dùng Bun runtime, hoặc app mới sẵn sàng dùng tool mới.
7. Quyết định mặc định 2026
Most new projects / teams → pnpm (speed + disk + strict + real node_modules)
Monorepos of any size → pnpm --filter (± Turborepo/Nx)
Install speed is top priority → Bun (choose linker policy explicitly)
Strict + zero-installs, PnP-OK → Yarn Berry (Part 4)
Maximum compatibility / tiny → npm
Trên thực tế: pnpm là mặc định hiện đại an toàn; Bun khi tốc độ install chiếm ưu thế; còn lại theo ràng buộc.
Giờ bạn đã biết gì
- pnpm lưu mỗi version một lần trong store địa chỉ-theo-nội-dung toàn cục.
- Hardlink + symlink cho tốc độ, disk nhỏ, và nghiêm ngặt — với
node_modulesthật. - Bun tối ưu cho tốc độ và hỗ trợ cả layout hoisted lẫn isolated.
- pnpm là mặc định 2026 thực dụng cho phần lớn team.
Bài tập
- Cài cùng dep bằng npm ở hai project, rồi pnpm ở hai project; so tổng disk.
- Trong project pnpm, theo symlink top-level vào
.pnpm/. - Thử import dep transitive chưa khai báo dưới pnpm và xác nhận nó fail như Yarn PnP.
Tiếp theo — Phần 6: Lockfile, tính xác định & integrity: