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
也可以通过命令行临时启用:
bashpnpm 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
确保项目能正常运行:
bashpnpm install pnpm build pnpm test
第二步:定位幽灵依赖
bash# depcheck 可以检测未声明但使用的依赖 npx depcheck # pnpm 自带命令查看依赖树 pnpm ls --depth=0
第三步:逐个修复
将 depcheck 报出的缺失依赖添加到 package.json:
bashpnpm 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。