### What?
Fix a race condition in the Pages Router SSR path where styled-jsx
styles were dropped from the rendered HTML.
### Why?
`styledJsxInsertedHTML()` reads and flushes the styled-jsx style
registry. Previously it was called concurrently with the page render via
`Promise.all`:
```js
const [rawStyledJsxInsertedHTML, content] = await Promise.all([
renderToString(styledJsxInsertedHTML()),
(async () => { /* render the page */ })(),
])
```
Because both ran at the same time, `styledJsxInsertedHTML()` could (and
in practice did) execute and flush the registry **before** the page
render had finished populating it. The result was that dynamic
styled-jsx styles — those with interpolated expressions that compute
their class names at runtime via DJB2 hashing — were silently dropped
from the SSR output, causing a flash of unstyled content on first load.
This is particularly visible in production deployments where all
components use dynamic styled-jsx (numeric `jsx-*` class names), since
those styles only exist in the registry after rendering completes.
### How?
Serialize the two operations: render the page first, then call
`styledJsxInsertedHTML()`. Since the registry is fully populated by the
time it is read, all styles are captured correctly.
A new e2e test (`test/e2e/styled-jsx-dynamic`) exercises this scenario
with multiple nested components that all use dynamic styled-jsx with
interpolated props, covering the exact FOUC pattern seen in production.
<!-- NEXT_JS_LLM_PR -->
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
This fixes adapter `onBuildComplete` outputs for dynamic metadata
routes.
## Bug
`handleBuildComplete` skipped metadata routes too aggressively via
`isStaticMetadataFile(...)`, so dynamic metadata routes (for example
`robots.txt` / `sitemap.xml` using `connection()`) were omitted from
`outputs.appRoutes`.
## Fix
Only skip metadata routes when they are actually prerendered/static
(present in prerender manifests). Dynamic metadata routes are now
included in adapter `outputs.appRoutes` as expected.
## Tests
Added e2e coverage in `test/e2e/app-dir/adapter-dynamic-metadata`:
- verifies dynamic `robots.txt`, `sitemap.xml`, and `favicon.ico`
functionality
- verifies adapter output classification for non-deploy runs
- skips output-shape verification in deploy mode
- skips the suite in dev mode
### What?
Fixed Turbopack incorrectly trying to resolve inline JavaScript code as module references when `new Worker()` is called with `{ eval: true }` option.
Now we skip creating a reference when `eval:true`, and report warnings if we cannot tell what the value is
### Why?
Libraries like jsPDF create Worker threads by passing inline JavaScript code as the first argument along with `{ eval: true }` as the second argument. Turbopack was incorrectly treating this inline code as a file path and attempting to resolve it as a module reference, causing build failures.
### How?
Added logic to detect when the `eval: true` option is passed to the Worker constructor. When this option is present, Turbopack now skips creating a worker reference since the first argument contains executable code rather than a file path. Added comprehensive test coverage using jsPDF to verify the fix works correctly.
Fixes#91642
The depth-based instant validation previously only iterated
URL-consuming segments as potential shared/new boundaries. Route groups
between URL segments were invisible — when a route group layout with
Suspense was shared in a client navigation, its Suspense appeared to
cover blocking code in the validation render even though it wouldn't in
reality.
This adds a second dimension to the validation iteration: group depth.
At each URL depth, we now also step through route group boundaries. A
new `discoverValidationDepths` function walks the LoaderTree and returns
an array where each entry represents the max group depth at that URL
depth. The validation loop iterates from deepest group depth to
shallowest within each URL depth, stopping on the first error.
Key design decisions:
- URL depth and group depth counters are advanced before the boundary
check, making the boundary condition a simple `nextUrlDepth > depth &&
currentGroupDepth >= groupDepth`
- The synthetic `(slot)` segment that Next.js inserts for parallel slots
is excluded from group depth counting since it doesn't represent a real
navigation boundary
- Group depths are discovered from the LoaderTree rather than the URL
pathname, making the tree the source of truth for validation bounds
## Move `adapterPath` from experimental to stable
Graduates `adapterPath` from `experimental.adapterPath` to a top-level
`NextConfig` option, following the same pattern used for
`cacheHandlers`, `cacheComponents`, `typedRoutes`, and other options
that moved out of experimental.
### Changes
- **Type definition** (`config-shared.ts`): Added `adapterPath` to
`NextConfig` interface, marked `ExperimentalConfig.adapterPath` as
`@deprecated`, moved default value to top-level in `defaultConfig`,
updated `NextConfigRuntime` and `getNextConfigRuntime`
- **Zod schema** (`config-schema.ts`): Added `adapterPath` to top-level
`configSchema`
- **Backward compat** (`config.ts`): Added
`warnOptionHasBeenMovedOutOfExperimental` call so existing
`experimental.adapterPath` configs still work with a deprecation
warning, updated `applyModifyConfig` to read from `config.adapterPath`
- **Runtime references**: Updated `build/index.ts`, `app-page.ts`,
`app-route.ts`, `pages-handler.ts` to use `config.adapterPath` /
`nextConfig.adapterPath`
- **Tests**: Updated 4 test fixture `next.config` files
- **Docs**: Updated app router doc (removed `version: experimental`
frontmatter, top-level config example), pages router doc title, and v16
upgrade guide
### What?
Removes the `experimental.devCacheControlNoCache` config option entirely
and hard-codes `no-cache, must-revalidate` as the dev server
`Cache-Control` header value.
Previously the option controlled whether the dev server responded with:
- `no-store, must-revalidate` (default, `false`)
- `no-cache, must-revalidate` (opt-in, `true`)
This PR first flips the default to `true`, then removes the option
altogether — making `no-cache, must-revalidate` unconditional in all dev
code paths.
### Why?
`no-cache` is strictly better than `no-store` for the dev server:
- `no-cache` allows the browser to revalidate (conditional
`If-None-Match`/`If-Modified-Since` requests), letting the server
respond with `304 Not Modified` when nothing changed → faster page loads
during development.
- `no-store` forces a full re-fetch every time, discarding valid cached
responses.
Since `no-cache` is the correct behavior for all dev users, the toggle
has no remaining value and can be removed to simplify the codebase.
### How?
**Two commits:**
1. `bcec825` — Flip the default from `false` → `true`; update
tests/fixtures/manifest to reflect the new default.
2. `e6e919f` — Remove the option entirely:
- Deleted `devCacheControlNoCache?: boolean` from `ExperimentalConfig`
interface, `config-schema.ts` Zod schema, `defaultConfig`,
`NextConfigRuntime`, and `getNextConfigRuntime()`.
- Replaced all four conditional ternaries in `base-server.ts`,
`router-server.ts`, `pages-handler.ts`, and `app-page.ts` with the
hard-coded string `'no-cache, must-revalidate'`.
- Deleted the `dev-cache-control-no-cache-disabled` test suite (was
testing the `false` path which no longer exists).
- Simplified the remaining `dev-cache-control-no-cache` test (removed
experimental framing).
- Synced `rspack-dev-tests-manifest.json`.
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
**Previous:**
1. https://github.com/vercel/next.js/pull/91487
2. https://github.com/vercel/next.js/pull/91488
**Current:**
3. https://github.com/vercel/next.js/pull/89297
---
Most of the vary params infrastructure was already implemented in
previous PRs for static prerenders. This wires up the remaining pieces
for runtime prefetches — creating the accumulator, setting it on the
prerender store, and resolving it before abort — and adds additional
test cases covering empty/full vary sets, searchParams, metadata, and
per-segment layout/page splits with runtime prefetching.
Follow-up to #91189 which added support for accessing root params inside
`generateStaticParams`. Previously, if a `generateStaticParams` function
tried to read a root param that hasn't been defined yet — either because
the current segment is supposed to define it, or because a child segment
defines it — `getRootParam` would silently return `undefined`. This is
especially problematic because #91019 will type root param getters as
`Promise<string>` without `undefined`, making this a silent type safety
violation.
This change makes `getRootParam` throw an explicit error with the
`generate-static-params` work unit store when the requested param is not
present in `rootParams`.
## Summary
- Fixes flaky test "per-page value overrides global staleTimes.dynamic
regardless of direction" introduced by #91437
- The test was flaky because `browser.back()` restored accordion state
from BFCache, causing previously-opened `LinkAccordion` links to be
immediately visible. This triggered uncontrolled re-prefetches outside
the `act` scope. When the `IntersectionObserver` fired inside a
subsequent `act` scope (after clock advancement), stale data would
trigger a prefetch that violated the `no-requests` assertion.
- Fix: instead of navigating back to a previously visited page, navigate
forward to fresh "hub" pages with their own `LinkAccordion` components.
Since these are never-visited pages, accordions start closed and no
uncontrolled prefetches are triggered.
- General principle: when using the router `act` test utility, always
use `LinkAccordion` to control when prefetches happen. Prefetches should
only be triggered inside an `act` scope by toggling an accordion, never
by navigating back to a page where links are already visible.
## Test plan
- [x] Ran the full test file 4 times locally — all 5 tests pass
consistently
Root params (e.g. `import { lang } from 'next/root-params'`) can now be read inside `"use cache"` functions. The read root param values are automatically included in the cache key so that different root param combinations produce separate cache entries.
Since which root params a cache function reads is only known after execution, the cache key is reconciled post-generation. When root params are read, a two-key scheme is used: the full entry is stored under a specific key (coarse key + root param suffix), and a lightweight redirect entry is stored under the coarse key. The redirect entry's tags encode the root param names using the pattern `_N_RP_<rootParamName>` (e.g. `_N_RP_lang`), following the convention of existing internal tags like `_N_T_<pathname>` (e.g. `_N_T_/dashboard`) for implicit route tags. This allows a cold server to resolve the specific key on the first request after restart. An in-memory map (`knownRootParamsByFunctionId`) provides a fast path for subsequent invocations. When no root params are read, the full entry is stored directly under the coarse key with no redirect involved.
The in-memory map grows monotonically — if a function conditionally reads different root params across invocations, the set accumulates all observed param names. The redirect entry's tags are built from this combined set, ensuring cold servers always resolve the most complete specific key.
The two-key scheme only applies to the cache handler. The Resume Data Cache (RDC) always uses the coarse key because each page gets its own isolated RDC instance, so root params are fixed within a single RDC and no disambiguation is needed. When an RDC entry is found during resume, it seeds `knownRootParamsByFunctionId` so that subsequent cache handler lookups can use the specific key directly.
Reading root params inside `unstable_cache` still throws. Reading root params inside `"use cache"` nested within `unstable_cache` throws with a specific error message explaining the limitation.
Alternatives considered: extending the `CacheEntry` interface (would be a breaking change for custom cache handlers), encoding root param metadata in the stream via a wrapper object, or sentinel byte, or Flightception (runtime overhead of stream manipulation on every cache read), and deferring `cacheHandler.set` until after generation (breaks the cache handler's pending-set deduplication for concurrent requests).
This PR adds `unstable_catchError()` API for a granular custom error
boundary. The error component generated by this API is not a true
component format, as the wrapper provides additional args. Therefore,
the API is a function call by design rather than a boundary component to
avoid merging the `errorInfo` with the user-provided props from the
wrapper.
This API works for both the App/Pages Router. However, `retry()` is not
allowed in Pages Router as it depends on `router.refresh()`, and will
throw. Also, it's exported from `next/error` as there was already an
`Error` component for the [Pages
Router](https://nextjs.org/docs/pages/building-your-application/routing/custom-error#reusing-the-built-in-error-page).
### DevTools
<img width="508" height="90" alt="CleanShot 2026-03-13 at 03 02 46@2x"
src="https://github.com/user-attachments/assets/8b79b1f6-877d-4901-99f7-6a2b4d3e34fe"
/>
Docs to follow up: https://github.com/vercel/next.js/pull/89847
Closes NAR-768
---------
Co-authored-by: Josh Story <story@hey.com>
## Background
Next.js has an existing global config,
`experimental.staleTimes.dynamic`, that controls how long the
client-side router cache retains data from dynamic navigation responses.
By default this is 0 — meaning dynamic data is always refetched on every
navigation. Some users set this to a higher value (e.g. 30 seconds) so
that navigating back to a recently visited page reuses the cached
response instead of making a new request.
The limitation of the global config is that it applies uniformly to
every page in the app. Users have asked for the ability to set different
stale times for different pages — for example, a dashboard page that
should always show fresh data vs. a product listing page where it's
acceptable to show slightly stale data for a smoother navigation
experience.
## What this PR adds
`export const unstable_dynamicStaleTime` is a per-page version of
`staleTimes.dynamic`. It accepts a number in seconds with the same unit
and semantics, but scoped to a single page. When present, it overrides
the global config for that page — whether the per-page value is larger
or smaller than the global default. Static and cached responses are
unaffected.
When parallel routes render multiple pages in a single response, the
minimum value across all parallel slots wins, consistent with how stale
times compose generally.
Exporting this from a layout is a build error (pages only).
## Relationship to Cache Components
The recommended way to control data freshness in Next.js is to use Cache
Components (`"use cache"`) with `cacheLife`. This API exists as a
simpler migration path for apps that are already using
`staleTimes.dynamic` and haven't yet adopted Cache Components. It's
prefixed with `unstable_` not because the implementation is unstable,
but because it's not the recommended long-term idiom — apps should
migrate to Cache Components for more granular and composable control
over data freshness.
Based on the design from #88609.
Co-authored-by: Jimmy Lai <laijimmy0@gmail.com>
Co-authored-by: Sam Selikoff <sam.selikoff@gmail.com>
The `outerWorkUnitStore` in the public cache context could previously be `undefined`, which was needed when `"use cache"` was called without a `WorkUnitStore` (e.g. inside `generateStaticParams`) and during background revalidation of stale cache entries (SWR). With the previous commit providing a `GenerateStaticParamsStore` for `generateStaticParams`, the only remaining source of `undefined` was the SWR background revalidation path, which intentionally discarded the store to prevent cache life and tags from propagating back to the outer scope.
A `skipPropagation` flag was added to `CacheContext` to decouple the propagation concern from the store reference. The SWR background regen now passes `skipPropagation: true` while keeping the outer store intact for reads (e.g. `implicitTags`). Previously, nested `"use cache"` calls during SWR regen would pass empty `softTags` to `cacheHandler.get()` because the store was `undefined`. Now they correctly receive the page's implicit tags.
With `| undefined` removed from `PublicCacheContext.outerWorkUnitStore`, all unnecessary existence checks (`?.`, `if (x)`, `case undefined:`, ternaries) were cleaned up throughout `use-cache-wrapper.ts`, and the `shouldForceRevalidate` and `shouldDiscardCacheEntry` signatures were tightened from `WorkUnitStore | undefined` to `WorkUnitStore`.
Note that module-scope `"use cache"` usage (calling a cached function at the top level of a module) was already prevented before this change by the `WorkStore` check, which throws if no `WorkStore` is present. The new `WorkUnitStore` check is an additional guard that comes after it.
This also unblocks a future change to support root params inside `"use cache"` with cache keying. Since the outer work unit store is now always defined, `rootParams` can be threaded from the outer store into the cache store. The only exception will be `UnstableCacheStore`, which doesn't carry `rootParams` and can't include them in its cache key.
A new `use-cache-swr` test suite was added with a persistent cache handler that does not drop entries at revalidate time (unlike the default in-memory handler), which enables testing the server-side SWR code path that was previously uncovered.
> [!TIP]
> Best reviewed with hidden whitespace changes.
A new `GenerateStaticParamsStore` work unit store type is now provided during `generateStaticParams` execution. This enables root param getters (`import { lang } from 'next/root-params'`) to be called inside `generateStaticParams`, allowing shared helpers that internally access root params via the special import to be used in both Server Components and `generateStaticParams` without manually threading params.
Each `generateStaticParams` call now runs within a `workUnitAsyncStorage.run()` context carrying a `GenerateStaticParamsStore` with the correct `rootParams` (extracted from `parentParams` using the already-available `rootParamKeys`). The store extends `CommonWorkUnitStore`, providing `phase` and `implicitTags` which are not strictly necessary but convenient to keep call sites simple.
Request-time APIs (`headers()`, `cookies()`, `connection()`, `draftMode()`) now throw specific errors when called inside `generateStaticParams` instead of the previous generic "called outside a request scope" message. Framework-internal functions like `createSearchParamsFromClient` and `createParamsFromClient` throw `InvariantError` since they should never be reached in this context.
This change also unblocks a follow-up PR that removes `| undefined` from `PublicCacheContext.outerWorkUnitStore` in the use cache wrapper, since `"use cache"` is already supported inside `generateStaticParams` today but previously ran without a `WorkUnitStore`. With this store in place, requiring a `WorkUnitStore` in `"use cache"` won't break that existing usage.
## What?
When a webpack/turbopack loader produces broken code, error messages now
display **both** the original source and the generated code with source
map information, making it much easier to debug loader issues.
## Why?
Previously, when loaders returned invalid code, error messages only
showed the original source file (after source-map remapping). Users had
no way to see what the loader actually generated, making it hard to
diagnose why the code failed to parse. Showing both sides gives full
context about what went wrong.
## How?
### Turbopack Core (`turbopack-core`)
- **`Source::description()`** — New method on the `Source` trait
providing human-readable descriptions of where code comes from.
Implemented across all source types (`FileSource`, `VirtualSource`,
`WebpackLoadersProcessedAsset`, `PostCssTransformedAsset`, etc.),
producing chains like `"loaders [sass-loader] transform of file content
of ./styles.scss"`.
- **`AdditionalIssueSource`** — New struct to hold a labeled source
location. The `Issue` trait gains an `additional_sources()` method so
issues can expose supplementary code frames.
- **`GeneratedCodeSource`** — A wrapper that strips `GenerateSourceMap`
support from a source, ensuring the *generated* code is displayed as-is
rather than being remapped back to the original.
- **`IssueSource::to_generated_code_source()`** — Helper that detects
sources implementing `GenerateSourceMap` and wraps them in
`GeneratedCodeSource` for display. Used by `AnalyzeIssue` and
`ParsingIssue` to automatically attach generated code frames.
### Error Formatting
- **`turbopack-cli-utils`** — Renders additional sources in CLI issue
output.
- **`format-issue.ts`** — Renders additional sources in the browser
error overlay. Extracted `formatSourceCodeFrame()` helper to deduplicate
code-frame rendering between primary and additional sources.
- Long-line truncation (e.g. minified CSS from SCSS) is handled natively
by the Rust-based `codeFrameColumns` implementation.
### Type Definitions
- Added `SourcePosition`, `IssueSource`, and `AdditionalIssueSource`
interfaces to TypeScript types.
- Updated `PlainSource` (added `file_path`), `PlainIssue` (added
`additional_sources`), and NAPI bindings to pass the data through.
### Test Coverage
- **E2e tests** (`test/e2e/webpack-loader-parse-error/`) with custom
broken JS and CSS loaders, covering all 4 modes:
- **Development (Turbopack)** — Verifies parse errors show both original
and generated code via browser error overlay
- **Development (Webpack)** — Verifies error overlay shows the parse
error (webpack doesn't support additional sources)
- **Production (Turbopack)** — Verifies build failure output with full
error extraction and inline snapshots
- **Production (Webpack)** — Verifies build failure output with inline
snapshots
- Updated `test/development/sass-error/` snapshot to include the new
generated code frame for minified SCSS output.
### Example Output
When a loader produces broken code, users now see:
```
⨯ ./app/data.broken.js:3:1
Parsing ecmascript source code failed
1 | // This file will be processed by broken-js-loader
2 | // The loader will return invalid JavaScript with a source map
> 3 | export default function Data() {
| ^
4 | return <div>original source content</div>
5 | }
6 |
Expected '</', got '{'
Generated code of loaders [./broken-js-loader.js] transform of file content of app/data.broken.js:
./app/data.broken.js:3:46
1 | // Generated by broken-js-loader
2 | export default function Page() {
> 3 | return <div>this is intentionally broken {{{ invalid jsx
| ^
4 | }
5 |
Import trace:
Server Component:
./app/data.broken.js
./app/page.js
```
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <lukeisandberg@gmail.com>
### What?
Two fixes for the Turbopack build tracing introduced in #90397:
1. **Don't block SSG on Turbopack shutdown**: `workerMain()` no longer awaits the shutdown promise before returning. Trace event collection is deferred to `waitForShutdown()`, which the parent process awaits *after* SSG completes. This allows static generation and Turbopack persistence/cache-flush to run in parallel.
2. **Add persistence spans to `trace-build` allowlist**: `turbopack-build-events`, `turbopack-persistence`, and `turbopack-compaction` are now included in the `to-json-build.ts` allowlist so they appear in `.next/trace-build`.
### Why?
- The `await shutdownPromise` in `workerMain()` was too eager — it prevented the caller from acknowledging the build as complete and starting SSG until Turbopack persistence finished flushing to disk.
- The persistence/compaction spans emitted by Rust (`turbopack-persistence`, `turbopack-compaction`) were not in the `to-json-build.ts` allowlist, so they were silently filtered out of `.next/trace-build`.
### How?
**`impl.ts` (worker)**:
- Removed `await shutdownPromise` from `workerMain()` — it now returns build results immediately
- `waitForShutdown()` now returns `{ debugTraceEvents }` after awaiting shutdown, so trace events are collected only after all compilation events (including persistence spans) have been processed
**`index.ts` (parent)**:
- Moved `recordTraceEvents(debugTraceEvents)` from the `workerMain` result handler into the `shutdownPromise` `.then()` chain, so events are replayed into the parent reporter after shutdown completes
**`to-json-build.ts`**:
- Added `turbopack-build-events`, `turbopack-persistence`, `turbopack-compaction` to the allowlist
**Test updates**:
- Enabled `turbopackFileSystemCacheForBuild: true` in the trace-build test fixture
- Updated the Turbopack inline snapshot to include `turbopack-build-events`
Replaces the segment-path-matching scroll system with a simpler model
based on a shared mutable ScrollRef on CacheNode.
The old system accumulated segment paths during navigation and matched
them in layout-router to decide which segments should scroll. This was
necessary when CacheNodes were created lazily during render. Now that we
construct the entire CacheNode tree immediately upon navigation, we can
assign a shared ScrollRef directly to each new leaf node. When any
segment scrolls, it flips the ref to false, preventing other segments
from also scrolling. This removes all the segment path accumulation and
matching logic.
Fixes a regression where calling `refresh()` from a server action
scrolled the page to the top. The old system had a semantic gap between
`null` (no segments) and `[]` (scroll everything) — a server action
refresh with no new segments fell through to a path that scrolled
unconditionally. The new model avoids this: refresh creates no new
CacheNodes, so no ScrollRef is assigned, and nothing scrolls.
Repro: https://github.com/stipsan/nextjs-refresh-regression-repro
There is extensive existing test coverage for scroll restoration
behavior. This adds one additional test for the server action refresh
bug.
These tests have started failing in deploy mode in environments outside
of the new Vercel adapter. Shell related behavior is changing as part of
the new `partialFallbacks` flag and will not be backported to the old
CLI, so this gates the test to ensure it runs in the right deployment
environment.
In #91158, I wired up the plumbing to indicate whether a partial
fallback shell is upgradeable. The previous heuristic was too coarse.
This wires up the handling in `next start`.
This PR teaches the runtime to complete a fallback shell only as far as
prerendering allows. Instead of promoting a generic shell like
`/[one]/[two]` directly to `/c/foo`, it promotes it to the most specific
prerendered shell for that branch, `/c/[two]`, where `[one]` has
`generateStaticParams`. That means later requests can reuse the more
complete shell, while fully dynamic params continue to stream as dynamic
content.
With this change:
- the first request still serves the generic fallback shell immediately
- the background revalidate writes a more specific shell, not a fully
concrete route
- later requests for the same prerendered branch reuse that shell
- upgrading stops once only fully dynamic params remain
This keeps the runtime behavior aligned with the build-time
`remainingPrerenderableParams` contract from the downstack PR.
Partial fallback upgrading is currently too broad. Any PPR fallback shell can be treated as upgradeable, including routes with no `generateStaticParams` and shells that are already as specific as prerendering can make them.
In practice, that means we preserve `allowQuery` and enable background promotion for shells that should remain as partial fallbacks. That broadens the feature beyond the medium-cardinality case it was meant for and makes the build output imply per-param upgradeability when there are no prerenderable params left to fill.
This PR teaches static-path generation to compute `remainingPrerenderableParams` for each fallback shell: the unresolved pathname params that can still be filled by `generateStaticParams` in that branch. We then use that signal to gate partial fallback behavior.
With this change:
- `partialFallback` is only emitted for shells that still have prerenderable params remaining
- `allowQuery` with params is only preserved for those shells
Effectively, this limits partial fallback upgrading to shells that can still be made more specific by prerendering.
### What?
Switch Turbopack's hash encoding for chunk and asset output filenames
from hexadecimal (base16) to base40, using the alphabet \`0-9 a-z _ - ~
.\`. Version hashes (used for HMR update comparison, not filenames) use
base64 instead.
### Why?
Base40 encodes the same number of bits in fewer characters than hex,
producing shorter output filenames. All 40 characters are RFC 3986
unreserved (URL-safe) and safe on case-insensitive filesystems (macOS
HFS+/APFS, Windows NTFS).
Hash truncation lengths are reduced proportionally to maintain
equivalent collision resistance:
| Context | Before (hex) | After (base40) | Entropy |
|---|---|---|---|
| Content hash in chunk filenames | 16 chars | 13 chars | ~69 bits |
| Content hash in asset filenames | 8 chars | 13 chars | ~69 bits |
| Ident disambiguator hash | 8 chars | 7 chars | ~37 bits |
| Long-path prefix hash | 5 chars | 4 chars | ~21 bits |
### How?
**New encoding module** (\`turbo-tasks-hash/src/base40.rs\`):
- Defines the base40 alphabet and length constants (\`BASE40_LEN_64 =
13\`, \`BASE40_LEN_128 = 25\`)
- Implements a generic \`encode_base40_fixed<N>\` helper to avoid
duplication
- Public API: \`encode_base40(u64) -> String\` and
\`encode_base40_128(u128) -> String\`
**New base64 encoding** (\`turbo-tasks-hash/src/base64.rs\`):
- \`encode_base64(u64) -> String\` — 11-char base64 (no padding) for
version hashes
- Version hashes don't appear in URLs or filenames, so base64 is safe
and shorter
**New \`HashAlgorithm\` variants** (\`turbo-tasks-hash/src/lib.rs\`):
- \`Xxh3Hash64Base40\` and \`Xxh3Hash128Base40\` added alongside
existing hex variants
- Existing hex variants kept for internal manifests and identifiers
**\`ContentHashing\` moved to \`turbopack-core\`**:
- Moved from \`turbopack-browser\` to
\`turbopack-core/src/chunk/mod.rs\` so both \`BrowserChunkingContext\`
and \`NodeJsChunkingContext\` can use it
**Separate chunk vs asset content hashing**:
- \`BrowserChunkingContext\`: \`content_hashing\` renamed to
\`chunk_content_hashing\` (optional), new \`asset_content_hashing:
ContentHashing\` field (non-optional, defaults to 13 chars)
- \`NodeJsChunkingContext\`: new \`asset_content_hashing:
ContentHashing\` field (non-optional, defaults to 13 chars)
- Builder methods: \`use_content_hashing()\` renamed to
\`chunk_content_hashing()\`, new \`asset_content_hashing()\`
**Version hashes switched to base64**:
- \`turbopack-nodejs/src/ecmascript/node/version.rs\`
- \`turbopack-dev-server/src/html.rs\`
- \`turbopack-browser/src/ecmascript/version.rs\`,
\`merged/version.rs\`, \`list/version.rs\`
**Other callers updated** (15 files across turbopack and next-core):
- All chunk/asset content hashing switched from \`Xxh3Hash128Hex\` →
\`Xxh3Hash128Base40\`
- \`ContentHashing::Direct { length }\` reduced from 16 → 13
- Asset path truncations use full 13-char base40 hash (matching chunk
filenames)
**Exception — \`wasm_edge_var_name\`** (\`turbopack-wasm/src/lib.rs\`):
- Kept as \`Xxh3Hash128Hex\` because the hash is used as part of a
JavaScript variable name (\`wasm_{hash}\`), and base40 characters \`-\`,
\`~\`, \`.\` are not valid JS identifier characters.
**Scope — NOT changed:**
- Webpack configuration (unchanged)
- Internal manifests (\`routes_hashes_manifest\`,
\`project_asset_hashes_manifest\`)
- Internal identifiers (font naming, external module hashing, data URI
sources, debug IDs)
- SRI hashes (SHA-based Base64, different purpose)
---------
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
### What?
- Add canonical edge entrypoint metadata to edge function definitions in
the middleware manifest.
- Surface that canonical entrypoint through `build-complete` as
`edgeRuntime` metadata in adapter outputs.
- Update middleware and adapter tests to assert the new metadata instead
of relying on bundler-specific emitted file names.
### Why?
Adapter consumers need a stable way to load and invoke edge outputs
without depending on `next-server`'s sandbox handling or guessing which
emitted file is the executable entrypoint. The existing checks were also
brittle across webpack and Turbopack because the emitted file lists can
differ.
### How?
- Add an optional `entrypoint` field to `EdgeFunctionDefinition` and
populate it in both the webpack middleware manifest generator and the
Turbopack manifest emitters for app, pages, and middleware edge
functions.
- In `packages/next/src/build/adapter/build-complete.ts`, use that
canonical entrypoint for `filePath` and expose `edgeRuntime.modulePath`,
`edgeRuntime.entryKey`, and `edgeRuntime.handlerExport` for edge
outputs.
- Adjust the middleware manifest tests to validate cross-bundler
invariants and add adapter-config coverage for the new `edgeRuntime`
fields.
### What?
This PR wires custom `cacheHandler` / `cacheHandlers` through the
remaining edge entrypoints so edge app pages, edge app routes, edge
pages SSR, middleware, and edge API routes can all see the configured
handlers.
It also adds an e2e regression suite covering:
- pages-router ISR revalidation with a custom incremental cache handler
- app-router cache handlers with cache components enabled
- edge app page + edge app route wiring when cache components are
disabled
### Why?
We already support custom cache handlers in non-edge paths, but several
edge code paths were not forwarding that configuration into the
generated entrypoints/runtime wrappers. That meant custom cache handlers
could be skipped in edge rendering and edge route execution, and we did
not have regression coverage around those cases.
### How?
- pass `cacheHandler` / `cacheHandlers` through the JS build entry
plumbing for edge server entries
- inject cache handler imports/registration into the webpack edge
templates and loaders
- mirror the same wiring in the Turbopack/Rust entry generation for edge
app pages, app routes, pages SSR, and middleware
- update the edge route wrapper to initialize and register cache
handlers before invoking the route module
- extend `next-taskless` template expansion with raw injection support
so the generated edge templates can add imports and registration code
- add `test/e2e/cache-handlers-upstream-wiring` fixtures to cover
pages/app, edge/non-edge, and revalidation behavior
Part 1 of 2. This commit adds the server-side infrastructure for
size-based segment bundling but does not change any observable behavior.
The client-side changes that actually consume bundled responses are in
the next commit.
At build time, a measurement pass renders each segment's prefetch
response, measures its gzip size, and decides which segments should be
bundled together vs fetched separately. The decisions are persisted to a
manifest and embedded into the route tree prefetch response so the
client can act on them.
The decisions are computed once at build and remain fixed for the
lifetime of the deployment. They are not recomputed during
ISR/revalidation — if they could change, the client would need to
re-fetch the route tree after every revalidation, defeating the purpose
of caching it independently.
Refer to the next commit for a full description of the design and
motivation.
## Config
experimental.prefetchInlining accepts either a boolean or an object with
threshold overrides (maxSize, maxBundleSize). When true, the default
thresholds are used (2KB per-segment, 10KB total budget). The auto
behavior will eventually become the default. The config will remain
available for overriding thresholds.
When the instant navigation testing cookie is set, the debug static
shell path previously called `getFallbackRouteParams()` which treats all
dynamic segments as fallback params regardless of
`generateStaticParams`. This caused two issues:
- Root params (via `next/root-params`) errored with a
`NEXT_STATIC_GEN_BAILOUT` 500 response because the root layout's param
access returned a hanging promise, which was treated as uncached data
accessed outside of `<Suspense>`.
- Params defined in `generateStaticParams` were incorrectly excluded
from the instant shell, shown behind Suspense fallbacks instead of
resolving.
The fix uses `prerenderInfo?.fallbackRouteParams` from the prerender
manifest instead, which correctly distinguishes between statically known
params (defined in `generateStaticParams`) and unknown params. For
prerendered URLs, the outer guard (`!isPrerendered`) prevents the
fallback path from being entered, so no fallback params are set and all
params resolve normally.
To make this work in dev mode, the dev server now populates
`fallbackRouteParams` in the prerender manifest's dynamic route entries,
which were previously left as `undefined`.
Additionally, the `fallbackParams` request metadata is now overridden
when rendering a debug static shell. Without this override, the staged
rendering would use the smallest set of fallback params across all
prerendered routes (set by `base-server.ts` for dev validation), which
may omit fallback params for the current URL when a different value for
the same param is defined in `generateStaticParams`. The override
ensures the route-specific fallback params are used instead.
When the Node.js inspector is active (e.g. via `next dev --inspect`),
dimming wraps console arguments in a format string which defeats
inspector affordances such as collapsible objects and
clickable/linkified stack traces.
This adds an early return in `convertToDimmedArgs` that skips dimming
entirely when `inspector.url()` is defined. Terminal output without
`--inspect` is unchanged.
Ideally we would only skip dimming when a debugger frontend is actually
attached, but Node.js does not expose a synchronous API for that —
detecting it would require async polling of the `/json/list` HTTP
endpoint.
Test plan:
- Unit tests in `console-dim.external.test.ts`
- Manual: dimming almost never triggers now since we switched to in-band
validation (no separate render pass). To reproduce manually, use a
client component that calls `console.error(new Error(...))` during SSR,
e.g.:
1. `__NEXT_CACHE_COMPONENTS=true pnpm next dev
test/e2e/app-dir/server-source-maps/fixtures/default --inspect`
2. Open `http://localhost:3000/` in Chrome
3. Click the Node.js DevTools button
4. Load `http://localhost:3000/ssr-error-log`
5. Verify the first error in DevTools has source-mapped, expandable
stack frames
Before:
<img width="1496" height="848" alt="before"
src="https://github.com/user-attachments/assets/8b4358e1-06ec-4078-b6cc-269986e295e7"
/>
After:
<img width="1496" height="842" alt="after"
src="https://github.com/user-attachments/assets/42f33c6f-f025-410f-b44c-2ccd88fe0cfe"
/>
Previously, MPA page loads (reload, anchor navigation) while the instant
navigation lock was held would skip hydration entirely and hard-reload
when the lock released. This meant the page was non-interactive during
the locked period and tests couldn't assert on the hydrated React tree.
Now the page actually hydrates from the static shell. The server injects
a script that fetches the static RSC payload, and the client uses
createFromFetch with allowPartialStream so React can hydrate without
erroring on the incomplete Suspense boundaries. When the lock is
released, a soft refresh fetches the dynamic data (falling back to hard
reload if the router hasn't initialized yet).
Also refactors the cookie protocol to encode the lock state as a JSON
array (`[0]` = pending, `[1, null]` = captured MPA, `[1, {}]` = captured
SPA) for better integration with the devtools UI. This is somewhat
entangled with the hydration changes so they're grouped together, but
can be reviewed as atomic commits.
Adds extra context to errors that we think may have prevented
validation:
<img width="816" height="765" alt="Screenshot 2026-03-12 at 22 07 40"
src="https://github.com/user-attachments/assets/35ce60d0-4840-496e-b91e-62ada4096f64"
/>
This is particularly useful for errors where the component comes from
node_modules, so it gets ignore-listed. previously, all we got is the
message with no stack, no we at least get a location. One notable case
of this is `next/dynamic` with `ssr: false`.
<img width="802" height="556" alt="Screenshot 2026-03-12 at 22 08 30"
src="https://github.com/user-attachments/assets/b1240732-3346-4056-89b2-9a864a3166f2"
/>
Initial version of build-time instant validation.
During prerendering, if we have `unstable_instant` configs, we perform a
complete staged render of the page (including dynamic data). This render
mocks request data (`params`/`searchParams`/`cookies`/`headers`) using
the `samples` provided in `unstable_instant`. Then, we use the staged
chunks to run instant validation in mostly the same way that we would in
dev. Most changes in the validation flow are related to samples, which
also need to be passed into the client render to mock APIs like
`useParams`.
### Providing Mock data
We run everything in a fresh workStore+workUnitStore to (hopefully)
avoid any data from the prerender from sneaking in. This is quite
involved.
A big part of the trickiness is that if you do e.g. `(await
cookies()).get('myCookie')` but the `samples` don't contain `myCookie`,
we want the validation to error and require explicitly saying what state
`myCookie` is in.
If it's available, use
```ts
export const unstable_instant = {
// ...
samples: { cookies: [{ name: "myCookie", value: "someValue" }] }
}
```
If it's not available, use
```ts
export const unstable_instant = {
// ...
samples: { cookies: [{ name: "myCookie", value: null }] }
}
```
This explicitness makes it harder to e.g. unintentionally validate a
logged-out state.
Note that even validating a `prefetch: 'static'` page might require
samples, because we need the shared parent for the navigation to be
(mostly) complete. We might be able to relax this in the future, but
it's not straightforward.
Another issue is that a segment might access a missing sample in a
"benign" way, i.e. where it wouldn't block validation (or be inside a
suspense boundary). Ideally we wouldn't require providing a sample for
usages like that, but currently we do.
### Instrumenting APIs to detect missing samples
All request APIs are instrumented in this way.
- `params` throw when accessing a property that was not declared in
`samples`. same for the object from `useParams()`. `in` is allowed
because we know the shape of the params object statically, based on the
routing structure.
- `searchParams` throw when accessing a property that was not declared
in `samples` and `in` checks.
- objects returned from `cookies()`, `headers()`, `useSearchPrams()`
throw on `get()` and `has()`.
There's a lot of tests for this behavior in `instant-validation-build`.
One gap is that iteration/enumeration is currently not instrumented.
We're punting on it for now and will revisit in a follow-up.
### Relation to prerender
We kick off the validation after the prerender, in
`renderToHTMLOrFlightImpl`. Ideally, we should kick this off somewhere
higher up, separately from the prerender itself, but we're leaving that
for future work.
Note that if we do multiple prerenders for the same page (e.g. due to
`generateStaticParams`, we'll only run validation for the first
prerender for that route. `generateStaticParams` is not currently
integrated into the validation at all, i.e. we don't use those params to
validate, so this is safe. All params need to be provided via `samples`.
This will likely change in the future, but we're not sure how yet.
### Disabling build validation
Build-time instant validation is experimental, and has more potential
problems to deal with than dev validation. To allow incremental rollout
and testing, we allow disabling it per instant config:
```ts
export const unstable_instant = {
unstable_disableBuildValidation: true
// ...
}
```
for parity, there's also `unstable_disableBuildValidation`, but we don't
expect that to see much use.
## Summary
- preserve Flight/RSC headers on the Proxy request instance when URL
normalization is skipped (`skipProxyUrlNormalize`)
- preserve the `_rsc` query on the request URL in that same mode so
custom proxy logic can access it
- document this behavior in the Proxy API docs, including guidance for
`NextResponse.rewrite()` vs custom `fetch` rewrites
- add an e2e regression in `skip-trailing-slash-redirect` to verify
middleware receives `_rsc` and all RSC headers on the request instance
## Testing
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_MODE=dev pnpm testheadless
test/e2e/skip-trailing-slash-redirect/index.test.ts`
### What?
Fixes webpack dev 404s when parallel routes have children nested inside
a route group (e.g. `@slot` + `(b1)/page`) and when a parallel route has
`page.tsx` directly inside it (e.g. `@slot/page.tsx`).
### Why?
Two separate issues caused 404s for parallel routes with route groups:
1. **Sort order**: The three `appPaths` sort sites used bare `.sort()`
(lexicographic ordering). `renderPageComponent` relies on
`appPaths[appPaths.length - 1]` being the children/root page. Without
route groups this works because `@` (0x40) sorts after lowercase letters
— `/@slot/page` comes before `/page`. But when the children page is
inside a route group, `(` (0x28) sorts before `@` (0x40), flipping the
order: `/(b1)/page < /@slot/page`. Now the last item is the slot page
instead of the children page, so the manifest lookup fails and a 404 is
returned.
2. **Double `__PAGE__` segment**: When a parallel route like `@slot` has
`page.tsx` directly inside it, `resolveParallelSegments` returns
`[PAGE_SEGMENT]` (an array) to signal that the loader should recurse
into the slot directory to pick up layout/default files. However, the
segment key normalization was mapping `PAGE_SEGMENT` to
`PAGE_SEGMENT_KEY` (`__PAGE__`), while the recursion would also produce
a `__PAGE__` for children — creating a double `__PAGE__` nesting (`slot:
['__PAGE__', { children: ['__PAGE__', ...] }, ...]`).
### How?
**Sort order fix**: Adds a `compareAppPaths` comparator in
`packages/next/src/shared/lib/router/utils/app-paths.ts` that sorts
paths containing `/@` segments before paths without them, ensuring the
children page is always last. Replaces the three bare `.sort()` calls
with `.sort(compareAppPaths)` in:
- `packages/next/src/build/entries.ts`
- `packages/next/src/server/lib/router-utils/setup-dev-bundler.ts`
-
`packages/next/src/server/route-matcher-providers/dev/dev-app-page-route-matcher-provider.ts`
**Loader tree fix**: Maps `PAGE_SEGMENT` to `'(slot)'` in the segment
key normalization (same as `PARALLEL_VIRTUAL_SEGMENT`), instead of
`PAGE_SEGMENT_KEY`. By the time this code is reached,
`parallelSegmentKey === PAGE_SEGMENT` only occurs for the array case
(the string case is handled by the early return), so this is safe. The
actual `__PAGE__` is correctly created by the recursion.
**Before:** `slot: ['__PAGE__', { children: ['__PAGE__', ...] }, ...]`
**After:** `slot: ['(slot)', { children: ['__PAGE__', ...] }, ...]`
Also adds an e2e test that verifies the fix across all modes (Turbopack
dev, Turbopack start, webpack dev).
### What?
Adds a new `experimental.lightningCssFeatures` config option that lets
users control which CSS features lightningcss should always transpile
(`include`) or never transpile (`exclude`), regardless of browserslist
targets.
```js
// next.config.js
module.exports = {
experimental: {
useLightningcss: true,
lightningCssFeatures: {
include: ['light-dark', 'oklab-colors'],
exclude: ['nesting'],
},
},
}
```
### Why?
Currently, lightningcss feature transpilation is determined solely by
browserslist targets. There's no way to force transpilation of a
specific feature (e.g., `light-dark()`) when targeting modern browsers
that already support it, or to skip transpilation of a feature that the
user wants to preserve as-is.
This is useful for:
- Forcing polyfill-style transpilation for features with incomplete
browser support
- Testing transpiled output for specific features
- Opting out of specific transforms that may interfere with CSS tooling
downstream
### How?
**TypeScript config & validation:**
- `config-shared.ts`: `LIGHTNINGCSS_FEATURE_NAMES` const array (single
source of truth) with `LightningCssFeature` type derived from it, and
`LightningCssFeatures` interface
- `config-schema.ts`: Zod validation using
`z.enum(LIGHTNINGCSS_FEATURE_NAMES)` for both include/exclude arrays
**Feature name → bitmask mapping (Rust, shared via NAPI):**
- `crates/next-core/src/next_config.rs`:
`lightningcss_feature_names_to_mask()` maps feature name strings to
`lightningcss::targets::Features` bitflag constants and returns a
`Result<u32>` bitmask. Unknown feature names produce an error via
`bail!`.
- Exposed to JS via NAPI as `lightningcssFeatureNamesToMaskNapi`,
callable through `bindings.css.lightning.featureNamesToMask(names)`
- Single source of truth for feature name resolution — both webpack and
Turbopack paths use this Rust function
**Webpack path:**
- `global.ts` / `modules.ts` pass config through to loader options
- `loader.ts` calls `featureNamesToMask()` via the native bindings to
compute include/exclude masks, then passes them to the SWC `transform()`
call
- The `lightningCssFeatures` without `useLightningcss` warning only
shows when using webpack (Turbopack always uses lightningcss)
**Turbopack path (Rust):**
- `next_config.rs`: `LightningCssFeatures` struct for deserialization,
accessor methods converting names → u32 bitmask via
`lightningcss_feature_names_to_mask()`
- Config flows through `CssOptionsContext` → `ModuleType::Css` →
`CssModuleAsset` (using `LightningCssFeatureFlags { include, exclude }`
struct) → `process.rs` where bitmasks are merged into
`lightningcss::targets::Targets { include, exclude }`
- Uses `u32` bitmask representation throughout to avoid adding
lightningcss dependency to non-CSS crates
**Defaults preserved:** `Nesting | MediaRangeSyntax` for Turbopack,
`Nesting` for Webpack. User `include` is OR-ed on top, user `exclude`
masks bits off.
**Feature names:** 21 individual features (bit 0–20) + 3 composite
groups (`selectors`, `media-queries`, `colors`), all in dash-case.
### Test
- **Include test**
(`test/e2e/app-dir/experimental-lightningcss-features/`) — targets
Chrome 123 (which natively supports `light-dark()`) with `include:
['light-dark']`, then asserts the CSS output contains lightningcss
transpilation markers (`--lightningcss-light`, `--lightningcss-dark`)
instead of raw `light-dark()`.
- **Exclude test**
(`test/e2e/app-dir/experimental-lightningcss-features-exclude/`) —
targets Chrome 100 (which does NOT support `light-dark()` natively) with
`exclude: ['light-dark']`, then asserts the CSS output preserves raw
`light-dark()` calls and does not contain transpilation markers.
This puts the partial fallback upgrading feature introduced in
https://github.com/vercel/next.js/pull/89063 behind a flag to make it
easier to test the rollout/eventual stabilization in isolation.
## What?
This PR improves error reporting for webpack loaders by:
1. **Preserving loader paths in error messages**: Modified the webpack
message formatter to rewrite "Module build failed" headers to preserve
the loader path information while removing the verbose prefix. This
makes errors more readable in both CLI output and error overlays.
2. **Better error wrapping for non-Error throws**: When loaders throw
non-Error values (strings, objects, etc.), the error is now wrapped with
loader path information in the stack trace, making it easier to identify
which loader caused the error.
3. **Catching deferred loader errors**: Added handling for errors that
occur after loader completion (e.g., unhandled Promise rejections,
setTimeout throws) by:
- Adding an `unhandledRejection` handler in the IPC layer
- Delaying resolution in webpack-loaders transform to allow error
handlers to fire before sending the 'end' message
4. **Conditional `(from ...)` annotation**: The `(from
/path/to/loader.js)` annotation is only added when the loader path is
**not already visible** in the error's stack trace. This avoids
redundant noise for errors that already include the loader in their
stack, while still providing the annotation for errors without loader
context (string throws, errors without stacks, etc.).
5. **Hiding internal Next.js loader paths**: Internal Next.js loader
paths (under `next/dist/`) are filtered from error messages since
they're noise for users. Only user-authored loader paths are shown.
6. **Comprehensive test coverage**: Added e2e tests covering various
error scenarios (Error throws, string throws, Promise rejections,
setTimeout errors, no-stack errors, filesystem errors) with verification
of both CLI output and error overlay display.
## Why?
Webpack loader errors were previously difficult to debug because:
- The verbose "Module build failed" prefix obscured the actual error
message
- Non-Error throws lost their context and loader information
- Deferred errors (Promise rejections, async throws) were not properly
surfaced
- Error overlays didn't clearly show which loader caused the problem
- The `(from ...)` annotation was always added, even when the loader
path was already in the stack trace (addressed per review feedback)
## How?
- **format-webpack-messages.ts**: Changed from filtering out loader
headers to rewriting them, preserving the `(from ./loaders/...)`
information. Internal Next.js loader paths are hidden.
- **webpack-loaders.ts**: Added error wrapping for non-Error values,
deferred resolution to catch async errors, and conditional `(from ...)`
annotation that checks `err.stack` before appending. Resolves loader
paths into an array, then checks if any path appears in the stack before
deciding to annotate.
- **ipc/index.ts**: Added `unhandledRejection` handler to surface
Promise-related errors.
- **Test loaders**: `error-loader.js` and `fs-error-loader.js` delegate
to a `create-error.js` helper that creates errors and invokes the loader
callback inside `setTimeout`. This ensures the loader file path is
absent from the stack trace, exercising the conditional `(from ...)`
annotation path. Other loaders (string-error, promise-error,
timeout-error, no-stack-error) test different error scenarios directly.
- **Test suite**: Comprehensive e2e tests verify CLI output and error
overlay for each error type. Turbopack-specific tests cover promise
rejections, timeout errors, no-stack errors, and filesystem errors.
---------
Co-authored-by: Claude <noreply@anthropic.com>
### What?
Changes Turbopack's error overlay to show specific SWC diagnostic
messages as the error title instead of generic messages like "Parsing
ecmascript source code failed" or "Ecmascript file had an error".
### Why?
Previously, all SWC parse/analysis errors in Turbopack showed a generic
title (e.g. "Parsing ecmascript source code failed") in the redbox
header, with the actual specific error message buried in the description
below the code frame. This made it harder for developers to quickly
understand what went wrong.
**Before:**
```
Parsing ecmascript source code failed
> 1 | export default () => <div/
| ^
Expected '>', got '<eof>'
```
**After:**
```
Expected '>', got '<eof>'
> 1 | export default () => <div/
| ^
Parsing ecmascript source code failed
```
### How?
**Core change** in
`turbopack/crates/turbopack-swc-utils/src/emitter.rs`:
When the `IssueEmitter` has a `self.title` set (the generic title like
"Parsing ecmascript source code failed"), the SWC diagnostic message is
now used as the issue title, and the generic title is demoted to the
description. When `self.title` is not set, the existing behavior is
preserved (first line of message becomes title, rest becomes
description).
**Test updates** across ~15 test files:
Updated all `isTurbopack` branches in test expectations to reflect the
swapped title/description. Only Turbopack-specific branches were
modified; webpack and rspack expectations are unchanged.
**New test suite** (`test/development/app-dir/ecmascript-error-title/`):
Dedicated tests verifying that both syntax errors (e.g. `Expected '>',
got '<eof>'`) and analysis errors (e.g. `the name 'Table' is defined
multiple times`) show the specific SWC message as the redbox title.
**Turbopack snapshot updates:**
4 snapshot files renamed to reflect new titles (e.g. `Parsing ecmascript
source code failed-*.txt` → `Expression expected-*.txt`).
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixes cross-zone server action redirect failure where `redirect()`
inside a server action produces a blank page when the target path is
served by a different Next.js zone (separate Vercel project, same
domain)
- Adds build ID validation to `server-action-reducer.ts`, matching the
existing pattern in `fetch-server-response.ts`
- Adds e2e test in the deployment-skew test suite to verify server
action redirects with build ID mismatch trigger MPA navigation
## Background
When a server action calls `redirect()`, the server pre-fetches the
redirect target and includes the RSC data in the response. In a
multi-zone setup (same domain, different Next.js projects), this
pre-fetched RSC data comes from a different build with a different build
ID.
The `fetch-server-response.ts` file already checks for build ID
mismatches during regular navigations (line 245-251) and triggers MPA
navigation when detected. However, `server-action-reducer.ts` was
missing this check, causing the client to try to apply the foreign RSC
payload — resulting in a blank page.
## Fix
Added a build ID check in `fetchServerAction()` after parsing the RSC
response. When a mismatch is detected, the flight data is discarded. The
existing reducer logic at line 364-370 already handles this case:
```typescript
if (flightData === undefined && redirectLocation !== undefined) {
return completeHardNavigation(state, redirectLocation, navigateType)
}
```
This triggers an MPA navigation (full page load), which is the correct
behavior for cross-zone redirects.
## Repro
- https://repro-main-app.vercel.app/test-redirect
- Red button (server action redirect) → blank page (before fix) / full
page load (after fix)
- Green link (normal `<a>`) → works fine
Closes https://linear.app/vercel/issue/NEXT-4856
---------
Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
When matching a URL against the known route trie, if a URL part matches
a static child but the remaining parts don't resolve within that child's
subtree, the algorithm incorrectly falls back to a dynamic catch-all
sibling at the same level. This causes the catch-all to consume the
already-matched static part plus the remaining parts, producing a
synthetic entry for the wrong route.
For example, given routes `/dashboard/settings/profile` and
`/dashboard/[...catchall]`, navigating to `/dashboard/settings/profile`
could incorrectly match the catch-all with params `["settings",
"profile"]` instead of the intended static route.
The fix treats the static child match as authoritative: if a real
(non-placeholder) static child exists but its subtree can't resolve the
remaining URL parts, bail out to server resolution rather than trying
the dynamic sibling. This is safe because it just means an extra network
request for routes whose subtree hasn't been fully discovered.
In https://github.com/vercel/next.js/pull/69282, we landed support for
Partial Fallback Prerendering. The intention behind this feature was to
be able to upgrade from a partial fallback (essentially the UI produced
when `await params` is dynamic, ie the param was not part of
`generateStaticParams` at build time) to a route shell (when the param
values are filled in, and thus we can prerender more of the page).
It was reverted in https://github.com/vercel/next.js/pull/79258 because
we didn't add first-class support to this in the build output API and
the necessary infrastructure changes to perform similar behavior that
this PR does to trigger a background revalidation with params filled in.
This re-lands the feature in Next.js now that upstream support landed
(https://github.com/vercel/vercel/pull/14703,
https://github.com/vercel/vercel/pull/15338, and Vercel infra changes
which are rolling out)
When a partially static page with `unstable_instant: { prefetch:
'runtime' }` is loaded as initial HTML (PPR resume), the server now
spawns a runtime prerender after the resume render fills the caches,
embedding the result as the `p` field on the `InitialRSCPayload`. The
client extracts this stream during hydration via
`processRuntimePrefetchStream` and writes the data into the segment
cache.
This mirrors the existing navigation path where
`generateStagedDynamicFlightRenderResult` embeds the runtime prefetch
stream in the `NavigationFlightResponse`. Both paths reuse the same
`spawnRuntimePrefetchWithFilledCaches` function (renamed from
`spawnRuntimePrefetchDuringNavigation` since it is no longer specific to
navigations).
The recent Cached Navigations feature introduced navigation-level
caching into the segment cache, where static and dynamic RSC data from
navigations and initial HTML loads is cached so that repeat visits can
be served instantly. This feature is currently gated behind
`cacheComponents`, meaning it activates for all projects that enable
Cache Components.
This change introduces a new `experimental.cachedNavigations` flag that
independently controls the Cached Navigations behavior. The flag
defaults to `false` so we can investigate potential regressions that the
feature might have introduced without affecting the broader Cache
Components functionality.
The flag requires `cacheComponents` to also be enabled and throws a
configuration error if `cachedNavigations` is set without it. On the
server, the staged rendering paths in `app-render.tsx` (both RSC request
handling and initial HTML rendering) now check `cacheComponents &&
cachedNavigations` before producing staged responses. On the client,
`process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS` is used alongside
`process.env.__NEXT_CACHE_COMPONENTS` for compile-time dead code
elimination in the stream tee (`app-index.tsx`) and the response
processing (`fetch-server-response.ts`).
The cached navigations test suite enables the flag via `next.config.ts`,
and the Cache Components CI jobs export
`__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true` to maintain test coverage.