Node-Modules 与 Hoisting 设置
node_modules settings#
modulesDir#
- 默认值:node_modules
- 类型:路径
将安装依赖项的目录(而不是 node_modules)。
In a workspace, a relative path is used inside every project. An absolute path,
or one that starts with .., is the root project's directory only. Every other
project uses a directory named after its last part, such as vendor for
../vendor.
nodeLinker#
- 默认值:isolated
- Type: isolated, hoisted, pnp, loaded, or a loaded-linker object
定义应该使用什么链接器来安装 Node 包。 The existing scalar values remain supported. The experimental loaded linker also accepts an object with type: loaded and an optional excluded list. Other linker object forms are not supported yet.
Configuration sources replace the whole nodeLinker value. A higher-priority scalar or an object without excluded clears exclusions from lower-priority sources. The environment variable PNPM_CONFIG_NODE_LINKER and --config.node-linker accept the object as JSON.
-
isolated - 依赖项从虚拟存储
node_modules/.pnpm中建立符号链接 -
hoisted - 创建一个没有符号链接的扁平的
node_modules。与 npm 或 Yarn Classic 创建的node_modules一致。 pnpm hoists with Yarn's hoisting algorithm. 使用此设置的正当理由:- 你的工具无法很好地与符号链接配合使用。
- 你的项目会被部署到 serverless 服务提供商。一些 serverless 提供商(例如 AWS Lambda)不支持符号链接。此问题的另一种解决方案是在部署之前打包你的应用程序。
- 如果你想使用
"bundledDependencies"发你的包。 - 如果你使用 --preserve-symlinks 标志运行 Node.js。
-
pnp — 没有
node_modules。 Plug'n'Play 是一种 Yarn Berry 使用的创新的 Node 依赖策略。当使用pnp作为你的链接器时,建议同时将symlink设置为false。 -
loaded - experimental loading directly from the content-addressable store. pnpm preloads its Node.js loader for scripts and commands. Selected compatibility packages and their dependency trees use the global virtual store. See Experimental store loader.
nodeLinker.excluded#
- 默认值:[]
- 类型:string[]
With nodeLinker.type: loaded, materialize these package names and their complete dependency trees in the global virtual store. Names match all installed versions and peer contexts. Packages outside these trees load directly from CAS. Build approval settings still apply.
nodeLinker:
type: loaded
excluded:
- vitest
nodePackageMap.mode#
Added in: v12.13.0
- 默认: off
- Type: off, write-only, load
Controls node_modules/.package-map.json, which pnpm writes during isolated and hoisted installs.
- off - pnpm writes no package map. An install also removes a map that an earlier install left.
- write-only - pnpm writes the package map for other tools to read. Node.js does not load it.
- load - pnpm writes the package map and adds Node's
--experimental-package-mapoption toNODE_OPTIONSin pnpm-managed Node.js script environments.
The environment variable is PNPM_CONFIG_NODE_PACKAGE_MAP_MODE.
nodePackageMap:
mode: write-only
nodePackageMap.type#
Added in: v12.13.0
- 默认:standard
- 类型:standard,loose
控制 node_modules/.package-map.json 的生成方式。
- standard - 只有已声明的依赖项可通过包映射访问。
- loose —— 还会映射那些可通过已安装的
node_modules布局访问到的包,这使得未显式声明但被提升的依赖项能够被解析。
The environment variable is PNPM_CONFIG_NODE_PACKAGE_MAP_TYPE.
nodePackageMap:
mode: load
type: loose
nodeExperimentalPackageMap#
Deprecated since v12.13.0: use nodePackageMap.mode instead. nodeExperimentalPackageMap: true is the same as mode: load, and false is the same as mode: off. When both are set in the same place, nodePackageMap takes precedence.
CLI 和环境变量配置使用 kebab-case 风格的名称 node-experimental-package-map。
nodePackageMapType#
Deprecated since v12.13.0: use nodePackageMap.type instead. When both are set in the same place, nodePackageMap takes precedence.
符号链接#
- 默认值:true
- 类型:Boolean
当 symlink 设置为 false 时,pnpm 创建一个没有任何符号链接的虚拟存储目录。这与 node-linker=pnp 一起是一个有用的设置。
enableModulesDir#
- 默认值:true
- 类型:Boolean
当为 false 时,pnpm 不会将任何文件写入模块目录
(node_modules)。这对于在用户空间的文件系统 (FUSE) 中挂载模块目录时很有用。有一个实验性 CLI 允许你在 FUSE 中挂载模块目录:@pnpm/mount-modules。
virtualStoreDir#
- 默认值:node_modules/.pnpm
- 类型:路径
带有指向存储的链接的目录。所有直接和间接依赖项都链接到此目录中。
Dependencies installed from a URL or local path get a hash suffix on their directory name when the part after the scheme contains +, #, :, ?, or another character that is not allowed in file names. This keeps distinct URLs in separate directories.
这是一个有用的设置,可以解决 Windows 上长路径的问题。如果你有一些路径很长的依赖项,你可以选择将虚拟存储放在驱动器的根目录中(例如 C:\my-project-store)。
或者你可以将虚拟存储设置为 .pnpm 并将其添加到 .gitignore。这将使堆栈跟踪更清晰,因为依赖项的路径将会提高一个目录层级。
NOTE: without a global virtual store, the virtual store cannot be shared between several projects. Every project should have its own virtual store (except for in workspaces where the root is shared).
When enableGlobalVirtualStore is on,
virtualStoreDir sets the location of the global virtual store, which several
projects can share. Each project still keeps its current lockfile and hidden
hoisted dependencies in its own node_modules/.pnpm.
virtualStoreDirMaxLength#
- 默认值:
- 在 Linux/macOS 上:120
- 在 Windows 上:60
- 类型:number
设置虚拟存储目录 (node_modules/.pnpm) 中目录名称的最大允许长度。如果你在 Windows 上遇到长路径问题,你可以将其设置为较低的数字。
virtualStoreOnly#
- 默认值: false
- 类型:Boolean
当设置为 true 时,pnpm 会装入虚拟存储店,而不会创建导入器的符号链接、钩子、二进制链接或运行生命周期脚本。这对于预先填充存储(例如,在 Nix 构建中)非常有用,而不会创建不必要的项目级工件。 pnpm fetch 内部使用此模式。
packageImportMethod#
- 默认值: auto
- 类型:auto、hardlink、copy、clone、clone-copy
控制从存储中导入包的方式(如果要禁用 node_modules 中的符号链接,则需要更改 nodeLinker 设置,而不是此设置)。
- auto - try the platform's cheap link tiers in order, falling back to copying when none of them is possible. On Linux and Windows it tries hardlinking before cloning; on macOS it tries cloning before hardlinking.
- hardlink - 从存储硬链接包
- clone-or-copy - 尝试从存储中克隆包。如果不支持克隆则回退到复制。
- copy - 从存储中复制包
- clone - 从存储中克隆(也称为 copy-on-write 或参考链接)包
A package that a lifecycle script or a patch will still write must not share files with the store, so pnpm imports it with clone-or-copy when this setting is auto, hardlink, or clone. With copy, such a package is copied like every other package.
Cloning, like copying, keeps node_modules separate from the store. You may edit files in your node_modules and they will not be modified in the central content-addressable store. Unlike a copy, a clone shares its data with the store file until one of them is written, so its contents take no extra disk space.
Not all file systems support cloning. To clone on Linux, use a copy-on-write (CoW) file system, for instance btrfs instead of ext4, and set packageImportMethod to clone. On Linux and Windows, auto tries hardlinking first.
What auto tries first#
auto is a per-platform order:
| Platform | Order |
|---|---|
| Linux | hardlink → clone → copy |
| macOS | clone → hardlink → copy |
| Windows | hardlink → clone → copy |
A reflink materializes a new inode and copies extent bookkeeping inside the
filesystem's metadata trees, where a hardlink is one directory entry. On btrfs
that difference roughly halves the time an install spends materializing
node_modules from a warm store, which is why pnpm reaches for the hardlink
first there. ext4 is unaffected. It never supported cloning, so auto already
hardlinked. On a Windows Dev Drive (ReFS), a block clone takes several calls
per file where a hardlink takes one, so Windows hardlinks first as well.
macOS keeps clone-first, since APFS clonefile is that platform's cheap
primitive.
If you edit files inside node_modules, ask for clone explicitly. Under a
hardlink, the file in node_modules is the file in the store, so editing it
changes every project that links the same package.
When a link succeeds but costs more than a copy#
auto steps down a tier only when a link fails, never when one succeeds
slowly. Two setups produce links that succeed and are expensive, and on both
packageImportMethod: copy can be considerably faster.
Neither is a reason to reach for copy generally. On an ordinary install
(store and node_modules on the same everyday filesystem, no image layer in
between), copying is the slowest option and the one that uses the most space.
Measure both on the machine in question.
A store baked into a container image layer#
Container runtimes assemble an image from stacked layers, usually with overlayfs. A hardlink to a file that lives in a lower layer succeeds there, but the file is copied into the writable upper layer first. An image that ships a pre-populated pnpm store leaves that store in a lower layer, so the first link to any store file copies that file up. pnpm's "Packages are hard linked from the content-addressable store" line stays accurate while the install copies most of the store out anyway.
The cost is per file, not per link: a package linked into several projects is copied up once and linked cheaply after that. It still lands on most of the store, because an install links nearly every file it needs at least once. The run below made 69k links and copied up 36.7k distinct files.
Measured with pnpm 12 (through @pnpm/napi 12.1.0) on a 13-project workspace,
2074 packages, store fully pre-populated so nothing was downloaded, timing
only the phase that writes node_modules. Rootless podman on the kernel
overlay storage driver with metacopy off, backed by btrfs, --cpus=8:
packageImportMethod |
Link phase | System time | Written |
|---|---|---|---|
auto (hardlinking) |
2.1s | 8.0s | 36.7k files copied up, 366 MB |
copy |
1.5s | 5.6s | 874 MB, nothing copied up |
Backing that overlay with btrfs is what keeps the gap small: a copy-up there
can share extents rather than duplicate them. On ext4 every copy-up is a full
data copy, and the gap widens. One project's CI, running this shape on an
ext4 boot disk, went from 38s to 16s by switching to copy.
The EXDEV fallback below does not help here. A cross-layer
hardlink does not fail. It copies up and reports success, so auto has no
failure to step down on.
Network and virtualized filesystems#
The same "succeeds, but slowly" shape appears when the store and
node_modules sit on the same filesystem and that filesystem services every
metadata operation over a round trip: a network share (NFS, SMB, Amazon EFS)
or a directory shared from a host into a VM or container (virtiofs,
gRPC-FUSE, 9p). Cloning, where the platform tries it first, fails there and
steps down; hardlinking then succeeds, so auto stays on it and the install
pays a round trip per file.
Copying reads and writes whole files instead of performing per-file metadata
operations the filesystem has to service remotely.
This only arises when both live on that filesystem. A store on a genuinely
different filesystem from node_modules cannot be hardlinked from at all.
The attempt fails with EXDEV, so auto already falls back to copying on
its own.
How many files macOS writes at once#
On macOS, pnpm writes package files into node_modules with 4 threads,
whatever the number of CPU cores. On the Macs measured, more threads made
installs slower.
Security software that monitors file operations through Endpoint Security, as
on many company-managed Macs, can reverse this. On one such Mac, installs took
about 40% less time when pnpm wrote files with the threads it uses for the rest
of the install. Set the PNPM_MACOS_LINK_THREADS environment variable to 0 for
that, or to a positive number to use that many threads. If your Mac runs such
software, compare install times with 0 and the default:
PNPM_MACOS_LINK_THREADS=0 pnpm install
modulesCacheMaxAge#
- 默认值:10080 (以分钟为单位的 7 天)
- 类型:number
孤立包应该从模块目录中被删除的时间(以分钟为单位)。 pnpm 在模块目录中保存了一个包的缓存。切换分支或降级依赖项时,这会提高安装速度。
dlxCacheMaxAge#
- 默认值:1440 (以分钟为单位的 1 天)
- 类型:number
Dlx 缓存过期的时间(以分钟为单位)。执行 dlx 命令后,pnpm 会保留一个缓存,该缓存会省略后续调用同一 dlx 命令的安装步骤。
virtualStoreType#
- Default: project
- Type: project, global
Names where the virtual store lives: one store per project, or one store per machine.
virtualStoreType: global
project is the default layout: every project gets its own virtual store inside node_modules/.pnpm. global is the global virtual store: a single store shared by every project on the machine, with each project's node_modules holding only symlinks into it.
This is the canonical spelling of enableGlobalVirtualStore, which keeps working; when a project sets both, virtualStoreType wins. It can also be set through the PNPM_CONFIG_VIRTUAL_STORE_TYPE env variable and read back with pnpm config get virtualStoreType.
The setting is independent of nodeLinker: isolated and pnp both work with either store type, and hoisted writes no virtual store at all, so it is unaffected.
enableGlobalVirtualStore#
- 默认值: false
- 类型:Boolean
注意
virtualStoreType is the canonical spelling of this setting: enableGlobalVirtualStore: true is virtualStoreType: global. Both work, and virtualStoreType wins when a project sets both.
注意
Global installs (pnpm add -g) use the global virtual store by default. pnpm dlx does not: each entry of the dlx cache keeps its own virtual store.
When enabled with nodeLinker: isolated, node_modules contains only symlinks to a central virtual store, rather than to node_modules/.pnpm. 默认情况下,此中央存储位于 STORE_PATH/links(使用 pnpm store path 来查找 STORE_PATH)。 Set globalVirtualStoreDir or virtualStoreDir to place it elsewhere.
在中央虚拟存储中,每个包都被硬链接到一个目录中,该目录的名称是其依赖关系图的哈希值。因此,系统上的所有项目都可以从磁盘上的这个共享位置符号链接它们的依赖项。这种方法在概念上类似于 NixOS 管理包的方式,使用依赖图哈希在 Nix 存储中创建隔离且可共享的包目录。
这不应与全局内容可寻址存储混淆。 The actual package files are still hard linked from the content-addressable store, but instead of being linked directly into
node_modules/.pnpm, they are linked into the global virtual store.
Using a global virtual store can speed up installations when a warm cache is available. 然而,在 CI 环境中(通常不存在缓存),它可能会减慢安装速度。
important
为了在使用全局虚拟存储时支持提升的依赖项,pnpm 依赖于 NODE_PATH 环境变量。这允许 Node.js 解析来自提升的 node_modules 目录的包。 Node.js does not respect NODE_PATH for ESM imports, so every process pnpm spawns for the project (pnpm run, pnpm exec, lifecycle scripts, and the tools pnpm dlx runs) receives both NODE_PATH and a NODE_OPTIONS --import flag that registers a resolve hook restoring NODE_PATH lookups for ESM. A dependency that imports a package not declared in its own package.json (which is considered bad practice) resolves for CommonJS and ESM alike.
Two things fall outside that: a node process you start yourself rather than through pnpm, and a project that sets extendNodePath to false, which turns the whole NODE_PATH mechanism off and takes the resolve hook with it. Use packageExtensions to declare the missing dependencies if you need them to resolve in either case.
globalVirtualStoreDir#
- Default:
<store-path>/links - 类型:路径
The location of the global virtual store. It takes precedence over virtualStoreDir. When it is unset and the global virtual store is on, an explicitly set virtualStoreDir places the global virtual store.
Dependency hoisting settings#
hoist#
- 默认值:true
- 类型:Boolean
当为 true 时,所有依赖项都会被提升到 node_modules/.pnpm/node_modules。这使得 node_modules 中的所有包都可以访问未列出的依赖项。
hoistPattern#
- 默认值:['*']
- 类型:string[]
告诉 pnpm 哪些包应该被提升到 node_modules/.pnpm/node_modules。默认情况下,所有包都被提升 — 但是,如果你知道只有某些有缺陷的包具有幻影依赖,你可以使用此选项专门提升幻影依赖(推荐做法)。
例如:
hoistPattern:
- "*eslint*"
- "*babel*"
你还可以在模式前面添加 ! 来避免提升。
例如:
hoistPattern:
- "*types*"
- "!@types/react"
In a workspace, the pattern is matched against the names of the workspace projects too, unless hoistWorkspacePackages is turned off.
publicHoistPattern#
- 默认值:[]
- 类型:string[]
不同于 hoist-pattern 会把依赖提升到一个虚拟存储中的隐藏的模块目录中,publicHoistPattern 将匹配的依赖提升至根模块目录中。提升至根模块目录中意味着应用代码可以访问到幻影依赖,即使它们对解析策略做了不当的修改。
当处理一些不能正确解析依赖关系的有缺陷可插拔工具时,此设置很有用。
例如:
publicHoistPattern:
- "*plugin*"
注意:设置 shamefully-hoist 为 true 与设置 public-hoist-pattern 为 * 是一样的。
你还可以在模式前面添加 ! 来避免提升。
例如:
publicHoistPattern:
- "*types*"
- "!@types/react"
In a workspace, the pattern is matched against the names of the workspace projects too, unless hoistWorkspacePackages is turned off.
hoistWorkspacePackages#
- 默认值:true
- 类型:Boolean
When true, the projects of the workspace are hoisting candidates too, not only the packages that come from a registry.
hoistPattern and publicHoistPattern then decide where each project lands, exactly as they do for a registry dependency. publicHoistPattern is checked first, so a project matching both patterns goes to the root only:
- A project whose name matches
publicHoistPatternis symlinked into<workspace_root>/node_modules. - A project whose name matches only
hoistPatternis symlinked into<workspace_root>/node_modules/.pnpm/node_modules. - A project whose name matches neither is not hoisted.
Because hoistPattern defaults to ['*'] and publicHoistPattern to [], the default is that every workspace project is symlinked into <workspace_root>/node_modules/.pnpm/node_modules.
This is how a tool that resolves packages by name from the workspace root finds a project of the workspace. The usual case is ESLint loading a plugin that is developed in the same monorepo:
publicHoistPattern:
- "*eslint*"
With that setting, a workspace project named eslint-plugin-acme appears at <workspace_root>/node_modules/eslint-plugin-acme, even if no package.json lists it as a dependency.
Details worth knowing:
- A project is matched by the
namein itspackage.json, not by its directory name. - Every project of the workspace is a candidate, including a project that nothing else depends on. The workspace root project is never hoisted.
- The symlink points at the project's own directory in the workspace, so there is nothing to keep in sync.
- A direct dependency wins a name conflict. If a package with the same name is a direct dependency of any workspace project, that package is hoisted and the workspace project is not.
- The setting only adds candidates. When
hoistPatternandpublicHoistPatternare both empty, nothing is hoisted at all, workspace projects included. SettinghoisttofalseemptieshoistPattern.
shamefullyHoist#
- 默认值: false
- 类型:Boolean
默认情况下,pnpm 创建一个半严格的 node_modules,这意味着依赖项可以访问未声明的依赖项,但 node_modules 之外的模块不行。通过这种布局,生态系统中的大多数的包都可以正常工作。但是,如果某些工具仅在提升的依赖项位于根目录的 node_modules 时才有效,你可以将其设置为 true 来提升它们。
hoistingLimits#
- 默认: none
- 类型:none, workspaces, dependencies
控制在使用 nodeLinker: hoisted 时依赖项的提升层级。此设置对应于 Yarn 的 nmHoistingLimits。
- none - 尽可能向上提升(默认值)。
- workspaces —— 仅将依赖提升至各自的工作区包层级,防止依赖被提升至依赖它们的那个工作区包之上。
- dependencies —— 仅向上提升至各工作区包的直接依赖,从而防止传递依赖被提升到该工作区包的
node_modules中。
externalDependencies#
- 默认值:[]
- 类型:string[]
Names of dependencies that another tool links into the root node_modules. With nodeLinker: hoisted, pnpm does not place a dependency whose alias is listed here at the top level, and leaves that slot to the other tool. This setting has no effect with other linkers.
externalDependencies:
- bit-bin