Commit Graph

1294 Commits

Author SHA1 Message Date
Sebastian "Sebbie" Silbermann 662b0355eb [test] Resolve workspace:* packages to the version under test (#98330)
Test fixtures that depend on monorepo packages (for example
`@next/third-parties` and `@next/mdx`) declared them at the `canary`
dist-tag, which installs the published npm canary instead of the build
under test. In deploy tests this also broke the remote install entirely:
the published canary's `peerDependencies` ranges (for example
`^16.0.0-beta.0`) reject the prerelease preview versions that
`NEXT_TEST_VERSION` installs for `next` (for example
`16.4.0-preview-<sha>-<date>`), so `npm install` failed with ERESOLVE.

Test dependencies can now be declared as `workspace:*`, which resolves
to the build from the current checkout in every test mode:

- dev/start: the locally packed tarball from `pack-for-isolated-tests`,
with a descriptive error when the package has no pack task or is not
part of the repository.
- deploy: the preview tarball of the tested commit when
`NEXT_TEST_VERSION` points at a preview build (preview tarballs already
rewrite their monorepo peer dependencies to the same preview URLs, so
peer resolution succeeds), falling back to the version in the worktree
otherwise. Private packages without published preview tarballs fail with
a descriptive error.

All existing tests that used `canary` for monorepo packages are migrated
to `workspace:*`.
2026-09-08 18:17:29 +02:00
Tobias Koppers e9c16094f2 Fix flaky successive HMR changes test (#97610)
### What?

Correct the fixture marker used to construct the second invalid source
state in the successive App Router HMR test.

### Why?

The test was replacing a marker that does not exist in its MDX fixture.
Because string replacement silently returned the original content,
supposedly successive patches sometimes wrote byte-identical source
while the test harness waited for an HMR completion callback.

A no-op write does not have to produce an HMR update, so the callback
could remain pending until the harness reported a misleading timeout. CI
logs showed Fast Refresh itself completing quickly, which ruled out an
insufficient timeout budget and made extending the timeout the wrong
fix.

### How?

Match the replacement to the fixture's actual marker. This restores four
genuinely distinct source states in every error/recovery cycle,
preserving the intended overlay coverage without changing Turbopack, the
shared HMR helper, or its timeout.

### Verification

- `pnpm test-dev-turbo
test/development/acceptance-app/app-hmr-changes.test.ts`
- Repeated the focused test three times under 14 CPU-contention workers
- Prettier and ESLint on the changed test file

<!-- NEXT_JS_LLM -->

---------

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-07 15:27:50 +02: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
Will Binns-Smith 724641c6a2 Fix Turbopack HMR recovery after dropped connections (#97966)
### What?

Track the latest applied Turbopack HMR 'hash' in the browser runtime and
include it when subscriptions are restored after reconnecting. Reload
the page when the client's hash differs from the server's current hash.

Add an App Router development test that interrupts HMR traffic, misses
an update, restores the connection, and verifies the page reloads into
the current revision.

### Why?

A browser that temporarily loses its HMR connection can miss updates and
remain out of sync after reconnecting.

### How?

Attach the current HMR hash to Turbopack connection and update messages.
The runtime records the last hash it processed and sends it with each
subscription. The development server compares that value against its
current hash and requests a full reload on mismatch.

<!-- NEXT_JS_LLM -->
2026-08-31 09:06:58 -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
Hendrik Liebau dae438dc0d Stop printing a stack frame for error message text (#97829)
Next.js printed a stack frame that does not exist for an error whose
message ends with text that looks like a file location. A logged
connection failure read `at <unknown> (Error: connect ECONNREFUSED
::1:45999)` in front of its real frames.

`stacktrace-parser` tries five patterns on every line, and the pattern
for JavaScriptCore makes the `<name>@` prefix optional. That pattern
`:<line>:<column>`, and a line of an error message can end that way. A
JavaScriptCore frame always holds `@`, also at the top level, so the
prefix is not optional in a real stack.

The patch for `stacktrace-parser` now requires that prefix. A line of a
message no longer becomes a frame, and every JavaScriptCore frame still
parses, including one whose path holds a space.

A Turbopack build error puts the file and the position of the error in
its message, and the parser read that line as a frame as well. That
frame is gone now, and the location stays in the message. The snapshots
of the build errors record the change.
2026-08-25 13:38:47 +02:00
Tobias Koppers 76e24273c7 test: stabilize read-only page recreation (#97674)
### What?

Stabilizes the read-only source HMR page deletion and recreation test in
webpack development mode.

### Why?

The test reloads after restoring a deleted page because webpack does not
automatically refresh when the route reappears. In polling watch mode,
that reload could wait for the full page load or be aborted by a
concurrent Fast Refresh navigation. Either error escaped the retry
callback, allowing one transient navigation race to fail the test before
it could retry.

### How?

The webpack-only reload now waits only until the document is available
instead of waiting on potentially slow subresources. Transient reload
failures are handled inside the retry callback, while the existing
page-content assertion remains the success condition. This preserves the
test's intent and ensures a route that does not recover still fails.

### Verification

- Fresh `pnpm build` followed by a cold headless webpack run: 3 passed
- Three additional headless webpack runs: 3 passed each
- Headless Turbopack run: 2 passed, 1 expected skip
- Focused Prettier and ESLint checks

<!-- NEXT_JS_LLM -->

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-08-25 10:25:07 +02: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
Tobias Koppers c0e749f609 test: stabilize poisoned proxy error overlay (#97609)
### What?

Stabilizes the RSC poisoned-import error-overlay test for `proxy.js`
without weakening or retrying its redbox assertion.

### Why?

An initially broken proxy can emit the expected build error and then
force a startup full reload. That reload can clear the overlay before
the test begins observing it, causing an intermittent `Expected Redbox
but found no visible one` failure even though validation worked.

### How?

The proxy parameter now starts from a valid module and introduces the
poisoned import only after the sandbox page has hydrated. This moves the
assertion onto the live HMR path and removes the startup-reload race.
Middleware and instrumentation retain their initial-compile setup
because their error overlays are reliable there and instrumentation does
not reliably surface this edit through Turbopack HMR.

### Verification

- `pnpm test-dev-webpack
test/development/acceptance-app/rsc-build-errors-poisoned-imports.test.ts`
- `pnpm test-dev-turbo
test/development/acceptance-app/rsc-build-errors-poisoned-imports.test.ts`
- Proxy case repeated 5 times with Webpack and 3 times with Turbopack
- Prettier, ESLint, and `git diff --check`

<!-- NEXT_JS_LLM -->

---------

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-08-21 14:53: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
zoomdong d820350579 feat(turbopack): isolate HMR listeners across microfrontends (#95997)
## Summary

When two Next.js microfrontend child applications run in development
mode at the same time, their HMR clients conflict because they share the
same global chunk-update listener registry.

Since turbopack added support for the chunkloadingglobal configuration,
I think this configuration can also consume HMR's global object
simultaneously: PR: https://github.com/vercel/next.js/pull/88790 and
https://github.com/vercel/next.js/pull/93488

This change scopes the listener registry to each runtime chunk-loading
global so the applications can receive HMR updates independently.

## Test

Update snapshot test case
2026-08-21 11:19:16 +02:00
Benjamin Woodruff ‮ ee6909213f Turbopack: Show last modified file when waiting for the filesystem to settle (#97648)
This is a follow-up to https://github.com/vercel/next.js/pull/96116

If we're waiting a long time for your filesystem to settle, we should
show you the last modified path, so at least you can have an idea of why
this is happening.
2026-08-20 16:59:56 -07: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
Will Binns-Smith e2fb664ceb Remove HmrTarget (#97253)
With #94948 we intended to move the client over to the firehose feed of
HMR events with the intent of unifying the code paths for maintenance.
However, now that Server HMR is moving to a pull-based model (which
client HMR will not be able to implement), let's keep the split.

There's no need to encode the HmrTarget into each surface, and we can
just use the function name to indicate which mode of HMR it's for.
2026-08-19 17:22:47 -07: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
Benjamin Woodruff ‮ b677feb02f Turbopack: More aggressively debounce filesystem watch events if we detected changes to node_modules (#96116)
Previously, we were debouncing update by sleeping 1ms at a time on macos
and windows, and 10ms at a time on Linux.

During a slow `pnpm install`, or a `git checkout`, this could cause us
to do a bunch of extra throwaway work.

Changes:
- Increase the debounce interval to a consistent 10ms everywhere. This
should still be small enough that it's not noticable on macos or
windows.
- If an event touches `node_modules`, there's a good chance that a
package manager is running and many other files will be modified, so
extend the batch deadline by 200ms instead of 10ms.
- Because there's a chance that the batch deadline could get extended
indefinitely (this was always possible, just more likely now) include a
compilation event that gets logged after 5 seconds.
2026-08-18 17:05:14 -07:00
Hendrik Liebau b18acf6712 Remove the development debug channel persistence (#97510)
Documents are now served with `no-store` in development, so a browser
never restores one from its HTTP cache and the page scripts never
re-execute against a debug channel that has already delivered its data.
The persistence and restore machinery that existed for that case has no
remaining trigger, so this removes it: the `IndexedDB` write scheduled
on every page load, the cache-restore detection across
`PerformanceNavigationTiming` fields and `deliveryType`, the `pageshow`
deferral for browsers that populate those fields late, and the
`location.reload()` fallback for a missing entry. It was built up over
#92892, #93486, #94128, #94317 and #94243, and takes `debug-channel.ts`
from 535 lines to 121.

The per-consumer `tee()` and the LRU-bounded pair map stay. They were
added for an unrelated reason, namely that one response can be decoded
more than once, so this is not a revert to the state before the
persistence landed. The rejection handler on `writer.closed` also stays,
because an errored stream would otherwise surface as an unhandled
rejection now that nothing else observes it.

`bfcache-regression` keeps the original regression test, which loads a
page, navigates away, comes back and asserts that the counter is still
interactive. That case now fails if the development `Cache-Control`
value ever goes back to `no-cache`, because the restored document would
block hydration with no reload to recover, so it is worth keeping as is.
The other three tests lose their premise and are deleted along with the
routes only they used: the pruning case that was skipped when the header
changed, the recovery case that needs a restore path to recover into,
and the streaming case that guarded the detection against treating an
in-flight response as a restore. The `large-debug-data` route goes too.
It existed only to make the persistence write expensive enough to
profile by hand when it moved to `IndexedDB`.
2026-08-18 16:54:46 +02:00
Hendrik Liebau 2839982a03 Stop the browser from restoring stale pages in development (#97505)
Development responses used `no-store, must-revalidate` until #88182
tried `no-cache, must-revalidate` behind
`experimental.devCacheControlNoCache`, and #91503 removed that option
and hard-coded the `no-cache` value everywhere. That was right for
static assets and wrong for documents. A browser may reuse a stored
response for a history navigation without revalidating it, and
development documents are streamed without an `ETag`, so there is
nothing to revalidate against. Going back therefore restored the
document the browser had stored earlier and showed output from before
the latest edit, and it is also what forced the debug channel
persistence workarounds in #92892, #93486 and #94243.

Documents and RSC or data responses now use `no-store` again, set in
`app-page-runtime.ts` for app pages, in `pages-handler.ts` for pages,
and in the legacy render pipe in `base-server.ts` so that the three do
not drift apart. None of them ever serves a static asset, so assets keep
`no-cache, must-revalidate` from the `nextStaticFolder` branch in
`router-server.ts` and stay cacheable: they are revalidated against the
`ETag` that `serveStatic` adds and reused from a `304` instead of being
downloaded again on every page load. `must-revalidate` is left off the
document value, because it only governs reuse of an already stale stored
response and nothing is stored any more.

A back navigation is no longer instant, since the document is fetched
again instead of being restored locally.
`test/development/dev-cache-control` covers both sides of that
trade-off: an edit that is visible after a back navigation, and
unchanged assets that still come back as `304`. It replaces
`dev-cache-control-no-cache` and asserts the header for both routers as
well, so there is one suite instead of two with nearly the same name.

A development document is now never restored from the HTTP cache, so the
debug channel persistence has no remaining trigger and its `IndexedDB`
write on every page load is no longer needed. Removing it is a follow-up
on top of this change. The pruning and recovery test in
`bfcache-regression` is the one case whose premise disappears entirely,
and it is skipped here with a note to delete it along with the
persistence.

closes #96503
2026-08-18 16:54:45 +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 b5538511ed test: update React 18 redbox snapshot (#97415) 2026-08-15 15:11:41 +02: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
Sebastian "Sebbie" Silbermann 17f6f135e2 [fragment-scroll] Rename ScrollAndMaybeFocusHandler to ScrollHandler (#96828)
As of https://github.com/vercel/next.js/pull/95602, the old
scroll-and-focus handler doesn't exist anymore
As of https://github.com/vercel/next.js/pull/96113, the new handler no
longer handles focus.

This updates the relevant variable names to reflect that we no longer
handle focus.
2026-08-10 13:26:51 +02: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
Tobias Koppers da8fc4fea3 fix(turbopack): point at the glob that matched a file with no module type (#96561)
A build error for a glob-matched file with no module type named only the
file,
with no route, call site or import trace — the file never becomes a
module, so
it has no place in the module graph. `import.meta.glob` now also reports
an
error at the call site with the pattern and matched key. Also fixed the
stale
`Read more` link (308s to a page whose anchor changed).

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-10 08:58:33 +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
Sebastian "Sebbie" Silbermann 2c04735944 [fragment-scroll] Remove config.experimental.appNewScrollHandler (#95602) 2026-08-06 09:53:09 +00: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
Tim Neutkens cbf0cef687 Enable TypeScript CLI by default (#96497)
## Summary

Enable the project-local TypeScript CLI checker by default during `next
build`, while preserving `experimental.useTypeScriptCli: false` as an
opt-out to the TypeScript compiler API. Update the TypeScript 7
guidance, diagnostics, tests, and documentation to match the new
default.

## Verification

- `pnpm exec jest --runTestsByPath test/unit/isolated/config.test.ts`
- Pre-commit lint-staged checks passed (Prettier and ESLint)
- Not run: `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts` (isolated
fixture dependency installation was blocked by unavailable npm registry
access)
- Not run: `pnpm --filter=next types` (repository has pre-existing
unrelated TypeScript errors)

<!-- NEXT_JS_LLM -->
2026-08-03 18:10:51 +02:00
Sebastian "Sebbie" Silbermann 7612eaeda1 Consolidate unhandled rejection logging into a single listener (#95999)
An unhandled rejection was previously logged by up to three independent
process listeners at once: the render runtime's crash-prevention handler
in `process-error-handlers.ts` (a bare `console.error`), the router
server's `Log.error('unhandledRejection: ', err)`, and the dev server's
`logErrorWithOriginalStack`. The runtime handler must exist on every
deployment target (#77997), but on self-hosted `next start`/`next dev`
it shares a process with the router server's and dev server's listeners,
so a single rejection was logged multiple times in different formats.

THe first commits adds a test showing the current behavior where we log
multiple times.

The second commit introduces `registerUnhandledRejectionListener` and
`isUnhandledRejectionListenerRegistered` in `process-error-handlers.ts`,
and converts the router server and dev server to check-then-register
instead of installing their own rejection loggers:

- The listener function is shared via a `Symbol.for` key on
`globalThis`, so multiple copies of the module (e.g. in the pre-compiled
server bundle and in a route module bundle) register and detect a single
listener instance.
- The registration check queries
`process.listeners('unhandledRejection')` instead of a module-global
flag, so it stays accurate even after external code calls
`process.removeAllListeners`. `installProcessErrorHandlers` therefore
calls the register function unconditionally.

The `uncaughtException` handlers are left as they are; they have the
same duplication and could be consolidated the same way in a follow-up.
2026-07-31 11:36:40 -04: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