Commit Graph

35737 Commits

Author SHA1 Message Date
Aurora Scharff 086294e2e5 docs: add layout stability guidance for videos and iframes (#98070)
## Summary

- Document how dimensions and poster images keep video layouts stable
while media loads.
- Add equivalent sizing guidance for lazy-loaded iframes.

## Verification

- `prettier --check docs/01-app/02-guides/videos.mdx`

<!-- NEXT_JS_LLM_PR -->
2026-08-31 11:22:47 +02:00
Niklas Mischkulnig a37a86acf0 tests: Improve use-cache-cross-deployment tests (#98049)
Followup to #95233

- unskip one client component test since that is actually expected
behavior already, we just need to test the observable behavior instead
- Fix instant insights warnings
- Don't use console.log since that errors when trying to print server
references
2026-08-31 09:46:38 +02:00
next-js-bot[bot] 2fe6f962a1 v16.4.0-canary.12 v16.4.0-canary.12 2026-08-29 23:19:26 +00:00
Sebastian "Sebbie" Silbermann 7421a868ad [ci] Remove the popular workflow and its action (#97753)
The `popular` workflow posted weekly Slack digests of the most-reacted
issues, PRs, and feature requests. That reporting is now handled by the
Next.js maintainer agent, so this PR deletes the workflow along with its
backing `next-repo-actions` action (sources and checked-in ncc bundles),
which had no other consumer, and regenerates the `.github` lockfile
without the action's dependencies.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-29 11:54:35 +02:00
Luke Sandberg 33a5d542e5 Store keys in key order in SST blocks that omit hashes (#97480)
## What

Key blocks for short keys (≤ 32 bytes) don't store a per-entry hash, but
were still sorted by `(hash, key)`. That forced every binary-search
probe to recompute the entry's xxh3 hash just to compare it — roughly
ten hashes per lookup. These blocks now store their entries in **key
order**, so the search compares key bytes directly and hashes nothing.

All the families `turbo-tasks-backend` produces hit this case, so this
is the common path in practice.

## Why this is safe

Hash-based routing is unchanged. Files are still assigned by hash, the
index block still routes to a key block by hash, and
`min_hash`/`max_hash`, the AMQF, and compaction's coverage model all
work exactly as before.

Thus only the order *within* a hash-less block changes. This adds a
small cost to writing, which now must re-sort blocks of keys and a
double cost to compaction which now must re-sort blocks in hash order to
iterate them, and then sort them back to key order when writing the new
file.

## Benchmarks

Measured against `canary`, baseline and comparison run back to back on
the same machine.

**Lookups ** All 16 `static_sorted_file_lookup` configurations improved
| entries | hit/uncached | hit/cached | miss/uncached | miss/cached |
| --- | --- | --- | --- | --- |
| 1 Ki | -30.4% | -24.8% | -29.1% | -29.4% |
| 10 Ki | -25.2% | -19.7% | -22.7% | -23.7% |
| 100 Ki | -17.3% | -19.3% | -15.2% | -16.8% |
| 1000 Ki | -7.0% | -25.7% | -7.0% | -10.9% |


**Commits ** Short keys pay for the added per-block sort.

| config | change |
| --- | --- |
| `key_8` 85Ki entries | +3.3% |
| `key_8` 853Ki entries | +3.1% |
| `key_8` 8.33Mi entries | +8.5% |
| `key_32Ki` / `key_4` (large-key configs) | -2.6% to -19.1% |

Six of thirteen write configurations came out as noise (p >= 0.05) and
are omitted.

**Compaction** `StaticSortedFileIter` must yield `(hash, key)` order
because `MergeIter` merges on it, so it reorders each hash-less block
back into hash order. Plus the additional cost of the commit (above)

| config | change |
| --- | --- |
| 4Mi entries / 8 commits | +6.0% (reproduced at +10.2%) |
| 16Mi entries / 8 commits | +7.5% |
| 4Mi entries / 32 commits | +7.8% |
| 16Mi entries / 32 commits | +5.2% |
| 16Mi entries / 128 commits | -9.5% |
2026-08-29 00:04:45 -07:00
Jiwon Choi 920a76735c Honor non-interactive mode in upgrade codemod prompts (#98030)
### Why?

`@next/codemod upgrade --yes` accepts defaults for the upgrade command's
prompts, but `next-request-geo-ip` asked its own deployment question
inside `runTransform`. Because the non-interactive state was not
forwarded, automated upgrades could stop at that nested prompt despite
using `--yes`.

### How?

Pass the resolved non-interactive state from `runUpgrade` into each
transform. The geo/IP transform skips its deployment prompt in
non-interactive mode while preserving the existing prompt for direct
interactive runs.
2026-08-28 23:44:28 +00:00
Jiwon Choi 6fb98fae8a Transform .mjs files with Next.js codemods (#98029)
### Why?

The shared codemod runner only passed `tsx`, `ts`, `jsx`, and `js` files
to jscodeshift. As a result, config transforms that recognize the
supported `next.config.mjs` format never received those files, leaving
migrations such as `experimental.turbo` to `turbopack` incomplete.

### How?

Add `mjs` to the existing jscodeshift `--extensions` argument. Existing
parser selection and Next.js config detection already support this
format. Unsupported `next.config.cjs` files remain excluded.
2026-08-29 01:42:42 +02:00
next-js-bot[bot] 0d4dc0c3a5 v16.4.0-canary.11 v16.4.0-canary.11 2026-08-28 23:18:43 +00:00
Marcos Hernanz a41e98e725 Fix optimistic routing for encoded dynamic params (#97948)
## Summary

Fixes #97842 by canonicalizing encoded pathname parts before using them
as dynamic segment cache keys in optimistic route predictions. This
keeps predicted route trees consistent with server Flight data and
prevents query-only navigation from remounting pages when regular or
catch-all params contain encoded characters.

Add coverage that verifies bfcache IDs and form state are preserved for
`@` in a regular dynamic param and for `@` plus `%2F` in a catch-all
param.

## Verification

- Existing encoded-slash route-cache E2E suite in Turbopack and webpack
production modes
- Existing optimistic-routing E2E suite in Turbopack and webpack
production modes
- `pnpm test-types`
- `pnpm types`
- `pnpm prettier-check`
- `pnpm lint-ast-grep`
- `pnpm lint-language`
- Changed-file Prettier and ESLint checks via the pre-commit hook

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-08-28 15:50:36 -07:00
Marcos Hernanz 4d36801fdb Fix intercepted route params after Proxy rewrites (#97953)
## What?

Preserve the final internal rewrite pathname in the RSC response when an
earlier Proxy rewrite has already populated the rewritten-path header.

This also adds an end-to-end regression assertion for a dynamic
interception route behind a locale-injecting middleware rewrite.

## Why?

The route resolver set the final marker-bearing pathname directly on the
response, but router-server later reapplied the accumulated Proxy
response headers. That restored the earlier markerless pathname. The
client then parsed an intercepted dynamic param against that stale path
and removed three real characters from the value.

## How?

Mirror the final internal rewrite pathname into the accumulated
response-header map. When router-server applies that map after route
resolution, the last rewrite remains authoritative.

Fixes #97939

## Tests

- Production interception regression suite: 4/4 passed
- Development interception regression suite: 4/4 passed
- Related production rewrite/interception suites: 43/43 passed
- Next package build, targeted ESLint, Prettier, and git diff checks
passed
2026-08-28 15:50:12 -07:00
Janka Uryga d8f95c12de [PPF] Fix navigation() in prospective runtime prerenders (#98000)
### Long story short

Fixes an analogous bug to #95665, but affecting
`prefetch()/navigation()`. Those two were accidentally always resolving
in the **prospective** runtime prerender, so we were warming content
that isn't needed for the final runtime prerender. We now guard them
properly.

Also, I replaced `PrerenderStoreModernRuntime.isSessionShell` with
`finalStage` (the same stage we're gonna use for the final prerender),
so we can include/exclude `navigation` properly -- the boolean only
distinguished shell/prefetch, not navigation.

### Long story long

We use runtime prerenders for three things: runtime shells, runtime
prefetches, and embedding a prefetch in navigations. These are aborted
after `ShellRuntime`, `Runtime`, and `NavigationRuntime` respectively
(accessible in `stageController.finalStage`). If we're doing the
**final** prerender for a shell, promises that would resolve in e.g.
`NavigationRuntime` would never resolve, because we abort the prerender
before we get there.

However, the **prospective** runtime prerender has no
`StagedRenderingController` and needs to model this separately (so that
the same APIs hang in both). This was [previously done via
`prerenderStore.isSessionShell`](https://github.com/vercel/next.js/pull/95665)
which we checked in URL data apis i.e. `params/searchParams` and the
metadata`pathname` to do this. `isSessionShell: true` meant they should
hang because it's a shell, and `isSessionShell: false` meant they should
resolve.

However, I forgot about this case when implementing
`navigation`/`prefetch`, and made them resolve unconditionally in the
prospective prerender. This means we'd potentially warm content that
would not actually be reached in the final prerender. In other words,
```ts
await navigation()
return <Expensive />
```
would needlessly render `<Expensive />` during the prospective render
but not in the final one. This kinda defeats `navigation`'s main
purpose, i.e. avoiding expensive work.

This PR fixes the bug by replacing `isSessionShell: boolean` with
`finalStage: AdvanceableRenderStage`, which is set to the same value we
put in `stageController.finalStage`. This lets us keep the logic of what
hangs and what resolves relatively close in both prerenders.

---

On a meta level, the fact that the same bug happened twice tells me that
having a `StageRenderingController` that is sometimes `null` is
error-prone, and we should figure out a better solution, but i'm gonna
keep this fix targeted and figure out a more holistic solution in a
follow-up
2026-08-28 22:10:21 +02:00
Janka Uryga 357e514cf7 Revert "test: re-enable sync IO root coverage" (#98050)
Reverts vercel/next.js#97996

The fix does not seem to help with the flakiness.
2026-08-28 19:41:32 +00:00
Niklas Mischkulnig 96b1b06d0b Turbopack: drop rust analyzer skip annotation (#98047) 2026-08-28 19:33:13 +00:00
Niklas Mischkulnig 7a15847525 Turbopack: add next_config.use_react_experimental getter (#98032)
Refactor to unify the multiple places where this computation was
happening
2026-08-28 10:33:43 -07:00
Niklas Mischkulnig e3790aa911 More granular cache keys for use-cache entries (#95233)
Caveats:
- I had to skip `react[-dom][/*]` and
`private-next-rsc-server-reference` and `private-next-rsc-cache-wrapper`
imports in the code hash and env-var tracking. Because those all end up
pulling in app-page-turbo.runtime.prod.js which reads many env vars and
would cause constant deopting. But this should still be correct. The
code of these imports is included via the Next.js version, and no env
vars should change the semantics of any of those imports.
- Static env var reads are collected, but too dynamic accesses are
silently ignored and don't lead to deopts (same goes for env var reads
in native NAPI addons). This means that this cache reuse is not
guaranteed to be 100% guaranteed to never lead to stale caches.
- Code hashing and env var collection happens on a per-module basis. So
if you put all use-cache functions into a single file and/or together
with react components, then you will see extraneous invalidations. This
will be fixed by either generally enabling module splitting for all of
Turbopack, or by adding a special transform that does it for use-cache
functions.

Followups:
- There are some env var static analysis gaps that will be immediate
followups before broader testing. These are the various TODOs added in
https://github.com/vercel/next.js/pull/95310
- Client components invalidation is very coarse grained right now
(statically imports any client component anywhere -> deopt completely).
Followup for the future. But the current setup is correct. This would
just improve effectiveness further
- Do this in dev as well. Currently there is no NFT at all in dev (for
performance reasons)
- Module splitting for more granular tracking
- Include entropy when serializing server reference arguments for cache
key


Todo:

- [x] Use implementation code hash (includes inlined env vars): from
#94234
- [x] Include non-inlined runtime env vars: from #95310
- [x] Include client reference manifest (very coarse for now)
- [x] Include Next.js version (for wire format, etc)
- [x] This is now done for all use-cache entries now. Not just for `use
cache: remote`. Is that the intended behavior? Yes
- [ ] ~~if `NEXT_DEPLOYMENT_ID` is in the env vars. just deopt and don't
care about stringifing and hashing the env vars~~
- [x] Is cache key size a problem? Currently you can get this: (values
are always hashed)
`CustomCacheHandler::get
["80e6f6560092f0078775e7e787c1c10ecf6dea0bc4",[],["d984fbaa996274737f3b59345a300a20","16.4.0-canary.5","__NEXT_NO_MIDDLEWARE_URL_NORMALIZE=undefined","NEXT_OTEL_PERFORMANCE_PREFIX=undefined","__NEXT_PRIVATE_ORIGIN=a04f4b9d6a8f42724740a480e9a2bc67c053dc04377be831c0e2c407a1422004","NEXT_PRIVATE_RESPONSE_CACHE_TTL=undefined","NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE=undefined","__NEXT_CACHE_COMPONENTS=b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b","__NEXT_ROUTER_BASEPATH=undefined","__NEXT_MANUAL_CLIENT_BASE_PATH=undefined","__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS=undefined","__NEXT_APP_NAV_FAIL_HANDLING=undefined","__NEXT_GESTURE_TRANSITION=undefined","__NEXT_USE_OFFLINE=undefined","NEXT_DEBUG_BUILD=undefined","__NEXT_VERBOSE_LOGGING=undefined...]]
[["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]]`
2026-08-28 10:17:15 -07:00
Sebastian "Sebbie" Silbermann 28195e9b75 [ci] Remove dormant code_freeze workflow (#97756)
Code freezes are now managed by toggling the existing `code freeze`
repository ruleset in the repository settings, so the `code_freeze`
workflow is no longer needed. It was also broken beyond repair: it
targeted the legacy branch-protection API with a `CODE_FREEZE_TOKEN`
secret that no longer exists instead of the rulesets the repository uses
now, and it referenced a `releaseType` input that was never declared.
This PR deletes the workflow and `scripts/code-freeze.js`, which had no
other consumer.

Note on the investigated alternative: resurrecting the workflow on top
of the rulesets API with just `GITHUB_TOKEN` is not possible. Creating
or updating repository rulesets requires the "Administration" repository
permission, which the workflow token cannot be granted (`permissions:
administration: write` is rejected as an invalid workflow, and even
`write-all` gets `403 Resource not accessible by integration`). Verified
empirically in
https://github.com/eps1lon/github-actions-repository-dispatch-admin-permissions.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-28 19:11:37 +02:00
Josh Story 5e390edb7f test: re-enable sync IO root coverage (#97996)
## Summary

Re-enable the `sync-io-blocks-root` production suite now that #97900
waits for child stdio to close before capturing build CLI output.

This reverts the temporary skip from #97986 so CI flake detection
exercises the route-specific sync I/O diagnostics again.

## Verification

- `HEADLESS=true IS_WEBPACK_TEST=1 pnpm test-start-webpack
test/production/app-dir/sync-io-blocks-root/sync-io-blocks-root.test.ts`

<!-- NEXT_JS_LLM -->
2026-08-28 10:08:40 -07:00
Sebastian "Sebbie" Silbermann bfc33e2bb0 [ci] Remove the unused wrong-issue-template workflow and action (#97749)
The `issue_wrong_template.yml` workflow closed issues labeled `please
use the correct issue template`. That label was only ever applied
manually by maintainers and has not been used since January 2025 (9
issues total, ever), so every recent run of the workflow was skipped.
This change removes the workflow along with its backing
`wrong-issue-template` action (source, checked-in ncc bundle, and build
script).

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-28 19:08:20 +02:00
Steven 1cb20a1d14 [image-optimizer] Refactor into lightweight transform module (#97988)
This PR is strictly a refactor, no logic should change.

The idea is that we eventually want to run the image optimizer transform
step in a child process. This PR refactors the code to make a new
`transform.ts` that is lightweight and should reduce some of the
overhead of loading a large module graph when spawning a new process.
2026-08-28 17:52:50 +02:00
Sebastian "Sebbie" Silbermann f889820802 [ci] Remove the stale-issue workflow (#97758)
The `issue_stale` workflow ran `actions/stale` once a day to mark issues
with no activity in 545 days (~1.5 years) as stale and close them a week
later, and to auto-close issues labeled `please add a complete
reproduction` (2 days), `please simplify reproduction` (14 days), or
`please verify canary` (14 days). Marking and closing issues based on
inactivity is no longer wanted, so the workflow is deleted.

The `STALE_TOKEN` secret has already been revoked.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-28 17:35:56 +02:00
Sebastian "Sebbie" Silbermann a18361694e [ci] Use presigned URLs to upload preview builds (#97922)
Follow-up to #97256, pairing with
[vercel/vercel-packages#107](https://github.com/vercel/vercel-packages/pull/107).
The token exchange endpoint no longer returns a client-upload token
derived from a store read-write token; it presigns the upload URL with
the deployment's own Vercel OIDC identity and returns the URL, pinning
the pathname, expiry, size cap, and access and overwrite policy
server-side. No store credential exists anywhere after this lands, and
no Blob SDK is needed here for the new path — the script PUTs the
tarball bytes to the URL directly.

The client only uses presigned-URL. The server still supports client
tokens until we landed this and synced the mirror.


Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-28 17:29:16 +02:00
Niklas Mischkulnig f6c48e199f Turbopack: allow get_definable_name to return a list (#97984)
Followup to #95310

Now we can correctly track env vars in cases like these were a
`JsValue::Alternative` is involved
```js
// An alternative (process|unknown).env
let p2
if (foo) {
  p2 = foo
} else {
  p2 = process
}
console.log(p2.env.FOO8)
```

The inlining logic behaves the same as before:
- If `get_definable_name` returns exactly one result, we can inline.
- Otherwise, we can't inline (previously this case returned `None`)


A bit of trivia why we need that boolean in the return type in the first
place (because conceptually it should be possible to just model it via
`JsValue::Alternative` anyway:

> but why is that a problem? if the value is reassigned, then there
should just an alternative, then get_definable_name returns multiple
values and it should still not be inlining
>
> Because module is a `FreeVar`, not a normal `Variable`.
>
> The assignment is stored separately in `VarGraph`:
>
> ```
> free_var_ids["module"] -> id
> values[id] -> assigned function
> ```
>
> But later reads still evaluate to:
>
> `JsValue::FreeVar("module")`
>
> The linker expands JsValue::Variable, but not JsValue::FreeVar.
Therefore there is no JsValue::Alternatives at this call site.
>
> Instead, `get_definable_name()` detects the graph entry and returns:
>
> `[Some((["module", TypeOf], true))]`
>
> That boolean is how reassignment is represented for free variables.
Cardinality remains one, so ignoring the boolean at
references/mod.rs:4007 incorrectly permits inlining.
>
> Actual `Alternatives` primarily represent multiple values assigned to
tracked variables. For free globals, the original ambient value plus
reassignment is represented by `FreeVar` plus `potentially_reassigned`.


No perf impact:

```
commit cfc7da3049 (HEAD -> canary, origin/canary, origin/HEAD)
393.17s user 31.34s system 722% cpu 58.775 total
391.91s user 35.37s system 746% cpu 57.256 total
389.48s user 30.40s system 770% cpu 54.513 total

commit 38f2708659be31538c951b12ba806ef53710f074 (HEAD -> mischnic/get-definable-name-list)
392.86s user 34.00s system 745% cpu 57.221 total
388.10s user 32.72s system 766% cpu 54.912 total
391.84s user 37.82s system 717% cpu 59.865 total
```
2026-08-28 09:20:02 +00:00
Sebastian "Sebbie" Silbermann 027205875b [ci] Run flake detection and new deploy tests when merged and on backport branches (#97991)
The previous detection mechanism special-cased `canary` which lead to
large diffs when backport branches ran CI.

We stop special casing `canary` reducing complexity and giving us
another flake-detecting attempt when the change is merged.

That way we can safely enable flake detection on backport branches
without having to test large diffs when PRs are merged targetting
backport branches. New backport branches will no longer have flake
detecton and new deploy test runs disabled.
2026-08-28 09:09:32 +00:00
Joseph 9a7061053e docs(examples): document env var handling in the Docker examples (#97968)
Improve with-docker example env var usage documentation.

Closes: https://github.com/vercel/next.js/issues/97959
2026-08-28 11:08:57 +02:00
Niklas Mischkulnig 978a0809bb Turbopack: unify member and in handling (#97985)
Followup to #95310

Purely a refactoring. These functions were 99% identical
2026-08-28 08:39:12 +00:00
Niklas Mischkulnig cfc7da3049 Update outdated snapshots (#98011)
I ran `UPDATE=1 cargo nextest run -p turbopack-tests`

1. https://github.com/vercel/next.js/pull/97672 was merged
2. But https://github.com/vercel/next.js/pull/97676 was merged without
rebasing, leading to outdated snapshots
2026-08-28 09:18:26 +02:00
Tobias Koppers 17a901a74f Guard filesystem reads against unresolved symlinks (#97902)
### What?

Adds debug-only OS realpath validation to successful `DiskFileSystem`
file and directory reads. When a successfully canonicalized path differs
from the supplied path, the read returns a normal task error naming both
paths.

Fixes pattern/glob traversal and NFT tracing so physical filesystem
access uses resolved paths while logical paths remain available for
user-visible specifiers and complete symlink-chain recovery.

### Why?

Reading through an unresolved symlink parent gives the same filesystem
object multiple path identities. That can make Turbo Tasks dependency
tracking and invalidation inconsistent and can produce invalid
deployment ZIPs when NFT output contains files below unresolved links.

The checks return errors rather than asserting because paths can
disagree temporarily under eventual consistency. Propagating a task
error avoids panicking a worker thread while still exposing invalid
callers during development.

### How?

The validation lives directly in `DiskFileSystem::read` and
`DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API
inline instead of the Turbo Tasks realpath task, keeping the diagnostic
out of the task dependency graph. The guard runs only after the OS read
succeeds, so missing/non-directory probes preserve their existing
behavior.

`read_matches` resolves each physical directory immediately before
enumeration while retaining logical `PatternMatch` paths.

`read_glob` and `track_glob` now resolve their initial directory before
enumeration. Symlinks discovered later through wildcard segments are
also traversed through resolved targets. `ReadGlobResult` deliberately
retains logical paths rooted at the supplied base, allowing consumers to
call `realpath_with_links` and recover the complete symlink chain.

Consumers follow that contract explicitly:

- NFT includes expand each logical match with `realpath_with_links`,
emit resolved files and every traversed symlink, skip resolved directory
targets, and deterministically deduplicate/sort output.
- `import.meta.glob` uses recursive logical keys as the source of
user-visible requests, while module resolution follows and tracks
symlinks.
- the hash-glob example resolves returned logical paths before reading.

Webpack-loader context dependencies are covered for both
`path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader
fixture performs its directory read with Node `fs`, reports the
directory using `addContextDependency`, and Turbopack tracks the
resolved target.

### Verification

- `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --
--check`
- `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api
--all-targets`
- `cargo test -p turbo-tasks-fs` (128 passed)
- `cargo test -p next-api` (7 passed)
- `cargo check -p turbo-tasks-fs --examples`
- `import.meta.glob` symlink execution fixture (1 passed)
- Nine targeted node-file-trace CI cases with `release-with-assertions`
(9 passed)
- `pnpm build-all`
- `webpack-loader-fs` Turbopack dev e2e (1 passed)
- `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed)
- twoslash Turbopack production, normal mode (4 passed)
- twoslash Turbopack production, cache-components mode (4 passed)
- `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200)

### Notes

The disk guard is cross-platform, while its symlink-parent regression
test is Unix-only, matching neighbouring symlink tests. On Windows, OS
canonicalization can also normalize casing and 8.3 short names; a debug
read using a non-canonical spelling will therefore return the same
diagnostic error.

<!-- NEXT_JS_LLM -->


<!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c -->

---------

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-28 08:31:06 +02:00
Tobias Koppers 6f44fd34a1 Turbopack: enable export mangling by default in production builds (#97676)
Stacked on #97672 — enable export mangling by default on canary
releases.

### What?

Two defaults change:

- **Next.js builds.** `experimental.turbopackMangleExportNames` is
pinned to `false` for stable
releases and left *unset* on canary, where Turbopack then defaults it
from the build mode: on for
production builds, off in development. Explicitly setting the option
wins in either direction, so
  setting it to `true` in development is honoured.
- **Turbopack's own execution test suite.**
`TestOptions::mangle_export_names` defaults to `true`,
so all 279 fixtures exercise mangling instead of only the handful that
opt in.

### Why?

The feature is verified by turbopack's own fixtures, targeted e2e
suites, and a couple of
bundle-size measurements — a narrow slice of what Next.js's test suites
actually cover. Defaulting
it on for canary puts every production-mode e2e and integration test
through the mangled code path
for real users of the canary channel, without committing stable users to
it yet. Turning it on for
stable is a separate, later decision that can stack on top of this once
canary has soaked it.

Broad exposure has already earned its keep several times over. Turning
mangling on in CI and in the
fixture suite surfaced bugs that no hand-picked suite had found,
including a module-fragments helper
handing out another module's exports value (#97672's
`EcmascriptExports::borrowed()`), a
client-reference proxy memoized per wrapper instance rather than per
content, and a
code-elimination bug for export-less modules (fixed in the follow-up
PR).

### How?

`defaultConfig` cannot see the build mode, so it only expresses the
stable/canary split
(`isStableBuild() ? false : undefined`) and
`NextConfig::turbopack_mangle_export_names(mode)`
supplies the mode-dependent default on the Turbopack side. This is
deliberately *not* the shape of
the neighbouring `turbopackSharedRuntime: !isStableBuild()`: mangling
should not apply in
development, and hard-forcing `false` there in Rust — as an earlier
revision of this PR did — would
silently ignore a user who asked for it explicitly.

Mangling does **not** depend on minification. An earlier version of this
PR gated it on
`minify(mode)`, which was wrong and has been removed; `--no-mangling` is
a minifier flag and does
not affect export mangling.

Verified with a real `next build` A/B on a small app: the option left
unset mangles (959,174 B total
emitted JS, 293,407 B gzipped) while explicitly setting it to `false`
does not (966,519 B / 294,963
B gzipped), confirming both the default and the override. All 279
execution fixtures pass with the
suite default flipped, with no fixture needing an opt-out.

Note this layer does not change the `turbopack-emit-collect` snapshots
on its own: making a module
without re-exports split additionally requires the split trigger added
in the follow-up PR, so those
snapshots move there.

This is the layer to revert if canary turns up problems specific to
running with the option on by
default; #97672 remains useful as an opt-in feature either way.

<!-- NEXT_JS_LLM -->


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


<!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee -->

---------

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-28 07:46:17 +02:00
Tobias Koppers a84bc8de2a Turbopack: mangle exported names for smaller bundle sizes (#97672)
### What?

Adds export-name mangling to Turbopack, behind a new experimental option
`experimental.turbopackMangleExportNames` (default `false`). It is
independent of minification:
`--no-mangling` is a minifier flag and does not affect it.

When enabled, each ECMAScript module's *used* export names — including
`default` and
`__esModule` — are replaced by short keys in the emitted output, both
where the module registers
its exports and where every consumer reads them. Modules whose export
names could be observed by
user code keep their original names, decided per module.

This is a reland of #89060 (on top of the already-merged #89406),
originally written by Matt
Mastracci, who is credited as a co-author on the commit.

Stacked on top: #97676 flips the default to `true` on canary releases,
so Next.js's own CI
exercises the feature broadly before it is considered for stable.

### Why?

Bundle size. A module's export keys exist only to link modules together:
the producing module
emits `{ someVeryLongExportName: … }` and every consumer reads
`ns["someVeryLongExportName"]`.
Both sides are generated by us, so as long as producer and consumer
agree — and the name isn't
observable from user code — the key can be a single character. Long
export names are extremely
common in real dependency graphs (icon sets, utility packages, barrel
files), and each one is paid
for once in the module that defines it and once per importing module.

### How?

**Ported, not rebased.** `canary` is ~2500 commits past the original
stack's base, and the files it
touched were independently rewritten in the meantime (export-analysis
refactor #92781, CJS analysis
for scope hoisting #95826, the `module_fragments` subsystem #95978). A
probe rebase produced 16
conflicting files on the first commit alone, so the original branches
were used as a reference
implementation — for intent, the identifier alphabet, and test coverage
— and the feature was
rebuilt on today's infrastructure. #89561 from the original stack
(erasing the Next.js wrapper
module types) is deliberately **not** part of this change; it turned out
to be unnecessary, because
those modules already declare whole-module export usage and therefore
back off on their own.

**The name table** (`references/esm/mangle/table.rs`) hashes each name
into a table of all valid JS
identifiers of the smallest length that fits the name set — 15 exports
get single-character keys —
and resolves collisions by open addressing. Hashing rather than
assigning `a`, `b`, `c`, … is what
keeps names stable: an unrelated edit elsewhere in the module doesn't
renumber every other export,
and a collision only perturbs its own cluster. Assignment happens in two
passes: every name that is
*already* a valid identifier at the chosen length keeps itself and
reserves its bucket first, and
only then is anything hashed — so an export called `a` keeps `a`, and
nothing else can be assigned
it. Both passes iterate in sorted order, so the mapping depends only on
the set of names.

A module with exactly **one** mangleable export is special-cased to a
fixed key, `f`, rather than a
hashed one. `f` is the most common character in JS keywords (`if`,
`for`, `function`), and every
single-export module in the graph then emits the same `.f` / `.f()` byte
sequences, which gzip's
back-references pick up across the whole bundle — a bigger win than
hashing, at the cost of that one
key changing when a second export is added.

A fixed list, `RESERVED_KEYS`, is withheld from every table for two
different reasons: JS reserved
words (`if`, `in`, `do`, `for`, `let`, `new`, `try`, `var`) are legal as
quoted property keys but a
minifier will not fold `ns["if"]` into the shorter `ns.if`, so handing
one out costs bytes instead of
saving them; and `__esModule` is withheld because the runtime's `esm()`
helper defines that property
on every module's exports object regardless of what the module itself
exports, so an assigned key
landing on it would collide. (`default` needs no such protection — once
it is mangled like any other
export, nothing else emits a property under that literal name.)

**One source of truth for the mapping.** `mangled_export_names(module,
chunking_context)` is a
turbo-task that both the producing side (`EsmExports::code_generation`)
and the consuming side
(`ReferencedAssetIdent::Module`, the single place a cross-module export
access is materialized) ask
for the *target* module's map. Neither side computes a table of its own,
so they cannot disagree,
and the task derives export usage from the chunking context itself
rather than accepting it as an
argument, so a caller can't supply usage from the wrong graph. Re-export
chains need no special
handling, because the consumer side already resolves through re-exports
to the module that produces
the binding.

The mangling decision itself lives on `EsmExports` as a
`mangle_export_names: bool` field, rather
than a separate trait method every module type has to override. A module
that derives its exports
from another one (a facade, a locals module, a part, a rename) inherits
the flag with the data,
which removed seven hand-written delegations and the possibility of a
new wrapper type forgetting
one.

**A mangling decision must not cross module identities.** A few module
types hand out *another*
module's exports value as their own (the WASM loader module, the
module-fragments side-effects
wrapper, the client-reference proxy). If that borrowed value carried a
real mangling decision, the
producing and consuming sides would key their lookups on two different
modules and could compute two
different keys for the same export — this actually broke every WASM- and
`@vercel/og`-based test
once the default-on layer exercised it in CI.
`EcmascriptExports::borrowed()` is the one place this
is handled: it always returns an unmangled view, and every such
pass-through site uses it.

**Back-off is per module**, built on the export-usage information that
landed after the original PR
(`BindingUsageInfo` / `ModuleExportUsageInfo`) rather than the
original's locals/facade-split
heuristic. A module keeps its names when its usage is `All` (a namespace
import that couldn't be
lowered, a computed property access, an unresolvable `export *`, or a
chunk-group entry — which
covers client references and the Next.js wrapper modules), when it is
read through a namespace value
at all, when its exports are dynamic or not statically known ESM, or
when names aren't being mangled
in this build.

`__webpack_exports_info__` gains `canMangle` and `mangledName` per
export, which is how a running
test can observe the mapping; with the option off it emits exactly what
it emitted before.

### Testing

- `turbo-tasks-hash`/table unit tests: encode/decode round-trip,
degenerate-name rejection, table
sizing, the single-export fixed key (including its own reservation),
reserved-word withholding
(including a reserved bucket-count test that stays in sync with the
reserved list), the
preserved-name pass running before any hashing, uniqueness under heavy
collision, order
  independence, wrap-around probing, and same-tier stability.
- 13 `turbopack-tests` execution fixtures under
`tests/execution/turbopack/exports/mangle-*`,
several ported from the original PR and from webpack's
`test/configCases/mangle`: named imports,
re-export chains and default exports (including one literally named
`__esModule`), escaping
namespaces (`Object.keys`, `delete ns.missing`, `export * as`, CJS
interop), destructuring,
prototype-shadowing names (`toString`, `$1`, `__1`), a 60-export
two-character table, dynamic
`import()` with `webpackExports` / `turbopackExports`, a CommonJS
consumer of an ESM module,
dynamic re-exports, scope hoisting on and off, and a control with the
option off.
- 3 committed snapshot fixtures under `tests/snapshot/mangle-exports`,
so the emitted keys, the
  back-off, and the fixed single-export key are visible in review.
- One fixture under `__skipped__`, which the harness asserts *fails*,
recording the
  namespace-materialization gap below.
- Full suite: 546 unit + 272 execution + 125 snapshot tests pass, with
no snapshot churn across the
  several refactors this PR went through in review.
- Verified against real builds: targeted app-dir, worker, WASM, and
`@vercel/og`-based e2e suites
pass with the option forced on (the failures that remain are
external-network tests that fail
identically with it off), and a small two-page app shrank by 0.76% of
total emitted JS / 0.53%
  gzipped.

### Known limitations, each intentional

- **A module read through `import * as ns` is never mangled**, even when
every read is statically
tracked, because the analysis doesn't yet distinguish a lowered named
read from a materialized
namespace object. Namespace imports are common, so this leaves real wins
on the table; unlocking
  it is the highest-value follow-up.
- **Escaping namespaces back off entirely.** Webpack instead keeps
mangling and materializes a
namespace object keyed by the original names. That is the `__skipped__`
fixture: implementing it
  turns the suite red until the fixture is moved out.
- **CommonJS export mangling is out of scope.** The producing side is
reachable, but the consuming
accesses live in user source and nothing rewrites them today; it needs
its own design pass.

Closes PACK-435

<!-- NEXT_JS_LLM -->


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


<!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee -->

---------

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-28 07:46:16 +02:00
next-js-bot[bot] 451985dcde v16.4.0-canary.10 v16.4.0-canary.10 2026-08-28 01:54:00 +00:00
Luke Sandberg ee3a50ccb9 Upgrade Turbopack to hashbrown 0.15 (#97808)
### What?

Upgrade Turbopack's workspace dependencies to DashMap 7.0.0-rc2 and
Hashbrown 0.15.4, removing Hashbrown 0.14 from the `turbo-tasks-backend`
dependency graph.

### Why?

`turbo-tasks-backend` previously compiled two Hashbrown versions because
its custom DashMap helpers shared Hashbrown 0.14's private raw table
types. Converging the backend graph on Hashbrown 0.15 removes that
duplicate backend codegen path and slightly reduces the shipped native
addon's size.

A clean release A/B of the unstripped x86-64 `next-napi-bindings` cdylib
measured:

| Revision | `libnext_napi_bindings.so` |
| --- | ---: |
| canary base `80576037` | 147,824,920 bytes |
| PR | 147,818,576 bytes |
| **Delta** | **−6,344 bytes (−0.0043%)** |

The final gain is small because unrelated SWC dependencies still pull
DashMap 6 / Hashbrown 0.14 into the same addon; this change removes only
backend-specific Hashbrown 0.14 codegen. Plus... nearly everything is
monomorphized anyway

### How?

DashMap 7 replaces its Hashbrown 0.14 raw table with Hashbrown 0.15's
public `HashTable` API. The backend helpers now use that public API
while retaining their existing shard locking and allocation behavior:

- heterogeneous cache lookup and insertion still reuse a pre-located
shard and precomputed hash;
- the entry helper passes Hashbrown's native `hash_table::Entry` into a
closure, preserving precise lock-release timing without a custom
self-referential entry type or cloned hasher;
- nonblocking removal still avoids lock-order deadlocks;
- two-key mutation uses a release-mode disjointness assertion followed
by `get_many_unchecked_mut` to avoid a redundant duplicate-pointer scan;
- snapshot draining still transfers ownership of each shard table and
releases task memory incrementally;
- custom mutable guards remain non-`Send`.

The migration also replaces several raw-pointer iteration and removal
paths with safe `HashTable` operations. Existing raw-entry users in
`auto-hash-map` and `turbopack-trace-server` explicitly enable Hashbrown
0.15's compatibility feature.

### Performance

Performance testing with the overhead.rs benchmark revealed no
regression/progression
<!-- NEXT_JS_LLM -->


<!-- fleet 53ff0bed-46a9-4361-befa-a1791d585f27 -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-27 22:40:19 +00:00
Aurora Scharff 49d3043e61 ci: skip framework tests for agent evals and skills (#97998)
Agent evals, skills, and the eval runner do not affect the framework
runtime, but they currently make `Determine changes` schedule the full
Rust, native, deployment, and framework test matrices.

Classify changes confined to `evals/`, `skills/`, and `run-evals.js`
with the existing lightweight documentation path. The always-on build
and lint checks still run.

Validated with Node syntax checking, Prettier, ESLint, and `git diff
--check`.
2026-08-27 22:05:19 +00:00
Will Binns-Smith 12bf495e0e Expand Turbopack dev cleanup (#97833)
## Summary

Builds on #97591. Webpack currently removes most of `.next` on devserver
startup. We can remove more as well.

This keeps the age-based removal introduced in #97591 and expands it to
`traces`, logs, etc. However, for any other directory that is not
`cache` or `lock`, completely remove it on startup. This includes types,
etc.

Apply age-based deletion to Turbopack-owned output that can be restored
from cache or recomputed, including route entries, chunks, assets,
service workers, middleware, and instrumentation. Completely remove
other dev output so logs, traces, generated types, diagnostics, and
future framework-owned output cannot be left as mixed generations.

Use exact-path exclusion sets in `recursiveDeleteSyncWithAsyncRetries`.
The complete pass retains `cache`, `lock`, and Turbopack-owned paths;
the age-based pass then sweeps those Turbopack paths. Both passes finish
before project creation.

## Verification

- `pnpm test-unit test/unit/recursive-delete.test.ts`
- `pnpm --filter=next types`

---------

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-27 14:54:23 -07:00
Andrew Clark b882af0899 Add Cache Components option to create-next-app (#97695)
When initializing a new project with create-next-app, the interactive
prompt will now ask the user if they want to enable Cache Components.

For now the option defaults to off. We'll flip the default back on once
there's a flow that guides users to the right recommendation based on
the kind of site they're building. The option can still be enabled
interactively, or with the `--cache-components` flag.

If the user chooses to enable Cache Components, the project will also be
initialized with Partial Prefetching enabled. There's no separate prompt
for Partial Prefetching; the only reason they are separate configs is
because Cache Components shipped in 16.0 before Partial Prefetching
existed.

This is not a breaking change because it does not change any behavior in
Next.js itself, only the create-next-app CLI. It has no impact on any
existing Next.js projects.
2026-08-27 17:44:31 -04:00
Maya 89ba4a32df Mark deploy release test skill as internal (#97961)
## Summary

- mark the deploy-release-test repository skill as internal
- prevent public skill discovery and telemetry publication for this
repo-only workflow

## Testing

- validated the SKILL.md frontmatter parses with metadata.internal set
to true
- git diff --check

Co-authored-by: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com>
2026-08-27 23:34:20 +02:00
Niklas Mischkulnig 1f52cb42ad Turbopack: shorten CSS module class names (#97944)
The class names were unnecessarily long.

Use the same as the lightningcss default:
`[hash]_[local]` which is
`<hash of the full file path>_<original class name or identifier>`

Keep the previous longer mechanism to aid in debugging in dev

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 21:22:53 +00:00
Josh Story 6d6228c0c6 Prune incomplete parallel route matchers (#97108)
## Summary

This adds an experimental `strictRouteMatching` flag that leaves a
matcher out when its finalized loader tree contains a synthesized
default that will always call `notFound()` for a slot actually declared
by the owning layout. Adding a `default.tsx` keeps the matcher, and
structural router-state branches that are not real slots of that layout
do not make it incomplete. The flag defaults to `false` so matcher
pruning can roll out independently from the preceding loader-tree
correction.

The main goal is to make `children` and named slots behave the same way.
If a URL cannot construct a complete route tree on its own, it should be
treated as an unmatched URL instead of sometimes showing a slot 404 and
sometimes producing a missing page or default error.

## Semantics

For example:

```text
app/split-matcher/
├── layout.tsx
├── foo/page.tsx
├── bar/page.tsx
└── @slot/[...parts]/page.tsx
```

With strict route matching disabled, Next.js emits matchers for
`/split-matcher/foo`, `/split-matcher/bar`, and the broad
`/split-matcher/[...parts]`. The broad matcher can match any URL, but
its `children` branch has neither a matching page nor a default, so it
can only construct a permanent 404 tree. With strict route matching
enabled, that broad matcher is omitted while `/foo` and `/bar` remain
because each combines a real `children` page with the named-slot
catchall. Adding `app/split-matcher/default.tsx` deliberately makes the
broad matcher complete and keeps it.

The same rule applies without a catchall:

```text
app/disagreeing-slots/
├── layout.tsx
├── @first/foo/page.tsx
└── @second/bar/page.tsx
```

`/foo` is incomplete because `@second` has no matching page or default,
and `/bar` is incomplete for the corresponding reason in `@first`, so
neither matcher is emitted. A later PR in this stack reports the
now-unreachable page files as a project misconfiguration.

A declared `children` route is treated like any named slot:

```text
app/declared-children/
├── layout.tsx
├── page.tsx
└── @panel/
    ├── default.tsx
    └── details/page.tsx
```

`/declared-children` is complete because `children` uses `page.tsx` and
`@panel` uses its default. `/declared-children/details` is incomplete
because `@panel` matches its page while the declared `children` slot has
no matching page or default at `/details`, so that matcher is pruned.

A route composed entirely from declared named slots is also complete:

```text
app/named-only/
├── layout.tsx
├── @left/[...slug]/page.tsx
└── @right/[...slug]/page.tsx
```

The preceding PR's default-on `explicitParallelRouteChildren` behavior
means this loader tree contains only `left` and `right`. Strict matching
keeps `/named-only/[...slug]` because both declared slots match; it does
not invent `children` and then prune the route for failing to satisfy
that nonexistent slot.

We got here incrementally.
[#47872](https://github.com/vercel/next.js/pull/47872) introduced the
404 fallback for unmatched parallel slots,
[#60186](https://github.com/vercel/next.js/pull/60186) added a
development warning because this was confusing in practice and linked
[#51805](https://github.com/vercel/next.js/issues/51805) and
[#49569](https://github.com/vercel/next.js/issues/49569), and
[#84702](https://github.com/vercel/next.js/pull/84702) later made a
missing default a build error for named slots while leaving `children`
on the old fallback for backwards compatibility. Strict matching takes
the next step and treats a matcher that can only construct a permanent
404 tree as unmatched.

This changes soft navigations that only worked by preserving a
previously active slot even though the URL could not be loaded directly,
which is why matcher pruning remains behind its own experimental flag.
The interception retention markers backed by `default-null`, including
named host slots from the first PR in this stack, are complete route
patches and are not pruned. This change however is well motivated
because if you did client nav to a route that only matches a named slot
and then hard refresh you will end up getting a 404. This is a sign our
current semantics are actually broken.

If you want to preserve the perma 404 behavior of the
slot-without-default you can just add a default and make it call
`notFound()` unconditionally.

## Verification

- `pnpm build-all`
- Focused `normalize-catchall-routes` unit coverage
- Production and development e2e coverage on Turbopack and Webpack
- The pruning e2e matrix with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 13:23:43 -07:00
next-js-bot[bot] 3243c970fc Upgrade React from f789f203-20260825 to 29d9d318-20260826 (#97995) 2026-08-27 20:13:20 +00:00
Aurora Scharff 24ed66abe9 docs(skills): preserve prefetched UI during Partial Prefetching adoption (#97712)
## What

- Documents how legacy `prefetch={true}` behavior changes with Partial
Prefetching and how to choose the UI worth preserving.
- Updates the adoption skill to capture a passing flag-off `instant()`
baseline, use the same assertions to drive the migration, and retain
them as regression coverage.
- Adds a reusable production test rig, keeps additional per-link
optimization separate from adoption, and surfaces `prefetch={false}`
links that may no longer be needed.
- Applies the shared test-rig fixes discovered during dogfooding to the
Cache Components optimizer.

## Why

Legacy `prefetch={true}` could include uncached dynamic content. With
Partial Prefetching, it includes the shared App Shell and cached
URL-specific content instead. A passing build or clean insight sweep
does not prove that the intended prefetched UI survived the migration,
so the skill now records that contract before adoption and verifies it
unchanged afterward. The guide presents the same test workflow as an
optional programmatic check for manual adopters.

## Testing

- Prettier, Alex, and skill frontmatter validation
- Dogfooded on v0 in https://github.com/vercel/v0/pull/28842
- Partial Prefetching adoption eval: 6/6 in
https://github.com/vercel/next.js/pull/97759
2026-08-27 19:46:19 +00:00
Josh Story 05ed7c1be2 Omit undeclared children slots from app routes (#97184)
## Summary

Parallel route layouts can be composed entirely from named slots, but
loader tree construction currently synthesizes a `children` fallback
whenever any named slot exists. This makes `children` semantically
required even when no page, default, or ordinary route branch declares
it.

This adds `experimental.explicitParallelRouteChildren` and enables it by
default. When enabled, `children` is included in a layout slot set only
when the filesystem declares an ordinary route that can render at that
level. A layout by itself is only structure and does not declare a route
target. Ordinary descendants are traced until they reach a page or
default, including through deeper named slots, before they cause
`children` to be included. Setting the flag to `false` temporarily
restores the legacy implicit `children` fallback.

This flag only controls whether `children` exists in the loader tree. It
does not prune incomplete matchers; that is the separate
`experimental.strictRouteMatching` behavior in the next PR.

Named slots keep their existing default and soft navigation semantics.
The preceding PR retains the slots owned by an interception host without
treating `children` specially, so an undeclared child is no longer
needed for that behavior. A real `children` branch still uses the
retention marker when it is one of the host layout slots.

## Semantics

For example, this layout declares only named slots:

```text
app/dashboard/layout.tsx
app/dashboard/@left/page.tsx
app/dashboard/@right/page.tsx
```

With `explicitParallelRouteChildren` disabled, Next.js adds a synthetic
`children` branch whose built-in default calls `notFound()`, even though
the layout never declared or rendered it. With the default behavior
enabled, the loader tree contains only `left` and `right`, so
`/dashboard` is matched from the route targets that actually exist.

This is different from an ordinary branch whose route targets are deeper
in the tree:

```text
app/nested/layout.tsx
app/nested/@sidebar/[...slug]/page.tsx
app/nested/content/layout.tsx
app/nested/content/@left/[...slug]/page.tsx
app/nested/content/@right/[...slug]/page.tsx
```

Here `content` really is the `children` branch of `nested`. The scan
follows `content` through its layout and deeper named slots, so
`children` remains required. `/nested/content/anything` can construct
every declared slot, while `/nested/incomplete` only matches `sidebar`
and is still incomplete. The distinction is whether the ordinary
descendant eventually reaches a page or default, not whether a layout
happens to exist along the way.

The focused children detection coverage proves that a layout-only
descendant does not synthesize `children`, while an ordinary branch
whose route targets live inside deeper named slots still does. The
limitation coverage also proves that named-only trees render pages, CSS,
metadata, and regular error boundaries. It intentionally asserts the
current broken behavior for HTTP access fallbacks and metadata or
viewport failures so those expectations can be flipped when renderer
ownership no longer depends on `children`.

## Verification

- `pnpm build-all`
- Turbopack and webpack development and production coverage for
`explicit-parallel-route-children-detection`
- Turbopack and webpack production coverage for
`interception-dynamic-segment`, `parallel-routes-layouts`, and
`explicit-parallel-route-children-legacy`
- The same existing production coverage with Cache Components enabled
- Turbopack and webpack production coverage for the documented
named-only limitations

<!-- NEXT_JS_LLM -->
2026-08-27 12:22:31 -07:00
Tobias Koppers cbe4687bf0 Turbopack: widen the chunk ident hash from 7 to 13 base38 chars (#97945)
Closes #97766.

### What?

Widen the chunk ident hash in `turbopack-core/src/ident.rs` from 7 to 13
base38 characters.

### Why?

7 base38 characters is ≈36 bits nominally, and closer to ~32 in practice
because the leading character is bounded — an effective space of roughly
6×10⁹. Any population of chunks that shares a readable name prefix and
is distinguished **only** by that hash is therefore a birthday problem:
at n names the collision probability is ≈ n²/2M, so around 10⁵ such
chunks collide with probability of order 1 and the build aborts with:

```
Two or more assets with different content were emitted to the same output path
```

This is not hypothetical at real app sizes:

- #97765 — deterministic reproduction (this PR fixes it): 120,000 chunks
sharing one prefix, fails byte-identically on every run, same collided
path and same rejected content hashes across consecutive builds.
- #96976 — production App Router app, ~69,000 emitted server chunks, ≈9%
failure probability per build measured, growing quadratically with chunk
count.

The ergonomics are the painful part: the colliding pair is chosen by the
hash, not by any structural property. The failure lands on whichever
branch last touched the module graph and looks like that branch's fault;
bisecting finds a commit but never a responsible file; and moving a
module only reshuffles which pair collides. We verified that a module
renamed *inside* a colliding chunk does not change the chunk's name at
all — same two chunks, same 7-character name, renamed file listed
inside.

### How?

```diff
             let hash = encode_base38(hasher.finish());
-            // 7 base38 chars ≈ 36 bits of collision resistance
-            let truncated_hash = &hash[..7];
-            write!(name, "_{truncated_hash}")?;
+            // Use the complete fixed-width base38 encoding of the 64-bit hash.
+            //
+            // This suffix is what distinguishes idents that share a readable
+            // name prefix, so truncating it makes distinct output paths collide
+            // for large module graphs, failing the build with "Two or more
+            // assets with different content were emitted to the same output
+            // path". Keeping all 64 bits removes that failure mode, at the cost
+            // of a slightly longer file name.
+            write!(name, "_{hash}")?;
```

The complete 13-character base38 encoding preserves all 64 bits of the
identity hash. Cost: 6 extra characters per emitted chunk filename.

### Notes for reviewers

- Output filenames change wholesale on upgrade, as with any naming
change; nothing reads these names across versions.
- Snapshot tests that pin emitted chunk names have been regenerated.
- If you would rather solve this by hashing more distinguishing input
rather than widening the truncation, I am glad to rework it; the length
is the smallest change that removes the failure mode.

<!-- NEXT_JS_LLM -->


<!-- fleet daf6a1b0-ebda-4eed-a89f-03e5ec7dc357 -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: jonnyuniverse <jon@fractionalventure.partners>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-08-27 18:33:44 +00:00
David Gray c5b960e2e5 Fix request-context retention in the default use cache handler (#97941)
Fixes #97934.

The default use cache handler uses live ReadableStream objects. Those
streams retain references to the request’s async context and closed HTTP
response, causing memory to grow with cached entries and potentially
produce OOMs.

The fix converts each stream into a plain Buffer before caching it.
Every cache hit then gets a fresh temporary stream created from those
bytes. The content remains cached, but completed requests can now be
garbage-collected.

---

Basically - the LRU cache stores live ReadableStream objects, which keep
completed request contexts and closed responses in memory until that
cache is evicted.
2026-08-27 20:21:22 +02:00
Rich Haines e80a85bcff docs: improve discovery summaries (#97982)
This PR improves the meta descriptions on certain pages.

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

We encourage you to use AI to assist you in researching, creating, and
reviewing changes. However, you must review and deeply understand the
contributions you are making. For this reason, **pull request
descriptions from external contributors must be written by a human**.

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Signed commits

- This repository requires verified commit signatures on protected
branches.
- If this pull request is blocked for unsigned commits, re-sign the
commits and force-push the branch.
- A `Signed-off-by` line in the commit message is not enough.

## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
2026-08-27 19:47:39 +02:00
Steven 04e96a5bfa test: add test for local font with deployment id (#97987)
Follow up to a previous PR:

- https://github.com/vercel/next.js/pull/82488
- https://github.com/vercel/next.js/pull/82384

Closes:

- https://github.com/vercel/next.js/pull/82547
2026-08-27 19:10:55 +02:00
Josh Story cce7bf2fc0 Retain interception route host slots (#97242)
## Summary

Interception routes represent a partial update to the layout that hosts
them. Today we model this for `children` by synthesizing a `__DEFAULT__`
route backed by `default-null`, but named siblings still use normal
default matching even though they should retain their active state too.

This makes retention relative to the interception host instead of the
`children` key. Every non-intercepting sibling gets the existing
`__DEFAULT__` marker backed by `default-null`, while slots inside the
newly selected interception subtree continue to use normal matching and
real defaults.

The coverage uses a named-only host to verify that named siblings retain
client state without evaluating a user default, that a sibling without a
default also retains, and that a hard refresh still loads the canonical
route.

## Example

Consider this route tree:

```text
app/
├── named-host/
│   ├── layout.tsx                  # renders canonical, content, secondary, and modal
│   ├── @canonical/
│   │   └── page.tsx
│   ├── @content/
│   │   ├── page.tsx                # renders a stateful counter
│   │   └── default.tsx             # throws if evaluated
│   ├── @secondary/
│   │   └── page.tsx                # has no default.tsx
│   └── @modal/
│       ├── default.tsx
│       └── (..)named-target/
│           └── page.tsx            # intercepted target
└── named-target/
    └── page.tsx                    # canonical route
```

Before this change, a soft navigation from `/named-host` to
`/named-target` could replace `@content` with its throwing default and
treat `@secondary` as missing because only `children` had retain
semantics. After this change, only `@modal` switches to the intercepted
page; `@content` and `@secondary` keep their existing UI and the counter
keeps its state. A hard refresh still renders the canonical
`/named-target` route instead of the interception host.

## Verification

- Turbopack and webpack production coverage in
`interception-dynamic-segment`
- The same production coverage with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 09:27:33 -07:00
Janka Uryga 0a2bd1d531 test: disable flaky sync-io-blocks-root.test.ts (#97986)
Something made this test very flaky on Aug 24/Aug 25


https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.status%3A%22fail%22%20%40test.suite%3A%22sync%20IO%20that%20blocks%20the%20root%22%20-%40test.type%3Aturbopack&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1787237730805&end=1787842530805&paused=true

Disabling until we can investigate.
2026-08-27 15:27:42 +00:00
Tobias Koppers d42ea11842 [test] Drain build output before start (#97947)
### What?

Make the production test harness wait for a build process's piped stdio
to close before `NextStartInstance.start()` continues to launch the
server or reports a build failure.

### Why?

A child process's `exit` event can fire before trailing stdout and
stderr have been delivered to the parent. This can make start-mode tests
intermittently miss diagnostics emitted near the end of a production
build, even though the build itself reported them correctly.

The standalone `build()` helper already uses the later `close` lifecycle
event to avoid this race. Applying the same rule to the build phase
inside `start()` removes the equivalent latent output-drain hazard and
makes child-process handling consistent across the harness.

### How?

Resolve the build phase on the child process's `close` event. Unlike
`exit`, `close` is emitted after piped stdio closes, so all trailing
build output has reached the harness before it starts the server or
rejects the build. Exit-code and signal handling remain unchanged.

### Verification

- `pnpm types`
- `pnpm test-start-webpack
test/production/production-build-dir/production-build-dir.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/sync-io-blocks-root/sync-io-blocks-root.test.ts`
- Repeated webpack stress campaign for the sync-IO suite: 5/5 runs,
including 20/20 `--debug-prerender` route cases
- Prettier and ESLint on `test/lib/next-modes/next-start.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet 821362ed-9f96-4e53-b56e-49bf00e878b9 -->

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-27 16:18:02 +02:00
Niklas Mischkulnig 3d85af96be Expose durableUseCacheEntries config in workStore (#97926)
https://github.com/vercel/next.js/pull/95233 needs this to decide
whether to perform the manifest lookup.
2026-08-27 14:42:02 +02:00
Tim Neutkens 89d017eac4 Pages Router: Deprecate React 18 support (#97689)
## Summary

- warn during `next dev` and `next build` when React 18 is installed,
while keeping `next start` quiet
- explain that React 18 remains supported in Next.js 16 but will be
removed in Next.js 17
- update the React version guidance with React 19 and TypeScript type
upgrade commands

## Verification

- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- `NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm
test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- targeted production build/start coverage passed with React 18 and
React 19
- Not passing: `pnpm --filter=next types` (existing unrelated errors in
`httpget.ts`, `image-optimizer.ts`, `proxy-request.ts`, telemetry
typings, and compiled declarations)
- Not passing: `pnpm lint` (existing unused Turbo Tasks check)

<!-- NEXT_JS_LLM -->
2026-08-27 13:00:17 +02:00
Niklas Mischkulnig 286fcc346f Turbopack: call loadActionManifest for app-route (#97921)
For #95233, I need to read server-reference-manifest.json even for App
Routes, which previously didn't emit server action manifest entries at
all (because thus far, it was unnecessary).
2026-08-27 10:14:32 +02:00