Turbopack traces a pnpm hoisted-store alias without its target files, producing a dangling symlink that fails Vercel deploys with ENOENT
Link to the code that reproduces this issue
https://github.com/davidbarratt/nextjs-nft-hoisted-symlink-repro
To Reproduce
git clone https://github.com/davidbarratt/nextjs-nft-hoisted-symlink-repro
cd nextjs-nft-hoisted-symlink-repro
pnpm install
pnpm build
find .next -xtype l
find prints one dangling symlink:
.next/standalone/node_modules/.pnpm/node_modules/scheduler
-> ../[email protected]/node_modules/scheduler
node check-nft.mjs inspects the *.nft.json traces directly — the same defect
before output: "standalone" materializes it:
alias traced, zero target files: scheduler -> .../.pnpm/[email protected]/node_modules/scheduler
hoisted aliases traced: 47
aliases with no traced target files: 1
The app is one route handler that imports @react-pdf/renderer, plus a layout.
Current vs. Expected behavior
Current: a route's *.nft.json lists pnpm's hoisted-store link
node_modules/.pnpm/node_modules/scheduler as a bare entry, and traces no files
from the copy that link resolves to. Two copies of scheduler are in the store —
0.28.0 from react-dom (what the hoisted link points at) and
0.25.0-rc-603e6108-20241029 from @react-pdf/reconciler (the only one whose
files are traced). The alias is therefore packed empty.
Deploying that output to Vercel fails during extraction, before the app runs:
Building: Extracting deployment files...
Error: ENOENT: no such file or directory, realpath
'/vercel/path0/node_modules/.pnpm/node_modules/scheduler'
Expected: an alias listed in a trace is packed with the files it resolves to, or is not listed at all. Either way the output contains no symlink without a target.
Provide environment information
Operating System:
Platform: linux
Arch: arm64
Binaries:
Node: 24.20.0
pnpm: 11.11.0
Relevant Packages:
next: 16.4.0-canary.32 (also reproduced on 16.3.5)
react: 19.3.0
react-dom: 19.3.0
typescript: 5.9.3
Next.js Config:
output: standalone
Which area(s) are affected? (Select all that apply)
Turbopack, Output (export/standalone), Module Resolution
Which stage(s) are affected? (Select all that apply)
next build (local), Vercel (Deployed)
Additional context
Impact. Every deploy of an affected app fails at extraction — there is no
partial or degraded mode, and no retry clears it. I first hit this across ten CI
runs on unrelated branches of a private monorepo, on three separate apps, each
blocked until the output was patched; a second app failed identically on
supports-color, which has four copies in its store. [citation needed] — that
CI is private, so the public evidence for the failure is the reproduction above.
What is and isn't required, each verified by rebuilding with only that changed:
| Changed | Result |
|---|---|
output: "standalone" removed |
still 1 bad alias (trace only; find has no tree to scan) |
serverExternalPackages removed |
still 1 bad alias |
both removed — empty next.config.mjs |
still 1 bad alias |
overrides: { scheduler: 0.28.0 } |
0 bad aliases |
So neither config option is implicated. The last row is the mechanism: once the hoisted link and the traced files agree on a version, the defect is gone.
I mention serverExternalPackages only because the app where I first saw this
listed @react-pdf/renderer there, and
#95816 points at externalized
packages — but removing it here changes nothing, so it is not the trigger.
pnpm's default symlinked linker is required. node-linker=hoisted has no
.pnpm/node_modules directory and cannot hit this.
Workarounds, in preference order:
- Dedupe the package so the alias and traced files agree (the table's last row). Only viable when one version actually satisfies both dependents.
- Delete targetless links from the build output before uploading —
find .vercel/output -xtype l -delete. A link with no target cannot serve a request, so removing it is safe, but it is a patch over the output rather than a fix.
Relationship to #95816. Same shape — an alias traced without its store target files — and possibly the same machinery, but a different alias class and failure point, so I filed separately rather than commenting there:
| #95816 | this | |
|---|---|---|
| Alias | Turbopack content-hashed .next/node_modules/<pkg>-<hash> |
pnpm hoisted store node_modules/.pnpm/node_modules/<pkg> |
| Package manager | bun isolated linker | pnpm |
| Surfaces at | runtime, ERR_MODULE_NOT_FOUND |
deploy, ENOENT on realpath |
| Trigger | shared SSR chunk referencing a package the endpoint does not import | two copies in the store; the hoisted link points at the untraced one |
This reproduction emits no .next/node_modules/<pkg>-<hash> entries at all, so
it is not the #95816 path. Happy to fold this in as a comment there if you read
them as one bug.
Open questions I could not answer:
- I have not bisected the first canary that introduced this. It reproduces on
16.3.5and16.4.0-canary.32; I have not tested the 16.2.x line, where #95816 reports its own variant absent. - In the private app this began failing partway through a day on which neither the Next version nor the lockfile changed — the same commit range built clean earlier and dangling later. I could not identify what shifted, so I cannot say whether emission here is deterministic or depends on chunking.
Source: vercel/next.js