Skip to content

[pull] canary from vercel:canary - #1345

Merged
pull[bot] merged 8 commits into
code:canaryfrom
vercel:canary
Aug 27, 2026
Merged

[pull] canary from vercel:canary#1345
pull[bot] merged 8 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Aug 27, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

sokra and others added 8 commits August 27, 2026 16:18
### 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>
## 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 -->
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 #

-->
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.
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>
## 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 -->
@pull pull Bot locked and limited conversation to collaborators Aug 27, 2026
@pull pull Bot added the ⤵️ pull label Aug 27, 2026
@pull
pull Bot merged commit 05ed7c1 into code:canary Aug 27, 2026
0 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants