Skip to content

Turbopack traces a pnpm hoisted-store alias without its target files, producing a dangling symlink that fails Vercel deploys with ENOENT #98791

Description

@davidbarratt

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
  -> ../scheduler@0.28.0/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/scheduler@0.28.0/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:

  1. Dedupe the package so the alias and traced files agree (the table's last row).
    Only viable when one version actually satisfies both dependents.
  2. 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.5 and 16.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Module ResolutionModule resolution (CJS / ESM, module resolving).OutputRelated to the the output configuration option.TurbopackRelated to Turbopack with Next.js.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions