## Summary
The existing metadata resolver combines two responsibilities: walking
the loader tree to collect and schedule route exports, and interpreting
the metadata or viewport value produced by an individual route layer.
Move the route-layer operations into metadata-resolution-primitives.ts.
This includes wrapping generateMetadata and generateViewport, loading
file-based metadata, merging values with their resolved parents,
post-processing metadata, and producing SelectedMetadata for rendering.
Keep resolve-metadata.ts responsible for the existing traversal and
scheduling flow, importing the extracted operations back into the same
call sites and preserving its existing exports. This creates a reusable
boundary for other traversal strategies without introducing another
resolution path.
This is intended to be behavior preserving: traversal order, eager
generator invocation, parent promise behavior, warnings, and rendered
metadata and viewport tags are unchanged.
## Verification
- pnpm build-all
<!-- NEXT_JS_LLM -->
The metadata resolver currently passes ResolvedMetadata directly to tag
rendering even though several fields only exist to carry state between
route layers.
Introduce SelectedMetadata as the post-processed, tag-ready
representation and convert the current resolved output before rendering.
This makes the final selection boundary explicit without changing
generated tags, preparing the metadata pipeline for multiple
independently resolved branches.
<!-- NEXT_JS_LLM -->
## What?
- remove manifest fragments when Turbopack's development entrypoint
cleanup removes a deleted page
- add an end-to-end regression for replacing concrete App Router pages
with an optional catch-all without restarting the dev server
## Why?
Turbopack already removes deleted entry keys from its asset mapper,
subscriptions, issues, and client state. It did not remove the
corresponding records from `TurbopackManifestLoader`, so the next
manifest write continued to serialize deleted App Router routes.
When a deleted concrete route is replaced by an optional catch-all,
those stale records conflict with the new route at the same specificity.
The dev server reports the route conflict and responds with 404 until it
is restarted.
This is independently consistent with the cleanup identified in #97194.
This draft adds end-to-end coverage that fails on current canary by
observing both the 404 and stale manifest records, then passes with the
lifecycle fix.
## How?
Pass the existing manifest loader to `handleEntrypointsDevCleanup` and
call its existing `delete(key)` method in the same branch that deletes
the stale asset mapping. This keeps all per-entry manifest maps
synchronized with the authoritative current entrypoint set without
special-casing any route shape.
The regression test:
1. compiles two concrete localized routes
2. deletes both pages and adds an optional catch-all
3. waits for three affected paths to return the new catch-all content
4. verifies the development app-paths manifest contains the catch-all
and no deleted entries
## Verification
- reproduced the report on `16.3.1-canary.10`: all four reported paths
returned 404 after the live route replacement and returned 200 after a
restart
- confirmed the regression fails on current canary before the source
change with the same-specificity route error and a 404 response
- `pnpm test-dev-turbo
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-rspack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `CI=1 pnpm build-all`
- `pnpm --filter=next build`
- formatting, ESLint, staged-diff, and signed-commit checks
Fixes#97035
## Summary
- trace instrumentation module loading and `register()` execution as
separate startup spans
- preserve the original timestamps when the OpenTelemetry provider is
installed by `register()` itself
- export both lifecycle spans by default without attaching them to an
unrelated request
- verify standard and direct-entrypoint startup with a collector
connected before the server starts
## Why
Loading `instrumentation.ts` and running the user-defined `register()`
hook have different owners and remediation paths. Separate spans make
startup delays attributable while preserving honest lifecycle parentage:
these spans normally occur before a request identity exists.
## Verification
- `pnpm build-all`
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- focused instrumentation-startup E2E in development and production with
Turbopack and Webpack
- Prettier, ESLint, and `git diff --check`
## Stack
Depends on **Trace route module preparation**.
## Summary
- trace the full route-module preparation boundary as
`RouteModule.prepare`
- record manifest loading as one aggregate `RouteModule.loadManifests`
child span
- keep route preparation visible by default while exposing manifest
detail only with verbose tracing
- cover normal and direct-entrypoint server lifecycles without relying
on test ordering
## Why
Route preparation is the stable boundary between loading a route module
and executing it. The parent span shows the total setup cost; the
aggregate manifest child explains manifest work without creating
per-file spans or exposing paths.
## Verification
- `pnpm build-all`
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- focused OpenTelemetry E2E in development and production with Turbopack
and Webpack
- Request Insights route-preparation E2E with Turbopack and Webpack
- Prettier, ESLint, and `git diff --check`
## Stack
Depends on **Trace route module loading**.
## Summary
- trace the primary App Router and Pages Router route-module load inside
`loadComponents`
- expose `LoadComponents.loadRouteModule` as the default `load route
module` OpenTelemetry span
- verify the span in development and production with both Turbopack and
Webpack
## Why
Route-module evaluation can account for cold request time before route
preparation and rendering begin. Keeping this boundary in the Next.js
server makes it available to `next dev`, self-hosted `next start`,
direct entrypoints, and deployment integrations without depending on an
adapter-specific launcher.
The span wraps only the requested route module. It does not include
`_app`, `_document`, rendering, or any adapter launcher work, and it
does not add filesystem paths, raw URLs, or other high-cardinality
attributes.
## Verification
- `pnpm build-all`
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- focused OpenTelemetry E2E in `test-dev-turbo`, `test-dev-webpack`,
`test-start-turbo`, and `test-start-webpack`
- Prettier, ESLint, and `git diff --check`
## Summary
### Original issue
Pages API functions produced through a build adapter can fail during
function initialization, before the customer handler executes:
```
Cannot find module 'next/dist/compiled/next-server/pages-turbo.runtime.prod.js'
```
The failure is triggered when an externalized dependency used by the API
route imports a Next.js module such as `next/head`. At runtime the load
path is:
```
external dependency
-> next/head
-> head-manager-context.shared-runtime
-> Pages vendored head-manager context
-> pages/module.compiled.js
-> pages[-turbo].runtime.prod.js
```
The two important edges at the end of this chain are selected
dynamically. The Next.js require hook redirects the shared-runtime
import to the Pages vendored context, and `pages/module.compiled.js`
selects the bundler-specific production renderer. As a result, a Pages
API entry trace does not discover the regular Pages renderer. It
naturally contains `pages-api[-turbo].runtime.prod.js`, which is the
runtime for the API route module, but not
`pages[-turbo].runtime.prod.js`, which is reached through the external
`next/head` import.
This is why the failure is specific to Pages API routes. A regular Pages
SSR entry already references and traces the Pages renderer. App Router
entries use different route modules and traces.
### Fix
Trace the hidden Pages renderer dependency using the mechanism
appropriate to each bundler:
- **Turbopack:** add `pages-turbo.runtime.prod.js` as an explicit entry
in `Project::pages_traced_modules`. The existing native Turbopack module
graph then traces its full runtime closure. This does not run Node File
Trace and remains scoped to Pages endpoints.
- **Webpack:** run the existing Node File Trace path on
`pages.runtime.prod.js` and merge that closure into the Pages shared
assets. This code remains inside the non-Turbopack branch.
The existing require-hook modules remain separate, and App Router or
neutral endpoint trace sets are unchanged. No adapter change is
required.
### Regression test
The fixture externalizes a package that imports `next/head`, matching
the reported dependency boundary. The test builds with an adapter,
materializes only the generated Pages API function entry and its
provided assets into a clean directory, initializes the production
environment, and requires the handler in a child process.
This verifies the observable initialization behavior under both
Turbopack and Webpack rather than asserting a particular output file
list.
## Verification
- `cargo check -p next-api`
- `pnpm --filter=next build`
- `pnpm test-start-turbo
test/production/adapter-pages-api-runtime/adapter-pages-api-runtime.test.ts`
- `pnpm test-start-webpack
test/production/adapter-pages-api-runtime/adapter-pages-api-runtime.test.ts`
<!-- NEXT_JS_LLM -->
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
A response may contain two payloads: the full response, plus a shell
stage that the client obtains by decoding the same bytes again,
truncated at the shell byte offset. Each flow had its own copy of the
logic that decides which payload fulfills the pending entries and what
fetch strategy each write records. Those copies are replaced by a single
function, writeResponsePayloadsIntoCache. The embedded runtime prefetch
stream, which previously ignored the byte offset entirely, now goes
through it too.
When a shell request receives more than it asked for, the extra content
is at least as complete as what the corresponding non-shell request
would have returned, so that's the strategy the write records: PPR for a
StaticShell request, PPRRuntime for a RuntimeShell request. Previously
some of these writes under-recorded, causing the scheduler to issue
runtime requests for content it already had. A rejected
needsRuntimeRequest row now also reads conservatively (as true) on the
client, matching how the server reads the same flag.
The second commit fixes an oversight where a static prefetch attempt was
mistakenly skipped: when the scheduler found a cached entry that was
only a shell, it deopted straight to a runtime request even though the
full static response contains more and the segment's hint says a static
attempt is worthwhile. A shell entry with the hint now takes the normal
static revalidation path first, and the response's own signals decide
whether to escalate afterward.
This is the last of the changes to convert all RSC requests to a single
unified response type, NavigationFlightResponse.
The old format was a positional array whose ordering had to be kept in
sync by hand between the server and the client. The new format is the
same root-anchored tree used by every other response, so the dedicated
client write path for per-segment responses is deleted and these
responses go through the shared one.
A few functions are renamed as their roles change:
convertServerPatchToFullTree becomes createNavigationSeed (it now also
accepts trees with no base to overlay), fetchSegmentsOnCacheMiss becomes
fetchSegmentPrefetchesUsingStaticRequest (the counterpart of
fetchSegmentPrefetchesUsingRuntimeRequest), and writeSeedDataIntoCache
becomes writeTreeDataIntoCache (seed data no longer exists as a
concept).
During the unification, a few inconsistencies and oversights were caught
and fixed: response data with no matching pending entry is written as a
detached entry instead of dropped, and a response's byte size is spread
across the entries it actually fulfilled rather than the old chain-based
count. The fallback retry loop also marks itself pending before the
first cache write instead of after, closing a window where the scheduler
could spawn a duplicate revalidation.
The second commit unifies cache keying for prefetched segments. There
were two implementations of the logic that decides which cache key a
prefetched segment is written to, one for per-segment responses and one
for responses produced by a live render. Now there's one: if the server
reported which params the segment depends on, key the entry using those;
otherwise fall back to the keying implied by how the payload was
fetched. A segment is only treated as param-independent when the server
says so. Two precedence fixes were needed to merge them, and a few other
inconsistencies found during the unification are fixed along the way;
see the commit message for details.
Every flow that writes a server response into the client cache now goes
through a single function, writeServerResponseIntoCache.
This is preparation for subsequent steps in this stack that migrate even
more callers to this unified path.
While we're modifying it, fetchSegmentPrefetchesUsingDynamicRequest is
renamed to fetchSegmentPrefetchesUsingRuntimeRequest: these responses
are produced by a runtime request, and "dynamic" refers to non-cached
data, which prefetch responses never contain.
One subtle fix that fell out of the unification: a runtime prefetch
response that carries no transport data used to make the flow bail
before writing anything; now the shared function rejects the pending
entries it was given, the same as a build id mismatch.
Part of a stack of changes to convert all RSC requests to a single
unified response type, NavigationFlightResponse.
This step converts static route tree prefetches (i.e. `/_tree`
requests).
The old format could omit a dynamic param's value from the response so
that a statically served response is cacheable across param values; the
client parses the value from the URL instead. That behavior moves into
the shared decoder, where it's now permitted for any transport response.
A cached /_tree response from an older build decodes without a build id
and falls back to an MPA navigation, like any other cross-build
response. The staleTime field of the old format is not carried over
because the client never read it.
### What?
Partially reverts #94905 (`668cd3a`), restoring i18n locale prefixing
for **dynamic Pages API routes** in the adapter build output:
```diff
-const shouldLocalize = Boolean(config.i18n) && !isAPIRoute(route.page)
+const shouldLocalize = Boolean(config.i18n)
```
The middleware rewrite query fix from the same PR (`next-server.ts`,
fixes#94647) is deliberately **left in place** — it is unrelated to
this routing regression.
Fixes#96935
### Why?
Whether `/api/...` carries a locale prefix has to be agreed on by two
independent halves:
1. **this matcher** — whether the emitted dynamic route regex expects
`(?<nextLocale>…)`
2. **the Vercel adapter's i18n rules** — whether they prepend
`/{locale}` to `/api/...`
When they disagree the matcher is unreachable and the request falls
through to `/{locale}/404`.
The counterpart adapter change (nextjs/adapter-vercel#81) shipped in
`@vercel/next@4.21.3` on Aug 10 — roughly two months after this half
landed in `v16.3.0-canary.69`. Every app in the resulting skew 404s on
dynamic Pages API routes with `x-matched-path: /{locale}/404`:
| | adapter beta.24 (`@vercel/next` ≤ 4.21.2) | adapter beta.25 (4.21.3)
|
| --- | --- | --- |
| Next < `16.3.0-canary.69` | ✅ | ❌ |
| Next ≥ `16.3.0-canary.69` | ❌ | ✅ |
Static Pages API routes are unaffected — they resolve directly against
the filesystem. `/api/echo/%5Bslug%5D` returns 200 for the same reason:
it filesystem-hits the literal `[slug]` output before any i18n rule
runs.
Reverting this collapses the two behaviors back into one that holds for
every Next.js version: dynamic Pages API routes are localized like any
other dynamic route.
### ⚠️ Must land with the adapter revert
On its own, against `@vercel/next@4.21.3`, this **extends** the 404 to
every Next.js version rather than fixing it. It pairs with
nextjs/adapter-vercel#101 (already merged, published as
`adapter-vercel@0.0.1-beta.26`), which has not yet been synced into a
published `@vercel/next`.
| | build container ≤ 4.21.2 / beta.26 | 4.21.3 (beta.25) |
| --- | --- | --- |
| with this revert | ✅ all versions | ❌ all versions |
### How?
Removes the `!isAPIRoute(route.page)` condition and the now-unused
`isAPIRoute` import, and inverts the corresponding assertion in
`test/production/adapter-config-i18n` so it asserts the localized shape
(`/$nextLocale/api/proxy/[[...slug]]?nxtPslug=$nxtPslug`), matching the
sibling `pageRoute` assertion.
A full `git revert` of `668cd3a` was not used: the test fixture has
since been renamed (`adapter-config-i18n-api` → `adapter-config-i18n`)
and extended, and one of its files was renamed into an unrelated test,
so a wholesale revert deletes later work.
### Verification
Reproduced offline without a deployment, by running real builds of a
Pages Router + i18n fixture (`/api/ping`, `/api/echo/[slug]`,
`/api/users/[...path]`, `/blog/[slug]`) against both adapter versions
and replaying the emitted `config.json` route tables. The replay
reproduces all four reported baselines, including both off-diagonal
404s.
- [x] Prettier clean on both changed files
- [ ] `pnpm test test/production/adapter-config-i18n` — **not run**
(needs a full `pnpm --filter=next build`); please confirm in CI
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Warn during prerendering when the exact UTF-8 size of the uncompressed
postponed-state body exceeds `experimental.maxPostponedStateSize` while
Resume Data Cache compression is enabled.
Add `experimental.disableResumeDataCacheCompression` as an opt-in
rollout flag. It defaults to `false`, preserving the existing compressed
representation. When enabled, both persistence and parsing use raw JSON,
allowing a controlled minimal-mode rollout without format negotiation.
RDC serialization now happens in explicit steps: stringify, check the
raw body size, then conditionally deflate. This avoids compression-ratio
estimates and duplicate serialization.
This is the lower PR in a two-PR stack. It can land first so the raw
representation can be enabled selectively for minimal-mode deployments
and observed on canary before compression is removed.
## Verification
- `pnpm exec jest
packages/next/src/server/resume-data-cache/resume-data-cache.test.ts
packages/next/src/server/app-render/postponed-state.test.ts --runInBand`
- `pnpm --filter=next types`
- `pnpm --filter=next build`
<!-- NEXT_JS_LLM -->
With `experimental.testProxy` on, `@mswjs/interceptors` (switched to
socket-level interception in #96059) also catches the passthrough
`fetch` that `handleFetch` makes for non-test requests. That request has
no `next-test-internal` marker, so the interceptor calls `handleFetch`
again, takes the passthrough branch again, and loops until it errors. So
any server-side request made outside a test context never resolves
during a test run.
This adds the `next-test-internal` marker to the passthrough request,
and to the `continue` case that has the same problem. It is the guard
#96059 already added for the proxy-protocol request, so the request
reaches the real server instead of being intercepted again.
The goal of this PR increase the expected benefit of merging two chunks
when their overlapping chunk groups are in a "cluster" (a cluster is a
set of routes that are commonly visited together). This is because in
these cases the Z+Z case (one of six in the N=2 set of cases) is more
likely than others. To this I have increased the probability of the Z+Z
case.
At the moment, to do this I've done the following to the probabilities:
```rust
/*
MERGED CASE (N = 2):
case X + X (p = (a_rem/groups) * ((a_rem - 1)/rem_g)): size = a_size, requests = 1
case Y + Y (p = (b_rem/groups) * ((b_rem - 1)/rem_g)): size = b_size, requests = 1
case Z + Z (p = (o_groups/groups) * (o_groups - 1)/rem_g): size = (a_size + b_size), requests = 1
case X + Y (p = (a_rem/groups) * (b_rem/rem_g) + (b_rem/groups) * (a_rem/rem_g)): size = a_size + b_size, requests = 2
case X + Z (p = (a_rem/groups) * (o_groups/rem_g) + (o_groups/groups) * (a_rem/rem_g)): size = a_size + (a_size + b_size), requests = 2
case Y + Z (p = (b_rem/groups) * (o_groups/rem_g) + (o_groups/groups) * (b_rem/rem_g)): size = b_size + (a_size + b_size), requests = 2
Request count is different in this case: Z + Z (better)
Requests size is different (worse) in these cases: X + Z, Y + Z
Each cost / benefit is weighted by the probabilities above. There are cases when
we know that Z + Z is more likely due to common user behaviour. This is based on
the "cluster" chunking heuristic we provide. We increase P(Z + Z) when two or more
routes in a cluster request both chunk items together (ie. request Z). P(X + Z) and
P(Y + Z) are therefore less likely. To increase P(Z + Z) while maintaining a total
probability of 1, we do the following (when chunk items overlap in a cluster):
P'(X + Z) = (1/2) * P(X + Z)
P'(Y + Z) = (1/2) * P(Y + Z)
P'(Z + Z) = P(Z + Z) + (1/2) * P(X + Z) + (1/2) * P(Y + Z)
Otherwise:
P'(X + Z) = P(X + Z)
P'(Y + Z) = P(Y + Z)
P'(Z + Z) = P(Z + Z)
*/
```
This increases `P'(Z + Z)` while reducing `P'(X + Z)` and `P'(Y + Z)`.
It is a bit of a blunt tool for doing this, however. So if anyone has
other suggestions for how to change this probabilities while maintaining
existing behaviour - let me know!
Fixes
```
[17:03:12] ncc_mswjs_interceptors failed because [tsl] ERROR in /home/runner/work/next.js/next.js/packages/next/src/server/og/cache-image-response.ts(27,17)
TS2307: Cannot find module 'next/dist/compiled/@vercel/og/index.node.js' or its corresponding type declarations.
```
--
https://github.com/vercel/next.js/actions/runs/31515393262/job/93859146712#step:8:723
Now we use externalize the `Promise.withResolvers` polyfill to allow
running `ncc` before we built `dist`.
## Summary
The onBeforeDeferredEntries callback can update deferred source files
after Webpack has already cached their contents while building initial
entries. This tracks the source directories represented by the deferred
entrypoint set and purges only those paths before adding deferred
entries so callback-time source is compiled without dropping unrelated
filesystem cache state.
The deferred entries fixture now primes both deferred sources through
Webpack before the callback and also primes a non-deferred source. It
verifies deferred entries use updated content while the unrelated source
remains cached.
## Verification
- `HEADLESS=true pnpm test-start-webpack
test/e2e/deferred-entries/deferred-entries.test.ts`
- Control run with the old global purge: the build fails because the
unrelated cached source is evicted
<!-- NEXT_JS_LLM -->
#94703 and #95116 both changed how internal headers are kept out of
userland `headers()`, and between them they left `headers()` detached
from the request it describes. #94703 started stripping the dev-only
request-id headers alongside the flight headers that had been stripped
for much longer. Those deletes ran against the shared request headers,
because `HeadersAdapter.from` returns a `Headers` instance unchanged
instead of copying it, and removing the request-id headers there broke
the dev debug channel when the dev server rendered a redirect target
after a server action. #95116 fixed that by copying the headers before
stripping them.
Neither change set out to alter whether `headers()` follows the request.
The sealed view has read through to the underlying headers for as long
as `HeadersAdapter.seal` has existed, and its own unit tests assert
that. The copy was only a way to keep the deletes from escaping into
`req.headers`, and the detached view was collateral that no test
covered.
The result, since 16.3.0, is that a Proxy which writes a header onto
`request.headers` and then reads it back through `headers()` gets the
value from before the write. Reading the same header directly off
`request.headers` returns the new value, so two APIs describing the same
request disagree, and a second `headers()` call returns the same stale
view. Whether the write is seen at all depends on ordering, because the
view is built on first access: a write made before the first `headers()`
call is observed, and the identical write made after it is not.
This change drops the copy and stops deleting anything.
`HeadersAdapter.seal` now accepts a set of header names to omit from
every read operation, and `getHeaders` seals the shared request headers
directly with the flight headers and the dev request-id headers hidden.
Both earlier intentions survive. The internal headers stay on the
request where the framework still reads them, they stay invisible to
userland `headers()`, and the sealed view tracks the request again, so
the two APIs agree throughout a Proxy run.
Hiding on read is slightly stricter than the delete it replaces. The
delete ran once, when the view was created, so an internal header
written to the request afterwards would have shown through. It now stays
hidden no matter when it is written.
While restructuring `seal` we also corrected the `parent` argument that
`forEach` passes to its callback. The native method passes the unsealed
target, which hands the callback a mutable handle on the underlying
headers and defeats the seal. Both the plain and the hiding handler now
pass the sealed proxy instead.
`cookies()` is unaffected and stays a snapshot, because `RequestCookies`
parses the `cookie` header when it is constructed. That predates 16.3
and is left alone here.
The fix is based on #97145 by @tachsin. That pull request comes from a
fork, and deploy tests do not run for fork pull requests, so it is
recreated here on a branch of this repository to get full CI coverage.
The commit carries a `Co-authored-by` trailer for them, so their credit
is preserved on merge. We also took the liberty of a few improvements
over the original: `seal` is restructured into two handlers rather than
a guard repeated in each case, `getSetCookie` and `forEach` are handled
as described above, and the unit and end-to-end coverage is wider.
fixes#97049closes#97145
---------
Co-authored-by: joaquin <tachsinachmet@gmail.com>
Claude noticed this while working on
https://github.com/vercel/next.js/pull/95391.
Basically, the queue's React state always gets updated in the order of
dispatching. So if a navigation displaced pending actions, it's the
*navigation's* promise that was last. So when the pending action runs
after it, that action's state is no longer taken into account, even if
it revalidated. This is why the new data won't reflect on the screen.
With the fix, the final state is properly reflected.
## Test Plan
New test, which was red before the fix.
Also tried it on a mini-app.
Before:
https://github.com/user-attachments/assets/2235b116-82c4-4ef4-a63c-78e157c90470
After:
https://github.com/user-attachments/assets/d7b685c7-d0a5-4b91-8da7-78bb14011313
## How
We re-render at the end with the final queue state — if we know it was
preempted.
In CI we do not retain old versions of the db at all, but in dev or
non-CI settings we would retain up to 2 indefinitely.
Given how large caches can grow, this PR reduces it to no more than 1
old version, and then only for a certain amount of time, currently 3
days. The idea being to support switching back and forth between feature
branches and a next.js upgrade branch, 3 days allows this worflow to
survive a weekend.
To support this the `CURRENT` file format is modified. Instead of just
storing a sequence number it now stores a small json object containing
the `max_sequence_number` and the mtime. We store the last_write_time in
the file to support the fact that we anticipate `cache` directories to
be copied around and the native mtime would record the copy time instead
of the commit time.
This should reduce dev time disk usage in the common case.
Fixes a few bugs related to optimistic routing.
Originally reported as a next-intl request waterfall in
MarkBekooy/prefetching-request-waterfall-bug#1, then extracted into an
isolated regression test: A proxy that rewrites every URL to inject a
leading path segment (`/one/two` → `/en/one/two`, i.e. i18n with the
default locale hidden from the URL) plus a fully dynamic target route
like `/[locale]/[...pages]` leads to an infinite prefetch loop that
never resolves.
This regression uncovered several oversights in the optimistic routing
implementation: when receiving a prefetch response, we did not check
whether the response matched the expected result. If the tree
mismatched, in some cases the prefetch task would fall into a loop,
repeatedly attempting to fulfill the missing data.
Now when this happens, we record on the local route definition that a
dynamic rewrite occurred, disabling further attempts to optimistically
resolve the route. This is the same strategy we were already using for
normal navigation responses, now applied to the prefetch path.
We also received another bug report with a similar root cause: parallel
routes with conflicting dynamic params at the same level
(`@modal/[...catchAll]` next to `[username]`) cannot be distinguished
using the current traversal algorithm, because it assumes that each
segment is resolvable independently without inspecting the children or
sibling branches. To work around this issue for now, when this scenario
is detected, we disable optimistic routing for the conflicting route. We
can model this properly in a future PR by having the server send down
the "sibling" dynamic route segments, similar to what we do for static
siblings already.
Fixes#97135
<!-- NEXT_JS_LLM -->
### What?
Adds support for passing a module type to the TypeScript overloads for
`import.meta.glob`:
```ts
const entries = import.meta.glob<Entry>("./entries/*.ts");
```
This works with both lazy and eager imports. If a module type isn’t
provided, it defaults to `unknown`, which matches the current behavior.
### Why?
Next.js already supports Vite’s `import.meta.glob` API at runtime, but
its TypeScript declarations don’t support [Vite’s module
generic](https://github.com/vitejs/vite/blob/57fea001d154e7dd8d5d74d3082731f1dcfd31be/packages/vite/types/importGlob.d.ts#L113-L130).
Because of this, a typed glob works in Vite but fails in Next.js:
```text
TS2558: Expected 0 type arguments, but got 1.
```
Consumers currently have to cast the result or add their own
`ImportMeta` declaration to describe the matched modules. Supporting the
generic removes the need for those workarounds and makes typed glob
imports consistent between Next.js and Vite.
### How?
Each existing overload now accepts a generic module type, `M`, which
defaults to `unknown`:
- Lazy imports return `Record<string, () => Promise<M>>`.
- Eager imports return `Record<string, M>`.
Using `unknown` as the default means existing calls without a generic
keep the same types as before.
I also updated the existing e2e fixture. It had an old local
`ImportMeta.glob` declaration that returned `Record<string, any>`.
Removing that declaration exposed the missing generic and allowed the
five `as any` casts in the fixture to be removed. Without the generic,
all five glob calls fail during `next build` with `TS2558`. With it, all
five pass.
The Turbopack documentation has also been updated with examples showing
how to use the generic with lazy and eager imports.
### Checklist
- [x] `pnpm --filter=next types` passes
- [x] `pnpm prettier-fix` run
- [x] Documentation follows the docs contribution guide
- [x] E2E test passes
---------
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
This PR does two things: adds a flag `turbopackCjsScopeHoisting` and
plumbs it through and adds the extra stuff we need in
`turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs`
(just the names of the things exported by CJS) for PRs down the stack to
work.
## Summary
`experimental.ppr` is deprecated, and Cache Components is now the only
internal path that enables partial prerendering. This makes the legacy
PPR renderer and its `prerender-ppr` work unit unreachable.
This removes that rendering path, the associated work-unit branches,
Next.js’s remaining integration with React’s deprecated
`unstable_postpone` API, the dead `__NEXT_PPR` client flag, and obsolete
legacy PPR tests.
Cache Components’ postponed-state and resume behavior remain unchanged.
<!-- NEXT_JS_LLM -->
## Summary
`experimental.ppr` is deprecated, and configuring it with a truthy value
already produces an error. However, enabling Cache Components still
backfilled this option internally, and several build and runtime paths
continued treating it as the source of truth for PPR.
This change:
- uses `cacheComponents` directly for application and route PPR checks
- removes the internal `experimental.ppr` backfill and associated
plumbing
- deletes the obsolete PPR configuration helpers
- removes legacy PPR telemetry and an unreachable non-Cache-Components
workaround
- preserves the deprecated config type, schema, and migration error for
user-provided `experimental.ppr`
This does not change supported behavior: Cache Components already
implied `experimental.ppr: true` internally, so these checks resolve to
the same value.
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
## Summary
Make the remaining legacy PPR implementation explicit by renaming the
render capability, prerender store, component, and tracking helper
around `React.unstable_postpone`.
`isLegacyPPR` is only enabled for route-level PPR without Cache
Components. The existing `prerender-ppr` work-unit discriminator and
rendering behavior are preserved, making this deprecated path easier to
audit and eventually remove.
## Summary
- Fix "occurence" → "occurrence" in `test/lib/router-act.ts`
- Fix "wether" → "whether" in
`packages/next/src/server/stream-utils/encoded-tags.ts`
## Test plan
- [x] Changes are comment-only, no code logic affected
Attribution: This change was originally authored by @evandroreichert in
#90785.
Co-authored-by: Evandro <evandroreichert07@gmail.com>
### What?
Fix two spelling mistakes in comments.
### Why?
Readability.
### How?
Updated comment text only.
Attribution: This change was originally authored by @adityagiri3600 in
#90594.
Co-authored-by: Aditya Giri <74224708+adityagiri3600@users.noreply.github.com>
A cache implementation may serialize cache metadata into HTTP request
headers, whose values are limited to Latin-1. `unstable_cache` assembles
a cache item name from the request URL and the name of the cached
callback, and neither part was encoded. When that name holds a character
above U+00FF the conversion throws before the request is dispatched, so
the read never reaches the cache and the write that follows it fails the
same way. Nothing is stored, nothing is found, and the entry falls back
to the origin on every render.
The name is built by `getFetchUrlPrefix`, which reads the pathname and
the search parameters out of the request URL. The pathname stays
percent-encoded, but `URLSearchParams` returns decoded keys and values,
so a non-ASCII query parameter is the reachable case: it applies to any
dynamic route that calls `unstable_cache`, whether or not the route
reads `searchParams`, and therefore also to a parameter a caller
appends. A callback whose name holds such a character is affected too,
though a production build usually renames the binding.
This change encodes the assembled name with `encodeHeaderSafe`. That
helper only replaces characters outside the class Node accepts in a
header value, so the separating spaces and the URL punctuation are
preserved and the name keeps its documented shape. Every name that is
representable today is returned unchanged, so this is inert for existing
entries. The item name is a label: it is not the cache key, which is
derived separately from the callback's key parts and arguments, and the
Suspense Cache API neither parses nor matches on it.
The test covers the two parts of the name on separate routes so a
failure names the part it comes from, and asserts the constraint rather
than either input, since which inputs are live depends on the bundler
and on minification. A deployment checks it through the real cache
handler implementation, where the failure shows up as an entry that is
recomputed on every request.
fixes#76286
The helper percent-encodes anything outside Node's valid header-value
class so a string can be serialized into an HTTP header. That is a
property of the transport, not of cache tags, and the next caller is
`unstable_cache`'s synthetic `fetchUrl`, which is a cache item name
rather than a tag. Naming the function after one of its callers would
make that call site read as though it were tagging something.
This change renames the module to `encode-header-safe.ts` and the
function to `encodeHeaderSafe`, and generalizes the leading paragraph of
the doc comment accordingly. The rationale specific to cache tags stays,
since tag construction and invalidation remain the callers today. The
doc now also names the `ByteString` conversion that `fetch` performs on
header values, alongside `validateHeaderValue`; both already applied to
tags, because the Vercel bridge sends them through `fetch`.
There is no behavior change. The function body is identical apart from
the parameter name.
Fix typo issue for comment
// writes the manifest again for builds.
Attribution: This change was originally authored by @keithjackson1031 in
#89841.
Co-authored-by: codexromeo88 <jakecass769@gmail.com>
Route discovery currently feeds app page keys both into compilation and
into post-build work like adapters and standalone output. This instead
filters the discovered keys against app-paths-manifest after compilation
and uses that emitted set for post-build processing. Today every
discovered app entry is emitted, so this does not change behavior. It
just makes the build output, rather than the earlier discovery input,
the source of truth for work that happens after compilation.