Commit Graph

9714 Commits

Author SHA1 Message Date
Janka Uryga db66a5094d Create next.config.ts when generating apps with pnpm new-test (#98364)
Updates the template used by `pnpm new-test` for app-dir tests to create
a `next.config.ts` instead of `next.config.js`.
2026-09-08 18:38:36 +00:00
Hendrik Liebau c11c0942f2 [test] Run deploy tests against local JavaScript changes by default (#98353)
Deploy tests now use the checkout's built JavaScript packages when
`NEXT_TEST_VERSION` is unset. The harness reuses the isolated-test
packer, includes the tarballs in the deployment source, and installs
them through relative `file:` references.

The previous published-build workflow remains available through
`NEXT_TEST_VERSION`. For example, `NEXT_TEST_VERSION=canary` skips local
packing and tests the published canary. Exact versions and CI preview
tarball URLs remain supported. Existing CI deploy jobs already set
`NEXT_TEST_VERSION`, so their package selection does not change. The
harness skips local package preparation when it reuses an existing
deployment or invokes a custom deploy script, preserving those
workflows.

Staged monorepo peer dependencies use matching local versions so npm
accepts prerelease builds without conflicting direct-package overrides.
Fixture overrides retain their nested rules, including qualified
identity overrides with or without an explicit `.` entry. Configuration
updates preserve linked source files.

Deployments of local JavaScript packages use published SWC binaries
matching the local Next.js version. They do not include locally built
native binaries because those may target a different platform.

**Verification**

```
VERCEL_TURBOPACK_TEST_TEAM=vtest314-next-turbo-e2e-tests \
  pnpm test-deploy-turbo test/e2e/app-dir/third-parties/basic.test.ts
```

```
VERCEL_TEST_TEAM=vtest314-next-turbo-e2e-tests \
  pnpm test-deploy-webpack test/e2e/app-dir/third-parties/basic.test.ts
```

Both deploy modes passed. Additional temporary checks, removed
afterward, verified that:

- Deployments install the local JavaScript packages and use matching
local versions for monorepo peers.
- Qualified npm overrides preserve a nested `sharp@0.34.5` pin, both
with and without an explicit `.` entry. The check resolved `sharp` from
Next's own dependency context.
- Archive preparation leaves checkout manifests and tarballs unchanged.
Repacked archives preserve other package files, permissions, links, and
external peer declarations.
2026-09-08 18:49:13 +02:00
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
Janka Uryga b0b7a429b9 [PPF] Fix missing runtime-follow up if hints are stale (#98278)
if a page didn't use runtime data during build but started doing so
after the revalidation (i.e. the hints say its statically-prefetchable
but now it's not), we're supposed to be doing a follow-up runtime
request based on the `needsRuntimeRequest` promise (introduced in
#96095), but this was not actually happening.

the bug goes as follows:
1. if we get an entry from a static prefetch, but `needsRuntimeRequest`
is true, we save it as `PPR` (or `StaticShell` if it's a shell rewound
from the static response). otherwise, we'd have saved it as `PPRRuntime`
(or `RuntimeShell` for shells) to indicate that it satisfies those
requests.
2. `wouldRuntimeRequestProvideMore` compares the runtime strategy for
the current static walk to the entry's strategy to check if it should do
a runtime request. this boils down to a comparison of `RuntimeShell >
PPR` for a shell request
3. however, in the `FetchStrategy` enum, `RuntimeShell = 2` and `PPR =
3`, so `RuntimeShell > PPR` is false, which makes
`wouldRuntimeRequestProvideMore` return `false`. so, if we have a `PPR`
entry, **we'll never do a follow-up runtime shell request**.

This is fixed by swapping the order of the two values in the enum.

Now, if we end up with a `PPR` response where `needsRuntimeRequest` is
true due to runtime data, that indicates that we *should've done a
runtime request in the first place*, we just didn't know we should do so
because the prefetch hint was stale, so the new ordering achieves the
correct behavior, i.e. `pingSegmentBundle` decides that it needs a
runtime shell request, which makes it match the intended PPF prefetching
behavior.

The enum ordering is a bit of a lie, because there is no single correct
ordering -- a `PPR` entry *might actually provide more content* than a
`ShellRuntime` entry if the route uses static params. In practice this
is resolved by the vary path (shell responses are keyed with fallback
params, and a PPR entry with static params would have the actual param
values, and the param-ful entry would end up being preferred) so the
strategy order wouldn't matter.

Now that `RuntimeShell` beats `PPR`, i had to disable the strategy
upgrading (`StaticShell -> RuntimeShell` and `PPR -> PPRRuntime`) on
routes that don't use Partial Prefetching -- otherwise, a
rewound`ShellRuntime` entry from a different param was winning over
`PPR` in `pingSegmentBundle`, so it was preventing issuing new static
requests even if we needed to do them. However, without PPF, the runtime
tiers are not meaningful anyway, so this should be fine.

### Other bugs

This fix also revealed a bug where `FetchStrategy.Full` requests were
writing a shell as `RuntimeShell` even though the byte offset only marks
a *static* shell. I've refactored the logic for picking the strategies
to fix this and make it more explicit.

### Edge cases

there is one (somewhat involved) edge case to this that i know of.
if the post-revalidation static response has:

1. a shell that didn't access runtime data (i.e. `needsRuntimeRequest =
false` in the shell stage)
2. a prefetch that did access runtime data (i.e. `needsRuntimeRequest =
true` in the static stage)

then we'll:

1. write the shell as `ShellRuntime` (because `needsRuntimeRequest =
false`, and a runtime shell would not in fact provide more data)
2. write the prefetch as `PPR` (and not `PPRRuntime`, because
`needsRuntimeRequest = true`, and a runtime prefetch *would* provide
more data)

with the new ordering, this means we'll end up preferring the
`ShellRuntime` entry over the `PPR` entry even though the shell is a
strict subset of it and would be missing static params and content gated
behind `prefetch()` or `navigation()`. (note that this only happens if
they share the same vary path)

this can be observed as follows:
1. when a `<Link prefetch={true}>` to such a page is revealed, we won't
request a runtime shell (due to already having a `ShellRuntime` entry),
but we will request a runtime prefetch (because `PPR` is not enough). so
far this is correct
2. if the navigation happens **before** this runtime prefetch completes,
we will display what we already have. but we'll prefer the rewound
`ShellRuntime` shell entry over than the `PPR` entry it originated from
because of the enum ordering. so we may end up not showing content
despite having it in the cache

i'm not sure how to resolve this yet, but i think the current
`recordedFetchStrategy` trick can't handle this, and we need a more
involved mechanism that can detect when the `PPR` entry is actually
better. i don't consider this a blocker for this fix though
2026-09-08 16:22:18 +02:00
Janka Uryga f4449e8fa5 Instant validation for caches excluded from prerenders (#98342)
Follow-up to #98339. Turns out we were not testing how caches excluded
from static/runtime prerenders and app shells behave in Instant
Validation, and it was somewhat broken. This PR adds test coverage and
fixes some bugs I found along the way.

### Fixes

- `use-cache-wrapper` was incorrectly gating cache delays in "request"
stores on `NODE_ENV === "development"`, which does not include
build-time instant validation. The correct check is now implemented in
`isValidationRender`
- `use-cache-wrapper` has divergent behavior for caches with `stale <
MIN_SHELL_STALE` across app shells and PPR/static shells, which needs to
be tracked so that we know that the same render can't be used for both
Instant and Static Shell validation. When we see a cache entry like
that, we now call `trackIncompatibleShellContent()`
- A render that had a cache miss could still report that it's compatible
with both SSV and IV, because we only do the above for a cache hit. As a
result so we'd incorrectly reuse `LAZY_FULL_RENDER` for both. Cache
misses now result in a `trackIncompatibleShellContent()` call to avoid
this

### Tests

We now have tests for:
- `stale < MIN_SHELL_STALE` - excluded from app shells, but included in
static and runtime prefetches
- `stale < MIN_PREFETCHABLE_STALE` - excluded from all prerenders
- `expire < MIN_PRERENDERABLE_EXPIRE` - excluded from static prerenders,
but allowed in runtime prerenders

Due to bugs mentioned above, some of the added tests were failing before
the fixes (mostly the ones that expect an error -- passing a "no
validation errors" test is easy, just don't create any dynamic holes)

In build, these tests incorrectly reported no errors when they should've
failed IV:

- `invalid - unguarded non-prefetchable cache (with short stale)` (both
PPF and non-PPF)
- `non-app shell validation > invalid - unguarded non-prerenderable
cache with short expire` in build:
- `app shell validation > invalid - unguarded cache with a
shorter-than-shell staleTime` in build:

This is because we were missing cache delays in build-time instant
validation (now fixed with `isValidationRender`), so the caches weren't
dynamic holes at all.

The `stale < MIN_SHELL_STALE` tests (`app shell validation > invalid -
unguarded cache with a shorter-than-shell staleTime`) were also failing
in dev:
- **{initial load, client navigation} with cold caches**: Should be an
IV error, but is an SSV error. The initial render had cache misses, so
we did a warm-cache full rerender with runtime shells, which resolved
`await nonShellCache()` in the Runtime stage. But the initial render
**did not track incompatible data**, so we incorrectly re-used it for
SSV and IV. The cache was resolved in `Runtime` so SSV saw a runtime
hole and `await nonShellCache()` errored in SSV with `Next.js
encountered runtime data during prerendering.`
- **client navigation with warm caches**: same as above, except there
weren't cache misses, so incorrectly reused the *original* runtime-shell
render for SSV and IV
- **initial load with warm caches**: No redbox when IV should've
errored. The main render was an initial load and did not use runtime
shells, so it resolved `await nonShellCache()` in `PrefetchStatic`. We
had no cache misses and **did not track incompatible data**, so we
incorrectly re-used the main non-runtime-shell render for SSV and IV.
The cache resolved in `PrefetchStatic` and it wasn't a hole in
`ShellRuntime` so no error was reported.
2026-09-08 16:22:18 +02:00
Janka Uryga e974969ddd [PPF] Model caches with stale < MIN_SHELL_STALE as prefetch-only data (#98339)
Caches that have `stale < MIN_SHELL_STALE` were using
`makeStageHangingPromise` in prerenders, which (in theory) was meant to
track a runtime data access during a static prerender, which in turn
would make the page use runtime shells. This is semantically incorrect,
because both static and runtime shells would exclude the cache, and both
static and runtime prefetches would include it, so it does not actually
signal runtimeness, only prefetchness. The only thing we should be doing
for these caches is excluding the content, same as
`unstable_prefetch()`.

This PR replaces `makeStageHangingPromise` with
`makePrefetchHangingPromise`, which is not tracked as runtime data at
all. We also use it for `unstable_prefetch()` to signal the
correspondence.

Note that in practice, the usage of `makeStageHangingPromise` in
`use-cache-wrapper` was *technically* fine, because its runtime data
tracking was effectively dead code. We only returned it if
`stagedRendering.finalStage < stage`, but for static prerenders `stage =
staticLinkDataStage = PrefetchStatic` and `finalStage = Static`, so the
condition was never actually true, and we never actually tracked it as a
runtime data access. However semantically it should still not be tracked
as runtime data.
(The condition *can* be true in a runtime prerender that only prerenders
the shell. We don't do any tracking in runtime prerenders, so it was
also a no-op).

(this was extracted from #98129, where we separate runtime data access
tracking into session data and URL data. i couldn't figure which one
`makeStageHangingPromise` should be tracked as, but then i realized it
shouldn't be tracked at all)
2026-09-08 16:22:17 +02:00
Jimmy Miller 9d9e1d5b00 Fix turbopacks webpack loader setup to support options being optional (#98270)
If you look at the types for webpacks loaders, you can see that options
is optional.
https://github.com/webpack/webpack/blob/c17742f631eaeaf1ad880a70b105eedd96dd21b9/declarations/LoaderContext.d.ts#L83

The implementation between the two is slightly different so webpacks
impementation doens't jsut set it to {}. But with the way turbopack
works, this is the equivalent.

I saw a bunch of failures all related to this in real loaders, so figure
it was an easy quick fix.

(also extensionOptions is optional in webpack and needed to be here for
the the types to all checkout
https://github.com/webpack/webpack/blob/c17742f631eaeaf1ad880a70b105eedd96dd21b9/declarations/WebpackOptions.d.ts#L2175)
2026-09-08 06:38:33 -07:00
Tobias Koppers 7167297192 test: stabilize server action HMR update (#98322)
### What?

Stabilize the development-mode server action HMR test when an unrelated
Fast Refresh reload occurs while the patched action is being applied.

### Why?

The test retried by dispatching another click before the previous
asynchronous server action had settled. Under CI load, those requests
overlapped with HMR and an unrelated full-page Fast Refresh reload. The
reload reset the client counter, so the correctly updated action
returned exactly `1000`, which the test rejected because it assumed the
pre-reload state was preserved.

### How?

Wait for network idle after each action invocation so retries do not
observe an in-flight response, allow the valid post-reload value of
`1000`, and use a retry window that accommodates CI HMR latency. The
assertion still distinguishes the updated `+1000` action from the
original `+1` implementation without requiring client state to survive
an unrelated reload.

### Verification

- `pnpm test-dev-turbo
test/e2e/app-dir/actions/app-action-node-middleware.test.ts -t "should
support updating the action"` (10 consecutive passes)
- `pnpm test-dev-webpack
test/e2e/app-dir/actions/app-action-node-middleware.test.ts -t "should
support updating the action"` (4 consecutive passes)
- Prettier and ESLint on `test/e2e/app-dir/actions/app-action.test.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet 5793f439-a569-44be-ad19-d456ce9b641c -->

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-08 10:40:35 +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
Xanaado 0ac302d3d8 Turbopack: fix source field in CSS sourcemaps (#88225)
Closes #88294


Currently, CSS source maps in Turbopack always use index `0` for all
source file references. This can lead to misleading mappings and makes
debugging CSS generated from multiple sources more difficult.
2026-09-04 22:54:25 +02:00
Janka Uryga 00b29d6654 fix: track dynamic accesses in final runtime prerenders (#98255)
The fix implemented in #95665 (`params/searchParams` not hanging
correctly in Runtime App Shells during prospective prerenders) was
incomplete. We started using `makeHanging[Search]Params` in the
prospective prerender (if it was an app shell and wouldn't resolve
them), but not during the final prerender. This meant that, when
encoding the cache key, we would only fire `dynamicAccessAbortSignal` in
the prospective prerender:


https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2160-L2174

...but the final prerender wasn't using `makeHanging[Search]Params`, so
it would not hit this early exit. The hanging input abort signal would
still correctly abort the key encoding (because it accounts for
`stageController.finalStage`) so the key would still be partial, as
expected. We would continue on, and attempt to look the key up in the
Resume Data Cache. But the prospective prerender exited early and never
put anything under that key, so we would incorrectly end up here, and
log:


https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2730-L2734

This is fixed by consistently using `makeHanging[Search]Params` if the
runtime prerender won't reach the stage where they resolve, i.e. if it's
an App Shell. This means we'll trigger `dynamicAccessAbortSignal` while
encoding the cache key in *both* renders, so we'll do the early exit
consistently and avoid the log.

(This feels a bit unsatisfying, because ideally this would all just work
based on stages, which is what I idealistically thought when writing the
code originally. But unfortunately our usage of
`dynamicAccessAbortSignal` for `params/searchParams` requires these
special cases to work consistently. Maybe this can be improved in the
future.)

### Tests

This scenario is covered by the existing `prefetch-runtime` tests -- i
discovered the bug by running them and seeing the `Unexpected cache miss
after cache warming phase` warning. We now assert that the warning is
not logged. The assertion is negative and would also pass if the error
message changed, so i've exported the message as a constant to ensure
`use-cache-wrapper` and the tests stay in sync.
2026-09-04 22:34:16 +02:00
Hendrik Liebau cc27d89b5d Turn on the adapter route collapses by default (#98218)
`experimental.collapseAdapterRoutes` now defaults to `true`, so a build
serves several dynamic routes from one entry in the route table that it
passes to an adapter. The default started off in #97774 so that the
behavior could reach projects one at a time. We have tested this
successfully internally, so the default moves back. The option stays in
place, so a build that hits a regression can set it to `false` and get
the table that a build emitted before the collapses.

The four fixtures under `test/production/app-dir/adapter-dynamic-routes`
no longer set the option, because the default now gives them the
collapsed table that their snapshots record. The snapshots themselves do
not change, since the new default produces what the explicit option
produced.

A full deploy test run with the flag on by default was already done as
part of #97738.
2026-09-04 08:30:09 +02:00
Marcos Hernanz 090f1b7275 fix: suppress missing package manager errors in next info (#97946)
## What

- capture stdout and stderr for both direct and shell-fallback
package-manager version probes
- keep successful version output unchanged and return `N/A` when a probe
fails
- add deterministic cross-platform regression coverage with a failing
Yarn shim

Closes #97932

## Why

PR #71134 added an `execSync` fallback so package-manager command shims
can be resolved on Windows. When `stdio` is omitted, Node mirrors stderr
from failed synchronous child processes to the parent, so an unavailable
Yarn or pnpm can print raw shell errors before `next info` reports
`N/A`.

Passing `{ stdio: "pipe" }` to both probes preserves captured stdout and
the Windows fallback while keeping expected probe failures silent. The
change stays scoped to package-manager version detection; shared
registry discovery is unchanged.

## Testing

- `pnpm --filter next typescript`
- `pnpm --filter next build`
- `pnpm test-types --pretty false`
- ESLint and Prettier on both changed files
- `pnpm test-start test/e2e/cli/cli.test.ts` (66 passed, 1 skipped)
- built-CLI reproduction with npm available and Yarn/pnpm absent: both
report `N/A` with no Yarn/pnpm command-not-found output
- autoreview with GPT-5.6 Sol xhigh and Claude Opus 5 xhigh: zero
findings

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-09-03 18:12:15 -07:00
Josh Story e82978ca63 test: remove stale deploy manifest exclusions (#98051)
## Summary

Remove two inert entries from the deploy-test manifest:

- the entry for
`test/e2e/app-dir/options-request/options-request.test.ts`, which no
longer exists
- the `test/production/standalone-mode/**/*` glob, which cannot match
the manifest's e2e-only include rule

This leaves the hard exclusion list limited to live, selectable test
suites so follow-up deploy-testing work starts from an accurate
inventory.

## Verification

- Parsed `test/deploy-tests-manifest.json` and confirmed all 26
remaining exclusions resolve to files selected by the manifest's include
rule.

<!-- NEXT_JS_LLM -->
2026-09-03 13:07:46 -07:00
Josh Story 65c832c8b8 Return client errors for unrecognized Server Actions (#98123)
## Summary

- return `400 Bad Request` when a Server Action reference ID does not
match the expected format
- return `409 Conflict` when a well-formed action ID is unavailable in
the current deployment, including deployment skew
- apply the same classification to IDs from the `Next-Action` header and
multipart MPA forms, including direct and bound actions in Node and Edge
runtimes
- return generic text bodies matching the client error category, without
exposing action-ID details
- derive marked forwarded-action responses from the original action ID
so an unexpected or mixed-version worker response cannot change the
client error semantics

Fetch actions previously had dedicated 404 handling, while equivalent
MPA form submissions fell through to the generic error path and returned
500. This removes that transport-dependent behavior and distinguishes
malformed or scanning traffic from legitimate stale clients without
treating either case as a missing page or an internal server failure.

## Verification

- `pnpm test-dev-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo test/e2e/app-dir/actions/app-action.test.ts -t
'forward.*action'`
- `pnpm test-start-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/no-server-actions/no-server-actions.test.ts`

<!-- NEXT_JS_LLM -->
2026-09-03 10:36:53 -07:00
Tim Neutkens fb4c498852 Upgrade web-vitals to v6 and report soft navigations (#97232)
## Summary

Upgrades the vendored `web-vitals` dependency to v6.0.1 and enables
soft-navigation reporting for CLS, INP, and LCP. This lets
`useReportWebVitals` report metrics against the navigation where they
occurred, including `navigationURL` for route attribution.

The upgrade also removes the obsolete FID integration and compatibility
patch, regenerates the standard and attribution bundles, and updates the
public metric types and documentation to match the current upstream API.

Related to #97161.

## Verification

- Added production-mode coverage for initial and soft-navigation Web
Vitals reporting with both Turbopack and Webpack.

<!-- NEXT_JS_LLM -->
2026-09-03 09:45:19 +02:00
Sebastian "Sebbie" Silbermann 5b8c866c32 [test] Skip hooks of suites skipped by a lazy @force-gate (#98169)
A `describe` gated by a lazy `// @force-gate` (a condition read off the
fixture's resolved config, like `cacheComponents`) cannot be skipped at
collection time, so the gate runtime force-passes its tests at runtime
and `nextTestSetup` skips the fixture build. Hooks registered inside
such a `describe` still ran, because only the `it`/`test` globals were
wrapped. A hook that touches the `next` instance (for example a
`beforeEach` that reads `next.cliOutput`) then failed with `next
instance is not initialized yet` and failed the supposedly skipped
suite.

The gate runtime now wraps the
`beforeAll`/`afterAll`/`beforeEach`/`afterEach` globals as well and
skips a hook registered under a lazy `@force-gate` when that gate
evaluates false against the fixture's resolved config. Only a false lazy
force-gate skips a hook: an inverted `@gate` still runs its tests and
therefore needs its hooks, and a static `@force-gate` already skips the
whole `describe` at collection time. The hooks `nextTestSetup` registers
itself are marked with the new `ungatedHook` helper so that the skip
decision and the fixture cleanup still run.

The `cacheComponents disabled, edge app router` describe in
`test/e2e/cache-handlers-upstream-wiring` is converted from the
`process.env.__NEXT_CACHE_COMPONENTS` `describe.skip` ternary to `//
@force-gate !cacheComponents`, replacing a fake-green skip and
exercising the fixed path. The `!deploy` conversion of the remaining
describes is left to the branch that introduces the `deploy` condition.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-09-02 19:45:27 +02:00
Niklas Mischkulnig 538f3f68e9 Bump swc to v77 (#97407)
There have been breaking changes to the AST:
- `BlockStmtOrExpr` is now `ArrowFunctionBody`
- function bodies are `FunctionBody` instead of `BlockStmt`
- JSX text literals are `Wtf8Atom`, instead of `Atom`
2026-09-02 16:09:18 +02:00
Luke Sandberg bcfa15d0ed Remove the worker thread for turbopack builds (#96862)
Avoid spawning a worker thread to run turbopack

The typical arguments for worker threads are that they isolate the JS
heap and enable parallelism, but turbopack does both these things by
default (native heap is isolated, the js deps are minor and have a lot
of overlap with existing next server deps).

There is also no substantial parallelism opportunity from worker threads
that aren't immediately delivered by turbopacks execution model

## Benchmarks

Measured with a paired A/B harness that alternates `head` and `base`
builds so thermal drift and background load cancel out, discarding a
warmup build per variant and clearing `.next` before each run. Both
variants are the same commit except for this change. 35 paired
iterations on a default `create-next-app` (App Router, TS), Node
20.11.1, Apple silicon.

| Metric | this PR | canary | Diff | 95% CI | Wins |
| --- | --- | --- | --- | --- | --- |
| `next-build` total | 2478 ms | 2595 ms | **-117 ms (4.5%)** | [50,
183] | 30/35 |
| `run-turbopack` | 1228 ms | 1335 ms | -107 ms (8.0%) | [62, 152] |
31/35 |
| overhead (total - turbopack) | 1250 ms | 1260 ms | -10 ms (0.8%) |
[-27, 46] | 20/35 |
| wall clock | 2775 ms | 2888 ms | -112 ms (3.9%) | [45, 180] | 30/35 |
| peak RSS | 1175 MiB | 1195 MiB | **-19 MiB (1.6%)** | [3.4, 35.3] |
22/35 |

Significant by both a paired t-test and a Wilcoxon signed-rank test (p =
0.0001 for total build time, p = 0.025 for RSS). The Wilcoxon is
included because a couple of slow builds per run would otherwise
dominate the variance.

Nearly all of the saving lands inside the `run-turbopack` span rather
than around it, which is expected: that span wraps the worker spawn, the
IPC to marshal the build across, and the teardown, so deleting the
worker removes work from inside it.

The win is proportionally smaller on larger apps, where compilation
dominates. On `bench/basic-app` (~400 source modules, ~19s builds) the
same ~115 ms is well inside the noise -- 12 paired iterations there
showed no significant difference on any metric. So this is a real but
small improvement that is only user-visible on small builds:

| Metric | this PR | canary | Diff | 95% CI |
| --- | --- | --- | --- | --- |
| `next-build` total | 19313 ms | 19436 ms | -123 ms (0.6%) | [-256,
502] |
| overhead (total - turbopack) | 1424 ms | 1415 ms | +9 ms (0.6% slower)
| [-52, 70] |

Caveats: single machine and two fixture shapes. Peak RSS is the build
process's own footprint (`/usr/bin/time -l`), which includes the worker
thread's isolate but not the separately spawned static-generation
workers. A CI runner with fewer cores would plausibly show a larger
effect.
2026-09-01 22:27:43 -07:00
Aurora Scharff e9180eae2a errors: shorten "use cache" messages and unify them into one factory (#94300)
### What?

Centralizes related <code>"use cache"</code> scope errors and rewrites
them as shorter, actionable messages with consistent <code>Learn
more:</code> links. It also updates the reachable revalidation errors
for rendering and <code>generateStaticParams</code>.

### Why?

The previous messages were long, inconsistent, and sometimes missing
documentation links. Some new cache-specific revalidation messages were
also hidden by an earlier render-phase error. The new messages name the
constraint, give the immediate fix, and link to the relevant
documentation.

### How?

Adds centralized error factories and dedicated error pages for request
data, cache configuration, private-cache composition, and revalidation.
When an active App Router route is available, the messages include it.
The error pages use consistent terminology and complete examples. The
<code>unstable_cache()</code> reference now documents the
request-dependent operations that these errors reject.

### Before and after

Route-based examples use <code>/products</code>. Bracketed values are
alternatives in matching order. For example, <code>[E1482, E1486,
E1489]</code> maps to <code>[headers(), cookies(), request.url]</code>.
Each runtime error contains one alternative, but the table groups
messages with the same template.

#### Messages that include the route

| Cases | Before | After |
| --- | --- | --- |
| E1480 · <code>searchParams</code> in <code>"use cache"</code> | Route
/products used <code>searchParams</code> inside "use cache". Accessing
dynamic request data inside a cache scope is not supported. If you need
some search params inside a cached function await
<code>searchParams</code> outside of the cached function and pass only
the required search params as arguments to the cached function. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": <code>searchParams</code> can't be read inside
<code>"use cache"</code>. Await it outside the cached function and pass
what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1482, E1486, E1489] · [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] in <code>"use
cache"</code> | Route /products used [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] inside "use cache".
Accessing Dynamic data sources inside a cache scope is not supported. If
you need this data inside a cached function use [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] outside of the cached
function and pass the required dynamic data in as an argument. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": [<code>headers()</code>, <code>cookies()</code>,
<code>request.url</code>] can't be read inside <code>"use cache"</code>.
Read it outside the cached function and pass what you need as an
argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1481, E1485, E1492] · [<code>cookies()</code>,
<code>request.url</code>, <code>headers()</code>] in
<code>unstable_cache()</code> | Route /products used
[<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] inside a function cached with
<code>unstable_cache()</code>. Accessing Dynamic data sources inside a
cache scope is not supported. If you need this data inside a cached
function use [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] outside of the cached function and pass the
required dynamic data in as an argument. See more info here:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
Route "/products": [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] can't be read inside
<code>unstable_cache()</code>. Read it outside the cached function and
pass what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
| [E1484, E1491] · <code>draftMode().enable()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used "draftMode().enable()" inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The enabled status of
<code>draftMode()</code> can be read in caches but you must not enable
or disable <code>draftMode()</code> inside a cache. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>draftMode().enable()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>]. Draft mode
can be read inside a cached function, but enabling or disabling it must
happen outside.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| [E1488, E1499] · <code>connection()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used <code>connection()</code> inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The
<code>connection()</code> function is used to indicate the subsequent
code must only run when there is an actual request, but caches must be
able to be produced before a request, so this function is not allowed in
this scope. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>connection()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>] because cached
functions may run during prerendering, without an incoming request. Call
it outside the cached function.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| E1494 · <code>connection()</code> in <code>"use cache: private"</code>
| Route /products used <code>connection()</code> inside "use cache:
private". The <code>connection()</code> function is used to indicate the
subsequent code must only run when there is an actual navigation
request, but caches must be able to be produced before a navigation
request, so this function is not allowed in this scope. See more info
here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route
"/products": <code>connection()</code> can't be called inside <code>"use
cache: private"</code> because private cached functions may run during
prefetching, without a navigation request. Call it outside the cached
function.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/directives/use-cache-private |
| [E1483, E1495] · <code>revalidateTag("products")</code> in [<code>"use
cache"</code>, <code>unstable_cache()</code>] | Route /products used
"revalidateTag products" inside [a "use cache", a function cached with
"unstable_cache(...)"] which is unsupported. To ensure revalidation is
performed consistently it must always happen outside of renders and
cached functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| During render | Route /products used "revalidateTag products" during
render which is unsupported. To ensure revalidation is performed
consistently it must always happen outside of renders and cached
functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| Inside <code>generateStaticParams</code> | Route /products used
"revalidateTag products" inside <code>generateStaticParams</code> which
is unsupported. To ensure revalidation is performed consistently it must
always happen outside of renders and cached functions. See more info
here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called inside <code>generateStaticParams</code>. Call it from a Server
Action or Route Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |

#### Other messages

| Cases | Before | After |
| --- | --- | --- |
| [E1477, E1490] · Nested cache with [short <code>expire</code>,
<code>revalidate: 0</code>] | A "use cache" with [short
<code>expire</code> (under 5 minutes), zero <code>revalidate</code>] is
nested inside another "use cache" that has no explicit
<code>cacheLife</code>, which is not allowed during prerendering. Add
<code>cacheLife()</code> to the outer "use cache" to choose whether it
should be prerendered [with longer <code>expire</code>, with non-zero
<code>revalidate</code>] or remain dynamic [with short
<code>expire</code>, with zero <code>revalidate</code>]. Read more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
| Route <code>"/products"</code>: A nested <code>"use cache"</code> with
[a short <code>expire</code> (under 5 minutes), <code>revalidate:
0</code>] is inside an outer <code>"use cache"</code> that has no
<code>cacheLife()</code>. Add <code>cacheLife()</code> to the outer one
to choose whether to prerender it [with a longer <code>expire</code>,
with a non-zero <code>revalidate</code>] or keep it dynamic [with a
short <code>expire</code>, with <code>revalidate:
0</code>].<br><br>Learn more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
|
| E1478 · External promise | Filling a "use cache" entry appears to be
stuck on shared state from the outer render scope. The same function
completed when run in isolation, which usually means a module-scoped
value (for example a top-level Map used to dedupe fetches) is joining a
promise created outside the cache. "use cache" already dedupes calls
with the same arguments within a request and across requests on the same
server instance, so the surrounding dedupe layer is both unnecessary and
the likely cause. Remove it and rely on "use cache" alone for
deduping.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function is
awaiting a promise created outside it. The same call completed when run
in isolation, so a module-scoped value (often a top-level
<code>Map</code> used to dedupe fetches) is most likely blocking it.
<code>"use cache"</code> already dedupes calls with the same arguments.
Remove the surrounding dedupe layer.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1479, E1498] · [<code>cacheTag()</code>, <code>cacheLife()</code>]
outside a cached function | [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a "use cache"
function.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a <code>"use
cache"</code> or <code>"use cache: private"</code>
function.<br><br>Learn more:
[https://nextjs.org/docs/messages/cache-tag-outside-use-cache,
https://nextjs.org/docs/messages/cache-life-outside-use-cache]<br><br>Outside
an App Router route, the same message is shown without the route prefix.
|
| E1487 · Prerender timeout | Filling a cache during prerender timed
out, likely because request-specific arguments such as params,
searchParams, cookies() or dynamic data were used inside "use
cache".<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function took too
long during prerendering. The most common cause is passing unresolved
request-specific arguments, such as <code>params</code> or
<code>searchParams</code>, into the cached function. Resolve the data
before calling the function and pass only the values you
need.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| E1493 · Private cache inside a public cache | "use cache: private"
must not be used within "use cache". It can only be nested inside of
another "use cache: private".<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be nested inside <code>"use cache"</code> because a shared cached
function can't depend on private request data. Nest it only inside
another <code>"use cache: private"</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1496 · Private cache without a request | "use cache: private" cannot
be used outside of a request context.<br><br><em>No documentation
link.</em> | Route <code>"/products"</code>: <code>"use cache:
private"</code> needs an active request, so it can't be used during
<code>generateStaticParams</code> or other build-time contexts. Move it
to a request-time component or function.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1497 · Private cache inside <code>unstable_cache()</code> | "use
cache: private" must not be used within
<code>unstable_cache()</code>.<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be used inside <code>unstable_cache()</code> because
<code>unstable_cache()</code> uses a shared cache that can't contain
private request data. Call the private cached function outside
<code>unstable_cache()</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |

### Runtime verification

- Exercised the 13 route-based message IDs through minimal dev-runtime
reproductions. Twelve reached the new factories from userland, with
byte-identical output across three requests each.
- The `request.url` error for `"use cache"` Route Handlers is currently
limited to the prerender path. A dynamic request captured by a cached
closure can bypass that tracking; this is existing framework behavior to
follow up separately.
- Passing the `searchParams` promise into a nested cached function can
still surface the earlier synchronous dynamic-API error before this
factory. The new message is verified when the cache scope reads its own
`searchParams` value.
- A follow-up preview verification triggered 17 of the 19 rewritten
messages from userland with byte-identical output. The timeout-driven
external-promise and prerender-timeout messages were verified in source
and through their focused test coverage.

### Verification

- <code>CI=1 pnpm build-all</code>
- <code>pnpm --filter=next types</code>
- <code>pnpm --filter=next build</code>
- <code>HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>NEXT_SKIP_ISOLATE=1 HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/cache-components-errors/use-cache.test.ts -t 'cacheLife
with (expire &lt; 5 minutes|revalidate: 0)'</code> (6 tests and 6
snapshots passed)
- <code>pnpm test-dev
test/e2e/app-dir/cache-components-errors/use-cache.test.ts --projects
jest.config.*</code> (42 tests and 42 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts --projects
jest.config.*</code> (10 tests and 8 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-configured-timeout/use-cache-configured-timeout.test.ts
--projects jest.config.*</code> (4 tests and 2 snapshots passed across
Turbopack and webpack)
- Prettier, ESLint, and Alex on the changed source, tests, and error
pages

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:29:56 +02:00
Janka Uryga adba1e3f2a [PPF] Pass in original searchParams for private-cache pages (#98041)
The handing for page components marked with `use cache: private` was
assuming that searchParams can never hang in `prerender-runtime`, so it
passed the serialized `innerSearchParams` to the page, which we expected
to just be a resolved promise. This *used to* be correct when runtime
prerenders always had search params available, because the serialized
search params were always equivalent to `outerSearchParams`.

However, for `partialPrefetching` we started using `prerender-runtime`
for runtime shells, where search params are not available, and if a
private cache awaits them, it needs to abort filling and become dynamic,
same as when it awaits params. So we need to trigger
`dynamicAccessAbortController` when `searchParams` is awaited:

```tsx
export default async function Page({ searchParams }) {
  'use cache: private'
  // this should trigger `dynamicAccessAbortController.abort()`, so we need the original instrumented
  // searchParams object instead of the serialized one
  await searchParams
}
```

This is fixed by using the outer searchParams object in the
`isPageSegmentFunction` codepath of `use-cache-wrapper` -- we're
preserving the instrumented promise, so the cache prerender will abort
as expected.
(before this fix, a shell prefetch of such a page would get a
deserialized hanging promise for `searchParams` and hang until it times
out)

2 of the 3 newly added tests ("params in a public cache" and "params in
a private cache") were already passing, because `params` was already
being preserved, but i'm adding them here to prevent regressions. The
"search params in a private cache" one was failing due to the cache
timing out, and is now passing. I'm not covering "search params in
public cache" because that's an error and is already tested elsewhere.
2026-08-31 19:15:43 +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
Steven d4c838a7d4 chore: remove unused code from image optimizer (#98060)
This PR removes some unused code from the image optimizer.

- Remove redundant MIME validation
- Remove unreachable fallback error

These used to be necessary until PR
https://github.com/vercel/next.js/pull/82118 removed the fallback.

Before 82118:

```js
upstreamType = detectContentType(upstreamBuffer) || imageUpstream.contentType?.toLowerCase().trim()
```

After 82118:

```js
upstreamType = detectContentType(upstreamBuffer)
```

Now there is never a case where upstreamType is invalid since its
already been validated, there is no more fallback.
2026-08-31 12:02:20 +02:00
Niklas Mischkulnig a37a86acf0 tests: Improve use-cache-cross-deployment tests (#98049)
Followup to #95233

- unskip one client component test since that is actually expected
behavior already, we just need to test the observable behavior instead
- Fix instant insights warnings
- Don't use console.log since that errors when trying to print server
references
2026-08-31 09:46:38 +02:00
Marcos Hernanz a41e98e725 Fix optimistic routing for encoded dynamic params (#97948)
## Summary

Fixes #97842 by canonicalizing encoded pathname parts before using them
as dynamic segment cache keys in optimistic route predictions. This
keeps predicted route trees consistent with server Flight data and
prevents query-only navigation from remounting pages when regular or
catch-all params contain encoded characters.

Add coverage that verifies bfcache IDs and form state are preserved for
`@` in a regular dynamic param and for `@` plus `%2F` in a catch-all
param.

## Verification

- Existing encoded-slash route-cache E2E suite in Turbopack and webpack
production modes
- Existing optimistic-routing E2E suite in Turbopack and webpack
production modes
- `pnpm test-types`
- `pnpm types`
- `pnpm prettier-check`
- `pnpm lint-ast-grep`
- `pnpm lint-language`
- Changed-file Prettier and ESLint checks via the pre-commit hook

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-08-28 15:50:36 -07:00
Marcos Hernanz 4d36801fdb Fix intercepted route params after Proxy rewrites (#97953)
## What?

Preserve the final internal rewrite pathname in the RSC response when an
earlier Proxy rewrite has already populated the rewritten-path header.

This also adds an end-to-end regression assertion for a dynamic
interception route behind a locale-injecting middleware rewrite.

## Why?

The route resolver set the final marker-bearing pathname directly on the
response, but router-server later reapplied the accumulated Proxy
response headers. That restored the earlier markerless pathname. The
client then parsed an intercepted dynamic param against that stale path
and removed three real characters from the value.

## How?

Mirror the final internal rewrite pathname into the accumulated
response-header map. When router-server applies that map after route
resolution, the last rewrite remains authoritative.

Fixes #97939

## Tests

- Production interception regression suite: 4/4 passed
- Development interception regression suite: 4/4 passed
- Related production rewrite/interception suites: 43/43 passed
- Next package build, targeted ESLint, Prettier, and git diff checks
passed
2026-08-28 15:50:12 -07:00
Janka Uryga d8f95c12de [PPF] Fix navigation() in prospective runtime prerenders (#98000)
### Long story short

Fixes an analogous bug to #95665, but affecting
`prefetch()/navigation()`. Those two were accidentally always resolving
in the **prospective** runtime prerender, so we were warming content
that isn't needed for the final runtime prerender. We now guard them
properly.

Also, I replaced `PrerenderStoreModernRuntime.isSessionShell` with
`finalStage` (the same stage we're gonna use for the final prerender),
so we can include/exclude `navigation` properly -- the boolean only
distinguished shell/prefetch, not navigation.

### Long story long

We use runtime prerenders for three things: runtime shells, runtime
prefetches, and embedding a prefetch in navigations. These are aborted
after `ShellRuntime`, `Runtime`, and `NavigationRuntime` respectively
(accessible in `stageController.finalStage`). If we're doing the
**final** prerender for a shell, promises that would resolve in e.g.
`NavigationRuntime` would never resolve, because we abort the prerender
before we get there.

However, the **prospective** runtime prerender has no
`StagedRenderingController` and needs to model this separately (so that
the same APIs hang in both). This was [previously done via
`prerenderStore.isSessionShell`](https://github.com/vercel/next.js/pull/95665)
which we checked in URL data apis i.e. `params/searchParams` and the
metadata`pathname` to do this. `isSessionShell: true` meant they should
hang because it's a shell, and `isSessionShell: false` meant they should
resolve.

However, I forgot about this case when implementing
`navigation`/`prefetch`, and made them resolve unconditionally in the
prospective prerender. This means we'd potentially warm content that
would not actually be reached in the final prerender. In other words,
```ts
await navigation()
return <Expensive />
```
would needlessly render `<Expensive />` during the prospective render
but not in the final one. This kinda defeats `navigation`'s main
purpose, i.e. avoiding expensive work.

This PR fixes the bug by replacing `isSessionShell: boolean` with
`finalStage: AdvanceableRenderStage`, which is set to the same value we
put in `stageController.finalStage`. This lets us keep the logic of what
hangs and what resolves relatively close in both prerenders.

---

On a meta level, the fact that the same bug happened twice tells me that
having a `StageRenderingController` that is sometimes `null` is
error-prone, and we should figure out a better solution, but i'm gonna
keep this fix targeted and figure out a more holistic solution in a
follow-up
2026-08-28 22:10:21 +02:00
Janka Uryga 357e514cf7 Revert "test: re-enable sync IO root coverage" (#98050)
Reverts vercel/next.js#97996

The fix does not seem to help with the flakiness.
2026-08-28 19:41:32 +00:00
Niklas Mischkulnig e3790aa911 More granular cache keys for use-cache entries (#95233)
Caveats:
- I had to skip `react[-dom][/*]` and
`private-next-rsc-server-reference` and `private-next-rsc-cache-wrapper`
imports in the code hash and env-var tracking. Because those all end up
pulling in app-page-turbo.runtime.prod.js which reads many env vars and
would cause constant deopting. But this should still be correct. The
code of these imports is included via the Next.js version, and no env
vars should change the semantics of any of those imports.
- Static env var reads are collected, but too dynamic accesses are
silently ignored and don't lead to deopts (same goes for env var reads
in native NAPI addons). This means that this cache reuse is not
guaranteed to be 100% guaranteed to never lead to stale caches.
- Code hashing and env var collection happens on a per-module basis. So
if you put all use-cache functions into a single file and/or together
with react components, then you will see extraneous invalidations. This
will be fixed by either generally enabling module splitting for all of
Turbopack, or by adding a special transform that does it for use-cache
functions.

Followups:
- There are some env var static analysis gaps that will be immediate
followups before broader testing. These are the various TODOs added in
https://github.com/vercel/next.js/pull/95310
- Client components invalidation is very coarse grained right now
(statically imports any client component anywhere -> deopt completely).
Followup for the future. But the current setup is correct. This would
just improve effectiveness further
- Do this in dev as well. Currently there is no NFT at all in dev (for
performance reasons)
- Module splitting for more granular tracking
- Include entropy when serializing server reference arguments for cache
key


Todo:

- [x] Use implementation code hash (includes inlined env vars): from
#94234
- [x] Include non-inlined runtime env vars: from #95310
- [x] Include client reference manifest (very coarse for now)
- [x] Include Next.js version (for wire format, etc)
- [x] This is now done for all use-cache entries now. Not just for `use
cache: remote`. Is that the intended behavior? Yes
- [ ] ~~if `NEXT_DEPLOYMENT_ID` is in the env vars. just deopt and don't
care about stringifing and hashing the env vars~~
- [x] Is cache key size a problem? Currently you can get this: (values
are always hashed)
`CustomCacheHandler::get
["80e6f6560092f0078775e7e787c1c10ecf6dea0bc4",[],["d984fbaa996274737f3b59345a300a20","16.4.0-canary.5","__NEXT_NO_MIDDLEWARE_URL_NORMALIZE=undefined","NEXT_OTEL_PERFORMANCE_PREFIX=undefined","__NEXT_PRIVATE_ORIGIN=a04f4b9d6a8f42724740a480e9a2bc67c053dc04377be831c0e2c407a1422004","NEXT_PRIVATE_RESPONSE_CACHE_TTL=undefined","NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE=undefined","__NEXT_CACHE_COMPONENTS=b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b","__NEXT_ROUTER_BASEPATH=undefined","__NEXT_MANUAL_CLIENT_BASE_PATH=undefined","__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS=undefined","__NEXT_APP_NAV_FAIL_HANDLING=undefined","__NEXT_GESTURE_TRANSITION=undefined","__NEXT_USE_OFFLINE=undefined","NEXT_DEBUG_BUILD=undefined","__NEXT_VERBOSE_LOGGING=undefined...]]
[["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]]`
2026-08-28 10:17:15 -07:00
Josh Story 5e390edb7f test: re-enable sync IO root coverage (#97996)
## Summary

Re-enable the `sync-io-blocks-root` production suite now that #97900
waits for child stdio to close before capturing build CLI output.

This reverts the temporary skip from #97986 so CI flake detection
exercises the route-specific sync I/O diagnostics again.

## Verification

- `HEADLESS=true IS_WEBPACK_TEST=1 pnpm test-start-webpack
test/production/app-dir/sync-io-blocks-root/sync-io-blocks-root.test.ts`

<!-- NEXT_JS_LLM -->
2026-08-28 10:08:40 -07:00
Steven 1cb20a1d14 [image-optimizer] Refactor into lightweight transform module (#97988)
This PR is strictly a refactor, no logic should change.

The idea is that we eventually want to run the image optimizer transform
step in a child process. This PR refactors the code to make a new
`transform.ts` that is lightweight and should reduce some of the
overhead of loading a large module graph when spawning a new process.
2026-08-28 17:52:50 +02:00
Tobias Koppers 17a901a74f Guard filesystem reads against unresolved symlinks (#97902)
### What?

Adds debug-only OS realpath validation to successful `DiskFileSystem`
file and directory reads. When a successfully canonicalized path differs
from the supplied path, the read returns a normal task error naming both
paths.

Fixes pattern/glob traversal and NFT tracing so physical filesystem
access uses resolved paths while logical paths remain available for
user-visible specifiers and complete symlink-chain recovery.

### Why?

Reading through an unresolved symlink parent gives the same filesystem
object multiple path identities. That can make Turbo Tasks dependency
tracking and invalidation inconsistent and can produce invalid
deployment ZIPs when NFT output contains files below unresolved links.

The checks return errors rather than asserting because paths can
disagree temporarily under eventual consistency. Propagating a task
error avoids panicking a worker thread while still exposing invalid
callers during development.

### How?

The validation lives directly in `DiskFileSystem::read` and
`DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API
inline instead of the Turbo Tasks realpath task, keeping the diagnostic
out of the task dependency graph. The guard runs only after the OS read
succeeds, so missing/non-directory probes preserve their existing
behavior.

`read_matches` resolves each physical directory immediately before
enumeration while retaining logical `PatternMatch` paths.

`read_glob` and `track_glob` now resolve their initial directory before
enumeration. Symlinks discovered later through wildcard segments are
also traversed through resolved targets. `ReadGlobResult` deliberately
retains logical paths rooted at the supplied base, allowing consumers to
call `realpath_with_links` and recover the complete symlink chain.

Consumers follow that contract explicitly:

- NFT includes expand each logical match with `realpath_with_links`,
emit resolved files and every traversed symlink, skip resolved directory
targets, and deterministically deduplicate/sort output.
- `import.meta.glob` uses recursive logical keys as the source of
user-visible requests, while module resolution follows and tracks
symlinks.
- the hash-glob example resolves returned logical paths before reading.

Webpack-loader context dependencies are covered for both
`path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader
fixture performs its directory read with Node `fs`, reports the
directory using `addContextDependency`, and Turbopack tracks the
resolved target.

### Verification

- `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --
--check`
- `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api
--all-targets`
- `cargo test -p turbo-tasks-fs` (128 passed)
- `cargo test -p next-api` (7 passed)
- `cargo check -p turbo-tasks-fs --examples`
- `import.meta.glob` symlink execution fixture (1 passed)
- Nine targeted node-file-trace CI cases with `release-with-assertions`
(9 passed)
- `pnpm build-all`
- `webpack-loader-fs` Turbopack dev e2e (1 passed)
- `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed)
- twoslash Turbopack production, normal mode (4 passed)
- twoslash Turbopack production, cache-components mode (4 passed)
- `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200)

### Notes

The disk guard is cross-platform, while its symlink-parent regression
test is Unix-only, matching neighbouring symlink tests. On Windows, OS
canonicalization can also normalize casing and 8.3 short names; a debug
read using a non-canonical spelling will therefore return the same
diagnostic error.

<!-- NEXT_JS_LLM -->


<!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c -->

---------

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-28 08:31:06 +02:00
Will Binns-Smith 12bf495e0e Expand Turbopack dev cleanup (#97833)
## Summary

Builds on #97591. Webpack currently removes most of `.next` on devserver
startup. We can remove more as well.

This keeps the age-based removal introduced in #97591 and expands it to
`traces`, logs, etc. However, for any other directory that is not
`cache` or `lock`, completely remove it on startup. This includes types,
etc.

Apply age-based deletion to Turbopack-owned output that can be restored
from cache or recomputed, including route entries, chunks, assets,
service workers, middleware, and instrumentation. Completely remove
other dev output so logs, traces, generated types, diagnostics, and
future framework-owned output cannot be left as mixed generations.

Use exact-path exclusion sets in `recursiveDeleteSyncWithAsyncRetries`.
The complete pass retains `cache`, `lock`, and Turbopack-owned paths;
the age-based pass then sweeps those Turbopack paths. Both passes finish
before project creation.

## Verification

- `pnpm test-unit test/unit/recursive-delete.test.ts`
- `pnpm --filter=next types`

---------

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-27 14:54:23 -07:00
Andrew Clark b882af0899 Add Cache Components option to create-next-app (#97695)
When initializing a new project with create-next-app, the interactive
prompt will now ask the user if they want to enable Cache Components.

For now the option defaults to off. We'll flip the default back on once
there's a flow that guides users to the right recommendation based on
the kind of site they're building. The option can still be enabled
interactively, or with the `--cache-components` flag.

If the user chooses to enable Cache Components, the project will also be
initialized with Partial Prefetching enabled. There's no separate prompt
for Partial Prefetching; the only reason they are separate configs is
because Cache Components shipped in 16.0 before Partial Prefetching
existed.

This is not a breaking change because it does not change any behavior in
Next.js itself, only the create-next-app CLI. It has no impact on any
existing Next.js projects.
2026-08-27 17:44:31 -04: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
Josh Story 6d6228c0c6 Prune incomplete parallel route matchers (#97108)
## Summary

This adds an experimental `strictRouteMatching` flag that leaves a
matcher out when its finalized loader tree contains a synthesized
default that will always call `notFound()` for a slot actually declared
by the owning layout. Adding a `default.tsx` keeps the matcher, and
structural router-state branches that are not real slots of that layout
do not make it incomplete. The flag defaults to `false` so matcher
pruning can roll out independently from the preceding loader-tree
correction.

The main goal is to make `children` and named slots behave the same way.
If a URL cannot construct a complete route tree on its own, it should be
treated as an unmatched URL instead of sometimes showing a slot 404 and
sometimes producing a missing page or default error.

## Semantics

For example:

```text
app/split-matcher/
├── layout.tsx
├── foo/page.tsx
├── bar/page.tsx
└── @slot/[...parts]/page.tsx
```

With strict route matching disabled, Next.js emits matchers for
`/split-matcher/foo`, `/split-matcher/bar`, and the broad
`/split-matcher/[...parts]`. The broad matcher can match any URL, but
its `children` branch has neither a matching page nor a default, so it
can only construct a permanent 404 tree. With strict route matching
enabled, that broad matcher is omitted while `/foo` and `/bar` remain
because each combines a real `children` page with the named-slot
catchall. Adding `app/split-matcher/default.tsx` deliberately makes the
broad matcher complete and keeps it.

The same rule applies without a catchall:

```text
app/disagreeing-slots/
├── layout.tsx
├── @first/foo/page.tsx
└── @second/bar/page.tsx
```

`/foo` is incomplete because `@second` has no matching page or default,
and `/bar` is incomplete for the corresponding reason in `@first`, so
neither matcher is emitted. A later PR in this stack reports the
now-unreachable page files as a project misconfiguration.

A declared `children` route is treated like any named slot:

```text
app/declared-children/
├── layout.tsx
├── page.tsx
└── @panel/
    ├── default.tsx
    └── details/page.tsx
```

`/declared-children` is complete because `children` uses `page.tsx` and
`@panel` uses its default. `/declared-children/details` is incomplete
because `@panel` matches its page while the declared `children` slot has
no matching page or default at `/details`, so that matcher is pruned.

A route composed entirely from declared named slots is also complete:

```text
app/named-only/
├── layout.tsx
├── @left/[...slug]/page.tsx
└── @right/[...slug]/page.tsx
```

The preceding PR's default-on `explicitParallelRouteChildren` behavior
means this loader tree contains only `left` and `right`. Strict matching
keeps `/named-only/[...slug]` because both declared slots match; it does
not invent `children` and then prune the route for failing to satisfy
that nonexistent slot.

We got here incrementally.
[#47872](https://github.com/vercel/next.js/pull/47872) introduced the
404 fallback for unmatched parallel slots,
[#60186](https://github.com/vercel/next.js/pull/60186) added a
development warning because this was confusing in practice and linked
[#51805](https://github.com/vercel/next.js/issues/51805) and
[#49569](https://github.com/vercel/next.js/issues/49569), and
[#84702](https://github.com/vercel/next.js/pull/84702) later made a
missing default a build error for named slots while leaving `children`
on the old fallback for backwards compatibility. Strict matching takes
the next step and treats a matcher that can only construct a permanent
404 tree as unmatched.

This changes soft navigations that only worked by preserving a
previously active slot even though the URL could not be loaded directly,
which is why matcher pruning remains behind its own experimental flag.
The interception retention markers backed by `default-null`, including
named host slots from the first PR in this stack, are complete route
patches and are not pruned. This change however is well motivated
because if you did client nav to a route that only matches a named slot
and then hard refresh you will end up getting a 404. This is a sign our
current semantics are actually broken.

If you want to preserve the perma 404 behavior of the
slot-without-default you can just add a default and make it call
`notFound()` unconditionally.

## Verification

- `pnpm build-all`
- Focused `normalize-catchall-routes` unit coverage
- Production and development e2e coverage on Turbopack and Webpack
- The pruning e2e matrix with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 13:23:43 -07:00
Josh Story 05ed7c1be2 Omit undeclared children slots from app routes (#97184)
## Summary

Parallel route layouts can be composed entirely from named slots, but
loader tree construction currently synthesizes a `children` fallback
whenever any named slot exists. This makes `children` semantically
required even when no page, default, or ordinary route branch declares
it.

This adds `experimental.explicitParallelRouteChildren` and enables it by
default. When enabled, `children` is included in a layout slot set only
when the filesystem declares an ordinary route that can render at that
level. A layout by itself is only structure and does not declare a route
target. Ordinary descendants are traced until they reach a page or
default, including through deeper named slots, before they cause
`children` to be included. Setting the flag to `false` temporarily
restores the legacy implicit `children` fallback.

This flag only controls whether `children` exists in the loader tree. It
does not prune incomplete matchers; that is the separate
`experimental.strictRouteMatching` behavior in the next PR.

Named slots keep their existing default and soft navigation semantics.
The preceding PR retains the slots owned by an interception host without
treating `children` specially, so an undeclared child is no longer
needed for that behavior. A real `children` branch still uses the
retention marker when it is one of the host layout slots.

## Semantics

For example, this layout declares only named slots:

```text
app/dashboard/layout.tsx
app/dashboard/@left/page.tsx
app/dashboard/@right/page.tsx
```

With `explicitParallelRouteChildren` disabled, Next.js adds a synthetic
`children` branch whose built-in default calls `notFound()`, even though
the layout never declared or rendered it. With the default behavior
enabled, the loader tree contains only `left` and `right`, so
`/dashboard` is matched from the route targets that actually exist.

This is different from an ordinary branch whose route targets are deeper
in the tree:

```text
app/nested/layout.tsx
app/nested/@sidebar/[...slug]/page.tsx
app/nested/content/layout.tsx
app/nested/content/@left/[...slug]/page.tsx
app/nested/content/@right/[...slug]/page.tsx
```

Here `content` really is the `children` branch of `nested`. The scan
follows `content` through its layout and deeper named slots, so
`children` remains required. `/nested/content/anything` can construct
every declared slot, while `/nested/incomplete` only matches `sidebar`
and is still incomplete. The distinction is whether the ordinary
descendant eventually reaches a page or default, not whether a layout
happens to exist along the way.

The focused children detection coverage proves that a layout-only
descendant does not synthesize `children`, while an ordinary branch
whose route targets live inside deeper named slots still does. The
limitation coverage also proves that named-only trees render pages, CSS,
metadata, and regular error boundaries. It intentionally asserts the
current broken behavior for HTTP access fallbacks and metadata or
viewport failures so those expectations can be flipped when renderer
ownership no longer depends on `children`.

## Verification

- `pnpm build-all`
- Turbopack and webpack development and production coverage for
`explicit-parallel-route-children-detection`
- Turbopack and webpack production coverage for
`interception-dynamic-segment`, `parallel-routes-layouts`, and
`explicit-parallel-route-children-legacy`
- The same existing production coverage with Cache Components enabled
- Turbopack and webpack production coverage for the documented
named-only limitations

<!-- NEXT_JS_LLM -->
2026-08-27 12:22:31 -07:00
Steven 04e96a5bfa test: add test for local font with deployment id (#97987)
Follow up to a previous PR:

- https://github.com/vercel/next.js/pull/82488
- https://github.com/vercel/next.js/pull/82384

Closes:

- https://github.com/vercel/next.js/pull/82547
2026-08-27 19:10:55 +02:00
Josh Story cce7bf2fc0 Retain interception route host slots (#97242)
## Summary

Interception routes represent a partial update to the layout that hosts
them. Today we model this for `children` by synthesizing a `__DEFAULT__`
route backed by `default-null`, but named siblings still use normal
default matching even though they should retain their active state too.

This makes retention relative to the interception host instead of the
`children` key. Every non-intercepting sibling gets the existing
`__DEFAULT__` marker backed by `default-null`, while slots inside the
newly selected interception subtree continue to use normal matching and
real defaults.

The coverage uses a named-only host to verify that named siblings retain
client state without evaluating a user default, that a sibling without a
default also retains, and that a hard refresh still loads the canonical
route.

## Example

Consider this route tree:

```text
app/
├── named-host/
│   ├── layout.tsx                  # renders canonical, content, secondary, and modal
│   ├── @canonical/
│   │   └── page.tsx
│   ├── @content/
│   │   ├── page.tsx                # renders a stateful counter
│   │   └── default.tsx             # throws if evaluated
│   ├── @secondary/
│   │   └── page.tsx                # has no default.tsx
│   └── @modal/
│       ├── default.tsx
│       └── (..)named-target/
│           └── page.tsx            # intercepted target
└── named-target/
    └── page.tsx                    # canonical route
```

Before this change, a soft navigation from `/named-host` to
`/named-target` could replace `@content` with its throwing default and
treat `@secondary` as missing because only `children` had retain
semantics. After this change, only `@modal` switches to the intercepted
page; `@content` and `@secondary` keep their existing UI and the counter
keeps its state. A hard refresh still renders the canonical
`/named-target` route instead of the interception host.

## Verification

- Turbopack and webpack production coverage in
`interception-dynamic-segment`
- The same production coverage with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 09:27:33 -07:00
Janka Uryga 0a2bd1d531 test: disable flaky sync-io-blocks-root.test.ts (#97986)
Something made this test very flaky on Aug 24/Aug 25


https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.status%3A%22fail%22%20%40test.suite%3A%22sync%20IO%20that%20blocks%20the%20root%22%20-%40test.type%3Aturbopack&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1787237730805&end=1787842530805&paused=true

Disabling until we can investigate.
2026-08-27 15:27:42 +00:00
Tobias Koppers d42ea11842 [test] Drain build output before start (#97947)
### What?

Make the production test harness wait for a build process's piped stdio
to close before `NextStartInstance.start()` continues to launch the
server or reports a build failure.

### Why?

A child process's `exit` event can fire before trailing stdout and
stderr have been delivered to the parent. This can make start-mode tests
intermittently miss diagnostics emitted near the end of a production
build, even though the build itself reported them correctly.

The standalone `build()` helper already uses the later `close` lifecycle
event to avoid this race. Applying the same rule to the build phase
inside `start()` removes the equivalent latent output-drain hazard and
makes child-process handling consistent across the harness.

### How?

Resolve the build phase on the child process's `close` event. Unlike
`exit`, `close` is emitted after piped stdio closes, so all trailing
build output has reached the harness before it starts the server or
rejects the build. Exit-code and signal handling remain unchanged.

### Verification

- `pnpm types`
- `pnpm test-start-webpack
test/production/production-build-dir/production-build-dir.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/sync-io-blocks-root/sync-io-blocks-root.test.ts`
- Repeated webpack stress campaign for the sync-IO suite: 5/5 runs,
including 20/20 `--debug-prerender` route cases
- Prettier and ESLint on `test/lib/next-modes/next-start.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet 821362ed-9f96-4e53-b56e-49bf00e878b9 -->

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-27 16:18:02 +02:00
Tim Neutkens 89d017eac4 Pages Router: Deprecate React 18 support (#97689)
## Summary

- warn during `next dev` and `next build` when React 18 is installed,
while keeping `next start` quiet
- explain that React 18 remains supported in Next.js 16 but will be
removed in Next.js 17
- update the React version guidance with React 19 and TypeScript type
upgrade commands

## Verification

- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- `NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm
test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- targeted production build/start coverage passed with React 18 and
React 19
- Not passing: `pnpm --filter=next types` (existing unrelated errors in
`httpget.ts`, `image-optimizer.ts`, `proxy-request.ts`, telemetry
typings, and compiled declarations)
- Not passing: `pnpm lint` (existing unused Turbo Tasks check)

<!-- NEXT_JS_LLM -->
2026-08-27 13:00:17 +02:00
Niklas Mischkulnig 529d290b8f [test] Unpin Vercel CLI version (#97974)
Reverts #97960
vercel@59.7.0 should fix this again
2026-08-27 10:07:14 +02:00
Niklas Mischkulnig 80f846c74e Support immutable static assets with output: 'export' (#97711)
There was no real reason why this wasn't done thus far.
`output: 'export'` already uses the adapter anyway.

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 09:55:24 +02:00
Niklas Mischkulnig d75bf1b85a Fix build error when aliasing typescript to @typescript/typescript6 (#97942)
This was previously broken, because the binary isn't called `tsc` but
`tsc6`:
```json
"dependencies": {
	"typescript": "npm:@typescript/typescript6@6.0.1"
}
```

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 09:49:52 +02:00
Jiwon Choi a65ccea59c Replace resumed render bailout error with ReactDOM.browser behind a flag (#96844)
Stacked on #96843.

This PR replaces the bailout error used to abort resumed renders with
`ReactDOM.browser()` behind the experimental flag.

### How?

Pass the flag to the App Router runtime, use `ReactDOM.browser()` as the
abort reason when enabled, and preserve existing behavior otherwise.

<!-- NEXT_JS_LLM -->
2026-08-27 02:07:27 +02:00
Jiwon Choi c11c48e42d Replace useSearchParams bailout error with ReactDOM.browser behind a flag (#96843)
Stacked on #96826.

This PR replaces the bailout error used by `useSearchParams` with
`ReactDOM.browser()` behind the experimental flag.

### How?

Move the bailout behind a browser-safe module boundary, use
`ReactDOM.browser()` when enabled, and preserve existing behavior
otherwise.

<!-- NEXT_JS_LLM -->
2026-08-27 02:07:26 +02:00
Jiwon Choi 23985382a7 Replace CSRBailout error with ReactDOM.browser behind a flag (#96826)
CSRBailout error is the use case for `ReactDOM.browser()`. Replace
behind an experimental flag.
This PR replaces the bailout error used in next/dynamic ssr: false.
2026-08-27 02:07:26 +02:00