Commit Graph

3897 Commits

Author SHA1 Message Date
Andrew Clark dd76599c34 Scaffolding for concurrentRouterQueue flag (#97413)
Sets up the scaffolding for a rewrite of the client router's queue.

I've named the new module concurrent-router-queue.ts and the existing
module sequential-router-queue.ts, since the main goal of the new one is
to improve the router's integration with React's concurrent rendering
model. The experimental flag is likewise named `concurrentRouterQueue`.

There's no implementation yet; all router operations throw an error when
the flag is enabled.

The main entry point into the router is navigator.ts. The flag swaps
between implementations at the module level to ensure the inactive code
is eliminated from the final bundle.
2026-08-15 23:46:34 -04:00
dan 2fbeebbaca Wait for back-before-hydration recoveries in the browser (#97321)
The `back-before-hydration` assertions read the heading with
`browser.elementByCss`, which waits for the load event before it
returns. Here the load event cannot fire until the stalled scripts are
released and finish running, and that is the whole window the test is
about. So the first attempt inside `retry` blocks for almost the entire
3s budget, looks at the DOM once, and then `retry` refuses a second look
because `waited + interval > duration`.

This waits in the browser instead, with the load event out of the
picture. Every page already has its own heading id, except the two
search pages, which shared one; they get a per-page id too. Then a
single `waitForSelector` resolves the moment that page commits, and the
URL assertion follows the wait rather than racing alongside it. The
assertions that check content *stayed* put keep polling with `retry`,
since a change there is the failure.

Holding the flight response for 1800ms and throttling the renderer 8x
reproduces the CI failure exactly on canary — same `Failed to retry
within 3000ms`, same expected "Home" / received "Post" — and passes with
this change; the suite is still 16/16 with the response held for 6s.

Test-only.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:03:50 -07:00
Tim Neutkens 6b9001934d Remove server route matcher stack (#94157)
### What?

Remove the legacy server route matcher managers, route matcher
providers, and server route matcher classes. Move route definition and
params metadata into fsChecker output and request metadata instead.

### Why?

The filesystem checker already owns the normalized route lookup path in
production. Keeping a separate matcher manager stack duplicates route
inventory, adds reload plumbing, and makes dev and production behavior
harder to keep aligned.

### How?

- Build pages and app route definitions in fsChecker from production
manifests and from the dev route inventory.
- Thread matched route definitions and params through resolve-routes and
router-server request metadata into base-server and next-server.
- Remove reloadMatchers propagation and delete the legacy matcher
manager, provider, and matcher implementations and tests.
- Preserve dev ensurePage behavior, including legacy custom-server
render methods, and Pages API loading by using matched route definitions
while loading compiled dev modules where needed.

### Verification

- `pnpm test-start-webpack test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-start-turbo test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-webpack test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-turbo test/e2e/custom-routes/custom-routes.test.ts`
- `pnpm test-dev-webpack
test/e2e/on-request-error/dynamic-routes/dynamic-routes.test.ts`
- `pnpm test-dev-turbo
test/e2e/on-request-error/dynamic-routes/dynamic-routes.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/app-routes/app-custom-routes.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/edge-route-rewrite/edge-route-rewrite.test.ts`
- `pnpm test-start-webpack
test/e2e/middleware-rewrites/test/index.test.ts`
- `pnpm test-start-webpack test/e2e/middleware-matcher/index.test.ts
test/e2e/middleware-custom-matchers-basepath/test/index.test.ts
test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts`
- `pnpm test-start-webpack test/e2e/api-catch-all/api-catch-all.test.ts
test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts
test/production/root-catchall-cache/root-catchall-cache.test.ts`
- `pnpm test-start-webpack
test/e2e/i18n-beforefiles-rewrite/i18n-beforefiles-rewrite.test.ts`
- `pnpm test-start-webpack
test/e2e/basepath/redirect-and-rewrite.test.ts`
- `pnpm test-dev-webpack test/e2e/custom-server/custom-server.test.ts`
- `pnpm test-start-webpack test/e2e/custom-server/custom-server.test.ts`
- `pnpm --filter=next types`
- `pnpm --filter=next build`
- `git diff --check`

<!-- NEXT_JS_LLM_PR -->
2026-08-15 13:07:22 +02:00
Niklas Mischkulnig d8be6c5b73 test: Reenable i18n-api-support deploy test for Turbopack with adapters (#96898)
Reverts #95739
It's fixed in the adapter:
https://github.com/nextjs/adapter-vercel/pull/81
2026-08-14 14:29:26 +02:00
David Alexandru Ilie 0311e1436b Trace instrumentation startup (#97296)
## 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**.
2026-08-14 12:39:01 +01:00
David Alexandru Ilie 44103a8702 Trace route module preparation (#97295)
## Summary

- trace the full route-module preparation boundary as
`RouteModule.prepare`
- record manifest loading as one aggregate `RouteModule.loadManifests`
child span
- keep route preparation visible by default while exposing manifest
detail only with verbose tracing
- cover normal and direct-entrypoint server lifecycles without relying
on test ordering

## Why

Route preparation is the stable boundary between loading a route module
and executing it. The parent span shows the total setup cost; the
aggregate manifest child explains manifest work without creating
per-file spans or exposing paths.

## Verification

- `pnpm build-all`
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- focused OpenTelemetry E2E in development and production with Turbopack
and Webpack
- Request Insights route-preparation E2E with Turbopack and Webpack
- Prettier, ESLint, and `git diff --check`

## Stack

Depends on **Trace route module loading**.
2026-08-14 12:39:01 +01:00
David Alexandru Ilie d481cfba9c Trace route module loading (#97318)
## 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`
2026-08-14 12:39:00 +01:00
Hendrik Liebau cdf5c37d84 Scope app-entry export validation to files inside the app directory (#97350)
Adopts #97150. Closes #97150.

Fixes #96859

### What?

Since 16.3.0, builds fail for pages-router files named `sitemap` or
`robots` that export `getStaticProps`/`getServerSideProps`:

```
Error: "getStaticProps" is not supported in app/.
```

This is a regression from #94962, which added the metadata conventions
(`sitemap`, `robots`, `manifest`, `icon`, …) to the app-entry filename
regex in `ReactServerComponentValidator::assert_invalid_api`. The regex
only looks at the filename, so a file like `pages/sitemap.js` is now
mistaken for an app entry and rejected for exporting `getStaticProps`.

### How?

A file is only treated as an app entry when it's inside `appDir`,
reusing the gate `assert_server_filename` already applies to `error.js`.
The pages compilation context has no `appDir`, so pages-router files are
never validated as app entries.

On the test side:

- The fixtures that exercise the app-entry checks moved under
`app-dir/`, since the test harness derives `appDir` from the fixture
path. Their contents are unchanged.
- A new fixture (plus a `sitemap.js` fixture glob) asserts that a
pages-style `sitemap.js` compiles without errors.
- A new e2e suite, `test/e2e/pages-metadata-filenames`, covers
`pages/sitemap.js` with `getStaticProps` and `pages/robots.js` with
`getServerSideProps`. It fails without the fix under Turbopack (build
and dev) and passes with it under both Turbopack and webpack.

Supersedes #96873 (same approach, closed by its author) and closes
#96967.

---------

Co-authored-by: Rodrigo Arias <rodrigo@arias.me>
2026-08-14 11:53:24 +02:00
Hendrik Liebau 40188b5e39 [test] Cover 'use cache: private' in route handlers (#97349)
The `use-cache-private` suite covered private caches in server
components only, so this adds a route handler that calls one `'use
cache: private'` function three times: a concurrent pair, and a third
call after a short delay. The concurrent pair joins a single in-flight
invocation, while the delayed call lands after the map of pending
intra-request invocations has dropped its entry, so it is served from
the completed entry that the request retains. Each call passes a fresh
object literal, which makes the React `cache()` memo miss on reference
equality so that the lookup falls through to the serialized cache key.
The cache function also reads a cookie, which covers that request APIs
are allowed inside a private cache in a route handler.

A second test asserts that nothing is shared across requests. In
production private entries are neither persisted nor deduped across
requests, so every request re-runs the cache function. In development
they are persisted and forced to `revalidate: 0`, so the next request is
served the stale entry while a fresh one warms in the background, which
the dev branch of the test follows. Both tests pass their own `id`
search param, and therefore use their own cache key, so the entry that
dev persists for one is never served to the other.

This also drops the claim that the directive is not available in route
handlers from the `'use cache: private'` reference. Nothing in the
source rejects it, and it was already allowed for request work unit
stores when that note was written.
2026-08-14 10:50:45 +02:00
Andrew Clark 730db756bc Convert tree prefetches to NavigationFlightResponse format (#96788)
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.
2026-08-13 18:18:22 -04:00
next-js-bot[bot] 9690bae79a Upgrade React from 11eddecd-20260805 to 22e4f993-20260811 (#97249)
Co-authored-by: next-js-bot[bot] <279046576+next-js-bot[bot]@users.noreply.github.com>
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2026-08-13 10:19:25 +00:00
Lazizbek Ergashev 3782922bdd [testmode] Fix infinite recursion in testmode passthrough fetch (#96525)
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.
2026-08-13 04:05:37 +02:00
Hendrik Liebau 3a20e2ab36 [test] Unflake use-cache-default-profile-expire-zero tests (#97250)
The first assertion in `use-cache-default-profile-expire-zero` retried a
full browser reload until the `expire: 0` cache produced a fresh value.
In dev that value is served warm across reads, so more than one read is
needed, and on a CI runner one of those reloads costs more than the
default 3000ms budget can grant a second attempt for. The dev failure
reports `waited 3162ms` after a single attempt, in a test that took
15.6s to get there.

Neither test asserts anything about the browser: both only read the
rendered value and check that the nested-cache error stayed out of the
CLI output. Reading the value with `next.render$` brings one attempt
down to between 30 and 174ms, so the default budget holds, and it drops
two browser launches. The helper rejects a missing element, because the
empty string that cheerio yields for one would otherwise satisfy the
assertion that the value changed.

The test predates #96354, which replaced the try counting in `retry`
with a clock. Before that change `retry` granted seven attempts
regardless of how long each one took, so a 3s reload was affordable;
after it, any attempt over 2500ms consumes the entire budget on its own.
#97187 makes the same change for two assertions that were failing this
way.

[Flakiness metrics][flakiness-metrics]

[flakiness-metrics]:

https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22use-cache-default-profile-expire-zero%20treats%20a%20short%20default%20cacheLife%20profile%20as%20a%20dynamic%20hole%2C%20not%20a%20nested-cache%20error%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1785948830138&end=1786553630138&paused=false
2026-08-12 21:50:11 +02:00
Josh Story 1690074b9b Invalidate deferred Webpack entry sources (#97205)
## 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 -->
2026-08-12 08:01:46 -07:00
Hendrik Liebau 3c97df56ea Restore the live headers() view of the incoming request (#97166)
#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 #97049
closes #97145

---------

Co-authored-by: joaquin <tachsinachmet@gmail.com>
2026-08-12 13:36:12 +02:00
Hendrik Liebau ac65c6b27c [test] Compile the middleware redirect routes up front in dev (#97190)
The `middleware redirect` test in the app dir navigation suite has been
flaky for months. After hydration the fixture posts to `/api/set-token`,
calls `router.refresh()`, receives a middleware redirect, and only then
does the browser location change. In dev each of those routes is
compiled on the first request that reaches it, so the assertion window
paid for compilation: on a CI runner the `POST /api/set-token` took
about 1.4s and the middleware redirect about 1.1s, while the second
occurrence of each of those same requests took 10 to 30ms. With the
default 3000ms budget and an attempt as cheap as `browser.url()`,
`retry` stops after about 2.5s, which is the `waited 2505ms` that every
failure reports, and a chain needing 2.6s of server time never fits.

The redirect itself always works. In the failing runs the `307` and the
dashboard `200` are both in the server log; they simply arrive after
`retry` has given up, so the assertion is the only thing that misses
them.

Hitting the two routes before opening the browser moves that compilation
out of the assertion window. The chain after hydration then costs about
50ms instead of 2.6s, so the existing budget is no longer the thing
under test and a real slowdown in the redirect would still fail. The
warm-up is skipped outside dev, where nothing is compiled on demand, so
production and deploy runs are unaffected.

The flakiness predates #96354, which replaced the try counting in
`retry` with a clock, but that change made it worse: the old
implementation ran a seventh attempt at around 3.0s, which is exactly
the window this location change lands in when the runner is slow.

[Flakiness metrics][flakiness-metrics]

[flakiness-metrics]:
https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22app%20dir%20-%20navigation%20middleware%20redirect%20should%20change%20browser%20location%20when%20router.refresh%28%29%20gets%20a%20redirect%20response%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40git.branch%3Acanary&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1785174487274&end=1786470487274&paused=false
2026-08-12 12:57:38 +02:00
Tim Neutkens b7a532c371 Fix unset crossOrigin in Turbopack manifests (#97164)
## Summary

Prevent Turbopack from adding `crossorigin=""` to generated script tags
when `crossOrigin` is not configured, and add production coverage for
static prerendering, dynamic rendering, and `output: 'export'`.

This regressed in #92070 when Turbopack added `crossOriginLoading`
support. The client reference manifest changed from an optional
cross-origin value to the shared `CrossOrigin` enum. Serde serialized
the enum's default `None` variant as the string `"none"`, so React saw a
present string and treated it as anonymous CORS, emitting
`crossorigin=""`. Webpack continued to omit its `undefined` value.

Skip serializing `CrossOrigin::None` in the Turbopack client reference
manifest so an unset option remains absent while preserving explicit
anonymous and credentialed modes.

## Verification

- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-start-turbo
test/e2e/app-dir/app-config-crossorigin/index.test.ts`
- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-start-webpack
test/e2e/app-dir/app-config-crossorigin/index.test.ts`

<!-- NEXT_JS_LLM -->
2026-08-12 09:19:23 +02:00
Hendrik Liebau c0c64a0ab1 Allow literal exports in 'use cache' files (#97181) 2026-08-12 06:42:24 +02:00
dan 1e113cf5cd Fix stale data after navigation despite revalidation (#95439)
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.
2026-08-12 01:43:17 +01:00
Andrew Clark 5942b37a42 Fix: Optimistic routing bugs leading to repeated prefetch loops (#97128)
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 -->
2026-08-11 08:44:48 -04:00
kelvin 7b848a4e2b Turbopack: accept a module type argument for import.meta.glob (#96991)
### 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>
2026-08-11 12:05:56 +02:00
Zack Tanner 268ae984aa Remove legacy PPR code paths (#96868)
## 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 -->
2026-08-10 18:39:14 -07:00
Zack Tanner d18e789721 Use Cache Components as the internal PPR signal (#96827)
## 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>
2026-08-10 18:39:14 -07:00
Zack Tanner d1123c92aa Make legacy PPR paths explicit (#96753)
## 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.
2026-08-10 18:39:14 -07:00
Hendrik Liebau 4286a42e2c Encode the cache item name built by unstable_cache (#96937)
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
2026-08-11 01:21:26 +02:00
Sam Poder 0bb7b8a22c [turbopack] Add e2e test that uses component chunks + workers (#96556)
Follow up to the bug that was fixed in
https://github.com/vercel/next.js/pull/96432.
2026-08-07 14:09:17 -07:00
Tobias Koppers 3ae062a107 fix(turbopack): support type: 'text' in rules, and error on binding imports of non-ESM modules (#96558)
`TurbopackModuleType` has listed `'text'` for a while, but the config
schema
and `ConfiguredModuleType::parse` both rejected it. It now maps to the
existing
`TextSourceTransform` (the same thing `import … with { type: 'text' }`
uses).
`'json'`, which Rust already accepted, is added to the TypeScript type
and
schema too.

`type: 'raw'` has always been documented as "Return raw contents as
string",
but it mapped to an opaque module with no exports instead — that
mismatch is
why `import * as ns from './alpha.md'` with `{ '*.md': { type: 'raw' }
}`
evaluated to `undefined`. `raw` is now an alias of `text`: both run the
file
through `TextSourceTransform` and export its contents as a string.
Neither
name is deprecated; `text` remains the descriptive spelling that matches
the
`import … with { type: 'text' }` import attribute, `raw` keeps working
exactly
as documented. No configuration value maps to the opaque module type
anymore;
`type: 'asset'` remains the way to emit a file and get its URL.

This is also what makes Vite's `?raw` workable: Turbopack has no
built-in
`?raw` handling, so a `{ condition: { query: '?raw' }, type: 'text' }`
(or
`'raw'`) rule is what turns those files into strings. Docs previously
implied
`?raw`/`?url` worked standalone; that's corrected, and the module type
table
is completed and describes `raw`/`text` as equivalent.

Reading a binding off a module that genuinely can't be placed in an
ECMAScript chunk (a stylesheet, a native addon, …) used to evaluate to
`undefined` with no diagnostic; it now reports an error naming the
module.
Side-effect-only imports still work.

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-07 11:38:59 +02:00
Zack Tanner ede8799c40 Require Cache Components for Instant Navigation testing (#96745)
## Summary

The Instant Navigation testing API is only supported with Cache
Components, but its testing cookie could previously force a non-Cache
Components route into the legacy PPR rendering path. This can cause
requests to fail because that path relies on React’s deprecated
`unstable_postpone` API, which is unavailable in the stable React
runtime.

This change couples testing API exposure and its client bundle machinery
to Cache Components, removes the testing-only PPR capability override,
and adds regression coverage confirming that the API remains inactive
without Cache Components. Supported Instant Navigation behavior is
unchanged.
2026-08-06 06:42:22 -07:00
Sebastian "Sebbie" Silbermann 2c04735944 [fragment-scroll] Remove config.experimental.appNewScrollHandler (#95602) 2026-08-06 09:53:09 +00:00
Niklas Mischkulnig 7916855653 Bump @swc/helpers (#96720)
https://github.com/vercel/next.js/issues/94634#issuecomment-5190067214:
> Module not found: Can't resolve '@swc/helpers/_/_wrap_reg_exp'
> This is on 16.3.0, if that matters. I'm not sure if I understood you
completely, but I want to try your fix before I open a new issue.

That helper was added in March 2026. We had upgraded the swc crates
itself but never upgraded the swc helpers.
2026-08-06 09:09:24 +02:00
Zack Tanner 89ca693c20 Derive foreground cache revalidation from the consumer (#96731)
## Summary

Foreground revalidation is necessary when a stale result will be
persisted by another server cache, not only when the request is
prerendering. Otherwise, a cache created during a dynamic request can
consume a stale inner value and extend its lifetime.

- derive foreground revalidation from whether the immediate work-unit
consumer will persist the result in a server cache
- model that capability as willConsumerServerCache rather than
inheriting it through the full outer scope chain
- treat server cache and static prerender work units as server-caching
consumers, while runtime prerenders are not
- add regression coverage for an outer "use cache" scope consuming a
stale unstable_cache entry

<!-- NEXT_JS_LLM -->
2026-08-05 15:44:29 -07:00
dan 0fd2e51ed6 Fix race when navigating Back before hydration (#96252)
Relands #95682 (reverted in #95853 due to #95848).

This contains:

- A bunch of tests (some failing and some passing before this PR; all
passing after this PR).
- The fix from #95682.

In short, we use Navigation API to detect during hydration that we're on
a different history entry than we started, and in that case, replay the
missed traversal with similar logic to what we do for `onPopState`.

The issue with the original PR was that under Activity, React would
sometimes hang applying the state update. This was fixed in
https://github.com/react/react/pull/37135 and synced, so we can do this
fix again now.

## Test Plan

Old and new tests.
2026-08-05 22:39:28 +01:00
Hendrik Liebau 1ff6a80358 Reuse completed cache entries for the rest of a request (#96727)
Calling the same `'use cache: private'` function twice in one request
executed its body twice in production. Preloading at the top of a
segment and reading the same function again lower down for
composability, which is the shape that motivates a preload in the first
place, therefore did the work twice instead of once.

The intra-request dedupe map dropped an entry as soon as its fill
completed, so it only ever covered concurrent invocations. A later
invocation fell through to the cache handler, and the `React.cache` memo
wrapping every cache function missed whenever the arguments were not
reference-equal. Public caches got a handler hit out of that, but
private caches have no handler in production and their entries are
excluded from the immutable Resume Data Cache of a dynamic request, so
nothing had stored the entry.

Completed invocations now move into `completedCacheInvocations` on the
work store instead of being dropped, and a later invocation joins that
entry. The pending map keeps its previous semantics untouched, because a
concurrent joiner shares a fill that is genuinely in flight and must not
re-run the discard checks against it, whereas a completed entry is a
stored value and is only reused when the caller has not asked to bypass
caches and nothing has invalidated it since. Private caches still get no
cache handler, so the map is what backs them, and it lives on the work
store and so cannot carry request-derived data beyond the request that
produced it.

Retention is limited to the kinds where it saves real work: private
caches, and kinds whose handler came from the platform or from
`cacheHandlers` config, where a read can be a network round trip.
`isBuiltInCacheHandler` answers that by tracking the handler instances
`initializeCacheHandlers` constructs itself. Recording instances rather
than kinds is what keeps aliasing right, since a self-hosted `remote`
resolves to the very same in-memory handler as `default` and retaining
its entries would duplicate that cache for nothing, while a
platform-supplied `remote` does not.

The `use-cache-custom-handler` suite covers the mechanism rather than
the observable value, counting `::get` calls to assert that two
sequential reads reach the handler once. Value equality alone cannot
distinguish a retained entry from a handler hit, which is also why the
new `dedup-sequential` fixture passes without this change and serves
only as a regression pin next to `private-dedup-sequential`.
2026-08-05 22:42:19 +02:00
Hendrik Liebau 5da1c1ae03 Discard only cache entries that predate a tag revalidation (#96726)
Calling `updateTag()` in a server action made every later read of a
cache carrying that tag regenerate for the remainder of the request,
including reads of an entry that had just been generated after the
invalidation and therefore already reflected it. Two sequential reads of
the same cache function during the re-render produced two different
values within a single render, and each one repeated the work.

`isRecentlyRevalidatedTag` only asked whether a tag appeared in
`pendingRevalidatedTags`, with no notion of when the revalidation
happened. That array lives for the whole `WorkStore`, which spans a
server action and the render that follows it, so once a tag was in it
every entry carrying that tag looked stale regardless of when it had
been produced.

Each pending revalidated tag now records a `revalidatedAt` timestamp
taken from the same clock as `CacheEntry.timestamp`, and the renamed
`isRevalidatedAfter` reports an entry as stale only when the
revalidation is newer than the entry. `CacheEntry.timestamp` is captured
before a fill begins, so a fill straddling a revalidation is still
discarded, which is the conservative answer for a body that may have
read pre-invalidation data. Revalidating the same tag again moves the
timestamp forward, since the later invalidation decides which entries
are stale.

`previouslyRevalidatedTags` are forwarded from an earlier request by a
redirecting server action and carry no timestamp of their own, so the
work store now records when the request started and treats them as
revalidated at that instant. Entries predating the request are still
discarded while entries generated during it survive. The hang-detection
probe worker and the dev validation worker take that value from the
request they serve rather than reading the clock when they start, which
would otherwise date every entry from the outer request as older than
the request's own start.

The `action-dedupe` fixture covers this end to end: it reads a tagged
cache twice inside a server action, revalidates the tag, then reads it
twice again during the re-render, asserting that each pair shares a
value and that the two pairs differ.
2026-08-05 22:42:18 +02:00
Janka Uryga 4b048b6a55 test: fix missing await in css-chunking test (#96725)
`cheerio.each()` does not handle async things, so this was effectively
just a race
2026-08-05 16:06:49 +00:00
Luke Sandberg 806fbd4367 fix(next/image): preserve image response after optimization (#96681)
Fork PR #96621 by @ceolinwill, re-opened as a branch PR so the "when
deployed" CI job can run — it requires Vercel deployment secrets that
GitHub does not expose to pull requests from forks, so it can never pass
on the original.

**The commit is unchanged and still authored by @ceolinwill.** Please
credit them; #96621 should be closed in favor of this one.

### What?

`getSharp()` calls `_sharp.block({ operation: ['VipsForeignLoad'] })` to
block every image loader, then unblocks a specific allowlist — and that
allowlist omitted `VipsForeignLoadSvg`. Because `_sharp` is a
module-level singleton, the block is process-wide and permanent: the
first `/_next/image` request in a process permanently disables Sharp's
SVG loader.

`ImageResponse` (`next/og`) rasterizes via resvg and then hands SVG to
Sharp, so once that loader is blocked it fails. The symptom is worse
than a bad image — the render throws `Input buffer contains unsupported
image format`, which surfaces as a **socket hang up / crashed response**
on any `ImageResponse` route requested after an uncached image
optimization in the same process.

Introduced by #96301, which aligned the Sharp allowlist with
`detectContentType()` but missed SVG.

Fixes #96612

### How?

Adds `'VipsForeignLoadSvg'` to the unblock list.

This does not weaken SVG protections for user-supplied images.
`detectContentType()` already returns SVG, and untrusted SVG is gated
separately by `dangerouslyAllowSVG`, which throws a 400 in
`imageOptimizer()` before Sharp is ever invoked. Unblocking the loader
only restores Sharp's ability to process SVG that Next.js itself
generates.

### Tests

The existing `og-api` test is extended to reproduce the exact
same-process ordering: render `/og-node`, perform an uncached
`/_next/image` optimization (asserting `x-nextjs-cache: MISS`, with a
random URL so it can't be served from cache), then render `/og-node`
again.

Verified locally that this fails without the one-line fix (`socket hang
up`, caused by `Input buffer contains unsupported image format`) and
passes with it.

No regressions in the SVG security coverage —
`test/e2e/image-optimizer/dangerously-allow-svg.test.ts` passes 94/94
including all `maintain vector svg` / blur-svg cases, and
`test/e2e/image-optimizer/image-optimizer.test.ts` plus the `og-*`
suites are green.

Co-authored-by: ceolinwill <4393133+ceolinwill@users.noreply.github.com>
2026-08-05 15:13:24 +00:00
Zack Tanner 77deebdfe0 Move App Router execution intent to entrypoints (#96640)
## Summary

App Router execution intent was previously inferred during WorkStore
construction from response capabilities and request state. App Page also
carried that intent through its handler context and lazy render API
before selecting the rendering operation inside app-render.

This change moves those decisions to entrypoints where the work is
known:

- Reusable output, including build-time generation, fallback shells, and
runtime revalidation, selects prerendering.
- Request-specific and resumed rendering selects request rendering.
- App Page exposes distinct render and prerender operations, so
execution mode no longer travels through `AppPageRouteHandlerContext` or
the lazy render API.
- App Route execution derives its intent from whether the response has
an ISR cache key.

WorkStore still consumes the explicit mode internally as a transitional
seam. Follow-up work can migrate downstream behavior to the active
WorkUnitStore and remove the mode from WorkStore entirely.

Fixes #96519

## Verification

- `pnpm --filter=next types`
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/static-shell-vary-params-regression/static-shell-vary-params-regression.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/segment-cache/static-shell-vary-params-regression/static-shell-vary-params-regression.test.ts`

<!-- NEXT_JS_LLM -->
2026-08-05 05:51:05 -07:00
Tim Neutkens 7577fa3c6e Fix Turbopack worker chunk loading with asset prefix (#96636)
## Summary

- propagate the effective worker asset prefix through the worker
bootstrap so runtime chunk registration and loading use the same base
path
- verify workers execute with both explicit and empty
`turbopackWorkerAssetPrefix` overrides
- add coverage for the Resvg WASM worker reproduction

Fixes #96613

## Verification

- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-start-turbo
test/e2e/turbopack-worker-asset-prefix/turbopack-worker-asset-prefix.test.ts`
- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/turbopack-worker-asset-prefix/turbopack-worker-asset-prefix.test.ts`
- Resvg WASM worker test in production and development Turbopack
- focused `turbopack-tests` worker and ignore-comments snapshots
- `pnpm swc-build-native`
- Full `pnpm build-all` did not complete locally because of unrelated
existing `httpxy` and proxy-request type errors

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com>
2026-08-05 07:41:54 +02:00
Luke Sandberg ff3a2cfaa9 [turbopack] Strip leading BOM before parsing CSS (#96678)
Fork PR #96379 by @lazerg, re-opened as a branch PR so the "when
deployed" CI jobs can run — those require Vercel deployment secrets that
GitHub does not expose to pull requests from forks, so they can never
pass on the original.

**The fix commits are unchanged and still authored by @lazerg.** This PR
only adds tests on top. Please credit them; #96379 should be closed in
favor of this one.

### What?

A CSS file beginning with a UTF-8 BOM (`EF BB BF`) is mishandled by
Turbopack. Lightning CSS does not skip the BOM, so it is tokenized as
content and the first token is misparsed:

```
./app/bom.css:1:2
Error: Parsing CSS source code failed
Unexpected token AtKeyword("layer")
```

The user-visible symptom is broader than a failed build. Turbopack
parses with `error_recovery: true`, and under that setting a leading BOM
makes Lightning CSS return `Ok` with **zero rules** — so a BOM-prefixed
stylesheet could silently drop all of its styles instead of erroring.
dart-sass (compressed style) and PostCSS >= 8.5.24 both emit or
round-trip such a BOM, so real projects hit this.

Fixes #96374

### How?

Strip a leading `U+FEFF` in `parse_css_stylesheet` before handing the
source to Lightning CSS, covering both `StyleSheet::parse` call sites
while leaving `ParseCssResult.code` as the original bytes that code
frames are rendered from.

That split makes parser positions relative to the stripped copy while
code frames still render the original line, so first-line positions need
compensating. `source_pos_for_loc` adds the stripped character back for
line 0 of BOM files. Only line 0 is affected, because the BOM contains
no newline.

### Tests

`test/e2e/app-dir/css-bom` — a BOM-prefixed stylesheet compiles and its
rules reach the page. Verified failing without the fix with the exact
error above, and passing with it, in dev-turbo, start-turbo and
start-webpack.

`test/development/app-dir/css-bom-code-frame` — covers the position
correction. Two fixtures hold the same invalid `@media (min-width: {})`
on line 1 and differ only by the leading BOM; the test asserts the BOM
file's reported column is exactly one greater:

| | `no-bom` | `bom` | |
|---|---|---|---|
| without `source_pos_for_loc` | 18 | 18 | fails |
| with it | 18 | 19 | passes |

Asserting the relationship rather than a literal column keeps this
robust if Lightning CSS changes its absolute column convention. It is
kept separate from the e2e suite because the fixtures are intentionally
invalid CSS, and scoped to Turbopack in dev, where the warning reaches
the CLI as the page is requested.

---------

Co-authored-by: lazerg <lazerg2@gmail.com>
Co-authored-by: vercel-gh-bot-3[bot] <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com>
2026-08-04 21:25:04 +00:00
Zack Tanner d87a203bcc Fix HTML-limited bot matching in prerender bypass rules (#96584)
## Summary

- preserve `htmlLimitedBots` substring semantics when generating the PPR
cache-bypass user-agent rule
- allow surrounding user-agent text so bots such as `googleweblight`
correctly bypass the prerender shell
- add deployment coverage confirming blocking metadata for bot
identifiers embedded in full user-agent strings, including custom
`htmlLimitedBots` patterns

<!-- NEXT_JS_LLM -->
2026-08-04 11:03:34 -07:00
Niklas Mischkulnig 479397867c test: skip action module instance deploy test (#96629)
There might be multiple function instances with their own
app/shared/action.js and thus multiple instances of the counter.
2026-08-04 18:11:11 +02:00
Sam Poder 1ba63dfa0e [turbopack] Add turbopack_ecmascript and turbopack_wasm's embeded FS to internal_assets_conditions (#96600)
Closes https://github.com/vercel/next.js/issues/96597; we were wrongly
applying the user-defined transforms to our internal web worker loaders
which caused this bug.

This is a regression caused by
https://github.com/vercel/next.js/pull/94372.
2026-08-04 01:29:44 -07:00
Zack Tanner 4be2ee7443 Preserve per-segment prefetching after dynamic navigation (#96583)
## Summary

Cache Components routes support per-segment prefetching, but dynamic
Flight responses only advertised this capability during static
generation. This could cause a subsequent `router.prefetch()` call to
fall back to loading-boundary prefetching and issue an unnecessary
request.

In other words, this logic was correctly determined
[here](https://github.com/vercel/next.js/blob/0db9e5f994ddf144bef72640d5be9bb17d77fe30/packages/next/src/server/app-render/app-render.tsx#L2204)
but not in the initial payload.

## Verification

- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts`
2026-08-04 02:02:32 +00:00
Andrew Clark 5f735c1ac5 Fix catch-all index page being served for every other slug (#96553)
Navigating between sibling pages of an optional catch-all could leave
the URL updated but the content stuck on the route's index page, with no
request made at all. On nextjs.org that is every /docs/* sidebar link,
once /docs itself has been prefetched.

The App Shell of a segment is the part that doesn't depend on params, so
the cache keeps one copy and reuses it for every param value. The client
decides it has that shell when a response arrives without a shell/full
split.

But a response arrives without a split whenever the render had no
dynamic hole, and that is also what a page fully prerendered at concrete
params looks like. The split alone can't tell the two apart, and the
second is not param-independent at all — its content is the params. So
the /docs page was stored as the shell for every slug. Because a payload
that needs nothing further is also recorded complete, sibling slugs read
it and stopped there: no prefetch, and no dynamic request during
navigation to correct it.

Treat a payload as param-independent only on evidence. The server
already reports which params each segment depends on, so use that.
Without it, assume every param varies, except for a shell variant, which
reduces param-dependent content to param fallbacks and so is good for
any value of them.

The new e2e suite prefetches a fully static catch-all index, navigates
there, then prefetches and navigates to a sibling slug. It fails on
canary with a prefetch that never fires, and is verified with vary
params both enabled and disabled.
2026-08-03 21:49:26 +00:00
Sam Poder 47a52c0d6b [turbopack / next.js] Add an end-to-end test for new root detection (#96544)
Follow-up PR to https://github.com/vercel/next.js/pull/96159, adding an
end to end test that specifically tests the new stuff in that PR!
2026-08-03 12:58:53 -07:00
Andrew Clark 8e878d4848 Remove implicit Partial Prefetching opt-in from instant (#96539)
Previously, adding a truthy `instant` config to a route segment (e.g.
`export const instant = true`) did two things: it enabled instant
navigation validation, and it implicitly opted the route into Partial
Prefetching.

Now those concerns are fully separate. `instant` only controls
validation, so enabling it no longer silently changes how a route is
prefetched. Partial Prefetching is enabled with the `prefetch` segment
config (`'partial'` or `'unstable_eager'`), or globally with the
`partialPrefetching` option in next.config.

For routes whose only opt-in was a truthy `instant`, `<Link
prefetch={true}>` goes back to performing a legacy full prefetch instead
of being downgraded to a partial one. `instant = false` still opts a
route out of the dev-time `prefetch={true}` warning.

Adds a regression test asserting that a route that only exports `instant
= true` keeps the full-prefetch behavior.
2026-08-03 19:56:47 +00:00
Joseph 459617a125 fix: double fragment on navigation (#93132)
Fixes: https://github.com/vercel/next.js/issues/93126,
https://github.com/vercel/next.js/issues/95551

Repro:

- from `/` navigate to `/abc#foo`
- on `/abc#foo` - now click `/abc#bar`
- the url is now at `/abc#foo#bar`

Unlike, https://github.com/vercel/next.js/pull/93855, this PR fixes what
gets stored in the segment cache navigation, rather than slicing away an
existing hash. It is plausible that dropping the hash from the segment
cache is not desired though.
2026-08-03 18:21:06 +02:00
Tim Neutkens cbf0cef687 Enable TypeScript CLI by default (#96497)
## Summary

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

## Verification

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

<!-- NEXT_JS_LLM -->
2026-08-03 18:10:51 +02:00
Zack Tanner dcdacfd036 test: skip hybrid not-found coverage for the legacy builder (#96534)
## Summary

The `pages-router-app-not-found` test verifies behavior implemented by
the Next.js adapter. The legacy `@vercel/next` builder has a
pre-existing behavior where it prefers the Pages Router error page for
this hybrid routing case.

Skip this test when deployment tests run without the adapter. It will
continue running in development, `next start`, and adapter deployment
tests.

## Verification

- `NEXT_TEST_MODE=deploy NEXT_ENABLE_ADAPTER=0 pnpm testheadless
test/e2e/app-dir/pages-router-app-not-found/pages-router-app-not-found.test.ts`
- `pnpm testheadless
test/e2e/app-dir/pages-router-app-not-found/pages-router-app-not-found.test.ts`
2026-08-03 08:36:57 -07:00
David Alexandru Ilie da90782311 Fix App Router scroll padding visibility (#96308)
## Summary

- Treat the root `scroll-padding-top` as the lower boundary of the
usable viewport during App Router navigations, so content obscured by a
sticky header is not incorrectly considered visible.
- Preserve the merged Fragment scroll-ownership state machine: empty
Fragments remain unavailable targets, while real targets are classified
against the padding-aware viewport.
- Resolve pixel and percentage values and use the same visibility rule
in both the legacy element handler and the Fragment-ref handler.
- Add regression coverage showing that Next scrolls when the destination
is hidden inside the padding boundary, while preserving the current
scroll position when the destination is genuinely visible below it.

## Performance

The root computed style is resolved lazily, only after the candidate
produces client rects. Empty Fragments and hash navigations do not
perform this lookup. For a real route-scroll target, the resolved value
is cached locally and reused by the second geometry check after
`scrollTop = 0`.

This does not add work to scroll events or every render, and keeping the
value local allows responsive CSS, root classes, and custom properties
to change between navigations. In a 50-iteration dirty-style Chromium
probe, both the existing and updated paths reported 50 style
recalculations and 50 layouts, indicating that the lookup reused the
style/layout update already required by the handler in that test.

## Related work

#96342 is now merged into `canary` and handles empty Fragment scroll
ownership. This PR composes with its `NoClientRects` / `InViewport` /
`OutOfViewport` state machine by changing only the visible-region
boundary for real targets.

## Verification

- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=false pnpm
test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/navigation-focus/navigation-focus.test.ts` (5/5)
- Manually compared the same sticky-header reproduction before and after
the change.

<!-- NEXT_JS_LLM -->
2026-08-03 16:00:14 +01:00