Commit Graph

35737 Commits

Author SHA1 Message Date
Hendrik Liebau dae438dc0d Stop printing a stack frame for error message text (#97829)
Next.js printed a stack frame that does not exist for an error whose
message ends with text that looks like a file location. A logged
connection failure read `at <unknown> (Error: connect ECONNREFUSED
::1:45999)` in front of its real frames.

`stacktrace-parser` tries five patterns on every line, and the pattern
for JavaScriptCore makes the `<name>@` prefix optional. That pattern
`:<line>:<column>`, and a line of an error message can end that way. A
JavaScriptCore frame always holds `@`, also at the top level, so the
prefix is not optional in a real stack.

The patch for `stacktrace-parser` now requires that prefix. A line of a
message no longer becomes a frame, and every JavaScriptCore frame still
parses, including one whose path holds a space.

A Turbopack build error puts the file and the position of the error in
its message, and the parser read that line as a frame as well. That
frame is gone now, and the location stays in the message. The snapshots
of the build errors record the change.
2026-08-25 13:38:47 +02:00
Joseph c51f582a70 docs: clarify revalidateTag profile expire semantics (#97836)
Once you call `revalidateTag`, the next request that reads the tagged
data kicks off revalidation.

That tagged data is SWR until the revalidation completes, or until the
expire time, whichever comes first.

It is the profile's expire time, counted from the `revalidateTag` call,
that defines the limit after which requests block.

If the first request after `revalidateTag` is called arrives after the
expire time, it'd block.

So the profile is a way to say, after this tag is revalidated, past the
expire time, data correctness is more important than being fast.
2026-08-25 12:46:13 +02:00
Joseph 03a09695be docs: param access on use-server (#97865)
Correctness pass — following
https://github.com/vercel/next.js/pull/97823
2026-08-25 12:45:54 +02:00
Tobias Koppers 3d57c6e581 docs: fix typos and correctness issues in App Router docs (#97823)
## Summary

A systematic review pass over the App Router documentation
(`docs/01-app`, 281 MDX files) fixing clear-cut typos, grammar issues,
and technical inaccuracies — code samples, file names, and API
signatures that had drifted from the implementation in
`packages/next/src`.

The review ran four passes: an automated spell check with a triaged
allowlist, targeted grammar-pattern searches, structural checks
(frontmatter, code fences, link targets), and a technical-accuracy pass
over all getting-started pages plus high-traffic API reference pages,
with every suspected inaccuracy verified against the source before
editing. Only unambiguous issues were fixed; anything debatable was
deliberately left alone (e.g. the `/aboot` route in the typedRoutes docs
is an *intentional* invalid-route example, and the CLI docs table
intentionally mirrors the actual `--help` output).

Notable fixes:

- JSX examples now `await params`, matching the Promise-based API and
their TypeScript counterparts
- `generateStaticParams()` is now exported in samples so the convention
actually takes effect
- `<Link prefetch>` props table includes `"auto"`, matching the `boolean
| "auto" | null` type in `link.tsx`
- `Navigator.geolocation` casing, the canonical react.dev `"use client"`
link, and a missing `Link` import in the typedRoutes docs
- Mismatched or wrong `filename=` labels in TS/JS switcher pairs
(`form.mdx`, `mutating-data.mdx`, `css.mdx`) and a wrong
`generateStaticParams` anchor target
- ESLint monorepo example registers the plugin as `@next/next` so the
documented rule prefixes resolve
- Spelling/grammar: `emphaize`, `invokations`, `behaviour` → American
spelling, "setup" → "set up" as a verb (5×), "a RSC" → "an RSC",
`NextJS` → `Next.js`, a stray comma, and "a `opengraph-image.tsx`"
article plus its folder path

## Verification

- `cspell` over all 281 files in `docs/01-app`: 0 issues outside a
reviewed allowlist of product names, API identifiers, and intentional
example strings
- `prettier --check` (3.6.2, repo config): passes on all 19 changed
files
- Grammar-pattern and structure greps: clean
- Technical claims cross-checked against `packages/next/src` and
`packages/eslint-plugin-next`
- Docs-only change; no runtime code touched

<!-- NEXT_JS_LLM -->


<!-- fleet eeae4385-18f5-406d-915e-986961503ed2 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 12:12:37 +02:00
Tim Neutkens b77a54df29 Add deploy release test skill (#97563)
## Summary

- add a repository-local skill for manually triggering
`test_e2e_deploy_release.yml`
- require the exact commit preview package to be available before
dispatch
- preserve default workflow inputs and verify the resulting branch, SHA,
and package URL


<!-- NEXT_JS_LLM -->
2026-08-25 11:29:03 +02:00
Hendrik Liebau 6826f5abe5 [test] Deflake the page config test for a string config value (#97848) 2026-08-25 11:26:29 +02:00
Tobias Koppers 76e24273c7 test: stabilize read-only page recreation (#97674)
### What?

Stabilizes the read-only source HMR page deletion and recreation test in
webpack development mode.

### Why?

The test reloads after restoring a deleted page because webpack does not
automatically refresh when the route reappears. In polling watch mode,
that reload could wait for the full page load or be aborted by a
concurrent Fast Refresh navigation. Either error escaped the retry
callback, allowing one transient navigation race to fail the test before
it could retry.

### How?

The webpack-only reload now waits only until the document is available
instead of waiting on potentially slow subresources. Transient reload
failures are handled inside the retry callback, while the existing
page-content assertion remains the success condition. This preserves the
test's intent and ensures a route that does not recover still fails.

### Verification

- Fresh `pnpm build` followed by a cold headless webpack run: 3 passed
- Three additional headless webpack runs: 3 passed each
- Headless Turbopack run: 2 passed, 1 expected skip
- Focused Prettier and ESLint checks

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 10:25:07 +02:00
Hendrik Liebau bf07db0713 Show the errors of an AggregateError behind a cause (#97830)
A failed `fetch` hid the reason of the failure. Node.js reports every
refused connection attempt in the `errors` of an `AggregateError`, and
the terminal logged `[errors]: [ [Error], [Error] ]` without the address
or the port of a single attempt.

An `AggregateError` keeps its errors in an array. Node.js spends one
level of the inspect depth on that array, and one more level on each
error in the array, while a `cause` costs one level only. The errors of
an `AggregateError` behind a cause therefore fell outside the default
depth of two. Node.js also returns an error early when it carries no own
properties, before it applies the depth, so the collapse hit only the
errors that carry diagnostics.

Next.js now adds the missing level back for an `AggregateError`, so its
errors print at the same nesting as a cause. A depth of `null` means
unlimited and stays unchanged, because `null + 1` would reduce it to one
level.
2026-08-25 10:07:55 +02:00
next-js-bot[bot] b2f3cb6a03 Upgrade React from eafeac09-20260819 to bd6ea412-20260824 (#97812) 2026-08-25 08:01:47 +00:00
Hamidreza Hanafi 1780fa0375 fix: reuse a single drain listener when piping Node streams through gzip (#97698)
## Summary

On Next 16.3+, App Router HTML and RSC responses are piped with
`pipeNodeReadableToNodeResponse`. Under gzip backpressure it called
`res.once('drain')` on every write that returned `false`, which leaks
one listener per backpressured write onto the zlib stream:

```
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 drain listeners added to [Gzip]. MaxListeners is 10.
```

The `compression` middleware overrides only `res.on`, forwarding it to
the zlib stream once that stream exists (or buffering it until then). It
leaves `removeListener` pointing at the response, so a `once` listener
is added to the stream but never removed from it. `off` cannot reach it
either.

`createWriterFromResponse`, directly above in the same file, already
uses a single `res.on('drain')` for the response lifetime. This makes
the Node-readable path match: pause on `write() === false`, resume from
one listener, `off` on `close`.

Resuming on that event is correct for both cases. With compression
active the wrapped `res.write` writes into the zlib stream, so `false`
means the zlib stream's buffer is full and its `drain` is the right
signal — the middleware separately wires the socket's `drain` to
`stream.resume()`. With compression inactive the listener stays on the
response, where `off` does remove it.

The pause is latched so a `drain` only resumes a readable that this
function paused, and a `drain` arriving after teardown is ignored.

### Scope

- Distinct from #96173 / `releaseCompressionStream`, which destroys the
zlib stream on **abort**. This warning fires on a **live** stream that
is still flowing: slow clients, large streamed HTML/RSC.
- Deliberately leaves the bare `readable.destroy()` in the `close`
handler alone. That is the separate abort-reason bug in #96704, with
fixes already open in #96715 and #96717.
- Not fixable by bumping the vendored `compression`: as #96173
established, upstream `compression@1.8.1` still has no premature-close
handling, and it is a prebuilt bundle here. `setMaxListeners` or
`compress: false` would only hide this.
- Mentioned as a `compress: false` workaround in #95130.

## Verification

`packages/next/src/server/pipe-readable.test.ts` drives real
backpressure (48 × 64 KiB through a throttled reader) and asserts
exactly one `drain` registration on the zlib stream, no
`MaxListenersExceededWarning`, and a complete, byte-correct payload
after `gunzip`. The payload assertion is what catches a latch that
stalls the stream.

Confirmed it fails without the fix. Measured against the real compiled
function on Next 16.3, patching only the drain handling:

| | before | after |
| --- | --- | --- |
| `drain` registrations on `[Gzip]` | 48 (one per backpressured write) |
**1** |
| `MaxListenersExceededWarning` | 1 | **0** |
| decompressed payload | complete | complete |

At 120 × 64 KiB the before-count scales with writes (120 registrations);
the after-count stays at 1. Separately reproduced end to end on a Next
16.3 standalone production build with `compress: true`: slow-consuming a
large gzipped page emitted the warning, and after this change it did
not, with `Content-Encoding: gzip` still set and valid gzip on the wire.

I could not run `pnpm test` in this checkout, so please treat CI as the
source of truth for harness integration. The assertions themselves were
validated against the real function as described above.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jude Gao <jude.gao@vercel.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:21:34 -04:00
Will Binns-Smith bc218dbb85 test: preserve server cache after compile error (#97724)
### What?

Adds a focused Turbopack development regression test covering server HMR
recovery from a compile error.

### Why?

A server compile error must not force a full server module reevaluation
when HMR recovers. Clearing the server module cache would discard stable
state held by unchanged dependencies and undermine server Fast Refresh
module preservation.

### How?

The test reuses the existing module-preservation fixture and its
unchanged server dependency evaluation timestamp. It introduces a
deterministic syntax error, verifies the Turbopack build-error redbox,
repairs the page, waits for the repaired server render, and then
confirms the dependency still exposes its original timestamp. The
patched fixture is restored in a `finally` block so failures cannot leak
state into later tests.

### Testing

- `pnpm test-dev-turbo
test/development/app-dir/server-hmr/server-hmr.test.ts`
- 17 tests passed

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-08-24 17:25:47 -07:00
next-js-bot[bot] 3bfdb29e5a v16.4.0-canary.6 v16.4.0-canary.6 2026-08-24 23:22:11 +00:00
Tobias Koppers 33b7edfc19 turbo-tasks: execute scheduled tasks inline when they are read (#96808)
### What?

Two changes to how the async turbo-tasks engine handles a read whose
value isn't ready yet:

1. When a read finds its task merely *scheduled* (queued, not started),
it takes the task out of
the scheduler queue and executes it on the reading thread instead of
waiting for a worker to
pick it up. If the execution completes without yielding, the read
returns synchronously; if it
yields, the partially-polled execution is handed to tokio and the read
waits as before.
2. When a read finds its task's execution already **in progress** (a
worker is running it), it now
waits directly on the task's completion event instead of also attempting
to take it over —
avoiding a futile acquisition of the scheduler's queue lock, which is
otherwise the most
   contended lock in the system.

An inline execution is visible in traces: the executed task's span
carries
`inline_execution = "complete"` when the reader's poll finished it, or
`"partial"` when it yielded
and was handed to the runtime. A task a worker executed leaves the field
unset.

Optional diagnostics (`InlineExecutionStats`) count queue pushes, claim
attempts and their outcomes,
and reads that waited for an already-running task. They are behind the
`inline_execution_stats` Cargo
feature, off by default, so a normal build has no counters, no atomics
and no extra fields.

### Why?

A large share of read misses target tasks that have merely been
scheduled, not tasks actually
being computed elsewhere. Parking on those adds two avoidable thread
hops (schedule → worker
pickup → wake) for work the reader could simply do itself. Conversely, a
read of a task a worker
is already executing gains nothing from trying to claim it, so it
shouldn't pay for trying.

### How?

- `PriorityRunner` gained a keyed claim: queued entries are indexed by a
`ScheduleKey` (task or
local task) and stored in a slot store, so one entry can be removed by
key under the existing
queue lock while the priority heap keeps a tombstone that a popping
worker skips. Every scheduled
  execution still runs exactly once.
- The backend already distinguished a queued task from one already
executing
(`InProgressState::Scheduled` vs. `InProgress`) when building a listener
for a read, and
discarded that distinction. It's now threaded through as `ReadOutcome<T>
= Value(T) |
Scheduled(EventListener) | InProgress(EventListener)`; only `Scheduled`
reads attempt a claim.
The state is a hint — a worker pops a task off the queue before it marks
it started, so a read can
see `Scheduled` for a task that is no longer claimable. Acting on a
stale hint costs one failed
  claim, never correctness.
- Inline execution nests (A reads B inline, B reads C inline, ...), so
it's capped at 16 levels per
thread to bound stack growth; at the cap, reads fall back to waiting for
a worker as before. The
cap is a thread-local counter because it guards the *thread* stack, not
the task stack.
- Recording the outcome on the *executed task's* span needs a small
handoff: when
`poll_once_or_spawn` returns, that task's span has already been exited,
so the executor registers
the span it instruments the task body with in a slot that only exists
while a claimed task is
  polled inline.
- Added `turbo-tasks-backend/tests/inline_read_execution.rs` (8 tests),
`inline_execution_span.rs`
and `inline_execution_span_worker.rs`, plus unit tests in
`priority_runner.rs`/`manager.rs`
covering keyed claim/exactly-once, inline completion for global and
local tasks, the
in-progress/scheduled distinction, nested inline executions, restoring
from a persistent cache,
  and a deep dependency chain (stack-depth guard).

### Benchmarks

`bench/nested-deps-app-router-many-pages` (1000 pages, 3020 generated
components, 3010 routes),
release builds of `@next/swc` for this branch and for its merge base,
runs interleaved, first run of
each variant discarded as warm-up. Shared 8-vCPU VM.

**Cold `next build --turbopack`** — 5 measured runs per variant:

| metric | canary (median) | this branch (median) | delta |
| --- | ---: | ---: | ---: |
| total build | 96.70 s | 89.29 s | **−7.7 %** |
| turbopack compile phase | 68 s | 61 s | **−10.3 %** |

Raw totals (ms) — canary: 93566, 95354, 96702, 96717, 97545; branch:
89070, 89140, 89287, 89892,
90462. The distributions don't overlap (the branch is faster in all 25
pairwise comparisons, exact
two-sided Mann-Whitney p ≈ 0.008), and the 7.4 s median gap is about
twice canary's own 4.0 s
run-to-run spread.

**Incremental build** (second build reusing the persistent cache) — 3
measured runs per variant:
36.14 s vs 37.29 s median, compile phase 10.4 s vs 10.5 s. The 1.2 s gap
is *smaller* than canary's
own 1.2 s spread, so this is **no measurable difference** — the
incremental case is dominated by
restore and by static generation in Node workers rather than by the
scheduling this PR changes.

**Cache-hit reads** (`task_overhead/turbo-cached-*`, criterion): no
regression. Measured before the
rebase, on the same read-path code — the hit path never enters this
code, since the inline attempt
only happens on the branch where a read reports a miss.

Caveats: one app and one workload shape, on a shared VM, so only the
relative comparison on the same
machine is meaningful, not the absolute seconds.

<!-- NEXT_JS_LLM -->


Co-authored-by: Luke Sandberg
<210140+lukesandberg@users.noreply.github.com>

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-24 15:43:43 -07:00
Andrew Clark 4b4b878c99 Revert "Record fallback cache lifetime for blocking PPR routes (#97821)" (#97835)
Reverts #97821, which shipped in v16.4.0-canary.5 and broke the deploy
e2e tests (run 32770221068): segment-cache/prefetch-app-shell's "uses
separate static App Shells for links with different root params" fails
deterministically.

The reverted change stamped the collected cache lifetime onto the cache
entries seeded from fallback files. Those entries are placeholders that
exist to be replaced — their short default lifetime is what drives the
regeneration that specializes them — so a blocking route's segment
entries were served as static-forever placeholders and the App Shell for
a non-prerendered root param never arrived.

A redesigned fix that records the rendered-content lifetime in separate,
additive fields (leaving the seeded-entry lifetime untouched) is staged
on `acdlite/fallback-entry-lifetimes`.
2026-08-24 22:36:53 +00:00
Will Binns-Smith 2cee98ab97 Sweep stale Turbopack output from distDir on dev startup (#97591)
## Summary

Turbopack dev never removes the content-hashed chunks it supersedes, so
`.next/dev` grows without bound. Webpack dev avoids this by wiping
`distDir` on every start.

Adds a `maxAgeMs` argument to `recursiveDeleteSyncWithAsyncRetries`:
delete files whose mtime is at least that old, remove directories once
empty. Turbopack dev passes one week.

## Verification

- `pnpm test-unit test/unit/recursive-delete.test.ts`
- ~Not run:~ dev-server behavior against a real `.next/dev`

<!-- NEXT_JS_LLM -->
2026-08-24 14:19:25 -07:00
Marcos Hernanz 95b77e8279 Fix metadata prefetch cache key for search params (#97729)
### What?

Treat a `LoadingBoundary` response as `Full` when writing the response's
head into the segment cache, matching the strategy already used when the
scheduler creates that head entry.

Adds a production e2e regression test with two concurrent prefetches for
the same pathname and different search params.

### Why?

`pingRuntimeHead` creates loading-boundary head entries with the
concrete `Full` strategy because a head has no loading boundary. The
canonical re-keying added in #96095 later wrote the fulfilled head with
raw `LoadingBoundary` semantics, which drops search params from the
metadata cache key. Concurrent query-specific metadata responses could
therefore replace each other under one fallback key; whichever response
settled last supplied the title for both URLs.

Fixes #97657

### How?

Normalize `LoadingBoundary` to `Full` at the head write boundary only.
Page segments retain loading-boundary keying, and shell/PPR responses
keep their existing strategies.

The test reveals alpha and beta links in one React render, waits for
both prefetches, navigates to alpha, and verifies both body and title.
It then performs a search-only navigation to an unprefetched gamma URL
and verifies that the head is replaced.

### Testing

- Regression boundary: test passes at `e70495c1ee` and fails at #96095
merge commit `dfa7f4f713` without the fix (`alpha` body, `beta` title).
- Test passes at `dfa7f4f713` with the fix.
- `NEXT_SKIP_ISOLATE=1 pnpm test-start-turbo
test/e2e/app-dir/segment-cache/metadata-search-params/metadata-search-params.test.ts`
(pass, plus 3 consecutive repeat runs)
- `NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack
test/e2e/app-dir/segment-cache/metadata-search-params/metadata-search-params.test.ts`
(pass)
- Relevant existing Turbopack suites: 16 tests pass across metadata,
static shell, metadata soft navigation, and stale search-param coverage.
- Existing search-param suites: 6 tests pass with webpack. Their
Turbopack builds are currently blocked on canary by an unrelated Edge
Runtime `process.cwd()` error.
- `pnpm --filter next build`
- Targeted ESLint and Prettier checks
- Structured external review: 0 findings
2026-08-24 14:13:21 -07:00
Benjamin Woodruff ‮ ef85120648 Turbopack: Improve file existence error handling in realpath_with_links and in module resolution (#97717)
Fixes some regressions from
https://github.com/vercel/next.js/pull/97395.

Human summary of changes:

- Plainly return `NotFound` for `realpath_with_link` whenever any step
in the resolution algorithm fails, matching the behavior the libc uses.
- When probing in the module resolution algorithm, explicitly check for
`NotFound`.
- Fully remove the extremely non-standard and confusing behavior of
trying to return the last processed path from `realpath_with_links` if a
failure occurs.
- Modify `realpath` to return a `RealPathError` instead of just an
`anyhow::Error`, so that callers that care about the actual error kind
but not all of the intermediate links can still use it instead of
`realpath_with_links`.
- Rename `RealPathResultError` to `RealPathError`, and make it actually
implement `Display` so that it's easier to coerce to an `anyhow::Error`.
This requires including a tiny bit more data on this object, but in the
hot path it's just one extra clone of a `FileSystemPath`.

Human discussion:
https://vercel.slack.com/archives/C09R44U5HQW/p1787322213695299

AI slop summary of why #97395 caused a regression:

> The regression came from newly added missing-target detection in
b2e835ded6.
>
> Before that commit, realpath_with_links:
>
> 1. Resolved directory symlinks.
> 2. Continued resolving the remaining path beneath the resolved
directory.
> 3. If that remaining path did not exist, returned the resolved path
successfully.
> 4. Let the caller’s subsequent existence check report that the
candidate was absent.
>
> The commit changed read_link so it no longer checked the symlink
target’s type. To detect dangling symlinks, realpath_with_links added
this condition:
>
> ```
> if (followed_link || !symlinks.is_empty())
>     && entry_type == FileSystemEntryType::NotFound
> {
>     // report a missing symlink target
> }
> ```
>
> The problem is that !symlinks.is_empty() does not mean the missing
path is a symlink target. It only means that some earlier path component
was a symlink.
>
> For the reported path:
>
> `node_modules/@workflow/core/dist/serialization/package.json`
>
> resolution worked like this:
>
> 1. Resolve node_modules/@workflow/core, a valid directory symlink.
> 2. Continue into dist/serialization, which exists.
> 3. Probe the optional dist/serialization/package.json, which does not
exist.
> 4. Because the earlier @workflow/core component was a symlink,
symlinks was nonempty.
> 5. The new condition incorrectly classified the missing package.json
as a dangling symlink target.
> 6. Turbopack aborted instead of treating package.json as an absent
resolution candidate and continuing to serialization.js.
>
> So the regression was not caused by resolving the directory symlink
incorrectly. It was caused by using “we traversed any symlink” as
evidence that a later missing descendant was the target of a dangling
symlink.
2026-08-24 21:12:28 +00:00
Luke Sandberg 8911fe3612 Write traces with buffered synchronous IO (#96874)
### What?

Writes `.next/trace` with a fixed buffer and `fs.writeSync` instead of
an `fs.WriteStream`. `Reporter.flushAll` becomes synchronous and drops
its unused `opts.end` parameter.

### Why?

**A production build could hang on the trace reporter.**

`flushAll` ended the write stream whenever the phase was not
`PHASE_DEVELOPMENT_SERVER`:

```ts
if (opts?.end || phase !== PHASE_DEVELOPMENT_SERVER) {
  return writeStream.end()
}
```

No caller ever passed `opts.end`, so in production the stream was ended
on *every* flush. But builds flush repeatedly — after each Turbopack
compilation event in `backgroundLogCompilationEvents`, and again in the
`finally` of `next build` before `uploadTrace` reads the file. Only the
last of those is terminal.

That gives a flush → write → flush sequence against a stream that is
already finished:

1. The first flush calls `writeStream.end()`.
2. A later span is reported. `write()` on an ended stream returns
`false` and, because the stream will never drain again, never emits
`'drain'`.
3. The reporter parks on `await this.drainPromise`.
4. The next flush awaits that write. Nothing resolves it.

The build hangs in its `finally`, and every span after the first flush
is missing from the trace file.

### How?

`RotatingWriteStream` now holds a 16KB `Buffer`, encodes each event into
it directly, and writes with `fs.writeSync` when it fills or when
`flushAll` is called. There is no close, so the end-of-stream state that
caused the hang no longer exists.

Trace files are small — under 1KB for a production build, a few MB for a
long dev session — so the async write path was never buying throughput.
Buffering to 16KB keeps a build at a handful of writes rather than one
per span, which matters because the webpack/rspack profiling plugins
open a span per module(!!).

### Testing

`packages/next/src/trace/trace.test.ts` covers the reporter directly:
flushing repeatedly, auto-flush when the buffer fills,
truncate-vs-append per phase, rotation at the size limit (counted in
UTF-8 bytes, not UTF-16 code units), complete batches across a sweep of
event sizes that land on the buffer boundary, an event too large to
buffer at all, and a partially encoded multi-byte event.

Verified end to end against a real `next build` (34 spans, valid NDJSON,
`next-build` span present) and a real `next dev` session (spans landing
mid-session, and accumulating across a restart).
2026-08-24 13:14:57 -07:00
next-js-bot[bot] 7d29e358c8 v16.4.0-canary.5 v16.4.0-canary.5 2026-08-24 19:23:49 +00:00
Andrew Clark 2690ad4922 Record fallback cache lifetime for blocking PPR routes (#97821)
A dynamic route that can't serve an HTML fallback (`fallback: null`,
e.g. because its cached shell is keyed on the route params) still
produces cacheable fallback artifacts during the build: the per-segment
prefetch payloads served to the client Segment Cache, and the RSC
template. Previously the prerender manifest only recorded
fallbackRevalidate/fallbackExpire when the HTML fallback itself was
servable, so these routes shipped their segment and .rsc outputs with no
lifetime at all.

Cache layers interpret a missing revalidate as a 1-second lifetime —
both deployment infrastructure and the self-hosted incremental cache
(calculateRevalidate). The result is that every segment entry for such a
route goes stale one second after it's written, so nearly every prefetch
of a non-prerendered URL serves stale, triggers a background
revalidation, and writes a new cache entry. On a site with dynamic
product/listing routes and default viewport prefetching, this amplifies
into millions of cache writes per hour.

The build already computes the correct lifetime: the export's collected
cache control, which describes exactly the caches that completed into
the fallback artifacts. The manifest now records it for every PPR route
regardless of fallback mode, and the adapter's segment and RSC template
outputs fall back to the manifest values when there's no HTML fallback
output to inherit them from. When the export collected no cache control,
the lifetime is `false` (keep until invalidated), matching how servable
fallbacks are already treated.

fallbackStatus and fallbackHeaders remain gated on the PRERENDER
fallback mode because they describe the prerendered fallback response
itself, which doesn't exist for blocking routes.
2026-08-24 15:04:55 -04:00
Jiwon Choi 9480f67d61 Document catchError for the Pages Router (#97748)
Supersedes https://github.com/vercel/next.js/pull/91848

`catchError` works in both the App Router and Pages Router, but its API
reference only documented the App Router behavior. This adds a Pages
Router reference backed by the same canonical document and scopes the
content that differs between routers.

The Pages Router examples use `reset()` because `retry()` is App
Router-only, omit App Router-only navigation behavior, and use Pages
Router component paths. The existing App Router guidance and shared API
reference remain unchanged.

<!-- NEXT_JS_LLM -->
2026-08-24 20:19:34 +02:00
Will Binns-Smith 6df5c4562f clippy: allow needless late init where it makes sense (#96410)
This exempts certain blocks from the `needsless_late_init` rule where it
hinders readability.

Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-08-24 10:43:37 -07:00
Niklas Mischkulnig 6617057757 Turbopack: emit/collect (#96631)
Only add this for using it for Server Actions for now
Undocumented, not API stable. But we can't really put it behind a flag,
because the next-custom-transform is going to add it

New traits:
```rust
/// A module that can collect other modules during the collect phase.
#[turbo_tasks::value_trait]
pub trait CollectingModule: Module {
    /// The namespace that this module is interesed in
    #[turbo_tasks::function]
    fn namespace(self: Vc<Self>) -> Vc<RcStr>;

    #[turbo_tasks::function]
    fn as_chunk_item(
        self: Vc<Self>,
        module_graph: Vc<ModuleGraph>,
        chunking_context: Vc<Box<dyn ChunkingContext>>,
        entry_chunk_group: Vc<Modules>,
    ) -> Vc<Box<dyn ChunkItem>>;
}

#[turbo_tasks::value_trait]
pub trait EmittedModuleReference: ModuleReference + ValueToString {
    #[turbo_tasks::function]
    fn data(self: Vc<Self>) -> Vc<CompileTimeDefineValue>;
}

```

Example:

```javascript
__turbopack_emit__('./c.js', {
  namespace: 'my-test',
  data: 'data-for-c', // optional
  exports: ['c'], // optional
  with: ... // optional, import attributes like with ESM
})

// -------------------------

const getList = __turbopack_collect__({
  namespace: 'my-test',
})
const list = getList();
```

Shortcomings in the current version (which are not a blocker for now
however):
- data-only emit not implemented
- stylegroups aren't properly implemented. emitting styles is probably
broken
- pages router is still a problem: it has two separate
ChunkGroup::Entry, so you only see what was emitted on the current
runtime, not both

Recreation of https://github.com/vercel/next.js/pull/91100
2026-08-24 18:39:28 +02:00
Joseph 30e73d2d14 docs: note that response headers must be set before calling handle() in a custom server (#97693)
Fixes: #82387

A custom server that calls `res.setHeader()` after `await handle(req,
res)` silently loses the header, because Next.js has already begun
sending the response by the time `handle` resolves. Unlike a normal Node
`setHeader()` after `headersSent`, this does not throw, so the header
just never reaches the client. This adds a note to the custom server
guide showing the correct ordering and pointing out that the same
applies to framework wrappers such as `reply.raw` in Fastify.
2026-08-24 15:41:23 +02:00
Luke Sandberg 88483bdedc Deduplicate the regress, wat/wasmparser and base64 dependencies (#97762)
### What?

Collapses three duplicated crate dependencies, so each is built once
instead of twice. **−150 KiB** of code

Manifests and `Cargo.lock` only — no source changes, and none were
needed.

**`regress` → 0.11.1** (−133 KiB, the bulk of the win)

**`wat` and `wasmparser` → the 239 family** (−13 KiB)

**`base64` → one workspace declaration on 0.22** (−1.2 KiB)
2026-08-24 13:03:55 +00:00
Tobias Koppers 75c1f1f6e0 perf: split cold TurboMalloc accounting paths (#97767)
### What?

Reduce the binary-size cost of TurboMalloc's allocation instrumentation
while preserving its hot-path performance and accounting semantics.

This adds a focused Criterion benchmark for TurboMalloc's raw allocation
paths and moves only the uncommon global counter refill/spill operations
behind cold, non-inlined helpers. Thread-local allocation counters,
buffer checks, and common buffer adjustments remain inline at allocation
sites.

### Why?

Rust gives global allocator methods special inlining treatment.
TurboMalloc's thread-local accounting was consequently duplicated across
allocation and deallocation call sites in `next-swc`, contributing
several megabytes of machine code.

Moving the whole accounting path out of line recovered most of that
space but made small allocations slower. Splitting only the rare atomic
paths provides a smaller size reduction without that hot-path cost.

### How?

The common path remains explicitly inlineable through `add`, `remove`,
`update`, and the thread-local access helper. When the local buffer must
be refilled or spilled, it calls a `#[cold] #[inline(never)]` helper
that performs the global relaxed atomic update.

The arithmetic, thresholds, target buffer values, counter increments,
and atomic ordering are unchanged. The counter test now also covers
realloc growth/spill and shrink/flush behavior.

### Benchmark results

Linux x86_64 Intel Xeon, Rust 1.99.0-nightly / LLVM 22.1.8. Native
artifacts used the repository's production release command. Direct
application measurements used clean `bench/heavy-npm-deps` `next build
--turbopack` runs, with 8 samples per arm and rotated/interleaved
ordering.

#### Binary size

| alternative | raw artifact delta | `.text` delta |
|---|---:|---:|
| No instrumentation | −6,260,168 B (−4.23%) | −5,466,304 B (−7.69%) |
| `inline(never)` on `add` / `remove` / `update` | −4,338,856 B (−2.93%)
| −4,342,464 B (−6.11%) |
| `inline(never)` on whole `GlobalAlloc` methods | −4,153,792 B (−2.80%)
| −3,714,688 B (−5.23%) |
| **Cold atomic refill/spill helpers (this PR)** | **−502,864 B
(−0.34%)** | **−732,736 B (−1.03%)** |

The current artifact was approximately 148.1 MB raw with 71.1 MB of
`.text`. The retained split recovers 8% of the raw and 13% of the
`.text` saving from removing instrumentation entirely.

#### Direct `next build --turbopack`

| alternative | mean effect vs current | bootstrap 95% CI | permutation
p |
|---|---:|---:|---:|
| No instrumentation | −0.70% | [−2.32%, +1.12%] | 0.466 |
| `inline(never)` on `add` / `remove` / `update` | **+3.53%** |
**[+0.65%, +6.43%]** | 0.0448 |
| `inline(never)` on whole `GlobalAlloc` methods | +1.02% | [−2.03%,
+4.44%] | 0.586 |
| **Cold atomic refill/spill helpers (this PR)** | **−0.18%** |
**[−3.44%, +3.61%]** | **0.929** |

No application-level difference was detected for the retained split or
the whole-method placement. The helper-level noinline placement showed
evidence of a modest regression.

#### Focused allocator Criterion benchmark

Percent changes are lower-is-better. Where two figures are shown, they
are independent runs in reversed order.

| alternative | alloc + dealloc | alloc + realloc + dealloc |
|---|---:|---:|
| No instrumentation | −43.4% | −36.1% |
| `inline(never)` on `add` / `remove` / `update` | +5.5% / +7.5% | +3.0%
/ −8.1% (inconclusive) |
| `inline(never)` on whole `GlobalAlloc` methods | **+19.4% / +21.4%** |
**+2.9% / +6.4%** |
| **Cold atomic refill/spill helpers (this PR)** | **−4.0% / −3.0%** |
**−2.8% / −5.4%** |

The full 28-cell turbo-tasks overhead suite was also run twice in
reversed order. Large process-level effects changed magnitude or sign
between runs, so no conclusion is drawn from that suite.

### Verification

- `cargo fmt -- --check`
- `cargo clippy -p turbo-tasks-malloc --all-targets -- -D warnings -A
deprecated`
- `cargo test -p turbo-tasks-malloc`
- `cargo check -p turbo-tasks-malloc --all-targets`
- `cargo bench -p turbo-tasks-malloc --bench allocation -- --test`
- Release `next-swc` builds and Node smoke-loads for every measured arm

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-24 13:47:42 +02:00
next-js-bot[bot] 6970724fba v16.4.0-canary.4 v16.4.0-canary.4 2026-08-24 11:43:30 +00:00
MB c0dfeffa53 Add next/cache-handlers types entrypoint (#97592)
### What?

Exposes the `cacheHandlers` types (`CacheHandler`, `CacheEntry`) as
types-only exports from `next/cache`, so handlers can be checked against
the real interface:

```ts
import type { CacheHandler } from 'next/cache'
```

### Why?

Custom cache handler authors currently import `CacheHandler` and
`CacheEntry` from `next/dist/server/lib/cache-handlers/types` (an
internal path that can move between versions) or hand-copy the
interfaces, which drift silently across releases.

### Motivation

I'm working on a custom community cache handler and it'd be great to
import the types directly and keep testing against the source of truth,
rather than maintaining a hand-copied mirror that has to be re-checked
on every Next.js release.

Raised in discussion #96356.

### How?

- Adds a type-only `export type { CacheHandler, CacheEntry }` to
`packages/next/cache.d.ts`, re-exported from
`./dist/server/lib/cache-handlers/types`. No new subpath or
`package.json` `"files"` entries needed since `next/cache` already
ships.
- Docs: `cacheHandlers.mdx` now shows the public `next/cache` import
instead of the GitHub source links.
- Tests: a `satisfies CacheHandler` / `satisfies CacheEntry` fixture in
the `typescript-basic` typechecking suite (runs `tsc` against the
installed package), and the `use-cache-custom-handler` e2e fixture's
JSDoc now uses the public import.

(Originally proposed as a separate `next/cache-handlers` types-only
subpath, following the `next/types` pattern moved the export into
`next/cache` per review.)

Related: #96356

closes #97781 (only created to run deploy tests)
2026-08-24 13:14:31 +02:00
Niklas Mischkulnig a1cc1eafb3 Remove unused return values from getPageStaticInfo (#97661)
Some cleanup, this was never used:
- `config` from `getStaticInfoIncludingLayouts`
- `getServerSideProps` and `getStaticProps` from pages router
2026-08-24 12:05:28 +02:00
Tobias Koppers a11f82c557 [turbopack] defer NFT module content hashes (#97773)
### What?

Separate trace-graph module path collection from module content hashing.
Exclusion filtering now requests only cached module identifiers, while
full NFT data still layers content hashes on top when they are required.

### Why?

Applying output-file-tracing exclusion globs needs module paths but not
content hashes. Keeping these computations separate avoids requesting
full-graph hashes solely to decide which modules should be skipped.

This is an inspection experiment, not a demonstrated performance
improvement. A seven-sample release A/B left the median compilation
phase unchanged at 2700 ms.

### How?

A private identifier-only graph task owns the existing DFS traversal.
`traced_module_data_for_graph` reuses those identifiers and computes
hashes for its full result. `traced_modules_for_entries` drops the
unused hash-salt input and consumes only identifiers for glob matching.

### Verification

- `cargo fmt -- --check`
- `cargo check -p next-api`
- `next-server.js.nft.json`: byte-identical SHA-256
`da4c5a494efbbb38eb966066115de96adb5d0ec693fba17bbed669c99cafae43`
- `next-minimal-server.js.nft.json`: byte-identical SHA-256
`7c58d40dc2ecbe6ddd26dd0c602467d80f1a8ea3320e2c6e8e5f4d6be14156cd`
- Not run: clippy, Rust test suite, or integration tests (inspection
experiment)

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-24 12:01:44 +02:00
Tobias Koppers cf5339d1be test: deflake basePath external navigation (#97611)
## Summary

The basePath external-navigation test depended on loading Google and
recognizing its DOM before exercising browser history. That made the
test sensitive to public network availability and third-party page
changes; the reported failure was confirmed in the Safari/WebKit CI run
on the initial attempt and both retries.

For local and CI test-server runs, this replaces the public dependency
with a minimal page served from a separate local origin, preserving the
cross-origin, full-document navigation being tested. Deploy mode remains
enabled and uses a deploy-accessible external target because deployed
browsers cannot reach the test runner. Both paths now synchronize on
browser origin and exact restored-page state instead of third-party DOM
or a fixed sleep.

## Verification

- Focused production/start-mode test passed three consecutive times in
Firefox
- Focused production/start-mode test passed in Chromium
- Prettier and ESLint passed for the changed files
- `pnpm build-all`
- Not run: Safari/WebKit locally (Playwright’s unsupported Amazon Linux
fallback requires incompatible Ubuntu system libraries; the Safari CI
job is the definitive validation)
- Not run: deploy-mode execution locally (deployment credentials
unavailable; the deploy path remains enabled for CI)

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-24 11:41:05 +02:00
Eddy (Frontend Engineer) 24f975677e docs(adapters): document assetsHashes and routing.middlewareMatchers (#96536)
While writing an adapter against `16.3.0-canary.107` I noticed the
adapter docs are missing two fields that the shipped `NextAdapter` types
already have.

**`assetsHashes`** — every `PAGES` / `PAGES_API` / `APP_PAGE` /
`APP_ROUTE` / `MIDDLEWARE` output carries it right next to `assets`, and
it's declared with a doc comment in `build-complete.d.ts`, but none of
the five shapes in Output Types mention it.

**`routing.middlewareMatchers`** — dumping the `routing` object from a
real `onBuildComplete` call gives:

```
afterFiles, beforeFiles, beforeMiddleware, dynamicRoutes, fallback,
middlewareMatchers, onMatch, rsc, shouldNormalizeNextData
```

but the docs list eight of those nine, everywhere the interface appears:
the Creating an Adapter snippet (which says "The interface is defined as
follows"), the API Reference parameter list, and Routing Information.
This one feels worth fixing soon — an adapter that does its own request
matching from the documented fields alone has no way to decide when
middleware should run. My guess for why nobody has hit it: if you pass
`routes: routing` wholesale into `resolveRoutes` from `@next/routing`,
everything works without ever looking at the field.

The wording I added comes from the doc comments in the shipped types,
not my own descriptions. `middlewareMatchers` is inserted where the type
puts it (right after `beforeMiddleware`).

For what it's worth: I checked the rest of the section against the same
build while I was at it — the `output: 'export'` behavior, the prerender
classification fields, `pprChain.headers`, the fallback fields, the
immutable-assets flow, and the `@next/routing` params/result — and
everything else matched the docs. These two were the only gaps I found.
2026-08-24 11:37:53 +02:00
Niklas Mischkulnig 4c4d523084 Migrate remaining async blocks to async closures (#97701)
**View diff without whitespaces**

Fix remaining cases of https://github.com/vercel/next.js/pull/97666

- Mostly replacing async blocks with async closures
- And second commit: some more making closures sync where possible
2026-08-24 11:33:46 +02:00
Hendrik Liebau d69a7041f3 Turn off the adapter route collapses by default (#97774)
`experimental.collapseAdapterRoutes` now defaults to `false`, so a build
keeps one entry per route unless the project opts in. The changes below
it in this stack stay as they are, and a project that sets the option to
`true` gets the route table that they produce.

The default starts off so that we can dogfood the behavior on selected
apps before every build gets it. The default moves back to `true` once
enough apps have run with it.

The four fixtures under `test/production/app-dir/adapter-dynamic-routes`
set the option, because their snapshots pin the collapsed table. A build
of the Cache Components fixture without the option emits the 27 entries
that the first commit in this stack recorded, which is what the option
now turns off.
2026-08-24 10:41:37 +02:00
Hendrik Liebau 6f1fb7f73a Serve a run of fallback shells from one route entry (#97738)
A fallback shell repeats the whole path of its source page and resolves
the leading params to concrete values. Take `/[team]/[locale]/[slug]`,
where the build prerenders three combinations of the two leading params.
It emits one entry per combination, and the entries differ only in that
leading part of the path:

```diff
- /acme/en/<slug><suffix>    ->  /acme/en/[slug]<matched suffix>
- /acme/de/<slug><suffix>    ->  /acme/de/[slug]<matched suffix>
- /globex/en/<slug><suffix>  ->  /globex/en/[slug]<matched suffix>
+ /(?<shellPrefix>acme/en|acme/de|globex/en)/<slug><suffix>  ->  /$shellPrefix/[slug]<matched suffix>
```

One entry now serves them all. Its pattern lists the leading part of
each shell path as an alternative, and its destination copies whichever
one matched.

Those alternatives are complete, and that matters. A pattern that
offered a choice per param instead, `(acme|globex)/(en|de)`, would also
match `globex/de`. The build never prerendered that combination, so a
request for it would resolve to an output that does not exist, and it
would then fall through to whichever route claims the rewritten path.

An entry serves neighbours in the manifest, and only those. It takes the
position of the first shell that it replaces, so every replaced shell
keeps its place relative to the routes around it. Any other route
between two shells ends the run, because an entry that reached across it
would move ahead of a route that a request matches first. The shells of
a run also have to agree on `fallback: false`, because an entry carries
one set of conditions.

A source page can therefore hold several runs, and a shell can belong to
none. That happens when the build resolves a different number of params
for neighbouring shells, which leaves them with different paths after
the resolved part.

This removes the multiplier that the number of prerendered combinations
applies to every route below the resolved params. We measured the same
in-progress feature branch of the v0 chat app as the previous changes in
this stack. Building it before and after this change removes 87% of the
routes that those changes left. Across the stack that branch loses 95%
of its routes.

This collapse follows `experimental.collapseAdapterRoutes`, which an
earlier change in this stack added. A build that sets it to `false`
emits one entry per shell.

**Verified with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32577936779).**
2026-08-24 10:41:36 +02:00
Hendrik Liebau 91d378dbb6 [test] Capture the route table for apps with several param shapes (#97728)
The existing fixtures take a single root param, so they do not cover the
shapes that the fallback shell entries can take. This adds fixtures and
suites for three more shapes, and pins the entries that each one
produces.

The first has a root layout that takes two root params, and returns
three combinations of them rather than the full product of four. An
entry that held each root param position separately would also match
`sparse/de`, a combination the build never prerendered, and a request
for it would resolve to an output that does not exist. One combination
is `acme.one-two,three`, which carries the characters a regex treats as
special, and the snapshot shows the build escaping them.

The second builds that same app with a single combination, so each
source page has one fallback shell.

The third has no root params at all. Its root layout sits above the
dynamic segments, so `team` and `locale` are ordinary dynamic params
that `generateStaticParams` enumerates on a nested layout. The build
then produces two shapes of entry for one source page: entries that
resolve both params, such as `/sparse/en/posts/[id]`, and entries that
resolve only the first, such as `/sparse/[locale]/posts/[id]`. The order
of those entries carries the behavior, because a request for
`/sparse/en/posts/1` has to reach the output that resolves both params
rather than the one that resolves only the first. The snapshot pins that
order.

Two comments on the Cache Components suite were wrong, and this corrects
them. That fixture takes one root param with two values, and not two
root params. Its entries also no longer come in pairs, because one entry
serves the request for the page, the request for its `.rsc` payload, and
a per-segment prefetch.
2026-08-24 10:41:36 +02:00
Hendrik Liebau 45d3aa06cf Stop emitting a separate route entry for a dynamic route's RSC form (#97726)
A dynamic app page in an app with an app directory needs two routes
today. One matches a request for the page, and one matches a request for
its `.rsc` payload or a per-segment prefetch. Simplified, with the
request pattern on the left and the artifact it resolves to on the
right:

```diff
- /de/<slug><.rsc|.segments/*.segment.rsc>    ->  /de/[slug]<matched suffix>
- /de/<slug>                                  ->  /de/[slug]
+ /de/<slug><.rsc|.segments/*.segment.rsc|>   ->  /de/[slug]<matched suffix>
```

One entry now serves both, because the suffix group gains an empty final
alternative. The group therefore always matches. It captures an empty
string for a request that carries no suffix, and the destination copies
whatever it captured, so a plain request resolves to the page and a
suffixed request resolves to the artifact it asks for. The parameter is
matched lazily, so a request for `/de/other.rsc` still prefers the
shorter parameter and the `.rsc` suffix over a parameter that swallows
the suffix.

The empty alternative is deliberate, and an optional group would not be
equivalent. A consumer of these entries can resolve the placeholders in
a destination from the match result rather than from the pattern. A
group that does not take part in the match is then absent from that
result, and the literal text `$rscSuffix` survives into the destination.
A group that always takes part avoids depending on how any one consumer
treats an absent key, and the cost of getting it wrong is a 404 on every
plain navigation to every dynamic route.

One case keeps two entries. A pages router route with `fallback: false`
requires the preview cookies on the request for the page, and not on the
request for its `.rsc` payload. An entry carries one set of conditions
and cannot express that difference, so the merge applies only when both
forms agree on their conditions.

Every app that has an app directory therefore halves the entries for its
dynamic routes, whether or not it uses Cache Components. We measured the
same in-progress feature branch of the v0 chat app as the previous
change in this stack, which enumerates precomputed flag, locale and
device permutations in `generateStaticParams` for a top-level dynamic
segment and so multiplies every route below it. Every dynamic entry on
that branch merges. Building it before and after the change removes 48%
of the routes that the previous change left, replaces each merged pair
with a single entry, and changes nothing else once the build ID is
normalized. Across both changes that branch loses 65% of its routes.

This collapse follows `experimental.collapseAdapterRoutes`, which the
previous change in this stack added. A build that sets it to `false`
emits a separate entry for each form.

**Verified with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32540912007).**
2026-08-24 10:41:35 +02:00
Hendrik Liebau 7678e6e953 Stop emitting a redundant route per prefetch segment (#97720)
For a dynamic app page that has a fallback shell, say `/[lang]/[slug]`
with a shell for the root param value `de`, the build emits three routes
to an adapter. Simplified, with the request pattern on the left and the
artifact it resolves to on the right:

```diff
- /de/<slug>.segments/$d$lang/$d$slug<seg>   ->  /de/[slug].segments/$d$lang/$d$slug<seg>
  /de/<slug><.rsc|.segments/*.segment.rsc>   ->  /de/[slug]<matched suffix>
  /de/<slug>                                 ->  /de/[slug]
```

This change removes the first one. The second already covers it: its
suffix group accepts `.segments/<path>.segment.rsc` as well as `.rsc`,
and it copies the matched suffix into the destination, so a per-segment
request resolves to the same artifact either way.

That second route is also the only one that ever answered `_tree` and
`_full` requests, because a per-segment route pins one literal segment
path in its regex. A `_tree` prefetch for `/de/<slug>` resolves to
`/de/[slug].segments/_tree.segment.rsc`, which the per-segment route
cannot produce. So this removes a duplicate, not a mechanism.

Only fallback shells emit per-segment routes, because their artifacts
sit under an unresolved param and need a rewrite to reach. Segment
artifacts for a concrete prerendered path need none, since a request for
that path matches them directly. Apps with many shells therefore lose
close to a third of their routes. We measured an in-progress feature
branch of the v0 chat app, which enumerates precomputed flag, locale and
device permutations in `generateStaticParams` for a top-level dynamic
segment and so multiplies every route below it. Building that branch
before and after the change removes 32% of its routes, adds none, and
changes nothing else once the build ID is normalized.

This also adds `experimental.collapseAdapterRoutes`. It defaults to
`true`, and it controls this collapse together with the ones that follow
in this stack. A build that sets it to `false` emits the same route
table as a build without any change in this stack.

`prefetchSegmentDataRoutes` stays in `routes-manifest.json`. A build
that does not use the adapter reads the field from that manifest and
derives the same routes from it. This change therefore leaves that path
alone.

**Verified upstack with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32540912007).**
2026-08-24 10:41:35 +02:00
Hendrik Liebau 0247e34bf9 [test] Capture the dynamic routes a build passes to an adapter (#97719)
We are about to reduce the number of these entries, and each of those
changes should arrive with a diff that shows which ones it removes or
merges. That is what these fixtures record. The existing behavioral
suites stay responsible for regressions, and they do detect a routing
change of this kind, e.g. a build that dropped the `.rsc` entries of
fallback shells would fail a root param case in
`segment-cache/prefetch-app-shell`. These snapshots add the part those
suites may not distinguish, since an entry that collapses into a less
specific match can still serve a response that renders the same content.

One fixture covers Cache Components with root params, the shape that
grows with the number of root param combinations. The other covers what
does not depend on Cache Components: the `.rsc` and plain entry pair
that every dynamic app page receives, a route handler, pages that share
one shape and differ in a static last segment, and a `fallback: false`
pages router route whose plain entry carries a preview bypass condition
that its `.rsc` sibling does not. It also holds a proxy next to static
pages router pages, which adds one entry per page and is the only case
here whose count grows with the number of pages rather than with the
number of route shapes. A third test builds the first fixture again
under a base path and asserts the prefix on every entry.

The projection stays narrow so that unrelated build output leaves it
alone. Both fixtures pin `cacheComponents` and `generateBuildId`,
because CI would otherwise vary them.
2026-08-24 10:41:34 +02:00
Luke Sandberg 713bd67572 turbo-tasks: compile conditional cell updates once, not per cell type (#97763)
### What?

Makes `CurrentCellRef::conditional_update_with_shared_reference` compile
once instead of once per cell type. **−345 KiB** of code `__text`
66,832,376 → 66,479,352, measured on `libnext_napi_bindings.dylib` with
the shipped release profile (thin LTO, `codegen-units = 1`,
`--icf=all`).

The 1,302 instantiations of this function collapse to **1**.

### Why?

The function took its callback as `impl FnOnce`, so it was monomorphized
for every `VcValueType` in the dependency graph — but its body is
identical for all of them: read the cell, call the callback, hand the
result to the *non-generic* `update_own_task_cell`. Nothing in it
depends on the cell type.
2026-08-24 07:24:42 +00:00
Tobias Koppers ba530fda6e Turbopack: add unit test coverage for FileSystemPath::hash_file (#97743)
### What?

Adds unit test coverage for `FileSystemPath::hash_file`, the helper
introduced in #97507. It pins the
contract per filesystem entry type: a regular file is hashed by its
content, a symlink by its *own*
link content (target and whether it is absolute or relative), and a
directory or a path that doesn't
exist can't be hashed at all.

Tests only — no production code is touched.

### Why?

`hash_file` exists because hashing a traced path through `read()` was
wrong: `read()` follows a
symlink, so a link pointing at a directory made the build fail outright
with
`reading file "…" Is a directory (os error 21)`, and a link pointing at
a file silently produced the
hash of the target's bytes. The latter matters because consumers of the
trace re-create a symlink
entry *as a symlink* (`copyTracedFiles` does `readlink` + `symlink`)
rather than copying the resolved
file, so the link — not the file behind it — is what actually gets
written out and therefore what the
hash has to describe.

Nothing guarded that. The distinction is invisible at the call site
(`hash_file` reads like an
ordinary "hash this path" helper) and "just read the file and hash it"
is a natural-looking
simplification, which is exactly how the original crash was introduced.
A missing symlink case is
also the kind of thing that only shows up in the wild, on someone else's
repository layout, at build
time.

### How?

The test drives a real `DiskFileSystem` over a temporary directory
rather than exercising
`LinkContent::hash` directly, because the bug being guarded lives in the
entry-type dispatch — which
branch gets chosen — not in the hashing itself. A test that hashed a
`LinkContent` it constructed
would still pass against the buggy implementation. It reuses the harness
style of the existing
`read_glob` tests (a `turbo_tasks::function(operation, root)` wrapper
read strongly consistently),
and creates symlinks through a helper that mirrors their platform
handling, using a junction point
for a directory link on Windows.

The interesting design point is how "the link is hashed, not its target"
is asserted. Doing it by
mutating a file and re-hashing would pull in filesystem watching and
invalidation, so it is expressed
structurally instead: two directories each contain a file of the same
name but with different
content, and each has a symlink pointing at it through the same relative
target. Those two links must
hash *equally* (proving the target's content is irrelevant) while a
third link with a different
target must hash *differently* (proving the target itself is what
counts). Both of those assertions
fail against the pre-#97507 behaviour, as does the symlink-to-directory
case.

The remaining cases cover a dangling link and a link whose target leaves
the filesystem root — both
still hashable, since they are still links — and assert that every
distinct link hashes distinctly,
so nothing collapses into one shared "is a symlink" hash. The two
assertions covering directories and
missing paths document today's hard-error behaviour and carry a comment
pointing at the open question
already noted in `hash_file` about whether those should return `None`
instead, so it is clear which
assertions to revisit if that changes.

### Verification

- `cargo test -p turbo-tasks-fs` (125 tests)
- `cargo clippy -p turbo-tasks-fs --all-targets`, `cargo check -p
turbo-tasks-fs --all-targets`
- Confirmed the test actually guards the regression: reverting the
symlink arm of `hash_file` to
`read().hash()` makes it fail with the original `Is a directory (os
error 21)` error, and it passes
  again once restored.

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-24 09:08:23 +02:00
next-js-bot[bot] 80576037a9 v16.4.0-canary.3 v16.4.0-canary.3 2026-08-23 23:22:09 +00:00
Marcos Hernanz 27a8eb0999 [devtools] Fix indicator dragging on touch screens (#97723)
### What?

- Restore `touch-action: none` on the draggable devtools indicator.
- Add integration coverage for the draggable computed touch action.

### Why?

PR #86816 made devtools content selectable, but also removed
`touch-action: none` with the selection styles. On touch screens, that
allows the browser to claim the gesture for viewport panning and
suppress the pointer event stream, causing the indicator to stop moving.

### How?

Keep the drag-lifecycle selection handling introduced by #86816 and
restore only `touch-action: none` before the gesture begins.

Fixes #97668

### Testing

- `pnpm build --filter next`
- Targeted Prettier and ESLint checks
- `NEXT_SKIP_ISOLATE=1 pnpm test-dev
test/development/app-dir/devtools-position/default-position.test.ts`
2026-08-23 11:56:26 -07:00
next-js-bot[bot] cb463843e7 v16.4.0-canary.2 v16.4.0-canary.2 2026-08-22 23:22:05 +00:00
Luke Sandberg d898c8f590 introduce an options struct for constructing backend storage (#97284)
Improve legibility a bit, requested by @bgw here
https://github.com/vercel/next.js/pull/95976#discussion_r3762093713
2026-08-22 02:53:32 +00:00
next-js-bot[bot] 74a30b10ac v16.4.0-canary.1 v16.4.0-canary.1 2026-08-21 23:22:21 +00:00
Marcos Hernanz e694c08b6f docs: fix parallelRouteKey parameter name in useSelectedLayoutSegment(s) (#97710)
The `useSelectedLayoutSegment` and `useSelectedLayoutSegments` hooks
accept a
parameter named `parallelRouteKey` (singular), as declared in
`packages/next/src/client/components/navigation.ts`:

export function useSelectedLayoutSegment(parallelRouteKey: string =
'children')

The docs wrote it as `parallelRoutesKey` (plural) in three files, which
doesn't
match the generated `navigation.d.ts` type that editors surface via
IntelliSense:

- `use-selected-layout-segment.mdx` — parameter signature and
description
- `use-selected-layout-segments.mdx` — parameter signature and
description
- `parallel-routes.mdx` — "With useSelectedLayoutSegment(s)" section

Docs-only change; no behavior change.

---

Credit: This contribution was originally authored by @see-stack in
#97690 and is recreated here with signed commits.

Co-authored-by: Said Nasser <sdnssr001@gmail.com>
2026-08-21 16:05:03 -07:00
Benjamin Woodruff ‮ 49bfb6506a Turbopack: Avoid cloning paths in fs watcher if the path is already in a map (#97655)
This is a spiritual continuation of
https://github.com/vercel/next.js/pull/96114 . See
https://github.com/vercel/next.js/pull/94735 for motivation.

This codepath should already be pretty cheap, but in some cases (notably
inotify on Linux where updates aren't debounced as aggressively by the
OS) it could become somewhat hot and waste CPU while other
somewhat-CPU-heavy work is happening in other software (e.g. `pnpm`).

This removes two more sources of clones:
- If we only have a `&Path`, but it's already in the `paths` map, we can
use the raw entry API to avoid cloning it.
- `new_paths` can use `usize` indexes into the `FxIndexMap` instead of
storing another clone of the path. We could also encode this information
as flags on `path`, but the iteration over `new_paths` must happen as a
separate loop before the normal batch execution loop (near the bottom of
`DiskWatcher::watch_thread`), so I'd like to keep it as a separate data
structure to avoid having to filter out a bunch of unrelated keys
without the flag when finally processing the batch.
2026-08-21 15:24:03 -07:00
Jiwon Choi d7aa66c345 Remove generated error codes (#97687)
### Why?

Should come up with better solution that does not block PRs with git
conflict

x-ref:
https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH

### How?

- Delete `errors.json`, the error-code SWC plugin, generated WASM, merge
driver, and validation/build tooling.
- Stop attaching error codes to server-rendering digests, redboxes, and
telemetry; native `Error.code` and `Error.name` remain available where
applicable.
- Remove the development-overlay error feedback UI, middleware, and
telemetry event that depended on stable codes.
- Update fixtures, snapshots, and guidance for code-free errors and
numeric-only digests.

<!-- NEXT_JS_LLM -->
2026-08-21 22:45:12 +02:00
Janka Uryga 43273a1d21 [PPF] Instant validation for unstable_navigation() (#97309)
Adds discriminated error messages for `unstable_navigation()` used
outside of Suspense.

Previously we only had to discriminate two kinds of errors in each
validation flow:
- App Shell -- 1. link data (if it resolves in `Runtime`) or 2. dynamic
data (if it resolves in `Dynamic`)
- Static Shell -- 1. runtime data (if it resolves in `Runtime`) or 2.
dynamic data (if it resolves in `Dynamic`)

However, with `navigation()` (96908), the App Shell flow needs to
distinguish three kinds: 1. link data, 2. navigation (if it resolves in
`NavigationRuntime`), and 3. dynamic data. This complicates the
validation code a bit, because we can no longer get away with one retry.
To accommodate this, i restructured the validation code to no longer
recurse as a method of retrying. We just loop over an array that defines
what order we should try the stages in + what kind of hole shows up in
each stage.

I've also removed `hasAmbiguousErrors` and the associated logic. since
we're no longer mixing Static+Runtime segments in a runtime prefetch,
this is no longer relevant -- all segments used for validation use one
stage, so whether or not the error is ambiguous only depends on the
stage.

For ease of reviewing, this is split into two commits:
1. updating the (many) places that need a new `DynamicHoleKind` in our
error messages. This one has a lot of pretty mechanical changes, and was
also machine-reviewed for consistency already, but the messages
themselves are worth looking at (we'll also need to update docs --
leaving that for a follow-up)
2. the actual instant validation changes, i.e. using the new
`DynamicHoleKind` and adding the new pass
2026-08-21 21:08:38 +02:00