Turbopack: fix error reporting with crashing webpack loaders (#93926)
### What?
When a Turbopack webpack-loader subprocess crashes (e.g. a loader calls
`process.exit()`, a native fatal error, or the IPC socket otherwise
closes mid-message), the error users see today is:
```
- Execution of <WebpackLoadersProcessedAsset as Asset>::content failed
- Execution of WebpackLoadersProcessedAsset::process failed
- Execution of evaluate_webpack_loader failed
- failed to receive message
- reading packet length
- unexpected end of file
```
After this PR, the same crash produces:
```
⨯ ./data/crash.data
Error evaluating Node.js code
Error: Node.js subprocess crashed while evaluating loaders [/path/to/loaders/crash-loader.js]: failed to receive message
Caused by:
- Node.js process exited with exit status: 7
- reading packet length
- unexpected end of file
Debug info:
- failed to receive message
- Node.js process exited with exit status: 7
Recent process stderr:
<whatever the loader wrote to stderr before exiting>
- reading packet length
- unexpected end of file
```
### Why?
The original message gave no actionable information: no exit code, no
captured stdout/stderr, no indication of which loader was running. It
also looked like an internal turbopack bug rather than a user-fixable
error, and a transient pool failure could cascade into an unrelated
"issue formatter crashed while reading the source for a code frame"
failure on the way out.
### How?
Four orthogonal fixes, plus a regression test:
1. **Capture stdout/stderr on subprocess crash.** `OutputStreamHandler`
now keeps a bounded ring buffer (last 100 lines per stream) shared with
the owning `NodeJsPoolProcess`. When `NodeJsPoolProcess::recv` fails,
the buffers and the child's exit status are attached to the error via
`anyhow::Error::context`.
2. **Recover from subprocess crash in `pull_operation`.** Instead of
propagating the recv error up through `evaluate_webpack_loader` →
`process()` → `Asset::content` (the cascade above), `pull_operation`
catches it, synthesizes a `StructuredError` via
`evaluate_context.emit_error(...)`, disables process reuse, and returns
`Ok(None)`. This mirrors the existing in-band loader-error path, so the
asset's existing `FileContent::NotFound` degradation kicks in naturally
— `Asset::content` never errors.
3. **Include the loader chain in the error message and issue detail.**
`WebpackLoaderContext` gained a `loader_names: Vec<RcStr>` field. A new
optional `EvaluateContext::crash_context_prefix()` trait method lets
webpack-loader evaluations describe what was being evaluated (\"loaders
[a, b, c]\") in the synthesized crash message. `EvaluationIssue` also
gained an optional `detail` field for the same chain, surfacing it in
`--log-detail` output. PostCSS evaluations are labelled \"postcss\".
4. **Crash-proof the issue formatter.** `PlainSource::from_source` and
`IssueSource::into_plain` previously propagated errors from
`asset.content()` with `?`. They now degrade to `FileContent::NotFound`
(and `range = None`) on read failure, so a future regression in some
other code path can never cause the issue reporter itself to crash on
top of whatever the user was debugging.
### Tests
- Added
`test/e2e/app-dir/webpack-loader-errors/loaders/crash-loader.js`: a
loader that writes a marker to stderr and calls `process.exit(7)`.
- Added an e2e test that fetches `/crash` and asserts the marker, the
absence of the internal cascade, the loader name, and the resource name
are all present in the CLI output.
- All 11 tests in `webpack-loader-errors.test.ts` pass; the 5 Rust
`turbopack-node` pool tests still pass.
Some snapshot/golden tests for error formatting may need updating in CI
since `EvaluationIssue` now emits a non-empty `detail`.
<!-- NEXT_JS_LLM_PR -->