5月28日 02:26

pnpm 的 shamefully-hoist 配置是什么?什么时候需要使用?

shamefully-hoist 是 pnpm 提供的一个配置项,设为 true 后会将所有依赖提升到 node_modules 根目录,模仿 npm/Yarn 的扁平化结构。

核心回答

是什么: pnpm 默认使用内容寻址存储 + symlink 的严格依赖结构,每个包只能访问自己 package.json 中声明的依赖。shamefully-hoist=true 会打破这一限制,把全部包提升到根 node_modules,允许访问未声明的依赖(即幽灵依赖)。

什么时候用: 只在两种场景下考虑使用——遗留项目迁移时临时启用,或者某些存在缺陷的工具(部分 webpack 插件、IDE 插件等)强制要求扁平化结构时。启用后应尽快迁移到 public-hoist-pattern 精细控制。

默认结构 vs shamefully-hoist

pnpm 默认的 node_modules 结构通过 .pnpm 目录和 symlink 实现严格隔离:

bash
# 默认结构(严格) node_modules/ ├── .pnpm/ │ ├── lodash@4.17.21/ │ │ └── node_modules/ │ │ └── lodash/ # 实际文件 │ └── express@4.18.2/ │ └── node_modules/ │ ├── express/ │ └── debug/ # express 的依赖,对 lodash 不可见 └── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash # require 行为 const lodash = require('lodash'); # OK - 声明了 const debug = require('debug'); # 报错 - 未声明,访问不到

启用 shamefully-hoist=true 后:

bash
# 扁平化结构 node_modules/ ├── .pnpm/ ├── lodash/ ├── debug/ # 被提升上来 ├── express/ └── ... # require 行为 const lodash = require('lodash'); # OK const debug = require('debug'); # 也能访问了(幽灵依赖)

配置方式

从 pnpm v8 开始,shamefully-hoist 推荐配置在 pnpm-workspace.yaml 而非 .npmrc(auth 和 registry 之外的设置都应如此):

yaml
# pnpm-workspace.yaml shamefullyHoist: true

旧版本仍可在 .npmrc 中配置:

ini
# .npmrc shamefully-hoist=true

也可以通过命令行临时启用:

bash
pnpm install --shamefully-hoist

更好的替代方案:public-hoist-pattern

shamefully-hoist=true 等价于 public-hoist-pattern=*,属于"一刀切"方案。绝大多数情况下,只需要提升特定包就够了:

ini
# .npmrc 或 pnpm-workspace.yaml public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* public-hoist-pattern[]=*types* public-hoist-pattern[]=*webpack*

三者的区别:

配置作用范围提升位置推荐度
shamefully-hoist所有包根 node_modules低(过渡用)
public-hoist-pattern匹配的包根 node_modules
hoist-pattern匹配的包.pnpm/node_modules中(内部可见)

public-hoist-pattern 提升到根目录,应用代码和工具都能访问;hoist-pattern 提升到 .pnpm/node_modules,只有其他依赖包能访问,应用代码看不到。

实际迁移步骤

从 npm/Yarn 迁移到 pnpm 时,推荐分步走:

第一步:临时启用 shamefully-hoist

yaml
# pnpm-workspace.yaml shamefullyHoist: true

确保项目能正常运行:

bash
pnpm install pnpm build pnpm test

第二步:定位幽灵依赖

bash
# depcheck 可以检测未声明但使用的依赖 npx depcheck # pnpm 自带命令查看依赖树 pnpm ls --depth=0

第三步:逐个修复

将 depcheck 报出的缺失依赖添加到 package.json

bash
pnpm add missing-dep

第四步:切换到精细控制

yaml
# pnpm-workspace.yaml shamefullyHoist: false publicHoistPattern: - "*eslint*" - "*prettier*" - "*types*" strictPeerDependencies: true

再次验证构建和测试通过。

不推荐长期使用的原因

幽灵依赖隐患: 扁平化后代码可以 require 任何包,即使 package.json 没声明。这在 CI/CD 环境或版本升级时容易出问题——某个间接依赖升级或移除,你的代码就直接报错。

版本冲突风险: 多个包依赖同一个包的不同版本时,扁平化只能保留一个版本,可能引发运行时错误。pnpm 的严格模式通过独立存储天然解决了这个问题。

丧失 pnpm 的核心优势: 内容寻址存储的硬链接机制能节省大量磁盘空间(多项目共享同一份包文件),扁平化后这部分优势被削弱。

相关追问

node-linker 是什么? pnpm 还提供了 node-linker 配置,可以切换 node_modules 的整体布局方式:isolated(默认,symlink)、hoisted(类 npm 扁平化)、pnp(Yarn PnP 风格)。设置 node-linker=hoisted 是另一种实现扁平化的方式,效果和 shamefully-hoist=true 类似但不完全相同——它改变了整个布局策略而非仅做提升。

pnpm v9 的变化? pnpm v9 进一步收紧了默认的 public-hoist-pattern,默认不再提升 eslint/types 等包。如果升级后构建报错,检查是否需要显式配置 public-hoist-pattern

标签:PNPM