Skip to content

/config/pnpm/node-linker

pnpm node-linker=hoisted: a flat node_modules like npm

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
pnpm
file
pnpm-workspace.yaml as nodeLinker (pnpm 10+, the only place from pnpm 11), or .npmrc as node-linker (pnpm 8 – 10)
key
node-linker
type
isolated | hoisted | pnp
default
isolated

node-linker decides how pnpm lays out node_modules. The default, isolated, puts only your direct dependencies at the top of node_modules, as symlinks into node_modules/.pnpm, so code cannot import packages it did not declare. node-linker=hoisted creates a flat node_modules without symlinks, like npm and Yarn Classic, for tools that do not follow symlinks (React Native's Metro in older versions, some Electron packagers, serverless bundlers). pnp uses Plug'n'Play with no node_modules.

Isolated vs hoisted

bash
cat > package.json <<'EOF'
{ "name": "app", "version": "1.0.0", "dependencies": { "is-odd": "3.0.1" } }
EOF
npx --yes pnpm@9 install > /dev/null 2>&1
echo "--- isolated (default): node_modules"
ls -A node_modules
rm -rf node_modules pnpm-lock.yaml
echo "node-linker=hoisted" > .npmrc
npx --yes pnpm@9 install > /dev/null 2>&1
echo "--- hoisted: node_modules"
ls -A node_modules

Output

--- isolated (default): node_modules
.modules.yaml
.pnpm
is-odd
--- hoisted: node_modules
.modules.yaml
.pnpm
is-number
is-odd

is-number is a dependency of is-odd. With isolated it is not visible at the top level, so require("is-number") from your code fails, which protects you from undeclared ("phantom") dependencies. With hoisted it sits next to is-odd and the import works, as in npm.

pnpm 11: set it in pnpm-workspace.yaml

pnpm 10 accepts the setting both as node-linker in .npmrc and as nodeLinker in pnpm-workspace.yaml. pnpm 11 reads only auth and registry settings from .npmrc; layout settings such as nodeLinker, hoistPattern, publicHoistPattern and shamefullyHoist must be in pnpm-workspace.yaml, and the .npmrc line is silently ignored:

bash
cat > package.json <<'EOF'
{ "name": "app", "version": "1.0.0", "dependencies": { "is-odd": "3.0.1" } }
EOF
echo "node-linker=hoisted" > .npmrc
npx --yes pnpm@11 install > /dev/null 2>&1
echo "--- pnpm 11, .npmrc: is-number hoisted?"
ls node_modules | grep -c '^is-number$'
rm -rf node_modules pnpm-lock.yaml .npmrc
echo "nodeLinker: hoisted" > pnpm-workspace.yaml
npx --yes pnpm@11 install > /dev/null 2>&1
echo "--- pnpm 11, pnpm-workspace.yaml: is-number hoisted?"
ls node_modules | grep -c '^is-number$'

Output

--- pnpm 11, .npmrc: is-number hoisted?
0
--- pnpm 11, pnpm-workspace.yaml: is-number hoisted?
1

pnpm-workspace.yaml works for single-package projects too, so put the setting there on pnpm 10 as well; it keeps working after an upgrade.

Values

ValueLayoutUse it when
isolatedDirect dependencies symlinked into node_modules/.pnpm; content-addressable store with hard linksDefault; strict and disk-efficient
hoistedFlat node_modules, no symlinksA tool or runtime breaks on symlinks
pnpNo node_modules; a .pnp.cjs resolverYou already use Plug'n'Play tooling

Softer alternatives

Before switching the whole project to hoisted, try these settings (in .npmrc up to pnpm 10; in pnpm 11 the same keys go in pnpm-workspace.yaml as publicHoistPattern and shamefullyHoist):

.npmrc
# hoist matching packages to the top-level node_modules (default: none)
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
# or make every package visible at the top level, keeping the symlinked layout
shamefully-hoist=true

Which value to choose

  • Keep isolated whenever the project works with it. The strict layout catches phantom dependencies: an import of a package you never declared fails locally instead of breaking later when a transitive dependency changes.
  • Try the softer options first when one tool needs a package at the top level, such as an ESLint plugin or a type package: public-hoist-pattern exposes only what matches.
  • Use hoisted for runtimes, bundlers and packagers that resolve files without following symlinks, or when migrating a large npm project in steps.
  • Use pnp only if your tools support Plug'n'Play resolution; many editors and native modules need extra setup.

After a layout change, errors like Cannot find module usually mean code imports a package that is not declared in its own package.json; add it as a dependency rather than relying on hoisting.

FAQ

Why does pnpm ignore node-linker=hoisted in my .npmrc?

From pnpm 11, .npmrc is read only for auth and registry settings, and settings that shape node_modules can only be set in pnpm-workspace.yaml. Move it there as nodeLinker: hoisted and reinstall.

Does hoisted lose pnpm's disk savings?

No. Packages still come from the content-addressable store and are hard-linked into node_modules; only the folder layout changes.

Should I use shamefully-hoist or node-linker=hoisted?

shamefully-hoist keeps the symlinked layout but exposes every package at the top level, which is enough for most code that imports undeclared packages. Use hoisted when a tool cannot follow symlinks at all.

Sources