Sync the React Ledger stack at c126173606 into the stable and
experimental vendored packages using local builds. Includes Ledger
forwarding and work-batch completion.
Changing a page’s search params doesn’t change which page it is, but it may
change the data that page needs. The router currently mixes these concerns
by appending search params to `__PAGE__` segments. Remove that encoding so
we can compare which route is being rendered separately from the param
values used to render it.
Param values are now compared through the existing VaryPath data structure,
while route structure is compared during the existing tree traversals.
Each traversal can decide which comparisons matter for the work it’s doing.
Client-side comparisons and history restoration still need the search
params, so store them in a separate FlightRouterState slot for now. As
before, previous-page search params are stripped from normal request
headers. This is a temporary step that lets us migrate incrementally.
Eventually, FlightRouterState will be replaced by a type that represents
the route and its params more directly.
This is mostly a refactor, though it fixes an accidental inconsistency in
search-param handling compared with regular route params, avoiding some
redundant prefetch work.
Fully cached pages already track their vary params correctly. This
separation prepares us to do the same for partially dynamic segments during
navigation, so changing an unrelated param won’t require rendering their
dynamic content again.
With the route structure and the param values compared separately, a
navigation can now tell whether a segment's data has to be re-rendered at
all: `didReadChangedParam` walks the current and next vary paths in
lockstep and reads the node's `varyParams` only when a param actually
differs. A regular navigation (Default or Gesture) keeps a page or layout
whose output read none of the changed params — under a new node keyed at
the new vary path, written to the BFCache like any other — and does the
same for the head, which is keyed under its page's position and shares
data only when that position is unchanged. Refreshes still fetch, and
back/forward still restores the exact entry from the BFCache. A test
covers the head case where the metadata read searchParams but the page
did not.
The head (metadata) is already fetched and cached like a page segment, keyed
at its own metadata vary path, but the render tree stored it as two extra
fields on the page node's CacheNode. Every navigation function threaded the
seed head and the metadata vary path down to every child so the page node
could resolve it, using a copy of the segment resolution logic that only ran
for pages, and the BFCache faked a head-only entry by writing the head into a
segment entry's rsc slot.
Store the head as an ordinary CacheNode instead, on a one-node route tree kept
beside the route's render tree. A new RootRouteTree type names this pair: it
is what a server response decodes to, what the router state holds, and what a
navigation produces. The head node is created by the same
createRenderTreeForSegment as page segments, so the BFCache, hydration,
history traversal, cache read and deferred-data paths apply to it unchanged,
and startPPRNavigation returns the tree and head tasks together. The Head
component reads the node directly, so findHeadInCache is gone.
The head is reused on the same terms as the page it belongs to: an unchanged
vary path and not a same-page navigation. When every segment is cached but
the head is not, the navigation sends the server's metadata-only request
instead of refetching the page and discarding it, and a response that carries
no head is treated as a mismatch like a missing segment. The head's BFCache
entry also has its stale time updated from dynamic responses, which never
happened before.
Refactors the CacheNode tree to use the RouteTree<T> data type. CacheNode
used to be a tree-shaped object, with a similar structure to RouteTree<T>.
CacheNode is now a data container instead, embedded inside a RouteTree
structure.
So the usage sites change from CacheNode to RouteTree<CacheNode>.
As a result, we can now diff against the render tree directly during
prefetches and navigations, instead of diffing against FlightRouterState.
This gets us closer to migrating everything away from FlightRouterState
to our shared data types.
Since CacheNode is now a data container, it also records which params its
data was rendered under: `varyParams` is the source of the params `rsc`
depends on, carried from the decoded response (as a settled thenable that
`readVaryParams` reads) onto the node and into its BFCache entry, and
filled in alongside `rsc` when a deferred response arrives. Nothing reads
it yet; it's here so the node describes its own data.
Full motivation and plan here:
https://app.notion.com/p/vercel/Turbopack-pnpm-Global-Virtual-Store-383e06b059c480579403ddfd71cc2d40?source=copy_link
The goal is to allow `DiskFileSystem` to traverse outside of it's own
root to other configured `DiskFileSystem`s when following symlinks. We
may allow traversal in other situations in the future, but this is
limited to symlink resolution for now.
## Global Virtual Store
The motivation for this is to enable [pnpm's Global Virtual Store
feature](https://pnpm.io/global-virtual-store) (and there are other
package managers doing this, including nub and bun).
We'd expose the ability to manually configure this in `next.config.js`,
but we should also auto-configure ourselves for popular package managers
(or at least make a best effort to do so, the `PNPM_HOME` semantics can
be complicated). The `ignoreIfMissing` option is provided for this
situation: We can configure a bunch of roots automatically, and they
only actually get set up if they exist, the check for directory
existence is cheap.
## NFT changes
This requires a couple extensions to the `*.nft.json` file format:
https://github.com/vercel/next.js/pull/98469
## Related Issues
- #93556
- https://github.com/pnpm/pnpm/issues/14972
Implements the following extension:
```typescript
export interface NftFileList {
// File paths relative to the directory containing the `.nft.json` file, or
// the current root (if inside of `NftAdditionalRoot`).
//
// When using webpack, these paths may exist outside of the tracing root. The
// [`@vercel/next` package ignores these paths][vc-next].
//
// When using Turbopack, these paths are all guaranteed to exist within the
// `turbopack.root` specified or inferred from the `next.config.js` file.
//
// [vc-next]: https://github.com/vercel/vercel/blob/%40vercel/next%404.20.5/packages/next/src/server-build.ts#L1022-L1026
files: string[]
// Turbopack extension: A parallel array to `files` (same indices and length)
// with file content hashes. For symlinks, the hash of the path of the target
// is stored.
fileHashes?: string[]
// Turbopack extension: Explicit symlink mapping information.
//
// If included, it's safe to assume that if a file is not in `symlinks` that
// it is not a symlink. If this is an empty array, there are no symlinks.
//
// If omitted, the processor of the nft file must call `read_link` on every
// file to determine if it is a symlink and determine the target path.
//
// This field is always included if `NftJson` includes `additionalRoots`, and
// it is always included on `NftAdditionalRoot`.
symlinks?: NftSymlink[]
}
export interface NftJson extends NftFileList {
version: 1
// Turbopack extension: A hash of the entrypoint that refers to these traced
// files. This hash only depends on the content of the entrypoint file, and
// not all of its traced dependencies.
entryHash?: string
// Turbopack extension: Paths stored with different base paths, typically
// outside of the tracing root.
additionalRoots?: NftAdditionalRoot[]
}
// Turbopack extension: A collection of paths stored with a different base path.
export interface NftAdditionalRoot extends NftFileList {
// Stable unique identifier provided in the `next.config.js`. This can be used
// to generate the output path where these files are copied to (e.g.
// `.additionalRoots/$[name}`).
//
// This is guaranteed to use a character set that is valid on most
// filesystems, and the identifiers are guaranteed to not have overlaps on
// case-insensitive filesystems.
name: string
// A source path on the build machine that the paths in `files` are relative
// to. The final build output directory should not depend on this path.
absolutePath: string
// Always specified on NftAdditionalRoot.
symlinks: NftSymlink[]
}
// Turbopack extension: Information on a symlink, including which additional
// root it maps to. Symlinks that do not cross root boundaries (the common case)
// omit the index into `additionalRoots`.
//
// It is often complicated to transform raw symlink targets to root-relative
// paths, and including this information here ensures that the NFT reader gets
// the same result that Turbopack's tracing system expects.
//
// Because the link target type is unspecified, on Windows the reader needs to
// call `stat` to determine if a link target is a directory or file.
export type NftSymlink =
| [
// Index of `files` that refers to a symlink.
number,
// The target path of the link. In `NftJson`, this path is relative to the
// directory containing the `.nft.json` file. In `NftAdditionalRoot`, this
// is relative to the current root.
string,
]
| [
// Index of `files` that refers to a symlink.
number,
// The target path of the link relative to the specified root.
string,
// An index into `additionalRoots`, -1 if the target path is relative to
// the `.nft.json` file's directory,
//
// If the symlink target is relative to the same root as the symlink
// itself (the "current root"), this field is omitted.
number,
]
```
Nothing currently populates `additionalRoots`.
https://github.com/vercel/next.js/pull/98003 will do it.
## Summary
Rename `NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN` to
`__NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN` to mark the environment
variable as internal and subject to change.
## Verification
- `pnpm --filter=next types`
- `pnpm prettier --with-node-modules --ignore-path .prettierignore
--check packages/next/src/cli/next-dev.ts
packages/next/src/server/dev/hot-reloader-turbopack.ts
test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`
- `npx eslint --config eslint.config.mjs
packages/next/src/cli/next-dev.ts
packages/next/src/server/dev/hot-reloader-turbopack.ts
test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`
- Focused test attempted: `pnpm test-dev-turbo
test/e2e/filesystem-cache/warm-restart-task-stats.test.ts` (failed
because the fixture did not emit
`.next/dev/server/pages/_app/build-manifest.json`, causing the page
request to return 500 before the assertion)
<!-- NEXT_JS_LLM -->
## Summary
Adds an off-by-default `experimental.agentFeedback` workflow for
collecting Next.js friction without interrupting the user’s task.
- `next dev` writes a small managed block to the project
agent-instructions file.
- Agent entry points, including `next-dev-loop`, queue possible issues
instead of opening duplicate forms.
- At the final stopping point, an internal command checks the remote
gate and returns the reporting protocol bundled with that Next.js
version. The agent attempts to anonymize each qualifying issue and opens
a separate review form once. If the browser does not open, it prints the
URL for the user without troubleshooting the failure.
- Report links no longer contain a public page token. Nothing is sent
until the user submits the form, which remains rate-limited and uses a
server-only ingest credential.
- Disabling `agentFeedback` or `agentRules` removes only its managed
block on the next `next dev`. Empty generated agent files are cleaned
up; user-authored content is preserved.
- Adds API references for both options and updates the AI agents guide.
Bundling the protocol keeps the managed block small and allows the
report format to evolve with each Next.js version. The receiving form is
implemented in
[vercel/front#85739](https://github.com/vercel/front/pull/85739) and
should deploy before this workflow is enabled.
## Verification
- `NEXT_SKIP_ISOLATE=1 pnpm test-dev-turbo
test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts`
- `pnpm jest packages/next/src/server/lib/generate-agent-files.test.ts
packages/next/src/cli/internal`
- `npx eslint --config eslint.config.mjs
packages/create-next-app/helpers/generate-agent-files.ts
packages/next/src/server/config-shared.ts
packages/next/src/server/lib/generate-agent-files.ts
test/development/app-dir/agent-rules-auto-generate/agent-rules-auto-generate.test.ts`
<!-- NEXT_JS_LLM -->
If upgrade handles the Node version check, upgrade from Next version
that allowed Node v18 -> Next version that require Node v20 can fail the
upgrade. Next handles compatible Node version so proceed upgrade and let
agent handle it afterwards.
### What?
Adds a CI job that runs selected Rust unit tests under
[Miri](https://github.com/rust-lang/miri) to detect undefined behavior
in unsafe code.
The job currently covers the low-level crates that are compatible with
Miri:
- `turbo-persistence`
- `turbo-rcstr`
- `turbo-tasks-malloc`
It also makes those crates Miri-compatible by:
- using provenance-free tagged-pointer operations in `turbo-rcstr`;
- disabling mimalloc and native compression under Miri;
- making mmap support a default-enabled Cargo feature that is disabled
for Miri and wasm;
- running persistence through file I/O when mmap is disabled;
- skipping only tests measured to exceed the Miri time budget;
- removing leaked test arenas from the analyzer predicate tests.
### Why?
Miri can detect invalid memory access and other undefined behavior that
normal Rust tests may not expose. Running it in CI gives low-level
unsafe code an additional correctness check.
The job uses an explicit package allowlist because Turbo Tasks builds
its generated registry from linker sections, and Miri does not support
the linker-defined section symbols required by that registry.
### How?
- Installs the `miri` Rust component in CI and the development
container.
- Adds a dedicated `test-cargo-unit-miri` task and reusable workflow
configuration.
- Makes `memmap2` optional behind the default `mmap` feature and
disables that feature in Miri CI and the wasm dependency graph.
- Uses structural `cfg(miri)` branches for allocator and compression
paths Miri cannot execute.
- Re-enables three persistence compaction tests after measuring them
successfully under Miri.
- Keeps normal native behavior unchanged and documents measured Miri
exclusions at the affected tests.
Verification included focused normal and Miri tests for persistence,
rcstr, allocation accounting, compression, and the refactored leak-free
predicate cases.
<!-- NEXT_JS_LLM -->
<!-- fleet fb42942f-173a-484c-b4de-4e18354834c6 -->
---------
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>
- in `dev`, wait for validation to run before asserting on a redbox
being open (encapsulated in `getInstantInsight`), which should reduce
flakiness in webpack
- in `start`, check if the build actually succeeded before running
tests. these builds seem to occasionally fail, but the cause is
currently unknown
Stacked on #98643.
This PR adds `experimental.agenticAutoUpgrade = 'future'` config which
enables nudging the agents to notify the user when there's unadopted
future default(s). The nudge will include guiding to upgrade via `next
upgrade --ai` (run "future" by detecting config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
Enabling `experimental.agenticAutoUpgrade = 'future'` also enables
security check and latest version check.
Stacked on #98640.
This PR adds `next upgrade --ai="future"` flag, which is targeted to
help users leverage agents to upgrade their app to adopt the future
defaults when available. Just like latest version upgrade, it covers
running codemods and a migration checklist for major-to-major upgrades
to support breaking changes more reliably.
This PR currently covers Cache Components only for the future default.
Stacked on #98633.
This PR adds `experimental.agenticAutoUpgrade = 'latest'` config which
enables nudging the agents to notify the user when there's new
major/minor Next.js version available to upgrade. The nudge will include
guiding to upgrade via `next upgrade --ai` (run "latest" by detecting
config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
Enabling `experimental.agenticAutoUpgrade = 'latest'` also enables
security check.
Stacked on #98637.
This PR adds `next upgrade --ai="latest"` flag, which is targeted to
help users leverage agents to upgrade their app to the latest major
version when available. Just like security upgrade, it covers running
codemods and a migration checklist for major-to-major upgrades to
support breaking changes more reliably.
Stacked on #98562.
> [!TIP]
> Recommended to review commit by commit.
This PR adds `experimental.agenticAutoUpgrade = 'security'` config which
enables nudging the agents to notify the user when the app's Next.js
version has any security advisories. The nudge will include guiding to
upgrade via `next upgrade --ai` (run "security" by detecting config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
> [!TIP]
> Recommended to review commit by commit.
This PR adds `next upgrade --experimental-ai="security"` flag (alias
`--ai`), which is targeted to help users leverage agents to upgrade
their app to the safe major version when their app's Next.js version has
any security advisories.
Once the command is ran from the user, Next.js will detect the installed
agent harness in user's device, currently limited to Codex and Claude,
and will proceed with starting an agent session once approved. If it is
called within an agent session, the work will continue off within that
agent.
`next upgrade --ai` simply does two things:
- prepare the relevant context to temporary dir
- print hand off prompt, guiding to read those context
The context will guide the agent to run relevant codemods and migration
checklist to proceed. This PR is a base core of the workflow, and will
have wrappers of entry point around this. Also, will add "latest" and
"future" as follow up, which will cover the app to be always latest, and
adopt the future defaults like Cache Components.
This PR also sets up the evals infra and adds evals.
#94694 delayed shared metadata until handler writes settled to protect
cross-request retries. Same-request consumers also awaited this promise,
so an outer cache could save its result before inheriting the inner
cache's root-param dependencies, tags, and lifetime.
Metadata now becomes available when collection finishes. Cross-request
key verification and pending-invocation cleanup still wait for handler
writes to settle.
The regression tests hold an inner write pending while both outer caches
complete. They check metadata at write time and verify that a later
request with different root params cannot reuse the earlier result. The
fixtures disable startup preloading to isolate these tests from a
separate custom-handler registration race that we'll address in a
follow-up.
In, we #98278 attempted to fix runtime follow-ups for shells (if a
static prefetch produced an insuffient shell).
However, it introduced a regression in this scenario, where neither
shells or prefetches use runtime data, so both of these links should use
static prefetches:
```tsx
<Link href="/static-param/one" prefetch={true} />
{/* Revealed later */}
<Link href="/static-param/two" prefetch={true} />
```
We'd correctly do a static prefetch for the first link. However, the
second link would see a `RuntimeShell`-tier shell from the first link,
and this check inside `isShellEntryEligibleForStaticAttempt` would fail
(because `RuntimeShell` was now greater than `PPR`):
https://github.com/vercel/next.js/blob/3c9d1ca77f7de845315dc166a57fc9f090c638b4/packages/next/src/client/components/segment-cache/scheduler.ts#L1261
as a result, `isShellEntryEligibleForStaticAttempt` would return
`false`, and we would deopt to a runtime prefetch for the second link
instead.
Importantly, a page with `ensureStatic = "prefetch"`(which will be
implemented in #98191) will currently be handled exactly like a page
that did not use runtime data at build -- the client doesn't know that a
page is meant to never use runtime requests at all, it's entirely based
on the existing static hints/`needsRuntimeRequest` promise mechanism. So
even if `/static/[slug]` sets `ensureStatic = "prefetch"`, we'd also do
an unnecessary runtime prefetch for it even though we really shouldn't.
Violating the `ensureStatic` contract seems worse than a missing runtime
shell follow-up , because those can currently only happen if the shell
starts using runtime data after a revalidation, and that's probably not
a common scenario. So i'm partially reverting that change until we
figure out how to handle this in a way that handles both cases correctly
(i've attempted doing that, but it seems nontrivial)
### Testing
We're now missing the runtime shell follow-up again, so tests related to
that in `prefetch-app-shell-revalidation.test.ts` are marked as failing
(but still assert on current behavior -- the follow-up failures can be
reproduced by setting `REPRODUCE_MISSING_RUNTIME_SHELL_FOLLOW_UP=1`).
Note that the runtime *prefetch* follow ups there work as expected --
those worked correctly even before #98278.
I've also added some pretty comprehensive tests in
`prefetch-static-shell.test.ts`. We now test the scenario that
regressed, which we weren't doing before. We also test scenarios where
we reveal multiple links, but some of them use runtime data in the
prefetch, and some don't. There's some failures there related to the
fact that we don't track runtime data accesses in shells and prefetches
separately (revealed by `REPRODUCE_UNNECESSARY_RUNTIME_SHELL=1`), which
will be addressed by #98129. I want to land that before doing anything
else on this topic to avoid dealing with failures that are going to be
fixed soon anyway. There's also one case
(`REPRODUCE_UNNECESSARY_RUNTIME_PREFETCH=1`) which will not be fixed by
it, and will need separate consideration.
## Motivation
Streaming and blocking metadata previously used different rendering
paths. Metadata placement should depend on when Next.js starts consuming
the response, rather than requiring the metadata component itself to
render outside Suspense.
## What this does
Selected metadata now always renders through the same hidden Suspense
boundary. For blocking responses, an always-present `MetadataBlocker`
waits for the same cached resolution before Next.js pulls the response,
ensuring metadata remains in the document head.
`MetadataOutlet` remains a single promise. It renders inside Suspense
for streaming responses and outside Suspense for blocking responses so
navigation and regular errors retain their existing behavior.
HTML-limited bot requests continue to bypass PPR shells. A prerendered
shell has already closed its head before dynamic metadata resolves, so
resuming that shell cannot provide blocking metadata in the raw head.
## Review notes
- Metadata resolution still starts from a rendered component so the
active `workUnitStore` and React cache scope are available.
- Fizz may begin rendering before metadata settles; blocking is enforced
before Next.js starts pulling the response.
- The hidden wrapper keeps top-level suspenseful metadata out of the
document preamble. Metadata tags hoist out, so the wrapper remains
empty.
- The Cache Components blocking and hydration coverage runs with
parallel metadata both disabled and enabled.
## Verification
- `pnpm build-all`
- `pnpm test-start-turbo
test/e2e/app-dir/metadata-streaming-cache-components/metadata-streaming-cache-components-custom-bots.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/metadata-streaming-cache-components/metadata-streaming-cache-components-custom-bots.test.ts`
<!-- NEXT_JS_LLM -->
## Summary
Commit preview tarballs carry versions like
`16.4.0-preview-84cee7e6-20260917` (no `canary` substring), so
`isStableBuild()` treated them as stable and canary-only defaults
(`turbopackSharedRuntime`, `turbopackMangleExportNames`) were disabled
on what are canary-quality builds. Versions containing `-preview-` now
count as unstable; numbered npm previews like `16.3.0-preview.10` stay
stable.
## Verification
- Added `canary-only-config-error.test.ts` covering the version matrix
(fails on the tarball case without the fix)
- `npx jest packages/next/src/server/config.test.ts` (existing consumer)
passes
<!-- NEXT_JS_LLM -->
<!-- fleet ae573e35-b551-453d-af1c-548ffb598944 -->
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
### What?
Replaces the Turbopack GC environment variables with an
`experimental.turbopackGc`
config option
```js
// next.config.js
experimental: {
turbopackGc: true, // on, defaults
turbopackGc: { minProgressMs: 100, rootTtlMs: 432e6 }, // on, tuned
}
```
This replaces the env vars which were unused and not the ergonomic
### What?
A follow up to https://github.com/vercel/next.js/pull/94979, now we
check if response headers have already been sent before trying to send a
`500` response when falling back.
### Why?
To get streaming respnses that already sent status `200` headers from
reporting as `500`s in telemetry.
### How?
Skip the fallback `500` status response if headers have already been
sent.
---------
Signed-off-by: Alex Langenfeld <alex.langenfeld@vercel.com>
### What?
A `"use cache"` segment that reads a root param via `next/root-params`
is reused after a client-side navigation that changes that param. With
`cacheComponents`, a production build and `<Link prefetch={false}>`:
open `/en`, click `de`. URL, `usePathname()` and layout say `de`; the
page segment still says `en`.
### Why?
Inside a `"use cache"` scope, `getRootParam` only records the name in
`readRootParamNames`, which keys the server cache entry. Nothing
forwards it to the response's root vary params, so the client re-keys
the segment as `Fallback` and reuses it for every locale. `await params`
masks the bug because serializing the cache key touches the tracked
params object.
### How?
`propagateCacheEntryMetadata` adds `readRootParamNames` to the outer
store's root vary accumulator when one exists. The client already unions
root vary params into every segment.
Not covered: `"use cache: private"` root-param reads are only tracked in
dev, so the same reuse can still happen there in production.
Review order: `use-cache-wrapper.ts` is the whole change (one block
after the propagation switch, plus the doc note above
`maybePropagateCacheEntryMetadata`). The fixture adds a `[locale]` route
under the existing `vary-params` app.
Test: `vary-params.test.ts`, "does not reuse a "use cache" segment
across root param values", fails with `Received: "Locale: en"` before
the fix. Reporter's repro renders `de` after navigation on the patched
package.
Fixes#98493
cc @unstubbable @lubieowoce (use cache vary params)
<!-- NEXT_JS_LLM -->
## Summary
Fix hybrid App Router and Pages Router projects using Pages i18n so
explicit or rewritten locale segments remain available to App Router
route matching, while Pages routes continue using locale-normalized
paths.
The built-in filesystem matcher now carries each route owner through
`RouteKind`, avoids duplicate App routes being treated as Pages routes
in production, and preserves literal App path parameters through the
router server, direct route-module invocation, and static export paths.
The same ownership information is maintained by the development bundler.
Adds regression coverage for domain locales, proxy rewrites, exact and
dynamic App pages, catch-all parameters, route handlers, inferred
default locales, and Pages routes. Adapter-generated routing metadata is
intentionally handled in the follow-up layers #97366 and #97500.
Closes#86048
## Verification
- `pnpm build-all`
- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-start-webpack
test/e2e/app-dir/i18n-app-pages-domain-routing/i18n-app-pages-domain-routing.test.ts`
(9 passed)
- Prettier and ESLint checks on all changed files
<!-- NEXT_JS_LLM -->
The Vercel adapter enables `passQuery: true`, which forwards named route
captures to prerender functions as query parameters instead of using the
legacy route-matches header. When a request bypasses prerendered output,
this also exposes internal path-building captures through application
`searchParams`. With `experimental.collapseAdapterRoutes` disabled, RSC
renders can receive `rscSuffix=.rsc`. With the flag enabled, document
and Server Action renders can also receive `rscSuffix=`, and collapsed
fallback shells can expose values such as `shellPrefix=en`. These
additions can overwrite user query parameters and cause unnecessary
navigation retries.
We fix this in `handleBuildComplete`, where Next.js generates these
path-only helper captures. Unnamed captures and numbered destination
references preserve the rewrite behavior without requiring adapters to
recognize and filter Next.js's internal helper capture names.
When using an adapter, we now emit unnamed captures for these path-only
values. For example, `(?<shellPrefix>de|en)` and its destination
reference `$shellPrefix` become `(de|en)` and `$1` when no locale
capture precedes the group. Likewise, `(?<rscSuffix>...)` and
`$rscSuffix` become `(...)` and a positional reference such as `$2` when
one capture precedes the suffix. The generator accounts for parameter
and locale captures and escapes literal base paths. Destination
substitution runs once, preserving captured text and correctly
distinguishing references such as `$1` and `$10`.
Cache-bypass rules, route counts, and route ordering remain unchanged.
User-supplied `rscSuffix` and `shellPrefix` remain valid query
parameters. Standard `next dev` and `next start` routing, and Vercel's
legacy deployment path, do not use these generated adapter routes, and
thus are not affected.
Verified by running the [full deploy test
matrix](https://github.com/vercel/next.js/actions/runs/34835432358)
against this PR.
With Cache Components enabled and Partial Prefetching disabled, blocking
prerenders in `next start` could cache params that no
`generateStaticParams` result supplies. For `/[top]/[bottom]`, where
only `top` is supplied, this included `bottom` in the static HTML and
created a separate cache entry for each bottom value.
Adapter deployments already enforce this rule. The legacy Vercel builder
has a known limitation that the existing prerender tests explicitly
exclude.
#95872 established that these params must remain dynamic. #96297 later
put self-hosted eligibility-based shell keys and fallback-param
selection behind the `partialPrefetching` flag, alongside automatic
fallback upgrades. #98512 then made resumes honor the parameter set
recorded by the selected prerender. That did not create the invalid
prerenders, but allowed their over-resolved params to enter Cached
Navigations.
The RSC test added in #98512 did not account for that known
legacy-builder limitation and consequently failed in this [canary deploy
run](https://github.com/vercel/next.js/actions/runs/34659100427/job/103469344769).
For `next start`, shell keys and unresolved params now follow GSP
eligibility even when upgrades are disabled. Cache writes, revalidation,
and navigation RDC reads use the same key, including shared shells that
cannot be completed further. Routes whose params are all prerenderable
retain their existing keys when Partial Prefetching is disabled.
Automatic upgrade triggers remain gated.
The aforementioned `cached-navigations` tests now separate cold RSC
navigation from initial HTML and assert eligibility independently of the
observed response. Added coverage checks shell sharing, explicit
revalidation, and generated optional paths. Legacy-builder failures are
documented with expected-failure gates; its implementation remains
unchanged.
Verified by running the [full deploy test
matrix](https://github.com/vercel/next.js/actions/runs/34771777150)
against this PR.
### What?
`Router.prefetch()` in the Pages Router evaluates the client router
filter (`_bfl`) against the `as` path, but stored the resulting `{
__appRouter: true }` marker in `router.components` under the `href`
pathname. `Router.change()` looked the marker up with the `href`
pathname as well. This PR keys the marker by the normalized `as`
pathname and effective locale, preserves existing cache entries, and
checks for markers before and after route resolution.
### Why?
`href` and `as` are the same URL for most links, so the mismatch was
invisible. They differ for the "route as modal" pattern
(`examples/with-route-as-modal`): `href` stays on the current route
(`router.pathname` + query) and `as` shows the pretty URL. When the
filter matches `as`, including a false positive, the marker could
replace the cached route info of the page the user is currently on:
- On a static route (`/`), the `change()` guard fired for later shallow
navigation and hard-navigated.
- On a dynamic route, the marker was stored under the pattern
(`/players/[name]`) but looked up with the concrete path
(`/players/alice`). The guard could miss it, and shallow navigation
could render without page props, causing missing content or an
application error.
A prefetch must also preserve already-loaded route info when its key
matches the current route. This can happen when the page was reached
through a rewrite and its canonical URL is prefetched. Hash-only
navigation renders that cached entry directly, so preserving it lets the
normal client-side hash update retain the page props and update
`router.asPath`.
Fixes#98180
### How?
- `getAppRouterMarkerKey(router, as, locale)` parses the pathname,
normalizes the trailing slash, and preserves the effective locale. It
expects a path without `basePath` and does not strip that prefix again.
An explicit locale prefix takes precedence over the supplied locale.
- The helper returns `null` for non-local URLs, leaving their existing
navigation handling unchanged.
- `prefetch()` stores the marker under that key only when the
corresponding cache entry is absent. It does not replace loaded route
info.
- `change()` checks both the normalized `as` key and the `href` route.
It checks the resolved route again after config rewrites, before
`getRouteInfo()` reads the cache.
- Non-shallow navigations continue to consult the client router filter
directly. Hash-only navigation retains its normal client-side path.
The complementary change in #98650 (adopts #98187) adds defensive checks
when `getRouteInfo()` reads marker entries.
### Tests
The e2e suites run in production (`next start`) and deploy modes.
`router.prefetch()` is a no-op in development.
`test/e2e/app-dir/pages-prefetch-as-app-route` covers:
- Hard navigation to an App Router destination after prefetching a link
whose `href` and `as` differ.
- Shallow navigation on static and dynamic Pages Router routes without a
reload or lost server-provided props.
- Hard navigation when the `href` route holds a marker and `as` differs,
or a config rewrite resolves to a marked route.
- Client-side hash navigation after an awaited prefetch of the current
route's canonical URL, with preserved props and an updated
`router.asPath`.
`test/e2e/app-dir/pages-prefetch-as-app-route-base-path` covers an
internal `/docs` route with `basePath: '/docs'`, including navigation to
`/docs/docs` and an unaffected shallow update on the index page.
`test/e2e/app-dir/pages-prefetch-as-app-route-i18n` covers a French-only
redirect prefetch without forcing an unrelated English navigation to
reload. Marker-dependent tests wait for the specific key they need,
rather than any marker.
Runtime behaviour was also verified against the reproduction in
https://github.com/Stanzilla/next-pages-router-prefetch-bloom-filter-repro
by patching the compiled `router.js`. The e2e suite was not run locally.
Disclosure: this change and its description were prepared with AI
assistance (Claude Code) on behalf of the author.
### What?
`Router.prefetch()` in the Pages Router evaluates the client router
filter (`_bfl`) against the `as` path, but stored the resulting `{
__appRouter: true }` marker in `router.components` under the `href`
pathname. `Router.change()` looked the marker up with the `href`
pathname as well. This PR keys the marker by the normalized `as`
pathname and effective locale, preserves existing cache entries, and
checks for markers before and after route resolution.
### Why?
`href` and `as` are the same URL for most links, so the mismatch was
invisible. They differ for the "route as modal" pattern
(`examples/with-route-as-modal`): `href` stays on the current route
(`router.pathname` + query) and `as` shows the pretty URL. When the
filter matches `as`, including a false positive, the marker could
replace the cached route info of the page the user is currently on:
- On a static route (`/`), the `change()` guard fired for later shallow
navigation and hard-navigated.
- On a dynamic route, the marker was stored under the pattern
(`/players/[name]`) but looked up with the concrete path
(`/players/alice`). The guard could miss it, and shallow navigation
could render without page props, causing missing content or an
application error.
A prefetch must also preserve already-loaded route info when its key
matches the current route. This can happen when the page was reached
through a rewrite and its canonical URL is prefetched. Hash-only
navigation renders that cached entry directly, so preserving it lets the
normal client-side hash update retain the page props and update
`router.asPath`.
Fixes#98180
### How?
- `getAppRouterMarkerKey(router, as, locale)` parses the pathname,
normalizes the trailing slash, and preserves the effective locale. It
expects a path without `basePath` and does not strip that prefix again.
An explicit locale prefix takes precedence over the supplied locale.
- The helper returns `null` for non-local URLs, leaving their existing
navigation handling unchanged.
- `prefetch()` stores the marker under that key only when the
corresponding cache entry is absent. It does not replace loaded route
info.
- `change()` checks both the normalized `as` key and the `href` route.
It checks the resolved route again after config rewrites, before
`getRouteInfo()` reads the cache.
- Non-shallow navigations continue to consult the client router filter
directly. Hash-only navigation retains its normal client-side path.
The complementary change in #98650 (adopts #98187) adds defensive checks
when `getRouteInfo()` reads marker entries.
### Tests
The e2e suites run in production (`next start`) and deploy modes.
`router.prefetch()` is a no-op in development.
`test/e2e/app-dir/pages-prefetch-as-app-route` covers:
- Hard navigation to an App Router destination after prefetching a link
whose `href` and `as` differ.
- Shallow navigation on static and dynamic Pages Router routes without a
reload or lost server-provided props.
- Hard navigation when the `href` route holds a marker and `as` differs,
or a config rewrite resolves to a marked route.
- Client-side hash navigation after an awaited prefetch of the current
route's canonical URL, with preserved props and an updated
`router.asPath`.
`test/e2e/app-dir/pages-prefetch-as-app-route-base-path` covers an
internal `/docs` route with `basePath: '/docs'`, including navigation to
`/docs/docs` and an unaffected shallow update on the index page.
`test/e2e/app-dir/pages-prefetch-as-app-route-i18n` covers a French-only
redirect prefetch without forcing an unrelated English navigation to
reload. Marker-dependent tests wait for the specific key they need,
rather than any marker.
The fix was also verified at runtime against the reproduction in
https://github.com/Stanzilla/next-pages-router-prefetch-bloom-filter-repro
by patching the compiled `router.js` and re-running the scenario. The
e2e suite was not run locally.
Disclosure: this change and its description were prepared with AI
assistance (Claude Code) on behalf of the author.
## Summary
Add `NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN` to make `next dev` wait for
Turbopack's full project shutdown before exiting. This waits for active
TurboTasks work and cache persistence instead of relying on fixed delays
or the parent's normal 100 ms child-exit timeout.
Use the option in the warm-restart task statistics test. Link documented
`Project` interface methods to their native binding documentation.
## Verification
- `pnpm --filter=next types`
- `npx eslint --config eslint.config.mjs
packages/next/src/cli/next-dev.ts
packages/next/src/server/dev/hot-reloader-turbopack.ts
test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`
- Pre-commit lint-staged checks
- CI tests passed
<!-- NEXT_JS_LLM -->
## Maintainer status
- Current with `canary` as of 2026-05-18; this branch includes a clean
merge from latest `origin/canary`.
- Lightweight checks pass; full fork workflows are still
`action_required` until a maintainer approves CI.
- No unresolved review threads or failing jobs are reported after the
refresh.
- Review focus: shared convention-file basename extraction for proxy,
middleware, and instrumentation under compound `pageExtensions`; TS and
Rust paths use the same rule.
---
## Summary
Fixes#85648Fixes#86303Fixes#91600Fixes#85646
Related to #86122Fixes#92342Closes#92934
When `pageExtensions` is set to compound extensions like `['page.ts',
'page.tsx']`, proxy files must be named `proxy.page.ts`. However, the
proxy detection logic used `file_stem()` (Rust/Turbopack) and
`path.parse().name` (JS/webpack), both of which only strip the **last**
extension — so `proxy.page.ts` becomes `proxy.page` instead of `proxy`,
and the proxy is never detected.
**Root cause:** `path.parse('proxy.page.ts').name` returns
`'proxy.page'`, not `'proxy'`. Same issue with Rust's `file_stem()`
which uses `rsplit_once('.')`.
**Fix:** Use `file_name().split('.')[0]` (JS) /
`file_name().split('.').next()` (Rust) to extract the first segment
before any dot. This correctly returns `'proxy'` for both `proxy.ts` and
`proxy.page.ts`.
The same `fileBaseName` extraction is also used for `middleware` and
`instrumentation` convention file detection, fixing compound
pageExtensions for those as well.
### Changes
- **Turbopack (Rust):** `crates/next-api/src/project.rs` (2 locations) +
`crates/next-api/src/middleware.rs` (1 location)
- **Webpack (JS):** `packages/next/src/build/index.ts` (build-time
detection) +
`packages/next/src/server/lib/router-utils/setup-dev-bundler.ts`
(dev-time detection)
- **E2e tests:**
- `proxy-page-extensions/` — proxy + instrumentation with compound
extensions
- `middleware-page-extensions/` — middleware with compound extensions
(separate fixture because the build refuses both `proxy.*` and
`middleware.*` simultaneously)
### Verified locally
| Mode | Bundler | Result |
|------|---------|--------|
| Dev | Turbopack | PASS (5/5) |
| Dev | Webpack | PASS (5/5) |
| Production (build+start) | Turbopack | PASS (5/5) |
| Production (build+start) | Webpack | PASS (5/5) |
Existing proxy test suites also verified (proxy-runtime-nodejs,
proxy-with-middleware, proxy-missing-export, proxy-runtime) — no
regressions.
## Test plan
- [x] `proxy-page-extensions.test.ts` covers proxy header injection,
page render through proxy, and `instrumentation.page.ts:register()`
running
- [x] `middleware-page-extensions.test.ts` covers middleware header
injection and page render through middleware
- [x] All 5 cases pass in dev/turbopack, dev/webpack, start/turbopack,
start/webpack
- [x] Existing proxy-runtime-nodejs tests pass (dev/webpack,
dev/turbopack, production/webpack)
- [ ] CI passes on all existing proxy tests
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Ported from #98040 by @marcoshernanz.
> [!TIP]
> Best reviewed commit by commit.
### Why?
External development tools need browser runtime-error state, not just
build errors, to display application failures. Reporting remains opt-in
so applications that do not need it avoid client serialization, HMR
transport, server formatting, buffering, and rebroadcast overhead.
### How?
Add `experimental.exposeRuntimeErrorsToHMR` for App Router development
with Webpack and Turbopack. Internal integrations can also enable the
same behavior without changing `next.config` by setting
`__NEXT_EXPOSE_RUNTIME_ERRORS_TO_HMR` to any non-empty value. When
enabled, the HMR WebSocket emits `runtimeErrors` snapshots containing:
- The current pathname and browser client/document identifiers.
- Error types, names, messages, and source-mapped stacks.
- Fatality and optional catching-boundary details (`default-global`,
`custom-global`, or `custom`).
Snapshots update as errors and navigation change. New or reconnected
observers receive the current state, and disconnecting a browser clears
its reported errors. An empty snapshot means there are no currently
reported errors; it does not guarantee application recovery.
Reporting is disabled by default. Pages Router, MCP `get_errors`, and
production behavior are unchanged.
<!-- NEXT_JS_LLM -->
Co-authored-by: Marcos Hernanz
<96699542+marcoshernanz@users.noreply.github.com>
### Why?
Async Request API codemods can leave temporary casts that still require
application-specific repairs. Those locations need explicit markers,
while optional feature adoption needs to be distinguishable from
required version migrations.
### How?
Add `@next-codemod-error` comments to temporary `UnsafeUnwrapped*`
casts, with instructions for completing the repair. Preserve documented
ignores and avoid duplicating casts or markers when the transform runs
again. Mark generated Cache Components opt-outs with
`@next-codemod-ignore`, a removal condition and a link to the adoption
guide.
Add `upgrade --skip-adoption` to skip the Cache Components `instant =
false` transform and partial-prefetch adoption cleanup. Required
migrations and normal dependency and bundler selection keep their
existing behavior. Both adoption transforms can still be run explicitly.
### Problem
1. Next computes the warmup cache key while `SOME_ENV_VAR` is unset.
2. Rendering ends up setting `SOME_ENV_VAR`.
3. Final prerender expects the cache entry to exist, but the env var
changed, so the key changed and it's a miss.
4. Unexpected miss and bailout.
This lead to
```
Error: Route "foo": Unexpected cache miss after cache warming phase during prerendering. This is
likely caused by non-deterministic arguments that differ between the cache warming phase and the
final prerender phase (e.g. unstable array order). Ensure that arguments passed to cached functions
are deterministic.
Error: Route "foo": Next.js encountered uncached or runtime data during
prerendering.
```
### Solution
~~Instead, snapshot the env vars once at module evaluation time of the
`use cache` function. This ensures that the cache key doesn't change
over time. This is also how it worked thus far (with deployment id as
the cache key): changing the env var over the lifetime of the process
didn't lead to a reexecution of the `use cache` function~~
RDC already stores cache entries generated in the previous phase (be it
during `next build` multi-phase rendering, or from prerender->resuming
at runtime). Root params are already excluded from the cache keys when
storing in RDC. Also exclude the env var hash bit, to conform to this
system of preventing tearing (at the cost of potential staleness).
### What?
Refactors the JavaScript-facing `TurbopackResult<T>` into a stable
wrapper whose payload lives under `value` and whose issues remain
top-level. Native API consumers, entrypoint conversion,
development-server paths, HMR handling, and direct API tests now follow
the disjoint shape.
Two event subscriptions that had inaccurate wrapper declarations now
expose their existing plain runtime payloads explicitly: update-info
events remain `UpdateMessage`, and compilation events remain
`CompilationEvent`. Nullable native entrypoint payloads are also
declared accurately and normalized at the JavaScript API boundary.
### Why?
The previous intersection-based representation merged payload fields
with result metadata. That allowed fields such as `issues` to overwrite
one another and caused non-object payloads to be discarded, making the
result shape depend on `T`. A dedicated payload property avoids those
collisions and preserves every payload type, including `null`.
Keeping plain event streams distinct from result wrappers also ensures
`TurbopackResult<T>` consistently means the native API actually provides
wrapper metadata.
### How?
The N-API serializer now always creates a fresh wrapper rather than
mutating an object payload. The shared TypeScript type models that
wrapper directly and requires an explicit payload type. Conversion
layers replace only the nested payload while explicitly preserving
wrapper issues. Call sites continue to process wrapper issues while
reading domain data through `value`.
The update-info and compilation-event declarations were aligned with
their native callback types instead of introducing new runtime wrappers
for streams that do not collect issues. Rust `Option<NapiEntrypoints>`
payloads are declared as nullable, then normalized to the existing
empty-entrypoints representation after a null-safe route check.
### Verification
- `pnpm build-all`
- `pnpm --filter=next types`
- `pnpm swc-build-native`
- `cargo check -p next-napi-bindings`
- `cargo fmt --all -- --check`
- ESLint and Prettier on changed files
- `pnpm test-dev-experimental-turbo
test/development/app-aspath/app-aspath.test.ts`
- `pnpm test-dev-turbo test/development/basic/next-rs-api.test.ts` — 26
passed, 1 skipped, 15 snapshots; Jest reported lingering open handles
after the green summary
- `pnpm test-dev-turbo
test/development/app-dir/concurrent-install/concurrent-install.test.ts`
- `pnpm test-dev-experimental-turbo
test/development/app-dir/concurrent-install/concurrent-install.test.ts`
<!-- NEXT_JS_LLM -->
<!-- fleet 74688340-f99a-462e-8f75-88d12c88000a -->
---------
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>
We now separately track code that accesses non-inlined env vars at
runtime in two categories
- (existing) actual reads, the full value is accessed
- (new) only unset/falsy/truthy is read at runtime
For the second case, we only need to invalidate use-cache functions when
the given env var transitioned betwen unset/falsy/truthy. But
transitioning between two different truthy values doesn't caused
invalidation.