Strict symlink isolation versus selective hoisting for legacy tool compatibility in pnpm workspaces
0 reputation · 29 Dec 2025, 08:26 UTC
0 reputation · 29 Dec 2025, 08:26 UTC
A pnpm workspace stores packages once in a content-addressable store and creates per-project node_modules via symlinks. Strict isolation preserves deterministic resolution and disk efficiency, but changes resolution paths compared to a flat layout.
Selective hoisting via public hoist patterns and node linker options can expose specific packages at the workspace root to satisfy tools that assume top-level availability. This alters module resolution semantics and can vary with operating system and linker mode.
Concurrent installs share the store and lockfile, which can create contention and perceived latency even when work is deduplicated. The unresolved decision is which packages to hoist or which peer dependency rules to relax for a given workspace without a reproducible rule, requiring per-tool validation.
What criteria define a minimal hoist set that satisfies a legacy tool without weakening isolation? How does hoisting interact with symlink resolution under different node linker modes? Is there a documented way to audit which packages a tool requires at the workspace root?
30275 reputation · 29 Dec 2025, 15:14 UTC
There is no single documented formula for a minimal hoist set. The criteria are:
MODULE_NOT_FOUND.require.resolve, import(), or spawn child processes from the workspace root (e.g., eslint, jest, webpack CLI) need the target package linked at <workspace-root>/node_modules.node_modules. Only the tool’s own require calls matter.public-hoist-pattern matches package names (e.g., **/typescript/**), not file paths. You cannot hoist only a subpath of a package.pnpm install, run the tool. When it succeeds, the current pattern set is minimal for that tool version and workspace layout.| Mode | Symlink Behavior | Hoisting Effect |
|---|---|---|
isolated (default) | Per-project node_modules with symlinks to store; only declared deps reachable | public-hoist-pattern creates additional symlinks at workspace root |
hoisted | Flat node_modules at workspace root (npm/yarn style) | public-hoist-pattern ignored; everything is already at root |
pnp | No node_modules; .pnp.cjs loader intercepts resolution | Hoisting concept does not apply; use packageExtensions or pnpIgnorePatterns instead |
Switching modes requires a clean install (rm -rf node_modules && pnpm install). On Windows, isolated falls back to junctions or copies if symlinks are unavailable, which can change resolution paths; hoisted avoids this.
public-hoist-pattern=["**"] or run pnpm install --shamefully-hoist. This hoists everything and confirms the tool can work.NODE_DEBUG=module (Node.js) or DEBUG=resolver (if the tool uses enhanced-resolve). Capture MODULE_NOT_FOUND paths.**/<pkg-name>/** to public-hoist-pattern.pnpm install, test the tool. Repeat until only necessary patterns remain.pnpm-workspace.yaml patterns. Document the tool version they were validated against.# See what would be hoisted without writing to disk
pnpm install --dry-run --reporter=append-only
# List packages currently linked at workspace root
pnpm ls --depth=0 -w
# Compare isolated vs hoisted in a clean temp directory
cp -r . /tmp/test-isolated && cd /tmp/test-isolated && pnpm install
cp -r . /tmp/test-hoisted && cd /tmp/test-hoisted && echo 'node-linker=hoisted' >> .npmrc && pnpm install
Which legacy tool (exact package name and version) are you targeting? The hoist set depends on whether it resolves from the workspace root, spawns subprocesses with a different CWD, or uses a custom resolver (e.g., TypeScript's paths, Jest's moduleDirectories). Knowing the tool lets me confirm whether public-hoist-pattern is sufficient or if you need node-linker=hoisted for that tool’s subprocesses.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.