Commit Graph

5 Commits

Author SHA1 Message Date
Jiwon Choi d7aa66c345 Remove generated error codes (#97687)
### Why?

Should come up with better solution that does not block PRs with git
conflict

x-ref:
https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH

### How?

- Delete `errors.json`, the error-code SWC plugin, generated WASM, merge
driver, and validation/build tooling.
- Stop attaching error codes to server-rendering digests, redboxes, and
telemetry; native `Error.code` and `Error.name` remain available where
applicable.
- Remove the development-overlay error feedback UI, middleware, and
telemetry event that depended on stable codes.
- Update fixtures, snapshots, and guidance for code-free errors and
numeric-only digests.

<!-- NEXT_JS_LLM -->
2026-08-21 22:45:12 +02:00
Hendrik Liebau f70564f742 Keep the dev validation worker alive across HMR updates (#96988)
Cache Components dev validation reported stack frames that pointed at
build output whenever a module had been updated while the dev server
ran. This affected both the static shell validation and the
instant-navigation validation, since both run on the same worker. The
overlay showed a raw `file:` URL and the terminal named the chunk rather
than the page, and because the frame never resolved to a source position
there was no code frame either, so nothing indicated which line caused
the error.

Turbopack's server HMR evaluates an updated module as a script of its
own, named `<chunk>?<module id>` and carrying its source map inline
rather than on disk, so only the isolate that ran that `eval` can
resolve a frame in it. The validation worker never ran it, and the map
beside the chunk describes the chunk's lines, not the running module's,
so nothing the worker could reach described the frame. React then wrote
the frame in its form for scripts without a source map, which encodes an
already-encoded URL a second time, leaving a frame no reader reverses.

The worker now mirrors what the dev server does to its own module state
rather than being dropped whenever that state changes. The dev server
reports each applied update, the manifest cache entries it cleared, and
the paths it evicted, and the worker replays them in the same order, so
its module state is the dev server's module state by construction. That
leaves each updated module's inline source map in the worker's own
Node.js cache, which is what makes the frame resolvable there.

The worker needs no coordination around a validation in flight. It runs
one call at a time, in the order the calls were made, so an update is
replayed before any validation requested after it, and never in the
middle of one. The dev server does not hold its own updates back for a
validation running in process either. Where it gives up and re-evaluates
every module from disk the worker is dropped, so that case keeps the
behaviour it had.

Not dropping the worker helps beyond the frames. Dropping it meant the
next validation had to spawn a worker thread and run `loadComponents`
again before it could start, and it paid that on every edit, which
delayed the insight at exactly the moment the user is waiting for it.
The case in the test suite that covers this went from around 870ms to
around 240ms.

The simpler fix was to revive the transported errors on the main thread
and print them there, where the scripts already are. It works, and it is
why this PR also touches the benchmark: the fixture produced no
validation errors, so nothing in the benchmark reached the error
reporting at all, and the cost of moving it was invisible. With insights
generated, the cost showed plainly. Printing an error costs around 218ms
the first time a source map is read and about a millisecond after that,
and moving it to the main thread cut the worker's p95 advantage on the
heaviest route from around 15ms to between 2ms and 5ms. Mirroring the
updates keeps the printing on the worker and leaves that advantage
intact.

The three commits are worth reading in order. The first adds the test
with the broken output snapshotted, so its snapshots deliberately record
what a user saw, a frame naming the chunk with no code frame beneath it.
The second is the benchmark change above. The third is the fix, and its
diff turns those snapshots into resolved frames, adds cases that edit
the same module twice, edit a module the page imports, and validate a
route that another route's update did not touch, and rewrites the
suite's header comment, which described the mechanism this replaces.

Verified on both bundlers, since the worker is gated on Turbopack and
Webpack validates in process, along with
`instant-validation-scheduling`,
`instant-validation/{server-errors,parallel-slots}`,
`instant-validation-causes`, `instant-validation-level-default` and
`hmr-rsc-cancellation`. Run with `BENCH_DEV_VALIDATION_INSIGHTS=1`, the
benchmark shows no steady-state regression: the worker column matches
canary at 106ms sprite p95 against 110ms and 109ms, and keeps its margin
over in-process.

Two things are deliberately left out. The benchmark still cannot measure
the edit case, because it never edits, so the timing above comes from a
test's wall clock rather than a purpose-built measurement. And
`use-cache-probe-pool` subscribes to the same invalidation and tears
down the same way, which is the obvious follow-up if this holds up.

One known gap remains. A worker dropped by its own failure, rather than
by the dev server giving up, cannot obtain the scripts the dev server
evaluated from earlier updates, so frames naming them stay unresolved
until those modules change again. The validation itself is unaffected,
because the worker loads the current code from disk.
2026-08-10 23:39:13 +02:00
Hendrik Liebau e1fbb1800f Run dev validation in process when using Webpack (#96219)
Node.js caches source maps per isolate, so the dev validation worker
only has maps for the chunk files loaded on its own thread. For chunks
it never loads, such as a module behind a dynamic `import()`, it reads
the `.map` that Turbopack writes next to the chunk. Webpack keeps its
dev source maps in the compiler instead, so the worker has nothing to
read and reports those frames with the position they have in the
compiled output rather than in the source.

This stops using the worker when the bundler is Webpack, so validation
runs in process again, where those frames resolve, and
`experimental.devValidationWorker` now has no effect there. Dev
performance under Webpack goes back to what it was before validation
moved to a worker, which is the better trade: the worker exists to keep
the event loop responsive during rapid navigation, and that is not worth
losing the source location on validation errors.
2026-07-25 16:30:39 +02:00
Hendrik Liebau 4bfd5c7170 Read chunk source maps from disk in the dev validation worker (#96218)
Node.js caches source maps per isolate, so the dev validation worker
only has maps for the chunk files loaded on its own thread. It never
renders server components, and the built entry reaches segment modules
through lazy getters, so it loads whatever `loadComponents` pulls in and
nothing else. A module the bundler splits into a chunk of its own, as
Turbopack does for a dynamic `import()`, is therefore missing from that
cache entirely, and the frames pointing into it are logged without a
source location. In-process validation resolves them, so this is a
regression from moving validation onto a worker.

On the main thread the same miss is covered by asking Turbopack for the
map through the `Project` handle, which cannot cross a thread boundary.
The worker instead reads the `.map` that Turbopack already wrote next to
the chunk, which needs no project handle and, unlike Node's cache, does
not depend on the chunk having been evaluated on this thread. Lookups
are restricted to `distDir` so a stack frame cannot point the reader at
an arbitrary file, and both hits and misses are memoised.

Installing that lookup also required moving
`bundlerFindSourceMapPayload` onto a `globalThis` symbol.
`patch-error-inspect` is bundled into several runtimes that each get
their own copy, and the copy that registers the implementation, here the
worker bundle, is not the copy that symbolicates the frame, which is the
app-page bundle's. The code frame renderer in the same file is already
shared this way for the same reason.

Webpack keeps its dev source maps in the compiler rather than writing
them next to the chunks, so there is nothing for the worker to read and
its frames are unchanged. A follow-up turns the worker off for Webpack
so that validation runs in process, where those frames still resolve.
2026-07-25 16:30:38 +02:00
Hendrik Liebau c1617dedc8 Retry the source map lookup with a plain path (#96215)
Running
`test/e2e/app-dir/instant-validation/suspense-boundaries.test.ts`
locally on Node.js 24 currently fails in the two "missing suspense
around search params" cases: stacks that should read
`app/…/page.tsx:40:18` are logged as raw
`about://React/Prefetch/file:///…/%5Broot-of-the-server%5D…` URLs, so
the validation errors printed to the terminal no longer point at any
source. CI is green on the same commit, because it runs Node.js 20. The
dev validation worker (#96153) and the switch to `file:` source maps for
Turbopack (#95946) landed hours apart, and the worker's branch predated
the source map change, so the two first met on canary, where the older
Node.js hid the result.

Symbolicating a frame that React spliced in from another environment
means decoding the chunk path out of its `about://React/…` URL and
asking Node.js for that chunk's source map. Decoding returns the path as
written, while Node.js keys its cache by the `pathToFileURL` encoding of
the same path, and from Node.js 22 on that encoding escapes the brackets
in Turbopack's `[root-of-the-server]` chunk names, so the lookup misses.
Node.js 20 leaves brackets alone, which is why the same code resolves
there.

PR #95946 anticipated the mismatch and left it as a follow-up, because
on the main thread a miss is only wasteful: Turbopack hands back the
chunk's map instead, which means parsing one the process had already
parsed and cached. The validation worker runs on its own thread with no
access to the Turbopack project, so there the miss is final. Node.js
also accepts the plain path as a cache key, unambiguously for both a
CommonJS absolute path and an ESM `file:` URL, so we retry with it
before giving up. Making React's fake frame URLs reversible, as proposed
in https://github.com/react/react/pull/37105, would let the first lookup
succeed on its own and retire the retry.

The test added here pins down what the worker can symbolicate at all. It
caches maps per chunk file in its own isolate and never renders server
components, so a statically imported module sits in a chunk it has
already loaded and resolves, while a dynamically imported one gets a
chunk of its own that is never loaded and does not. The snapshots for
that second route record broken output on purpose: in-process validation
resolves the frame, so losing it is a regression from moving validation
onto a worker, and it affects Turbopack (no location) and Webpack
(compiled positions) alike. Follow-ups address each.

The snapshots are recorded with the retry applied rather than before it.
Without it Turbopack's output differs between Node.js 20 and 22, and the
broken URLs embed the test's temporary install directory, which changes
on every run, so no stable recording exists. One consequence is that CI
cannot fail on a regression of the retry itself; that was checked by
running the new suite under Node.js 24.
2026-07-25 15:15:50 +02:00