Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable (#93877)
### What?
When `node_modules/next` is briefly unresolvable mid-session (for
example because pnpm is mid-install of a package with a `next` peer
dependency, reshuffling its symlinks) Turbopack would:
- Emit a `MissingNextFolderIssue` with severity `Fatal`
- Propagate `anyhow!("Next.js package not found")` through ~10 levels of
the build graph
- Surface the failure at the napi boundary as a
`TurbopackInternalError`, which the Next.js JS-side treats as fatal and
shuts the dev server down
The reported user impact was a dev session that became wedged and could
not recover even after restarting `next dev`, because the persistent
cache had stored the failed operation state. Users reported it as
"stuck", "catastrophic", and required wiping `.next/` to recover.
Unfortunately while i was able to reproduce a number of bad outcomes, i
couldn't actually reproduce the issue of `next dev` getting stuck in a
bad state. Still this PR will improve a number of apis and error
messages
See internal discussion:
https://vercel.slack.com/archives/C046HAU4H7F/p1778792876550309
### Why?
A transient filesystem race during an install is the canonical
recoverable error — once the install finishes, `next` is resolvable
again. There is no reason this should bring down the dev server or
poison the persistent cache.
The bug had two contributing causes:
1. The error message was poor. It assumed the only cause was a mis-set
`turbopack.root`, with the entire message in `title()`. It didn't
acknowledge the install-race case or any of the other realistic causes
(broken symlink, monorepo hoisting, removed `node_modules/next`, global
install). Users had no way to know that the right response was to wait
or refresh.
2. The Rust→napi boundary did not treat this as a recoverable error. Two
specific code paths propagated `Err` instead of catching it:
- `issue_filter_from_endpoint` in `endpoint.rs` calls
`endpoint_op.connect().await?` — if the upstream endpoint resolution
errors (as it does when `directory_tree_to_loader_tree` fails), this `?`
propagates **before** the surrounding
`strongly_consistent_catch_collectables` gets a chance to convert the
failure into Issues.
- `Project::hmr_version_state` is called bare from `project_hmr_events`
at the napi boundary. There is no `_with_issues_operation` wrapper, so
any error from the underlying
`hmr_content`/`versioned_content_map.compute_entry` chain propagates
straight to JS.
### How?
Three changes:
**1. Rewrite `MissingNextFolderIssue`**
([next_import_map.rs](crates/next-core/src/next_import_map.rs))
- Severity downgraded from `Fatal` to `Error`. `Fatal` triggers an
explicit JS-side server shutdown in `print-build-errors.ts`; `Error`
lets the dev server recover.
- Title is a single line: `"Could not find the Next.js package
(next/package.json)"`.
- Description lists all realistic causes (concurrent install,
missing/broken symlink, mis-set workspace root, monorepo hoisting,
global install).
- Doc link moved to `documentation_link()` so it doesn't pollute the
inline message.
**2. Make `issue_filter_from_endpoint` Err-tolerant**
([endpoint.rs](crates/next-napi-bindings/src/next_api/endpoint.rs))
- Wrap the `endpoint_op.connect().await` in a `match` and fall back to
`IssueFilter::warnings_and_foreign_errors()` on `Err`.
- This unblocks the existing `strongly_consistent_catch_collectables`
recovery path, which then absorbs the upstream failure as Issues that
surface through the napi boundary normally.
**3. Add `hmr_version_state_with_issues_operation`**
([project.rs](crates/next-napi-bindings/src/next_api/project.rs))
- Mirrors the existing `hmr_update_with_issues_operation` pattern.
- Drives `hmr_version_operation` as an `OperationVc`, catches any `Err`
and falls back to `NotFoundVersion`, and collects issues from the
operation via `peek_issues`.
- The napi `project_hmr_events` handler now uses this wrapper instead of
calling `Project::hmr_version_state` bare. Issues from both
`hmr_version_state` and `hmr_update` are merged before being sent to JS.
- Required lifting the inner `hmr_version_operation` out of
`Project::hmr_version_state`'s closure to make it callable from the napi
layer.
A new regression test at
[test/development/app-dir/concurrent-install/](test/development/app-dir/concurrent-install/)
reproduces the failure by moving `node_modules/next` aside
mid-HMR-session and asserts that the dev server does not emit `FATAL: An
unexpected Turbopack error occurred` or `TurbopackInternalError`, and
that the friendly Issue text surfaces. The test is skipped under
`NEXT_SKIP_ISOLATE=1` since that mode doesn't produce a manipulable
`node_modules`.
Out of scope: after recovery, the dev server still returns 500 on
subsequent requests until the user manually refreshes (the JS-side
per-route error state isn't invalidated by Turbopack's successful
recompile). That's a separate dev-server caching issue.
<!-- NEXT_JS_LLM_PR -->