#16436·pnpm

Global virtual store ESM loader breaks ts-node on Node.js < 22.15

Author: zkochanCreated Sep 30, 2026Updated Sep 30, 2026

With the global virtual store enabled, pnpm puts an ESM resolve hook into NODE_OPTIONS (--import=data:text/javascript,..., built in pnpm/crates/config/src/esm_node_path_loader.rs; the TypeScript CLI embeds an identical copy from pnpm11/exec/esm-node-path-loader). On Node.js versions without module.registerHooks() (older than 22.15), the hook falls back to module.register(). Under that flag, ts-node can't run a .ts entry point:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /…/index.ts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:189:9)
    …
    at async handleMessage (node:internal/modules/esm/worker:196:18) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

As far as I can tell, Node.js then loads the CommonJS entry point through the ESM loader, which bypasses the require.extensions hook that ts-node registers. Any tool that relies on CommonJS require hooks for its entry point is probably affected the same way.

Reproduction

package.json:

json
{ "name": "repro", "private": true, "scripts": { "start": "ts-node index.ts" }, "devDependencies": { "ts-node": "10.9.2", "typescript": "5.5.4", "@types/node": "20.19.0" } }

pnpm-workspace.yaml:

yaml
enableGlobalVirtualStore: true

index.ts:

typescript
const x: number = 1
console.log("ok", x)

Then pnpm install && pnpm start:

pnpm Node.js GVS Result
12.8.0 20.20.2 on ERR_UNKNOWN_FILE_EXTENSION
12.8.0 20.20.2 off ok 1
12.8.0 24.21.0 on ok 1

pnpm isn't needed to trigger it: running node node_modules/ts-node/dist/bin.js index.ts on Node 20 with only the NODE_OPTIONS value pnpm injects fails the same way.

Impact

The global virtual store is turned off in CI unless it is set explicitly. pnpm 11 and 12 keep it on when pnpm-workspace.yaml sets it, and pnpm 10 turned it off in CI regardless. So a project that enables it and runs ts-node on Node 18/20/22.x (<22.15) breaks as soon as it moves to pnpm 11 or 12. That's what happened to release/10 in this repo, which now sets PNPM_CONFIG_ENABLE_GLOBAL_VIRTUAL_STORE=false in its workflows as a workaround (#16330).

Possible directions

  • Inject the flag only for Node.js versions that have module.registerHooks(). That hook runs in-thread and didn't break ts-node on Node 24. Older runtimes would keep plain NODE_PATH resolution, which still covers CommonJS.
  • Or keep the module.register() fallback but find a registration that doesn't make Node route the CommonJS entry point through the ESM loader.

Both CLIs build the same loader (the Rust side has a golden test against pnpm11/exec/esm-node-path-loader/test/import-flag.txt), so a fix needs to land in both.


Written by an agent (Claude Code, claude-opus-5-5).