Commit Graph

516 Commits

Author SHA1 Message Date
Aurora Scharff 41ef17c645 Add experimental agent feedback workflow (#98582)
## Summary

Adds an off-by-default `experimental.agentFeedback` workflow for
collecting Next.js friction without interrupting the user’s task.

- `next dev` writes a small managed block to the project
agent-instructions file.
- Agent entry points, including `next-dev-loop`, queue possible issues
instead of opening duplicate forms.
- At the final stopping point, an internal command checks the remote
gate and returns the reporting protocol bundled with that Next.js
version. The agent attempts to anonymize each qualifying issue and opens
a separate review form once. If the browser does not open, it prints the
URL for the user without troubleshooting the failure.
- Report links no longer contain a public page token. Nothing is sent
until the user submits the form, which remains rate-limited and uses a
server-only ingest credential.
- Disabling `agentFeedback` or `agentRules` removes only its managed
block on the next `next dev`. Empty generated agent files are cleaned
up; user-authored content is preserved.
- Adds API references for both options and updates the AI agents guide.

Bundling the protocol keeps the managed block small and allows the
report format to evolve with each Next.js version. The receiving form is
implemented in
[vercel/front#85739](https://github.com/vercel/front/pull/85739) and
should deploy before this workflow is enabled.

## Verification

- `NEXT_SKIP_ISOLATE=1 pnpm test-dev-turbo
test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts`
- `pnpm jest packages/next/src/server/lib/generate-agent-files.test.ts
packages/next/src/cli/internal`
- `npx eslint --config eslint.config.mjs
packages/create-next-app/helpers/generate-agent-files.ts
packages/next/src/server/config-shared.ts
packages/next/src/server/lib/generate-agent-files.ts
test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts`

<!-- NEXT_JS_LLM -->
2026-09-18 18:18:20 +02:00
Jimmy Miller e6688470ef Properly disable laziness on next/dynamic (#98828)
next/dynamic assumes a eager semantic for css gathering, we are making
sure that holds even when lazy dynamic imports are enabled
2026-09-18 06:59:28 -07:00
Jimmy Miller 7c366e61be [turbopack] Track webpack loader build dependency files (#98777)
Track exact existing files and warn when loaders register unsupported
build dependency inputs.

I'm going to take this bit by bit so it is nice and reviewable. Does add
some noise. But the final fully supported thing was just too large. So I
will keep removing error cases on each commit.
2026-09-17 11:53:21 -07:00
Niklas Mischkulnig bdbf63aef7 fix(next-custom-transforms): never place generated imports before directives (#98717)
## Summary

**What?** Fixes the error reported for a module that has a top-level
`"use client"` directive and an inline `"use server"` function
directive, e.g. `<form action={async () => { "use server" }} />` in a
client page.

**Why?** Under Turbopack this reported a bogus error — `The "use client"
directive must be placed before other expressions` — even though the
directive was already at the top of the file. The actual mistake (an
inline server action inside a Client Component) was never surfaced.

**How?** On Turbopack's RSC layer the server actions transform runs
*before* the React Server Components assert. When it hoists an inline
action it prepended its generated imports (`registerServerReference`,
action encryption, cache runtime) at index 0 of the module — *above* the
`"use client"` directive, which the transform does not consume. The RSC
assert then saw an import before the directive and reported it as
misplaced.

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-09-16 17:09:32 +02:00
Jimmy Miller c70b96b373 Fix addMissingDependency (#98588)
While before this change we had addMissingDependency, we didn't properly
respond when the file for that missing dep was added.

The way webpack does this internally is not the exact same shape as the
IPC here, it has them as a separate field. I could not find any reason
not to just add them to the filePaths here. So that's what I did to keep
things simple.
2026-09-15 07:21:32 -07:00
Jiwon Choi 4d925698a8 Expose App Router runtime errors over HMR (#98438)
Ported from #98040 by @marcoshernanz.

> [!TIP]
> Best reviewed commit by commit.

### Why?

External development tools need browser runtime-error state, not just
build errors, to display application failures. Reporting remains opt-in
so applications that do not need it avoid client serialization, HMR
transport, server formatting, buffering, and rebroadcast overhead.

### How?

Add `experimental.exposeRuntimeErrorsToHMR` for App Router development
with Webpack and Turbopack. Internal integrations can also enable the
same behavior without changing `next.config` by setting
`__NEXT_EXPOSE_RUNTIME_ERRORS_TO_HMR` to any non-empty value. When
enabled, the HMR WebSocket emits `runtimeErrors` snapshots containing:

- The current pathname and browser client/document identifiers.
- Error types, names, messages, and source-mapped stacks.
- Fatality and optional catching-boundary details (`default-global`,
`custom-global`, or `custom`).

Snapshots update as errors and navigation change. New or reconnected
observers receive the current state, and disconnecting a browser clears
its reported errors. An empty snapshot means there are no currently
reported errors; it does not guarantee application recovery.

Reporting is disabled by default. Pages Router, MCP `get_errors`, and
production behavior are unchanged.

<!-- NEXT_JS_LLM -->

Co-authored-by: Marcos Hernanz
<96699542+marcoshernanz@users.noreply.github.com>
2026-09-14 16:15:39 +02:00
Jimmy Miller d155ba9ebf [turbopack] Lazily compile dynamic imports in development (client side) (#97203)
## Summary

Defer compiling client-side dynamic import targets in Turbopack
development until the browser requests their manifest chunk. This avoids
compiling untouched dynamic imports while preserving server-side
imports, CSS loading, Server Actions, source maps, and Fast Refresh
behavior.


One caveat on this is next/dynamic still does some eagerness. I looked a
bit at changing things. But it breaks some of the guarantees there and
decided to leave it off the table.

---------

Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-09-11 07:29:43 -07:00
Joseph c5741d5230 fix(devtools): handle pointercancel when dragging the indicator (#98506)
Fixes #98468

## Claude explanation of the fix

`useDrag` listened for `pointermove` and `pointerup`, but never
`pointercancel`.

When a user agent cancels a gesture — a browser or system gesture takes
over, a second finger arrives — it fires `pointercancel` and no
`pointerup`, implicitly releasing pointer capture as it does. So
`cancel()` never ran: the state machine stayed `{ state: 'drag' }` and
`cleanup.current` was never invoked, leaving the
`pointermove`/`pointerup` listeners on `window`.

The next `pointerup` anywhere on the page then hit those orphaned
listeners, reached `cancel()` with the state still `'drag'`, and called
`releasePointerCapture()` on a pointer that no longer existed — the
reported `NotFoundError`. Each cancelled drag also leaked another
listener pair.

This registers `pointercancel` alongside `pointerup` and removes it in
the same cleanup, so the machine unwinds when a gesture is cancelled;
and it releases pointer capture only when `hasPointerCapture()` says it
is still held, which is preferable to `try/catch` swallowing genuine
faults too.

Note `touch-action: none` (#97723) removed the common touch trigger, but
not the defect: on canary a forced `touchCancel` still throws.


https://github.com/user-attachments/assets/45a9524d-9ffa-40b6-b59a-5af9d947f8c7

## Fixed version


https://github.com/user-attachments/assets/b5ada68c-7442-42ef-a256-bacc512fbeca

## Browser checks

- [x] Chrome, as describe by the bug report
- [x] FF works fine pre and post fix (with and without the pointer
simulation)
- [x] iOS safari simulator, before the fix, I can't see an error, but,
the drag gets frozen, with this fix it works correctly


https://github.com/user-attachments/assets/bcfdc786-bada-4462-b530-299523fdca6d

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 16:01:14 +02:00
Hendrik Liebau 1a295a631c Align fallback parameter staging with shell validation (#98512)
`next dev` reported missing Suspense boundaries in layouts that the
build accepted. For `/[top]/items/[bottom]`, `generateStaticParams`
returned `[{ top: 't1' }]`, and the page wrapped its `bottom` access in
Suspense. A request for `/t2/items/b2` still reported the layout's
access to `top` as an error. Development treated both parameters as
unresolved because the requested `top` value was not generated, although
the required static shell only needed to defer `bottom`.

Production Cached Navigations used the same overly broad parameter set
and omitted eligible static content from repeat visits. Resumes also
reconstructed that set from the original build manifest, even after an
on-demand prerender had produced a more complete shell.

This replaces the alternative proposed in #98460. That proposal fixes
the development error by selecting a separate fallback parameter set for
validation while keeping response staging unchanged. The two sets are
not intended to differ for the same shell target. Correcting only
validation would preserve the incorrect staging decision and leave
production Cached Navigations without the eligible static content.

Staging and static-shell validation now use one `stagedFallbackParams`
set for each selected shell target. Required partial shells retain their
unresolved parameters, even when a later request can complete them.
Prerenders record their parameter set in postponed state, and resumes
use that recorded set rather than reconstructing it from the generic
source.

Dynamic RSC requests now read and revalidate the completed-shell cache
key, so they find partial artifacts that the fully resolved pathname
lookup missed. Request metadata and `RequestStore` both expose the set
as `stagedFallbackParams`. Action-only fallback detection checks actual
unresolved parameters instead of treating deferred values as missing.
2026-09-11 10:46:54 +02:00
Sebastian "Sebbie" Silbermann 6ba71046b0 [test] Move the harness off node-fetch (#98195)
Node.js ships with a built-in `fetch` now so `node-fetch` is no longer
necessary. Mostly motivated by tracing Node.js deprecation warnings
which originated from `node-fetch` by calling the deprecated
`url.parse`.

Call sites keep working through a compatibility type on `fetchViaHTTP`
that translates node-fetch-only options: Instead of `agent` we pass to
`http(s)` directly, `timeout` becomes `AbortSignal.timeout`, and Node.js
readable streams are accepted as bodies with `duplex: 'half'` set
automatically.

The `abort-controller` polyfill is dropped since its signal type
predates the current AbortSignal and undici would not honor it.
`node-fetch` stays installed because `scripts/generate-release-log.mjs`,
`scripts/reset-project.mjs`, and `scripts/update-google-fonts.js` still
import it (follow-up material). Fixture apps will be migrated
separately.
2026-09-10 13:50:35 +02:00
Tobias Koppers b145ad6f3f Fix initial Turbopack HMR update handling (#98385)
### What?

Prevent a real client HMR update from being discarded when it arrives as
the first subscription emission.

### Why?

Capturing the chunk baseline and installing the update subscription are
separate asynchronous operations. A change between them turns the first
emission into a real update, but the client handler previously assumed
that emission was always initialization-only and dropped it after the
server had advanced its version state.

### How?

Inspect the first subscription result and preserve the existing
issues-only initialization behavior while forwarding partial or restart
updates through the normal issue and browser-update paths. This is the
narrower option because it does not change handling of the usual initial
issues payload.

### Verification

- `pnpm --filter=next build`
- `HEADLESS=true pnpm test-dev-turbo
test/development/app-dir/hmr-dynamic-component/hmr-dynamic-component.test.ts`
(including a locally controlled baseline-to-subscription race)
- Prettier and ESLint on the changed source

<!-- NEXT_JS_LLM -->

<!-- fleet 3820fb8a-c662-4c74-8316-e2d7cd42bfbd -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-09 08:07:34 -07:00
Will Binns-Smith ec107c16dd lazy server hmr (#96566)
## Summary

Make Turbopack server HMR demand-driven. Server updates are now compiled
and applied when the next relevant request writes an endpoint, instead
of eagerly evaluating changed server modules after every file change.

This replaces the aggregate server HMR subscription with an on-demand
update API, while preserving incremental updates and falling back to
full cache eviction when a restart is required.

Test Plan: added an e2e test

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-08-31 09:12:51 -07:00
Niklas Mischkulnig 8330e4c4cd test: Improve test cache handler implementations (#98098)
- Some of them were not forwarding `getExpiration` or `softTags` to
`defaultCacheHandler`
- use-cache-cross-deployment was ignoring softTags and expiration. (This
patch was written by Sol)

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-08-31 13:59:14 +02:00
Niklas Mischkulnig 1f52cb42ad Turbopack: shorten CSS module class names (#97944)
The class names were unnecessarily long.

Use the same as the lightningcss default:
`[hash]_[local]` which is
`<hash of the full file path>_<original class name or identifier>`

Keep the previous longer mechanism to aid in debugging in dev

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 21:22:53 +00:00
Will Binns-Smith bc218dbb85 test: preserve server cache after compile error (#97724)
### What?

Adds a focused Turbopack development regression test covering server HMR
recovery from a compile error.

### Why?

A server compile error must not force a full server module reevaluation
when HMR recovers. Clearing the server module cache would discard stable
state held by unchanged dependencies and undermine server Fast Refresh
module preservation.

### How?

The test reuses the existing module-preservation fixture and its
unchanged server dependency evaluation timestamp. It introduces a
deterministic syntax error, verifies the Turbopack build-error redbox,
repairs the page, waits for the repaired server render, and then
confirms the dependency still exposes its original timestamp. The
patched fixture is restored in a `finally` block so failures cannot leak
state into later tests.

### Testing

- `pnpm test-dev-turbo
test/development/app-dir/server-hmr/server-hmr.test.ts`
- 17 tests passed

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-08-24 17:25:47 -07:00
Marcos Hernanz 27a8eb0999 [devtools] Fix indicator dragging on touch screens (#97723)
### What?

- Restore `touch-action: none` on the draggable devtools indicator.
- Add integration coverage for the draggable computed touch action.

### Why?

PR #86816 made devtools content selectable, but also removed
`touch-action: none` with the selection styles. On touch screens, that
allows the browser to claim the gesture for viewport panning and
suppress the pointer event stream, causing the indicator to stop moving.

### How?

Keep the drag-lifecycle selection handling introduced by #86816 and
restore only `touch-action: none` before the gesture begins.

Fixes #97668

### Testing

- `pnpm build --filter next`
- Targeted Prettier and ESLint checks
- `NEXT_SKIP_ISOLATE=1 pnpm test-dev
test/development/app-dir/devtools-position/default-position.test.ts`
2026-08-23 11:56:26 -07:00
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
Janka Uryga 3cdb56c251 [PPF] unstable_prefetch() (#97622)
Implements `unstable_prefetch()`, which is intended for use in
`partialPrefetching`. `await unstable_prefetch()` excludes content from
the app shell -- it will only be available when using `prefetch={true}`
(speculative prefetch) or during navigations.

As a rule of thumb, `unstable_prefetch()` resolves whenever static
`params` would:
- in a static prerender
  - but NOT the app shell extracted from it, which is param-less
- in a runtime prefetch (`prefetch={true}`)
  - but NOT a runtime app shell, which is param-less

Note that `prefetch()` is URL data, so using it in an App Shell without
Suspense will trigger an instant insight.

### Implementation notes

`prefetch()` is treated like URL data, so it resolves in the
`PrefetchStatic/PrefetchRuntime` stages added in #96908. The
implementation is basically analogous to `unstable_navigation()` except
using different stages. i've considered abstracting them into one
implementation, but decided against that for now, we can deduplicate
later.

Error messages about URL data have not been updated to mention it yet --
we will do that as a follow up, along with docs.

`await prefetch()` does not count as a runtime data access, meaning that
it won't affect the static prefetch hint for a route. however `await
prefetch(); await cookies()` does deopt the route, because using a
speculative runtime prefetch would reveal more content. Note that this
may cause us to unnecessarily deopt a shell to runtime even if only the
speculative part of the content would be improved by a runtime request;
this is not a new issue, but it's something we should optimize.
2026-08-21 13:50:23 +02:00
Janka Uryga 1be0ab80c4 [PPF] unstable_navigation() (#96908)
`navigation()` is a new API that allows omitting contents from runtime
shells and runtime prefetches. Conceptually, the point is to express
that something is expensive to compute, so we shouldn't do it for
requests that may not get used (shells and prefetches). Notably, this
means that it's fine to include it in a static prerender -- it'll be
computed once and used for many requests, so it doesn't make sense to
exclude it.



## Implementation

The split in behavior across static and runtime prerenders is a
departure from how most of our APIs behave -- usually, if something
resolves statically, then it also resolves in "more complete" prerender.
Departing from this leads to some implementation complexity.

We include three new stages, used by two facets of the implementation:

```diff
export enum RenderStage {
  Before = 1,
  //
  ShellStatic = 10,
+ PrefetchStatic = 11, <------- params, prefetch()  [static prerenders]
+ NavigationStatic = 12  <------navigation()        [static prerenders]
  Static = 13, <--------------- finish accumulators [static prerenders]
  //
  ShellRuntime = 20,
  Runtime = 21, <-------------- params, prefetch() [runtime prerenders]
+ NavigationRuntime = 22, <---- navigation()       [runtime prerenders]
  //
  Dynamic = 30,
  Abandoned = 40,
}
```

### NavigationRuntime

In runtime prerenders (or dev renders that simulate them),
`navigation()` resolves in `NavigationRuntime`
We only reach this stage in 1. the embedded runtime prerender produced
for Cached Navigations and 2. during dev/prod full staged renders --
runtime shells end in `ShellRuntime`, and runtime prefetches end in
`Runtime`.

Notably, this means that content gated behind `navigation()` is included
in the embedded runtime prefetch stream.

### PrefetchStatic & NavigationStatic

This is a helper stage added before `Static`. Static prefetches still
use the `Static` stage for their output. This new stage exists so that
we can resolve static `params` (and `prefetch()` when we implement it)
which the stage is named after) separately from `navigation()`, which
resolves in `NavigationStatic`, after which the prerender ends in
`Static`. This separation is important, because during static prerenders
we track whether or not runtime APIs are used (see
`trackRuntimeDataAccessed`) to determine if a runtime shell (or runtime
prefetch) might give us more content than the static ones. However, a
runtime shell/prefethc **would not resolve navigation()**, so `await
navigation(); await cookies()` would not reveal more content, and thus
shouldn't count as a usage that prevents static optimization.
We achieve this by checking the stage inside
`trackRuntimeDataAccessedImpl` and not tracking anything if we reached
the `NavigationStatic` stage.

### Behavior of shells and validation

As noted before, `navigation()` has an incompatible resolution order
between static and runtime prerenders. In #97040, we did some groundwork
to deal with this in validation.

Static prerenders resolve `navigation()`, which means that static shells
include content gated behind navigation(). This means that Static Shell
Validation allows them.

On the other hand, App shells **do not** resolve `navigation()`. This
leads to an inconsistency for Instant Validation -- a `await
navigation()` might be fine if a page is prefetched statically, but
would become blocking as soon as the page starts using runtime data and
switches to a runtime shell. To avoid this pitfall, we pessimistically
assume that any `navigation()` _might_ be part of a runtime
shell/prefetch, so any `navigation()` unguarded by Suspense will error
in IV.

In practice, this is handled analogously to static params: we do a dev
render with `needsAppShell: true`, which makes `navigation()` resolve in
`NavigationRuntime`, and then we use the `ShellRuntime` stage when
validating, which means that `navigation()` will be a hole. Note that
the discriminated error message logic currently only retries errors
using the `Runtime` stage, which won't have `navigation()` resolved
either, so it will be incorrectly reported as dynamic data. This will be
improved in a follow up.
2026-08-20 15:27:11 +02:00
Sebastian "Sebbie" Silbermann 55e7e1903a [test] Cover the prerender worker-thread backend with an addon we control (#97543)
`experimental.workerThreads` decides whether static generation runs in
real worker threads or forked child processes, and a native addon
declared with `NODE_MODULE` can only be loaded once per process. That is
why the flag defaults to false (#9199) and why the static export worker
was fixed to respect it rather than hardcoding threads on (#25063).
Nothing tested it: the only suite that tried, `firebase-grpc`, had its
assertion skipped since 2019, and modern `firebase` ships no native
module at all, so its remaining test asserted only that a build
succeeds.

This adds a `single-context-addon` fixture, deliberately declared with
`NODE_MODULE`, and a production suite asserting both directions: the
build succeeds by default and fails with "Module did not self-register"
once worker threads are enabled. The fixture loads the addon from
`next.config.js` as well as from the page, because that failure only
happens on a second `dlopen` within one process, so an addon loaded only
inside the worker would register there and the build would pass.

A third case documents a bug rather than intended behaviour. `next
build` runs Turbopack in a worker thread and that worker re-evaluates
`next.config.js`, so requiring a non-context-aware addon from the config
breaks the build even with `experimental.workerThreads` off. Webpack and
rspack are unaffected, since their build workers are forked child
processes. Isolated with an unguarded `require` and default flags,
Turbopack exits 1 with "Module did not self-register" where webpack
exits 0. It is the same class of failure #9199 and #25063 fixed, in a
worker those PRs did not touch. The assertion is branched on the bundler
and carries a note to drop the branch once Turbopack stops evaluating
the config on a worker thread. The `isMainThread` guard in the fixture's
config keeps the first two cases pointed at the static generation worker
instead.

A development suite covers the other direction, asserting that
evaluating a route does not put such an addon on one of the threads
`next dev` uses regardless of the flag, so it would catch a change that
moved route evaluation onto the dev validation pool. The expectation is
the same with and without Cache Components; both were checked by logging
the thread id from the page's module scope, and the route is evaluated
on the main thread either way.

`firebase-grpc` is removed, since it covered the same flag with a
skipped assertion and a vacuous one.
2026-08-20 11:57:04 +02:00
Niklas Mischkulnig da4888c8df test: better isolate concurrent-install suite (#97546)
these were failing with 50% probability since today, for some reason:
https://app.datadoghq.com/ci/test/runs?query=test_level%3Asuite%20%40test.service%3Anextjs%20%40test.type%3Aturbopack%20%40test.suite%3Aconcurrent-install&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&start=1786528015972&end=1787132815972&paused=false
2026-08-19 13:15:13 +02:00
Hendrik Liebau 5817bd1def Anchor the async local storage instances to global symbols (#97255) 2026-08-16 23:15:51 +02:00
Josh Story c18acf5cef test: deflake use-cache-size-zero warm reload (#97421)
## Summary

Keep the first warm reload assertion focused on browser-visible
stale-while-revalidate behavior, then poll the route with independent
HTTP requests until a later response observes the fresh value. After
convergence, perform one final browser reload to confirm the
user-visible path also receives an updated value.

This avoids fixed delays, cache debug logging, and repeated browser
navigations that can cancel or outpace background regeneration. The
convergence poll uses the standard 3-second `retry()` window and returns
as soon as freshness is observed; the browser only reloads again after
that condition is satisfied.

## Verification

- `__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_CI=true
HEADLESS=true pnpm test-dev-webpack
test/development/app-dir/use-cache-size-zero/use-cache-size-zero.test.ts`
- `__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_CI=true
HEADLESS=true pnpm test-dev-turbo
test/development/app-dir/use-cache-size-zero/use-cache-size-zero.test.ts`

<!-- NEXT_JS_LLM -->
2026-08-16 14:40:01 +00:00
Tim Neutkens 6b9001934d Remove server route matcher stack (#94157)
### What?

Remove the legacy server route matcher managers, route matcher
providers, and server route matcher classes. Move route definition and
params metadata into fsChecker output and request metadata instead.

### Why?

The filesystem checker already owns the normalized route lookup path in
production. Keeping a separate matcher manager stack duplicates route
inventory, adds reload plumbing, and makes dev and production behavior
harder to keep aligned.

### How?

- Build pages and app route definitions in fsChecker from production
manifests and from the dev route inventory.
- Thread matched route definitions and params through resolve-routes and
router-server request metadata into base-server and next-server.
- Remove reloadMatchers propagation and delete the legacy matcher
manager, provider, and matcher implementations and tests.
- Preserve dev ensurePage behavior, including legacy custom-server
render methods, and Pages API loading by using matched route definitions
while loading compiled dev modules where needed.

### Verification

- `pnpm test-start-webpack test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-start-turbo test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-webpack test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-turbo test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-webpack
test/e2e/on-request-error/dynamic-routes/dynamic-routes.test.ts`
- `pnpm test-dev-turbo
test/e2e/on-request-error/dynamic-routes/dynamic-routes.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/app-routes/app-custom-routes.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/edge-route-rewrite/edge-route-rewrite.test.ts`
- `pnpm test-start-webpack
test/e2e/middleware-rewrites/test/index.test.ts`
- `pnpm test-start-webpack test/e2e/middleware-matcher/index.test.ts
test/e2e/middleware-custom-matchers-basepath/test/index.test.ts
test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts`
- `pnpm test-start-webpack test/e2e/api-catch-all/api-catch-all.test.ts
test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts
test/production/root-catchall-cache/root-catchall-cache.test.ts`
- `pnpm test-start-webpack
test/e2e/i18n-beforefiles-rewrite/i18n-beforefiles-rewrite.test.ts`
- `pnpm test-start-webpack
test/e2e/basepath/redirect-and-rewrite.test.ts`
- `pnpm test-dev-webpack test/e2e/custom-server/custom-server.test.ts`
- `pnpm test-start-webpack test/e2e/custom-server/custom-server.test.ts`
- `pnpm --filter=next types`
- `pnpm --filter=next build`
- `git diff --check`

<!-- NEXT_JS_LLM_PR -->
2026-08-15 13:07:22 +02:00
Marcos Hernanz ae0e0a5006 Turbopack: remove stale manifests for deleted routes (#97333)
## What?

- remove manifest fragments when Turbopack's development entrypoint
cleanup removes a deleted page
- add an end-to-end regression for replacing concrete App Router pages
with an optional catch-all without restarting the dev server

## Why?

Turbopack already removes deleted entry keys from its asset mapper,
subscriptions, issues, and client state. It did not remove the
corresponding records from `TurbopackManifestLoader`, so the next
manifest write continued to serialize deleted App Router routes.

When a deleted concrete route is replaced by an optional catch-all,
those stale records conflict with the new route at the same specificity.
The dev server reports the route conflict and responds with 404 until it
is restarted.

This is independently consistent with the cleanup identified in #97194.
This draft adds end-to-end coverage that fails on current canary by
observing both the 404 and stale manifest records, then passes with the
lifecycle fix.

## How?

Pass the existing manifest loader to `handleEntrypointsDevCleanup` and
call its existing `delete(key)` method in the same branch that deletes
the stale asset mapping. This keeps all per-entry manifest maps
synchronized with the authoritative current entrypoint set without
special-casing any route shape.

The regression test:

1. compiles two concrete localized routes
2. deletes both pages and adds an optional catch-all
3. waits for three affected paths to return the new catch-all content
4. verifies the development app-paths manifest contains the catch-all
and no deleted entries

## Verification

- reproduced the report on `16.3.1-canary.10`: all four reported paths
returned 404 after the live route replacement and returned 200 after a
restart
- confirmed the regression fails on current canary before the source
change with the same-specificity route error and a 404 response
- `pnpm test-dev-turbo
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-rspack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `CI=1 pnpm build-all`
- `pnpm --filter=next build`
- formatting, ESLint, staged-diff, and signed-commit checks

Fixes #97035
2026-08-14 14:17:29 -07:00
David Alexandru Ilie 44103a8702 Trace route module preparation (#97295)
## Summary

- trace the full route-module preparation boundary as
`RouteModule.prepare`
- record manifest loading as one aggregate `RouteModule.loadManifests`
child span
- keep route preparation visible by default while exposing manifest
detail only with verbose tracing
- cover normal and direct-entrypoint server lifecycles without relying
on test ordering

## Why

Route preparation is the stable boundary between loading a route module
and executing it. The parent span shows the total setup cost; the
aggregate manifest child explains manifest work without creating
per-file spans or exposing paths.

## Verification

- `pnpm build-all`
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- focused OpenTelemetry E2E in development and production with Turbopack
and Webpack
- Request Insights route-preparation E2E with Turbopack and Webpack
- Prettier, ESLint, and `git diff --check`

## Stack

Depends on **Trace route module loading**.
2026-08-14 12:39:01 +01:00
Hendrik Liebau 529ddc3c8d [test] Unflake two cache-components-dev-streaming assertions (#97246)
The streaming assertion in `should stream suspense boundaries while
filling caches in the background` polled a bare `<p>` through `retry()`
with its 3000ms default. Because `retry` gives up as soon as `waited +
interval > duration`, the effective budget is about 2.6s, and the
fixture's cache fill alone takes 2000ms, so the assertion had roughly
600ms of headroom. That budget also had to absorb one browser round trip
per attempt, because a selector that matches the fallback as well as the
content cannot wait for anything: `waitForSelector` returns the fallback
immediately, so the waiting had to happen in the test process at a 500ms
granularity.

This change gives the two paragraphs the ids `#cached-fallback` and
`#cached`, in line with every other route in this fixture, which is what
lets the wait move into the browser. Playwright now resolves the moment
the content is revealed, in a single round trip, and #95466 had already
moved the rest of the suite to that pattern. The two assertions that
check what the shell itself delivers still need the original "whichever
element arrives first" probe, and they now express it as the selector
list `#cached, #cached-fallback` instead of relying on `p` to match
both.

The budget for the reveal is 10s rather than the 5s that `elementByCss`
narrows the harness default to, because the wait has to cover more than
the fill. In the linked failure the content bytes had arrived one second
after the shell committed, and the reveal was still at least 2.4s away,
since the browser was busy evaluating the dev bundle. A shorter fill
would not help with that, as it does not shorten the part of the wait
that CI actually spends.

The convergence assertion in `serves a short-expire cache warm on reload
and converges to a fresh value` ran out of the same budget for a
different reason: every attempt performed a full dev page reload on top
of the 1.5s regeneration, and measured runs needed 1.8s to 3.4s. It now
reads the value over HTTP with `next.render$`, the way #97187 does,
which observes the same server-side cache state without downloading and
evaluating the dev bundle. Under the same contention those reads
converge in 1.1s to 1.9s, and they stay there when the load is
quadrupled, because only the 1.5s regeneration gates the loop. The
default budget therefore covers it, and the test drops from 15.5s to
3.6s.

Both fixes were verified by raising the fill to 3.2s to emulate the
CI-side delay: the previous assertions then fail deterministically with
the CI signature, and the new ones pass.

[Flakiness
metrics](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22cache-components-dev-streaming%20should%20stream%20suspense%20boundaries%20while%20filling%20caches%20in%20the%20background%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22&agg_m=count&agg_m_source=base&agg_t=count&citest_explorer_sort=timestamp%2Casc&cols=%40test.status%2Ctimestamp%2C%40test.suite%2C%40test.name%2C%40duration%2C%40test.service%2C%40git.branch&currentTab=overview&eventStack=&fromUser=true&index=citest&start=1783948256248&end=1786540256248&paused=false)
2026-08-12 21:53:40 +02:00
Sam Poder 04e9933b33 [turbopack] Fix HMR for dynamic imports evaluated from layouts (#97213)
Tbh, I don't really understand this code path but this appears to fix
the bug. This includes `referenced_output_assets` in `async_chunks`. The
bug report went away when `experimental.turbopackServerFastRefresh` was
disabled.

The agent's explanation (more for entertainment):

<img width="967" height="290" alt="Screenshot 2026-08-11 at 5 07 46 pm"
src="https://github.com/user-attachments/assets/e57425be-e4e9-44b8-a6eb-8e93d04b37f0"
/>

Closes https://github.com/vercel/next.js/issues/97206
2026-08-12 09:35:54 -07:00
Hendrik Liebau b93ed4fa0f [test] Unflake use-cache-custom-handler-dev tests (#97187) 2026-08-12 10:58:16 +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
Andrew Clark 5dc3ae1fe7 Fix Nav Inspector request loop on repeat captures (#97050)
## Summary

Repro (from #96692): enable the Nav Inspector, click a `<Link
prefetch={true}>`, close the inspector, navigate home, re-enable it, and
click the same link. The app hung in a pending "Compiling..." state
while firing prefetch requests in an infinite loop (~30/sec).

The Instant Navigation Testing lock restricted navigation reads to
entries created within the current lock scope (`ownedEntries`), enforced
as a post-hoc filter after the cache lookup. But the segment cache
resolves lookups by most-specific-match, so a previous scope's
runtime-prefetch entries at concrete param keypaths kept winning the
lookup, the filter kept rejecting them, and the locked prefetch created
its replacement at a more generic keypath that could never win — every
scheduler pass discarded and refetched forever.

Rather than patch the filter, this replaces the `ownedEntries`
mechanism:

- Each lock scope owns a private segment `CacheMap` that starts empty
and is discarded at release, so a captured navigation structurally
observes only data fetched under the lock — cross-scope shadowing
becomes impossible by construction.
- The map is an explicit capability bound when work is created: a
prefetch task captures its map when scheduled
(`PrefetchTask.segmentCacheMap` — the single place that consults lock
state), a locked navigation inherits its driving task's map, and
everything else — unlocked navigations, hydration, refreshes,
traversals, server actions and patches — binds to the shared map. Reads
and response writes receive the map explicitly, so a request that
straddles a scope boundary still writes into the map its entries live
in.

In production builds without the testing API this compiles down to the
previous single-map behavior.

Includes the failing test from #96692 (thanks @samselikoff), hardened to
use the retry-based panel-reopen helper the sibling tests use.

## Verification

- `pnpm test-dev-turbo
test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts`
(32/32, includes the new regression test)
- `pnpm test-dev-webpack
test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts
-t "repeat clicks"`
- `pnpm test-dev-turbo
test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts`
(production paths unregressed)

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: Sam Selikoff <sam.selikoff@gmail.com>
2026-08-10 20:39:29 +00:00
David Alexandru Ilie ec8b5435bf Trace development route compilation (#96454)
## Summary

- Trace development route compilation through
`DevBundlerService.ensurePage` after route preparation selects a route.
- Present the existing development bundler work as `compile route` in
Request Insights so cold compilation can be distinguished from route
matching.
- Keep the span internal by default without expanding the default public
OpenTelemetry allowlist.
- Preserve compilation error propagation and the existing
route-preparation parentage.

Warm requests can still record the span when `ensurePage` runs, but the
work is normally negligible after the route is compiled.

## Verification

- `HEADLESS=true pnpm test-dev-turbo
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- `HEADLESS=true pnpm test-dev-webpack
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- Both bundlers passed 2/2 tests at the final stack head.
- `pnpm --filter=next build`
2026-08-10 16:15:56 +01:00
David Alexandru Ilie 2966db4458 Trace development route preparation (#96453)
## Summary

This PR introduces the first bounded route-preparation phases used by
Request Insights. It instruments existing matcher and bundler boundaries
without changing their behavior or making the spans default-public
OpenTelemetry data.

- trace development route matching and route preparation while a route
is ensured
- keep production matcher reload as a separate sibling phase so reload
work is not charged to matching
- surface the internal phases as `match route`, `prepare route`, and
`reload route matchers` in Request Insights
- cover cold and warm page, App Route/API, and dynamic-route behavior in
both development bundlers
- preserve the existing matcher results, errors, and public OTel
allowlist

## Verification

- `pnpm test-dev-turbo
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- `pnpm --filter=next types`
2026-08-10 12:25:22 +02:00
dan d792fcf5e7 Fix use cache over- and under-invalidation in dev (#96235)
In dev, `"use cache"` entries are keyed by an HMR refresh hash so that
edits invalidate cached data.

This hash was managed wrong in both directions:

- webpack refreshed the hash at moments when no code changed: on dev
server startup, and when a page file was added or removed. Each refresh
threw away all cached data for nothing.

- The hash never distinguished one dev server run from another. That
matters with a custom cache handler that persists entries: a restarted
server would serve data cached by the previous run, whose code may have
changed while the server was down.

To fix this:

- Adding or removing a page no longer refreshes webpack's hash. Edits
and env file changes still do.
- The hash now includes a random per-run value.

This fixes the `cache-components-dev-warmup` flakes.

## How to reproduce

Here's the first problem in action, on webpack. Render a cached
`Math.random()` on a page, then create an unrelated page file:

```
value: 0.199426
value: 0.199426
$ echo '...' > app/unrelated/page.tsx    # never visited
value: 0.990973                          # cached entry gone a second later
```

Adding the page threw the cached data away.

Starting the dev server throws it away too, once, shortly after boot.
That one is a race: normally the cache is still empty when it fires, so
nobody notices — but if the first page request gets in before it,
everything that request cached is thrown away and the next reload is
silently cold. That race is the cause of the `dev-warmup` flakes on
loaded machines, and it's how I found this.

The hash refresh is specific to webpack, so Turbopack doesn't lose
cached data this way. On Turbopack, these events instead clear the
server's module cache. Whether the next render picks up a fresh module
instance is a race, so module-scope state can reset. Turbopack also has
a separate bug in this area — it advances its hash once per
change-subscription emission, and an added or removed page can produce
an emission without any edit — which #96248, stacked on this PR, fixes
by deriving the hash from compiled output.

## Underlying cause

Next computes a "client router filter" (a bloom filter over the app's
routes, used by the client-side router), and a filter change is treated
like an env file change. An env change means cached data might be stale,
so the reaction is strong: the browser is told to re-fetch, and on
webpack the hash is refreshed, which discards every cached entry. But
the filter is derived from the routes, so it "changes" whenever you add
or remove a page — and once right after startup, because the first
computation has nothing to compare against. Neither makes cached data
stale.

The second problem is the opposite: dev server runs share cache keys.
The hash starts from the same value on every run (unset on webpack,
`"0"` on Turbopack), so a restarted server generates the same keys as
the old one. With the default in-memory handler that's moot, but a
custom cache handler that persists entries will serve entries written by
the previous run. webpack can also hit this after edits: its hash is
derived from compiled output, so re-applying an edit that a previous run
also made lands on that run's keys.

Judging by the code (?), dev cache reuse was only ever meant to last a
single run: every dev cache mechanism is in-memory, and #75474 takes it
for granted that restarting the dev server clears cached content. So I
treat cross-run reuse as a bug. Not sure I got this constraint right —
in particular, #75474 deliberately made reverting an edit reuse
previously cached data, and this PR keeps that working within a run but
stops it from working across runs.

## Fix

Two changes:

- A filter change is no longer treated as an env change. It still
updates the compiled-in filter value (the part that's actually needed),
but it no longer refreshes the hash, and on Turbopack it no longer wipes
the server's module cache. Real env file changes still trigger the full
reaction.

- The hash now starts from a random per-run value on both bundlers. So
it exists before the first request and never matches another run's keys.

The per-run value is deliberately conservative: it also prevents reuse
across restarts where *nothing* changed. Allowing exactly that reuse
safely needs keys derived from the cached function's implementation (the
existing TODO in `use-cache-wrapper.ts`), which would replace the
per-run value.

## Test plan

The first commit adds five tests
(`test/development/app-dir/cache-components-spurious-cache-invalidation`).
They run on both bundlers, except the module-state test, which is
Turbopack-only: on webpack, adding a page recompiles the server bundle,
which replaces the module instances regardless. So Turbopack runs five
and webpack four.

Before the fix, these fail:

```
webpack:    ✕ discards use cache entries when a previous run made the same edit
            ✕ discards use cache entries across dev server restarts   (most runs — see below)
Turbopack:  ✕ keeps module state when an unrelated page is added
            ✕ discards use cache entries across dev server restarts
```

The webpack restart test does not fail every run. The start-time refresh
replaces the unset hash with a content-derived one at an unpredictable
moment. When one server run caches before its refresh and the other
reads after its own, the keys don't collide, so there is no cross-run
reuse for the test to catch that run.

The webpack side of the over-invalidation fix is covered by the refetch
count below rather than by a cached-value test: the equivalent test on
Turbopack depends on emission timing, so it lands with #96248, which
removes that dependence.

The same commit also tightens the refetch count in the test from #96250
below: since a page add is no longer an env change, adding a page
refetches an open tab once instead of twice, on both bundlers. Before
the fix, both bundlers fail that expectation (`Expected: 1, Received:
2`).

To reproduce the flake this PR fixes, run the dev-warmup suites under
CPU load (`for i in $(seq 1 12); do yes > /dev/null & done`). Before the
fix, 6–8 of the 11 tests fail per run on webpack (`Prerender` flips to
`Server` on warm reloads). After the fix, all 11 pass. Turbopack passes
before and after. Also still green: `typed-env` (env definitions were
previously only written at boot via the false positive this removes; now
they're written on the initial scan explicitly), `env-config`,
`use-cache-custom-handler`, and `pages-to-app-routing` (it exercises the
router filter update path).
2026-08-06 14:25:52 +01:00
dan f58c669ab2 Fix which pages the dev server announces, and when (#96250)
The dev server tells open tabs when a page is added or removed, so a tab
showing a 404 picks up a page you just created, and a tab showing a page
you just deleted falls back to the 404.

Both bundlers computed the list of changed pages wrong, in opposite
directions.

**Turbopack announced pages that hadn't changed.** To find what changed,
it compared the new route list against the entry maps in
`currentEntrypoints`. Those maps key App Router entries by page name
(`/blog/page`), but the route list is keyed by route (`/blog`), so no
App Router route ever matched: every update announced every existing app
route as added and every page name as removed, and starting the server
announced every route once. Each announcement makes every connected tab
refetch. Fixed by comparing the new route list to the previous route
list, which uses the same naming. Starting the server now announces
nothing on either bundler, since the first route list is not a change.

**webpack didn't announce pages that had changed.** It only announced
when `prevSortedRoutes.every((val, idx) => val === sortedRoutes[idx])`
was false. That is a prefix comparison: when a new route sorts after all
existing ones, the old list is a prefix of the new one, and nothing is
announced. In App Router this mostly went unnoticed, because adding a
page was also (wrongly) treated as an env change, which refreshed tabs
anyway; the next PR in this stack removes that. In Pages Router nothing
hid it: the route manifest was never refetched, so client-side
navigation to the new page kept returning 404 until a manual reload.
Fixed by also comparing the lengths.

**What this PR does not fix:** on Turbopack there is a window right
after a page is added where the dev server has already announced it to
tabs, but can't serve it yet (the route isn't in the dev router's route
table until the watcher pass in `setup-dev-bundler` runs). A tab that
reacts inside that window gets the 404 again, and since each page is
only announced once, it stays on the 404 until you reload it by hand.
That bug predates this PR and is being fixed separately.

So that the new App Router tests don't hit this window, each one edits
the added page again once it's confirmed servable. The edit makes the
server announce again, and this time the tab can only get the page. The
trade-off is that these tests don't prove the first announcement alone
updates the tab — the refetch-count test covers how often changes are
announced, and its tab never sits on the added page's 404, so it can't
hit the window. A TODO in the tests says to drop the extra edits once
the underlying bug is fixed. The Pages Router test needs no such edit:
Turbopack updates the route matchers before it announces, and a Pages
Router tab reloads the document instead of refetching.

## Test plan

New tests cover adds, removals, dynamic routes, route groups, and a
dynamic route being shadowed by a more specific page and then
unshadowed. One more counts how often an open tab refetches when a page
is added: it wraps the tab's `fetch` and counts requests carrying the
`next-hmr-refresh` header, which distinguishes a refetch the dev server
asked for from a navigation or a prefetch. It runs on a dev server no
other test shares, because the count depends on what has been compiled
so far.

Before the fix, adding one page makes an open tab refetch:

```
Turbopack: 12 times   (every app route re-announced; grows with the number of routes)
webpack:    1 time    (the announcement is never sent; only the env-change refetch happens)
```

The correct count is currently 2: one refetch for the announcement, one
for the env-change false alarm. The next PR removes the false alarm and
tightens the expectation to 1.

Before the fix, webpack also fails the Pages Router manifest test (the
manifest update is sent from the same skipped branch).

With the fix, both bundlers pass all ten tests.
2026-08-06 14:25:52 +01:00
Luke Sandberg ff3a2cfaa9 [turbopack] Strip leading BOM before parsing CSS (#96678)
Fork PR #96379 by @lazerg, re-opened as a branch PR so the "when
deployed" CI jobs can run — those require Vercel deployment secrets that
GitHub does not expose to pull requests from forks, so they can never
pass on the original.

**The fix commits are unchanged and still authored by @lazerg.** This PR
only adds tests on top. Please credit them; #96379 should be closed in
favor of this one.

### What?

A CSS file beginning with a UTF-8 BOM (`EF BB BF`) is mishandled by
Turbopack. Lightning CSS does not skip the BOM, so it is tokenized as
content and the first token is misparsed:

```
./app/bom.css:1:2
Error: Parsing CSS source code failed
Unexpected token AtKeyword("layer")
```

The user-visible symptom is broader than a failed build. Turbopack
parses with `error_recovery: true`, and under that setting a leading BOM
makes Lightning CSS return `Ok` with **zero rules** — so a BOM-prefixed
stylesheet could silently drop all of its styles instead of erroring.
dart-sass (compressed style) and PostCSS >= 8.5.24 both emit or
round-trip such a BOM, so real projects hit this.

Fixes #96374

### How?

Strip a leading `U+FEFF` in `parse_css_stylesheet` before handing the
source to Lightning CSS, covering both `StyleSheet::parse` call sites
while leaving `ParseCssResult.code` as the original bytes that code
frames are rendered from.

That split makes parser positions relative to the stripped copy while
code frames still render the original line, so first-line positions need
compensating. `source_pos_for_loc` adds the stripped character back for
line 0 of BOM files. Only line 0 is affected, because the BOM contains
no newline.

### Tests

`test/e2e/app-dir/css-bom` — a BOM-prefixed stylesheet compiles and its
rules reach the page. Verified failing without the fix with the exact
error above, and passing with it, in dev-turbo, start-turbo and
start-webpack.

`test/development/app-dir/css-bom-code-frame` — covers the position
correction. Two fixtures hold the same invalid `@media (min-width: {})`
on line 1 and differ only by the leading BOM; the test asserts the BOM
file's reported column is exactly one greater:

| | `no-bom` | `bom` | |
|---|---|---|---|
| without `source_pos_for_loc` | 18 | 18 | fails |
| with it | 18 | 19 | passes |

Asserting the relationship rather than a literal column keeps this
robust if Lightning CSS changes its absolute column convention. It is
kept separate from the e2e suite because the fixtures are intentionally
invalid CSS, and scoped to Turbopack in dev, where the warning reaches
the CLI as the page is requested.

---------

Co-authored-by: lazerg <lazerg2@gmail.com>
Co-authored-by: vercel-gh-bot-3[bot] <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com>
2026-08-04 21:25:04 +00:00
Marcos Hernanz 39b7da2ee8 Turbopack: terminate failed plugin worker threads (#96592)
## What

Terminate a worker-thread plugin runtime when an evaluation or IPC error
marks
its `WorkerOperation` non-reusable.

`disallow_reuse()` already removed the worker from pool statistics and
disabled
the callback that returns it to the pool, but it did not stop the
underlying
Node.js `Worker`. The worker then stayed strongly owned by
`loaderWorkers`, kept
its V8 isolate/module graph alive, and waited forever for another task
that
could never arrive.

This calls the existing worker terminator used by `wait_or_kill`,
scale-down,
and scale-to-zero. It removes the routed native channel and dispatches
the
existing JavaScript `Worker.terminate()` callback, which also deletes
the map
entry.

## Why this matters for v0

v0's planned worker-thread runtime turns transient loader/PostCSS errors
during
agent edits into a deterministic lifecycle leak. On the real v0
`/button`
route, each unique Tailwind/PostCSS failure retained one complete worker
runtime
on the baseline.

The balanced eight-process A/B panel (80 unique failures per lane)
measured:

| Lane | `WorkerThread` slope | Private-memory slope | Median failure |
Median recovery | >5 s failures |
|---|---:|---:|---:|---:|---:|
| baseline | **+1.000/error** | **+49.81 MiB/error** | 108.04 ms |
329.26 ms | 3 / 80 |
| candidate | **0.000/error** | **+5.12 MiB/error** | 108.15 ms | 317.65
ms | 2 / 80 |

That is a **100% elimination of leaked-worker growth** and an **89.7%
reduction
in retained private-memory slope** (exact process-bootstrap 95%
interval:
85.7%–99.5%). Every candidate process was below every baseline process;
exact
4-vs-4 process-label permutation `p=0.0143` one-sided / `0.0286`
two-sided.
Median failure latency changed by only +0.11 ms.

The independent 100-cycle-per-lane endurance confirmation showed the
same
causal result:

| Lane | `WorkerThread` cycle 1→100 | Private memory cycle 1→100 |
Private slope | Median failure | Median recovery |
|---|---:|---:|---:|---:|---:|
| baseline | **38→137** | **4.28→7.34 GiB** | **+27.02 MiB/error** |
104.45 ms | 331.32 ms |
| candidate | **32→32** | **3.93→4.10 GiB** | **+0.98 MiB/error** |
102.94 ms | 297.24 ms |

That long run eliminated all observed worker growth and reduced the
fitted
private-memory slope 96.4%. Every one of the 200 unique failures
recovered to
the complete real route, and the source hash was restored after every
cycle.

The benchmark used fresh `.next` state per process, five error/recovery
warmups,
a five-minute quiescence window, unique uncached errors, exact source
restoration
after every cycle, and within-process slopes. Baseline and candidate
native
binaries were built from the same Next.js base, with only this lifecycle
change
affecting candidate native code, and selected by SHA-verified
`NEXT_TEST_NATIVE_DIR` paths.

This is independent of #96433. That PR coordinates explicit multi-file
edit
transactions; this PR fixes the lifecycle of a worker that has already
failed.
It also does not change idle-worker scale-down policy: failed workers
are absent
from the idle pool, so scale-down cannot see them.

## Regression test

The new development test schedules a one-second filesystem marker inside
a
custom loader, then throws an evaluation error under
`turbopackPluginRuntimeStrategy: 'workerThreads'`:

- baseline native binary: fails because the orphan worker remains alive
and
  writes the marker;
- candidate native binary: passes because the worker is actually
terminated;
- candidate then renders a recovered value, proving replacement/recovery
works.

## Validation

- `pnpm build-all`
- `cargo fmt --all -- --check`
- `cargo check -p turbopack-node --all-targets`
- `cargo test -p turbopack-node --lib`
- `cargo clippy -p turbopack-node --all-targets -- -D warnings`
- focused Prettier and ESLint checks for the new test
- candidate integration test pass; exact baseline integration test fails
at the
  intended liveness assertion
- GPT-5.6 Sol xhigh + Claude Opus 5 xhigh autoreview panel: zero
actionable
  findings, patch correct at 0.96 confidence

Exact optimized native SHA-256 values used by the benchmark:

- baseline:
`609eeeed41e1425f06136b54b7997e5b1b1add992169b17a797889dfb830dd7f`
- candidate:
`141211112ae47b8bd814aaab437060114e92f60a1bf6f23e1b0e153b1e4837cc`

Both stripped binaries were 205,863,784 bytes. Raw per-process JSON,
`/proc`
samples, server logs, harness source, analysis output, and A/B plus
endurance
graphs are retained in the investigation archive.

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-08-04 13:29:48 -07:00
Will Binns-Smith 466cdfaa27 Turbopack server hmr: avoid complete clear on graph changes (#95546)
Currently, importing a new module causes a complete eviction (`clear()`)
and re-evaluation of server chunks, both in the Turbopack runtime's
module cache and Node's `require.cache`.

Here's what happens today:

- A new module is imported into a chunk's graph
- This changes its `availability_info`, which in dev for non-entry
chunks is encoded into the chunk's filepath
- `VersionedContentMap` works entirely on chunk paths. When we construct
instructions to transition a chunk into its new state, we fail to find
the prior state and fall back to `clear()` as described above.

An ideal version of this is a refactor that implements
`VersionedContentMap` on a per-module basis, not a per-chunk one. This
PR doesn't do that, but achieves consistent module-level updates when a
chunk's availability info changes.

It does this by implementing chunk lists for server entry chunks the
same way the client hmr implementation does: an entry chunk's version
aggregates the versions of its dependent chunks (including dynamically
imported ones), keyed by merger rather than path. Entry chunks do not
encode availability info in their paths, so they are insulated from
missing versions. Updates are applied in Node.js through the same shared
merged-update machinery in the unified hmr runtime that the client
already uses, plus a new `ChunkListUpdate` branch in the server hmr
client to unwrap the merged updates.

Details:

- The merged-update wire format (`EcmascriptMergedUpdate` etc.) moves
from turbopack-browser into shared `turbopack_ecmascript::chunk_list`,
along with runtime-agnostic `ChunkListVersion` and `update_chunk_list`
implementations.
- `turbopack-nodejs` gains a chunk content merger mirroring the browser
one. `EcmascriptBuildNodeEntryChunk`'s versioned content is now a
chunk-list content over its sync and async chunks. It is version-only
and never emitted; the entry chunk still inlines its loader calls.
- The aggregate server hmr subscription now tracks only entry chunks.
Shared chunks under `server/chunks/` ride the entry's `ChunkListUpdate`
as module deltas, so their content-hash paths no longer matter.
- The Node hmr client applies `ChunkListUpdate` by feeding each nested
merged update through the shared apply path. Modules that appear in an
"added" chunk but already exist in the module cache were moved by a
chunk rename and are treated as modified.
- On a successful partial apply, the hot reloader clears the manifest
cache for updated chunks and notifies browsers to refetch RSC, without
clearing `require.cache`.

### Test Plan

Adds a series of e2e tests to catch the described manual test plan in
both entry and dynamic chunks.
2026-07-28 13:49:30 -07:00
David Alexandru Ilie 925edc3dd6 Classify Instant Insights as internal in Request Insights (#96147)
## Summary

- Classifies Instant Insights records as internal and hides them by
default.
- Adds a persisted “Internal activity” toggle.
- Nests internal records below their matching foreground request.
- Preserves orphaned internal records exactly once.
- Signals hidden internal errors without littering the main request
list.
- Raw endpoint and CLI data remain complete; filtering is UI-only.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-28 14:15:21 -04:00
David Alexandru Ilie c294cb7539 Persist Request Insights display settings (#96277)
## Summary

- Replaces the per-request verbose checkbox with a global Request
Insights settings menu.
- Persists `requestInsights.verbose` through the existing DevTools
configuration path.
- Restores the setting across reloads.
- Does not include the unrelated global `pagehide` flush behavior.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-28 14:15:21 -04:00
Tim Neutkens efcbb27450 Add Instant Insights pipeline spans (#95961)
## Summary

- Adds a detached `Instant Insights` root span.
- Surfaces `Prepare validation inputs` and `Run validation` as child
phases.
- Covers both the validation worker and Webpack’s in-process path.
- Stores these spans separately from the foreground request.
- Preserves scheduling, cancellation, and concurrent-document behavior.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-turbo test/development/app-dir/instant-validation-scheduling/instant-validation-scheduling.test.ts
```

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: David Ilie <david@davidilie.com>
2026-07-28 14:15:21 -04:00
Andrew Clark 3de2d1a213 Unify allow-runtime with Partial Prefetching (#96106)
Removes the "allow-runtime" prefetch config, and turns its behavior on
implicitly wherever Partial Prefetching is enabled.

The original motivation for "allow-runtime" was to give apps more
control over server costs triggered by prefetches. Until a route
explicitly opts in, prefetches would only be served from the CDN, not
from the server. The problem, though, was it was very confusing to know
when to add or remove this configuration. The incentive for many apps
was to add it everywhere, with no clear signal for when to remove it.

Our updated thinking is that Partial Prefetching itself already provides
sufficient protection against runaway prefetching costs: per-link
prefetches only happen on Link components that explicitly opt in with
the prefetch prop.

The optimizations landed earlier in this stack also make allow-runtime
less necessary: on pages where all the content is statically renderable,
prefetches are served from the static cache and no runtime request is
ever issued; only a page that accesses non-static data is prefetched at
runtime.

The upshot of this decision is that runtime versus static becomes an
internal optimization; the same content gets prefetched regardless of
whether or how Next.js is able to optimize it.
2026-07-28 11:51:29 -04:00
David Alexandru Ilie a619237b1c Use a safe clock for Request Insights bookkeeping (#96274)
Draft replacement for #96267 after restructuring the Request Insights
stack.

## Summary

- Request Insights bookkeeping used `Date.now()`, which Cache Components
patches to detect unstable application wall-clock reads.
- Framework bookkeeping could therefore be incorrectly attributed to
user code.
- Implicit timestamps now use `performance.timeOrigin +
performance.now()`.
- Explicit OpenTelemetry timestamp inputs remain unchanged.
- Tests distinguish framework bookkeeping from a genuine application
`Date.now()` call.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/server/lib/trace/local-span-recorder.test.ts packages/next/src/server/lib/trace/span-store.test.ts packages/next/src/server/lib/trace/request-insights.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-27 18:58:54 -04:00
Tim Neutkens 31b78bbed9 Add Instant Insights request timing (#95958)
## Summary

- record deferred Instant Insights work as a separate typed Request
Insights item
- keep validation spans, fetches, timing, and live updates separate from
the foreground request
- label Instant Insights consistently in DevTools and CLI output
- stack this observability change on #95939, which moves validation off
the navigation response path

## Verification

- `pnpm jest --runInBand
packages/next/src/server/lib/trace/request-insights.test.ts
packages/next/src/server/lib/trace/local-span-recorder.test.ts
packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
packages/next/src/next-devtools/dev-overlay/shared.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/development/app-dir/request-insights/request-insights.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-webpack
test/development/app-dir/request-insights/request-insights.test.ts`
- `pnpm --filter=next types`

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: David Ilie <david@davidilie.com>
2026-07-27 07:27:19 -04:00
dan b51ca60fd4 Fix dev overlay symbolication for project paths needing percent-encoding (#96221)
In a project whose absolute path contains characters that percent-encode
in URLs — e.g. a space — the dev overlay showed no code frame and raw
`file://` URLs instead of source locations for server frames.

Two encoding mismatches caused this:

- Turbopack's `trace_source` percent-decoded the source map's original
file URL before comparing it against the still-encoded project root URI,
so the containment check failed ("Original file ... outside project")
for any project path with such characters. The comparisons now stay in
the encoded domain and only the outputs are decoded back into paths.

- React synthesizes stack frame `file:` URLs by prepending the scheme to
a filesystem path, so the overlay receives URLs with e.g. raw spaces.
These aren't well-formed, and they don't match the `url.pathToFileURL`
form Node.js keys its source map cache by. The overlay middleware now
re-encodes them through WHATWG URL parsing, which tolerates such input.

## Test Plan

New tests.

Known cases that remain broken are commented out with a TODO.
These could be resolved if https://github.com/react/react/pull/37105 or
equivalent lands.

## Before

<img width="825" height="732" alt="Screenshot 2026-07-25 at 14 39 18"
src="https://github.com/user-attachments/assets/0f119e52-ec86-495a-bf5c-5339faa1d606"
/>


## After

<img width="1126" height="733" alt="Screenshot 2026-07-25 at 14 58 14"
src="https://github.com/user-attachments/assets/a263ee8a-4cfe-4358-9a8e-2e58b4b16aa5"
/>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:36:45 +01: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
Hendrik Liebau 3c4afc1a3a Run Cache Components dev validation on a worker thread (#96153)
This moves the dev-mode Cache Components validation renders off the dev
server's main thread onto a worker thread, so rapid navigation no longer
starves the event loop. The worker crosses into the app-page bundle
exactly once, calling a new
`ComponentMod.routeModule.runValidationInDev` entry that rebuilds the
render context, work store, and request store from a serializable
snapshot and runs the whole validation there, so the client prerender
and the user's client components resolve the single app-page React
instance rather than a second copy.

A thin worker shell (`dev-validation-worker.ts`) plus a single-worker
pool (`dev-validation-worker-pool.ts`) load the user bundle via
`loadComponents`, install code-frame support for CLI output, and forward
the validation errors back as Flight bytes; the main thread only
delivers them to the overlay through `sendErrorsToBrowser`. The
snapshot, the globalThis-symbol handoff, and the shared error-delivery
helpers live in their own modules. The dev server installs the worker
when `experimental.devValidationWorker` is not `false`, and
`runDevValidationInBackground` uses it when present, falling back to the
in-process path otherwise. A one-slot `SharedArrayBuffer` propagates a
supersede abort into the worker so a newer navigation cancels an
in-flight validation.

The runtime bundle gains an `app-worker` entry for the worker with the
`build/swc` boundary externalized for the code-frame native binding, and
`patch-error-inspect.ts` now backs its code-frame renderer with a
globalThis symbol so all copies of the module share it across the
thread.

The synthetic `bench/dev-validation` benchmark navigates back-to-back,
so every click lands inside the validation window — the worst case for
main-thread contention. Browser-observed navigation TTFB in that window,
worker vs in-process:

| Route  | Worker (p50/p95/max) | In-process (p50/p95/max) |
| ------ | -------------------- | ------------------------ |
| client | 19 / 24 / 27 ms      | 40 / 66 / 7762 ms        |
| server | 42 / 45 / 46 ms      | 122 / 158 / 252 ms       |
| sprite | 109 / 117 / 169 ms   | 196 / 208 / 299 ms       |

The steady-state difference is only tens of milliseconds; the effect
that matters is the tail. In-process, a navigation that collides with an
in-flight validation render can stall for seconds (this run peaked at
~7.8s on the client route, and the peak varies run to run) because a
staged render does not yield until it finishes; off-thread the main
thread stays free and that stall disappears.

Read these as an upper bound, not a speedup that generalizes. The work
moved off-thread is the validation render's CPU — bounded,
route-dependent, and free of IO — so it does not grow with the main
render's cost. In an app whose main render is dominated by IO the same
absolute saving is a small fraction of the request, and it only appears
when a navigation lands in the brief validation window; at ordinary
click speed it is largely invisible. This is a dev-only responsiveness
improvement whose benefit varies widely with the app and the navigation
pattern.

As a follow-up, the validation work that still runs on the main thread
could move to the worker as well. When the main render can't be reused
for validation (for example after a cache miss), the validation
re-renders on the main thread to produce its inputs, resuming from the
Resume Data Cache (RDC) the main render already filled, and only then
hands the resulting Flight chunks to the worker. A later iteration could
run those renders inside the worker too, transporting the (serializable)
RDC so the worker can resume from the filled caches rather than reading
them back on the main thread.

closes NAR-895
2026-07-25 07:21:03 +02:00
Hendrik Liebau 62084e3fbd [test] Unflake the enabled-features-trace test suite (#96175)
<img width="428" height="179" alt="Screenshot 2026-07-24 at 22 42 04"
src="https://github.com/user-attachments/assets/38478940-d23e-4faa-bd88-5758190be158"
/>

[Flakiness
metrics](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22enabled%20features%20in%20trace%20should%20denormalize%20inherited%20enabled%20features%20during%20upload%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%28%22fail%22%20OR%20pass%29&agg_m=count&agg_m_source=base&agg_t=count&fromUser=true&index=citest&start=1784320894753&end=1784925694753&paused=false)

The `render-path` span is recorded when a request's response closes,
which is too late for any flush other than the one the dev server
performs while shutting down. The parent `next dev` process escalates to
SIGKILL 100ms after signalling the child, and on a machine running eight
test files at once the child does not reliably get scheduled to run its
cleanup within that window, so the span never reached the trace file and
the upload assertions failed. This change raises the budget for the test
through `NEXT_EXIT_TIMEOUT_MS`, which was added alongside that timeout
in #67165 so that it can be increased when the child's exit work matters
more than a fast exit. The same approach is already used in
`test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`, where the
timeout would otherwise cut off a Rust `on_exit` handler before it
writes its task statistics.

Both test cases previously guarded their request with a check for the
existence of the trace file, which the dev server creates on its own
once the first compile finishes. When that happened before the first
test body ran, neither case issued a request and the trace file
contained no `compile-path` or `render-path` span at all. The request
and the shutdown now happen once in `beforeAll`, and the fixed 500ms
sleep that followed the shutdown is replaced by a `retry` that waits for
the spans the assertions depend on.


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-25 07:21:02 +02:00