Commit Graph
1 Commits
Author SHA1 Message Date
Zoltan Kochan ae462e10ea feat(gvs): restore NODE_PATH resolution for scripts, ESM included (#13931)
With a global virtual store, package directories live outside the
project, so Node's upward node_modules walk from their real paths never
reaches the project's hoisted node_modules — undeclared (phantom)
dependencies stop resolving, and Node ignores NODE_PATH entirely for
ESM imports (Related to pnpm/pnpm#9618).

When enableGlobalVirtualStore is on (and extendNodePath is not
disabled), every child process pnpm spawns — run, exec, lifecycle
scripts, dlx — now receives NODE_PATH pointing at the private hoist dir
and the root node_modules, plus a NODE_OPTIONS --import flag that
registers a resolve hook restoring NODE_PATH lookups for ESM, replacing
the plugin-esm-node-path config dependency.

The flag is a self-contained constant: the registration module and the
hook are nested data: URLs, so no loader file has to exist on disk and
the flag stays valid regardless of which project or pnpm version
spawned the child. It prefers module.registerHooks() (in-thread, no
DEP0205 deprecation warning) and falls back to module.register() on
Node >=18.19 <22.15. Everything outside the RFC 3986 unreserved set is
percent-encoded: encodeURIComponent alone leaves single quotes bare,
and the NODE_OPTIONS tokenizer treats those as quote delimiters.

Both stacks embed identical hook sources and each asserts its derived
flag against the shared golden file
pnpm11/exec/esm-node-path-loader/test/import-flag.txt, so they cannot
drift apart silently.

Where the nodeOptions setting overwrites NODE_OPTIONS (run, exec,
recursive run, install lifecycle), the flag is re-applied. pacquet's
dlx and global installs force GVS off for their self-contained trees
and now strip the injected env from the caller's cloned config, so a
tool never resolves phantoms from the invoking project's tree; the
TypeScript dlx (GVS on by default) adds the flag and lets the bin shim
supply NODE_PATH.

This deliberately does not address the TypeScript-compiler side of
pnpm/pnpm#13210: tsc, tsserver, and bundlers implement their own
resolution and honor neither NODE_PATH nor Node loader hooks. That
class remains packageExtensions / compat-DB territory.
2026-08-16 11:20:12 +02:00