Closes https://github.com/vercel/next.js/issues/92082
This is now a proper compile-time constant:
```js
import { IS_DEV } from './other'
if (IS_DEV) { // statically evaluates to `true`
console.log('x')
} else {
require("library") // not bundled
}
console.log(IS_DEV); // is replaced with console.log(true);
```
```typescript
// other.ts
export const SOME_VALUE = 'x'
const node_env = process.env.NODE_ENV
const development_ent = 'development'
export const IS_DEV = node_env === development_ent
```
You can use code to compute constants just fine, and use any existing
constants such as `process.env.NODE_ENV`.
Currently, you can't use imports to other constants modules, but we can
add that later on.
We can't perform this constants check for every single import, so you
need to either
- have `UPPER_CASE` import names as in the example above
- or use `import { lower } from './other' with { turbopackConstants:
'true' }`
Then that referenced module will be analyzed for constants, and if the
referenced export is a constant, it will participate in constant
inlining just as `process.env.FOO`.
This also works fine with barrel imports, you can still do `import {
IS_DEV } from './barrel.js'` and it will find the `constants.js` file
which in itself will indeed only have constants exports.
Because it's not always opt-in at the import site, we can't
automatically make non-constant exports an error. For that you have to
add `'use turbopack: constants'` at the top of the module, which will
make it an error if any constant import references that module, and the
module has any non-constant exports:
```
error - [analysis] /turbopack/crates/turbopack-tests/tests/snapshot/comptime/cross-module/input/other.constants.js:8:7
Export NO_CONSTANT is not a constant
4 | const development_ent = 'development'
5 |
6 | export const IS_DEV = node_env === development_ent
7 |
+ v--------------------------------v
8 + export const NO_CONSTANT = globalThis.foo
+ ^--------------------------------^
9 |
It was analyzed to be FreeVar(globalThis)["foo"]
Import trace:
test:
./turbopack/crates/turbopack-tests/tests/snapshot/comptime/cross-module/input/other.constants.js
./turbopack/crates/turbopack-tests/tests/snapshot/comptime/cross-module/input/index.js
```
Some prior art:
https://rspack.rs/blog/announcing-1-5#const-inline-optimization,
https://rspack.rs/config/optimization#optimizationinlineexports
No compile-time impact on a big app:
```
canary dfbc3dc6b7:
438.11s user, 68.35s system, 801% cpu, 1:03.22 total
439.44s user, 71.47s system, 756% cpu, 1:07.56 total
440.32s user, 68.57s system, 750% cpu, 1:07.81 total
constants 2d7eb8220d298133212813b7267a5847eed03a22
433.22s user, 65.44s system, 800% cpu, 1:02.32 total
440.52s user, 67.68s system, 770% cpu, 1:05.95 total
433.13s user, 69.47s system, 776% cpu, 1:04.74 total
```
a8620556af64ef550373cca1e1fc701a1a8583cc
LCP can be affected if the LCP element is sitting within a Suspense
boundary, even if its nothing but static content, if the static content
is "too large".
Apps should prefer, as long as possible, to not have a Suspense boundary
around the LCP element.
-
https://react.dev/reference/react/Suspense#what-activates-a-suspense-boundary
> Waiting for a large boundary’s HTML to arrive during streaming server
rendering. Sending HTML takes time, so a boundary with enough content
activates even when nothing in it suspends. React reveals the content as
the HTML arrives.
## Summary
After a fallback-shell cache prerender completes, snapshot whether its
timeout fired and then abort the existing timeout controller when it
participates in an `AbortSignal.any()` composite. This triggers the
composite so React removes its abort listener; no additional controller
or signal is needed. Cache prerenders without a dynamic-access source
keep their existing direct timeout signal.
Node retains non-empty composite abort signals while they have abort
listeners. React attaches such a listener during `prerender()` and
removes it when the signal aborts, so aborting the already-owned timeout
source releases the successful render. Snapshotting `didTimeout` first
keeps cleanup aborts distinct from real timeouts.
This preserves the early aborted-prerender guard from #96426, which
prevents a cache fill that starts after its outer prerender aborts from
caching an empty React stream.
Fixes#97363
Related #97464
Alternative to #97391
## Verification
- `pnpm --filter=next build`
- `pnpm test-start-turbo
test/e2e/app-dir/use-cache-after-uncached-io/use-cache-after-uncached-io.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts`
- Actual vendored React `prerender()` GC probe on Node 20.19.5 and
22.20.0: valid preludes, no cleanup errors, and 0/100 composite signals
retained while their source controllers remained reachable.
<!-- NEXT_JS_LLM -->
Previously, we were debouncing update by sleeping 1ms at a time on macos
and windows, and 10ms at a time on Linux.
During a slow `pnpm install`, or a `git checkout`, this could cause us
to do a bunch of extra throwaway work.
Changes:
- Increase the debounce interval to a consistent 10ms everywhere. This
should still be small enough that it's not noticable on macos or
windows.
- If an event touches `node_modules`, there's a good chance that a
package manager is running and many other files will be modified, so
extend the batch deadline by 200ms instead of 10ms.
- Because there's a chance that the batch deadline could get extended
indefinitely (this was always possible, just more likely now) include a
compilation event that gets logged after 5 seconds.
### What?
Documents the `pagination` metadata field in the `generateMetadata` API
reference.
### Why?
The `Metadata` type supports `pagination.previous` and
`pagination.next`, but the Metadata Fields documentation did not list
the field.
Fixes#83264
### How?
Adds a `pagination` section with an example and the generated `<link
rel="prev">` / `<link rel="next">` output.
### Tests
- `pnpm prettier --check
docs/01-app/03-api-reference/04-functions/generate-metadata.mdx`
- `pnpm lint-eslint
docs/01-app/03-api-reference/04-functions/generate-metadata.mdx`
- `git diff --check HEAD~1..HEAD`
<!-- NEXT_JS_LLM_PR -->
## Summary
While reviewing the examples in the Next.js repository for potential
improvements, I noticed a redundant `justify-content` declaration in the
`.submit` styles.
## Changes
- Removed the redundant `justify-content: flex-end` declaration.
- Kept `justify-content: space-between`, which overrides the previous
declaration.
## Testing
No functional changes. This is a CSS cleanup only.
Co-authored-by: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
## What
Vercel's **Edge Config** product is now **Global Config**. This updates
the two stale-name occurrences flagged in the redirecting guide.
The URL was already migrated in #96723 (`/docs/edge-config/get-started`
→ `/docs/global-config/get-started`), but that pass only rewrote link
targets — the visible product name and the SDK import in the code
samples still said `Edge Config` / `@vercel/edge-config`, so the page
read *"Vercel's Edge Config"* while linking to the Global Config docs.
## Changes
`docs/01-app/02-guides/redirecting.mdx`, in *Managing redirects at scale
→ Creating and storing a redirect map*:
| | Before | After |
| --- | --- | --- |
| Prose | Vercel's [Edge Config] | Vercel's [Global Config] |
| `proxy.ts` / `proxy.js` samples | `import { get } from
'@vercel/edge-config'` | `import { get } from '@vercel/global-config'` |
The link target, the neighbouring [Redis](https://vercel.com/docs/redis)
link, and the surrounding sentence structure are unchanged.
## Why the package import changed too
The renamed package is published and is the same library —
`@vercel/global-config@1.5.1`, from `vercel/storage` at
`packages/global-config`, matching `@vercel/edge-config@1.5.1` — and it
exports the same `get`. Vercel's own [Global Config
quickstart](https://vercel.com/docs/global-config/get-started) now uses
`import { get } from '@vercel/global-config'`. Leaving the old specifier
would have left the sample importing a legacy package name on a page
that calls the product Global Config. `@vercel/edge-config` is not yet
formally deprecated on npm, so this is a naming-consistency change
rather than a fix for broken code.
## Both flagged pages
Two docs paths were flagged — `/docs/app/guides/redirecting` and
`/docs/pages/guides/redirecting` — but they share one source.
`docs/02-pages/02-guides/redirecting.mdx` is a generated stub (`source:
app/guides/redirecting`, carrying the *DO NOT EDIT* banner), and the
affected paragraph is not wrapped in `<AppOnly>`/`<PagesOnly>`, so it
renders on both. Editing the App Router source fixes both pages; no
separate edit to the Pages file is needed or allowed.
## Verification
- `grep -rn "Edge Config\|edge-config" docs/` → no matches remain.
- `prettier --check` and `alex` pass on both files; the pre-commit
`lint-staged` (prettier + eslint) pass ran clean.
- `https://vercel.com/docs/global-config/get-started` → `200`; the old
`/docs/edge-config/get-started` → `308`, confirming the link already
points at the final target.
Docs-only change; no code or tests affected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich Haines <22930449+molebox@users.noreply.github.com>
## Summary
This PR improves the accessibility of the `with-apivideo-upload` example
by associating form labels with their corresponding form controls using
the `htmlFor` attribute.
## Changes
- Added `htmlFor="link"` to the "Play button color" label.
- Added `htmlFor="linkHover"` to the "Buttons hover color" label.
- Added an `id` to the "Hide controls" checkbox and associated its label
using `htmlFor`.
## Why
Associating labels with their corresponding form controls improves
accessibility by:
- Allowing screen readers to correctly announce form labels.
- Enabling users to focus or toggle controls by clicking their labels.
- Following HTML and WCAG best practices for accessible forms.
## Before
- Labels were visually displayed but were not programmatically
associated with their respective inputs.
## After
- Each label is associated with its corresponding form control via
`htmlFor` and `id`, improving accessibility without changing
functionality.
### Testing
- Verified that clicking each label focuses or toggles the associated
control.
- No visual changes.
Co-authored-by: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
### What?
Fix grammar issues in the "Linking and Navigating" guide.
### Why?
This sentence had a few grammar issues:
- "makes" should be "make"
- "a server-rendered apps" should be "server-rendered apps"
- "it enables" should be "they enable"
### How?
Updated the sentence in
`docs/01-app/01-getting-started/04-linking-and-navigating.mdx` to
correct grammar and improve subject-verb agreement.
---------
Co-authored-by: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
## Summary
While reviewing the examples in the Next.js repository for potential
improvements, I noticed a typo in the error message displayed when an
unexpected error occurs.
## Changes
- Corrected `"An unexpected error happened occurred:"` to `"An
unexpected error occurred:"`.
## Testing
No functional changes. This is a text-only fix.
Co-authored-by: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
## Summary
This is the behavior-preserving base of a four-PR stack that introduces
explicit prerender matching policy without coupling the foundational
refactor to the proposed API.
The build currently uses `PrerenderedRoute` values for two related but
distinct concepts:
- a logical request matcher that says which parameterized URL shapes the
route can handle
- a build-time render candidate that may or may not become a persisted
prerender artifact
A candidate is not guaranteed to be an output. It can be rendered only
to validate a shell, discarded, and still leave behind a matcher that
tells future requests to block.
The new relationship is:
```text
logical pathname matcher
-> zero or more render candidates
-> zero or more persisted artifacts
```
## Concrete example
Consider `/[top]/items/[bottom]`:
```ts
export function generateStaticParams() {
return [{ top: 't1', bottom: 'b1' }]
}
```
Static-path generation may consider three shapes:
| Shape | Purpose |
| --- | --- |
| `/[top]/items/[bottom]` | A generic shell candidate and logical
matcher |
| `/t1/items/[bottom]` | A shell after resolving `top` |
| `/t1/items/b1` | A concrete build-time prerender |
Suppose the generic candidate renders an allowed empty shell. The build
should discard that candidate artifact and use blocking behavior for the
generic matcher. It should not remove `/[top]/items/[bottom]` from the
valid matcher set, and it should not discard the concrete `/t1/items/b1`
artifact.
This is why the build needs two sets:
- route matchers, which describe valid request shapes
- prerender candidates, whose render results determine whether an
artifact is retained and can refine inferred fallback behavior
## Variants compatibility
Variants will make pathname-only candidate maps insufficient. Several
variant combinations can share `/t1/items/b1` as their logical pathname
while writing distinct artifacts under variant-specific output paths.
This PR keeps the route matcher keyed by logical pathname but retains
every candidate associated with it. Candidate finalization can therefore
evaluate each variant artifact independently without changing the route
tree's matcher set.
## Behavior preservation
This PR does not add a user-facing API or change `generateStaticParams`
semantics:
- a usable static shell remains a fallback prerender
- an allowed empty shell is discarded and represented by a blocking
matcher
- a route that requires a non-empty shell still fails validation
- unresolved matchers remain gated by route-level PPR support
- the most-specific shell continues to supply first-writer-wins metadata
such as prefetch hints
Render results can decide whether a candidate artifact is published and
can refine inferred matcher behavior, but they do not remove the logical
route matcher itself.
## Stack plan
1. **#97431 — model prerenders as render candidates:** land the
behavior-preserving matcher/candidate separation and post-render
finalization first.
2. **#97393 — add the experimental matcher API:** add `unstable_matcher`
and `unstable_generateMatcher`, policy aggregation, validation, and
local diagnostics.
3. **#97426 — test complex route shapes:** add test-only coverage for
catch-alls, optional catch-alls, root parameters, and parallel slots.
4. **#97427 — test foreground policy behavior:** add test-only coverage
showing blocking misses generate before responding while fallback misses
return the shared shell immediately.
This layering lets the internal model be reviewed and landed
independently of the API design. The upper test PRs validate the final
behavior without increasing the implementation diff.
## Verification
- `pnpm --filter=next types`
- `pnpm test-start-turbo
test/e2e/app-dir/sub-shell-generation/sub-shell-generation.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/prefetch-inlining/prefetch-inlining.test.ts`
<!-- NEXT_JS_LLM -->
## Summary
Document that `permanentRedirect()` throws and should be called outside
a broad `try/catch` block.
The `redirect()` and `notFound()` references already explain this
control-flow requirement. The `permanentRedirect()` reference omits it,
so application error handling can accidentally suppress the redirect.
Align the page with the `redirect()` reference by adding a dedicated
Behavior section for the try/catch guidance and temporary-redirect
cross-link.
## Verification
- Prettier, ESLint, and `git diff --check`
Documents are now served with `no-store` in development, so a browser
never restores one from its HTTP cache and the page scripts never
re-execute against a debug channel that has already delivered its data.
The persistence and restore machinery that existed for that case has no
remaining trigger, so this removes it: the `IndexedDB` write scheduled
on every page load, the cache-restore detection across
`PerformanceNavigationTiming` fields and `deliveryType`, the `pageshow`
deferral for browsers that populate those fields late, and the
`location.reload()` fallback for a missing entry. It was built up over
#92892, #93486, #94128, #94317 and #94243, and takes `debug-channel.ts`
from 535 lines to 121.
The per-consumer `tee()` and the LRU-bounded pair map stay. They were
added for an unrelated reason, namely that one response can be decoded
more than once, so this is not a revert to the state before the
persistence landed. The rejection handler on `writer.closed` also stays,
because an errored stream would otherwise surface as an unhandled
rejection now that nothing else observes it.
`bfcache-regression` keeps the original regression test, which loads a
page, navigates away, comes back and asserts that the counter is still
interactive. That case now fails if the development `Cache-Control`
value ever goes back to `no-cache`, because the restored document would
block hydration with no reload to recover, so it is worth keeping as is.
The other three tests lose their premise and are deleted along with the
routes only they used: the pruning case that was skipped when the header
changed, the recovery case that needs a restore path to recover into,
and the streaming case that guarded the detection against treating an
in-flight response as a restore. The `large-debug-data` route goes too.
It existed only to make the persistence write expensive enough to
profile by hand when it moved to `IndexedDB`.
Development responses used `no-store, must-revalidate` until #88182
tried `no-cache, must-revalidate` behind
`experimental.devCacheControlNoCache`, and #91503 removed that option
and hard-coded the `no-cache` value everywhere. That was right for
static assets and wrong for documents. A browser may reuse a stored
response for a history navigation without revalidating it, and
development documents are streamed without an `ETag`, so there is
nothing to revalidate against. Going back therefore restored the
document the browser had stored earlier and showed output from before
the latest edit, and it is also what forced the debug channel
persistence workarounds in #92892, #93486 and #94243.
Documents and RSC or data responses now use `no-store` again, set in
`app-page-runtime.ts` for app pages, in `pages-handler.ts` for pages,
and in the legacy render pipe in `base-server.ts` so that the three do
not drift apart. None of them ever serves a static asset, so assets keep
`no-cache, must-revalidate` from the `nextStaticFolder` branch in
`router-server.ts` and stay cacheable: they are revalidated against the
`ETag` that `serveStatic` adds and reused from a `304` instead of being
downloaded again on every page load. `must-revalidate` is left off the
document value, because it only governs reuse of an already stale stored
response and nothing is stored any more.
A back navigation is no longer instant, since the document is fetched
again instead of being restored locally.
`test/development/dev-cache-control` covers both sides of that
trade-off: an edit that is visible after a back navigation, and
unchanged assets that still come back as `304`. It replaces
`dev-cache-control-no-cache` and asserts the header for both routers as
well, so there is one suite instead of two with nearly the same name.
A development document is now never restored from the HTTP cache, so the
debug channel persistence has no remaining trigger and its `IndexedDB`
write on every page load is no longer needed. Removing it is a follow-up
on top of this change. The pruning and recovery test in
`bfcache-regression` is the one case whose premise disappears entirely,
and it is skipped here with a note to delete it along with the
persistence.
closes#96503
Closes https://github.com/vercel/next.js/pull/96999
Make sure we don't do `.read().hash()` which is incorrect with symlinks.
Instead, hash the symlink itself instead of its target. This is what we
copy into the function source anyway
---------
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
The forms guides recommend validating with a library like Zod. This
broadens the wording to also mention Valibot, which covers the same use
case with a smaller bundle footprint. The authentication guide already
lists Zod or Yup, so Valibot is added to that list as well. The existing
Zod examples are unchanged. I'm the author of Valibot.
- Ran `prettier --check` on the touched files with the repo's pinned
version.
### What?
Adds a public OpenTelemetry span for the cold evaluation of an App
Route's
lazy userland module:
- span type: `AppRouteRouteModule.loadUserland`
- display name: `load app route module`
- route-specific attribute: the normalized `next.route` pattern
### Why?
`LoadComponents.loadRouteModule` measures loading Next.js's compiled
route
entry. App Routes then defer the actual userland module behind a
`LazyModule`, so expensive top-level module evaluation can occur after
that
existing span has ended and remain invisible in traces.
### How?
The App Route module wraps only its lazy userland factory with the
Next.js
tracer. `LazyModule` still memoizes that factory, so each route-module
instance
emits one span for its successful cold initialization and warm requests
reuse
the initialized value. The HMR getter remains untraced, and the span
records no
resolved filename or request URL.
The integration fixture uses top-level await to exercise the
asynchronous
module path. Coverage verifies the span is a descendant of the request,
appears exactly once across cold and warm requests, and works with the
direct
entrypoint handler.
### Verification
- `pnpm exec jest packages/next/src/server/lib/trace/tracer.test.ts
--runInBand`
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts -t 'should
trace cold App Route module loading'`
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts -t 'should
trace cold App Route module loading'`
- `HEADLESS=true pnpm test-start-turbo
test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts -t 'should
trace cold App Route module loading'`
- `HEADLESS=true pnpm test-start-webpack
test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts -t 'should
trace cold App Route module loading'`
- Exact-head GitHub build-and-test run `31950299347`: 102/102 jobs
passed.
- Exact-head Generate Stats run `31950299340`: passed.
In #80281 we moved the README from
`../../packages/next/src/client/components/react-dev-overlay/README.md`
to `packages/next/src/next-devtools/README.md`, but did not update the
link in the contributing guide.
- Adding
docs/01-app/03-api-reference/05-config/01-next-config-js/cacheMaxMemorySize.mdx
- Various use cache snippet fixes
- Break nuance for fetch default
- Point to correct turbopack cache flags
### What?
Change the pointer-width assertion in `usize_from_u32` from `<` to `<=`,
allowing the conversion to compile on 32-bit targets.
### Why?
The surrounding comment states that Turbopack assumes a pointer width of
at least 32 bits, but the strict comparison rejects exactly 32-bit
targets. Since `u32` and `usize` have the same width on these targets,
the conversion remains lossless.
This currently causes `rspack-turbo-persistence` to fail to compile with
`error[E0080]` in Rspack's [`i686-pc-windows-msvc` CI
job](https://github.com/web-infra-dev/rspack/actions/runs/31665978273/job/94340544786#step:25:446).
### How?
Allow equal source and target bit widths in the compile-time assertion.
### What?
Restores the missing `node_modules` fixtures for the `exports`,
`exports-fallback`,
`exports-only` and `exports-path` cases in `turbopack-tracing`'s NFT
unit test suite
(`tests/unit.rs`), and enables the two that pass as-is (`exports-only`,
`exports-path`).
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
## Summary
- Use the job-scoped `GITHUB_TOKEN` with explicit `actions: write`
permission to dispatch `trigger_release.yml` after a stable backport.
- Avoid requesting Actions write access from the release GitHub App,
which currently prevents the dispatch token from being created.
- Preserve the existing forced preminor canary release behavior.
This fixes the failure that left canary semver-behind stable after the
`v16.3.1` backport. The evaluation succeeded, but the dispatch failed
while creating the App token in the [failing dispatch
job](https://github.com/vercel/next.js/actions/runs/31750536217/job/94615208366).
## Verification
https://github.com/vercel/next.js/actions/runs/31830288265/job/94864091005
```
should_dispatch=true
reason=Dispatching canary preminor release because stable release 16.3.1 is ahead of 16.3.1-canary.17
released_version=16.3.1
current_canary_version=16.3.1-canary.17
dispatch_input=false
auto_dispatch_enabled=true
```
Change the semantics of `task` so that it enforces that tasks actually
exist (either in memory or storage). For the rare cases we expect to be
creating tasks, have callers call `get_or_create_task`
generally creating a task is _racy_ so it is always `get_or_create` but
when doing things like updating the aggregation graph or reading cells,
we _know_ it must exist so assert it.
Currently this is a `debug_assert` but with the GC we may update it to a
full assert.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What
The persistence-layer mechanism to tombstone (delete) tasks from the
on-disk cache. A GC-collected task is represented as a
`SnapshotItem::Delete` that rides the same streaming iterator
`save_snapshot` already consumes for puts, so tombstones are applied in
the same commit/batch as the snapshot with no side-channel.
- `SnapshotItem` becomes an enum: `Put { … }` (the existing path) and
`Delete(TaskDeletion)`.
- `TurboWriteBatch::delete/value_delete` passthrough to
`turbo_persistence`.
- `save_snapshot` applies deletes inline in the parallel put phase
## Note
`SnapshotItem::Delete` is `#[allow(dead_code)]` here: this PR lands the
persistence-side *handling* and its tests, but nothing *emits* a
`Delete` yet. The GC pass that soft-deletes tasks and serializes them to
`Delete` lands in the next PR in the stack. So this PR is a no-op at
runtime.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a new `tombstone` format to the persistence layer so we can delete
key-value pairs out of MultiValued tables
This is in service of the upcoming GC support, but also fills a basic
API gap in the db.
To delete a key-value-pair you need to call `value_delete` and currently
the values are limited to only those that are able to be stored inline
in key blocks. This is a non-trivial limitation but it fits our current
usecase, and makes the compaction/query algorithms a bit simpler (don't
need to 'resolve' values)
One non-trivial complexity was maintaining the 'FixedLayout' block
optimization for a mix of tombstones and values, so now we support a
slightly different mode where all keys have the same length but possibly
different types.
Finally, this branch solves a problem with deleting tombstones.
Tombstones 'shadow' older values and allow us to drop them during
compaction. With GC getting ready to start writing tombstones the risk
becomes 'when can we delete a tombstone! This is solved
probabilistically using the amqf filters, during compaction we drop
tombstones if they could not possibly match anything in an older SST.
Without this, tombstones in the TaskCache table would fill up over time.
## Summary
Keep the first warm reload assertion focused on browser-visible
stale-while-revalidate behavior, then poll the route with independent
HTTP requests until a later response observes the fresh value. After
convergence, perform one final browser reload to confirm the
user-visible path also receives an updated value.
This avoids fixed delays, cache debug logging, and repeated browser
navigations that can cancel or outpace background regeneration. The
convergence poll uses the standard 3-second `retry()` window and returns
as soon as freshness is observed; the browser only reloads again after
that condition is satisfied.
## Verification
- `__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_CI=true
HEADLESS=true pnpm test-dev-webpack
test/development/app-dir/use-cache-size-zero/use-cache-size-zero.test.ts`
- `__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_CI=true
HEADLESS=true pnpm test-dev-turbo
test/development/app-dir/use-cache-size-zero/use-cache-size-zero.test.ts`
<!-- NEXT_JS_LLM -->
Sets up the scaffolding for a rewrite of the client router's queue.
I've named the new module concurrent-router-queue.ts and the existing
module sequential-router-queue.ts, since the main goal of the new one is
to improve the router's integration with React's concurrent rendering
model. The experimental flag is likewise named `concurrentRouterQueue`.
There's no implementation yet; all router operations throw an error when
the flag is enabled.
The main entry point into the router is navigator.ts. The flag swaps
between implementations at the module level to ensure the inactive code
is eliminated from the final bundle.
Pure structural refactor, no behavior changes.
This continues some of the clean-up work I've been doing in the client
router now that more of the implementation has settled, post-Segment
Cache and Instant Navigations.
I have two primary motivations here. The first is purely
maintainability. The current module structure is a mess, and the naming
is misleading when browsing the codebase for the first time.
The second motivation is to prepare for an upcoming rewrite of the
router queue. There will likely be two simultaneous implementations for
a while as the new one settles, so it's extra important the interface
into the router queue is well-defined. I want the fork between the two
worlds to be as clean as possible. This is a similar approach to the one
I took when rewriting the prefetching implementation.
Some of the key name changes:
- ppr-navigations.ts → render-tree.ts. This module's responsibility is
to create the tree that gets passed into React for rendering, currently
called the CacheNode tree. I considered renaming it to cache-node.ts,
but I kept it more generic because I have plans to refactor that type
soon.
- segment-cache/navigation.ts → app-router-state.ts. This module's
responsibility is to compute the next AppRouterState given inputs. It
does perform network requests, but it's conceptually a "pure" operation.
It doesn't deal with any of the router queueing logic. This is the most
reducer-y of the code that still exists, and along with render-tree.ts,
it will remain mostly shared between the old and new state machine
implementations.
- navigator.ts. This is a new module I created to act as the single
entry point for router-level operations: push, replace, navigate,
traverse, restore, refresh, hmrRefresh. This is the primary place where
we'll fork the old and new queue implementations. It's also how we'll
ensure the inactive implementation gets dead-code-eliminated properly
while the two implementations live side-by-side.
The `back-before-hydration` assertions read the heading with
`browser.elementByCss`, which waits for the load event before it
returns. Here the load event cannot fire until the stalled scripts are
released and finish running, and that is the whole window the test is
about. So the first attempt inside `retry` blocks for almost the entire
3s budget, looks at the DOM once, and then `retry` refuses a second look
because `waited + interval > duration`.
This waits in the browser instead, with the load event out of the
picture. Every page already has its own heading id, except the two
search pages, which shared one; they get a per-page id too. Then a
single `waitForSelector` resolves the moment that page commits, and the
URL assertion follows the wait rather than racing alongside it. The
assertions that check content *stayed* put keep polling with `retry`,
since a change there is the failure.
Holding the flight response for 1800ms and throttling the renderer 8x
reproduces the CI failure exactly on canary — same `Failed to retry
within 3000ms`, same expected "Home" / received "Post" — and passes with
this change; the suite is still 16/16 with the response held for 6s.
Test-only.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
The existing metadata resolver combines two responsibilities: walking
the loader tree to collect and schedule route exports, and interpreting
the metadata or viewport value produced by an individual route layer.
Move the route-layer operations into metadata-resolution-primitives.ts.
This includes wrapping generateMetadata and generateViewport, loading
file-based metadata, merging values with their resolved parents,
post-processing metadata, and producing SelectedMetadata for rendering.
Keep resolve-metadata.ts responsible for the existing traversal and
scheduling flow, importing the extracted operations back into the same
call sites and preserving its existing exports. This creates a reusable
boundary for other traversal strategies without introducing another
resolution path.
This is intended to be behavior preserving: traversal order, eager
generator invocation, parent promise behavior, warnings, and rendered
metadata and viewport tags are unchanged.
## Verification
- pnpm build-all
<!-- NEXT_JS_LLM -->
Closes https://github.com/vercel/next.js/issues/97358
`ResolveResult::with_replaced_request_key` overwrote the `conditions` of
every result key with the conditions of the replacement key (empty at
all call sites). Those conditions are what distinguishes the results one
request resolved to under different export conditions, so both the
`module-sync` and the `default`/`require` target of the same subpath
ended up under the same `RequestKey`.
That is lossy as soon as a request resolves in more than one
`node_modules` directory: the results are merged through a
`RequestKey`-keyed map, so one of the two targets is silently dropped.
With a single candidate directory `ResolveResult::alternatives`
short-circuits and never builds that map, which is why this only showed
up in nested layouts.
In practice this broke `output: 'standalone'` with the Turbopack
production bundler for pnpm installs, whose virtual store hoists every
package into `node_modules/.pnpm/node_modules` and thereby creates that
second candidate directory: `next-server.js.nft.json` recorded only
`@swc/helpers/cjs/_interop_require_default.cjs`, while Node >= 22.12
resolves `@swc/helpers/_/_interop_require_default` to the `module-sync`
target `esm/_interop_require_default.js` (`@swc/helpers` 0.5.23 lists
`module-sync` first). The copied `.next/standalone/server.js` then
exited with MODULE_NOT_FOUND before listening.
---------
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
The metadata resolver currently passes ResolvedMetadata directly to tag
rendering even though several fields only exist to carry state between
route layers.
Introduce SelectedMetadata as the post-processed, tag-ready
representation and convert the current resolved output before rendering.
This makes the final selection boundary explicit without changing
generated tags, preparing the metadata pipeline for multiple
independently resolved branches.
<!-- NEXT_JS_LLM -->
## What?
- remove manifest fragments when Turbopack's development entrypoint
cleanup removes a deleted page
- add an end-to-end regression for replacing concrete App Router pages
with an optional catch-all without restarting the dev server
## Why?
Turbopack already removes deleted entry keys from its asset mapper,
subscriptions, issues, and client state. It did not remove the
corresponding records from `TurbopackManifestLoader`, so the next
manifest write continued to serialize deleted App Router routes.
When a deleted concrete route is replaced by an optional catch-all,
those stale records conflict with the new route at the same specificity.
The dev server reports the route conflict and responds with 404 until it
is restarted.
This is independently consistent with the cleanup identified in #97194.
This draft adds end-to-end coverage that fails on current canary by
observing both the 404 and stale manifest records, then passes with the
lifecycle fix.
## How?
Pass the existing manifest loader to `handleEntrypointsDevCleanup` and
call its existing `delete(key)` method in the same branch that deletes
the stale asset mapping. This keeps all per-entry manifest maps
synchronized with the authoritative current entrypoint set without
special-casing any route shape.
The regression test:
1. compiles two concrete localized routes
2. deletes both pages and adds an optional catch-all
3. waits for three affected paths to return the new catch-all content
4. verifies the development app-paths manifest contains the catch-all
and no deleted entries
## Verification
- reproduced the report on `16.3.1-canary.10`: all four reported paths
returned 404 after the live route replacement and returned 200 after a
restart
- confirmed the regression fails on current canary before the source
change with the same-specificity route error and a 404 response
- `pnpm test-dev-turbo
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `pnpm test-dev-rspack
test/development/app-dir/hmr-deleted-page/hmr-deleted-page.test.ts`
- `CI=1 pnpm build-all`
- `pnpm --filter=next build`
- formatting, ESLint, staged-diff, and signed-commit checks
Fixes#97035