## Summary
Set `NEXT_PRIVATE_LOCAL_DEV=1` for directory fixtures in deploy mode,
matching the next-start setup. This keeps copied test runner files
excluded from TypeScript checks when deployment tests use
`tsconfig.test.json`.
## Verification
- `pnpm exec tsc --project test/lib/tsconfig.json --noEmit`
- Not run: deployment tests (requires Vercel deployment credentials)
<!-- NEXT_JS_LLM -->
## Summary
Enable the project-local TypeScript CLI checker by default during `next
build`, while preserving `experimental.useTypeScriptCli: false` as an
opt-out to the TypeScript compiler API. Update the TypeScript 7
guidance, diagnostics, tests, and documentation to match the new
default.
## Verification
- `pnpm exec jest --runTestsByPath test/unit/isolated/config.test.ts`
- Pre-commit lint-staged checks passed (Prettier and ESLint)
- Not run: `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts` (isolated
fixture dependency installation was blocked by unavailable npm registry
access)
- Not run: `pnpm --filter=next types` (repository has pre-existing
unrelated TypeScript errors)
<!-- NEXT_JS_LLM -->
How long do you expect this to wait at worst (before failing)?
```js
await retry(async () => {
expect(await browser.elementById('never-appears').text()).toBe('x')
}, 15_000)
```
After this PR, the answer is 15 seconds. (Well, technically, 15 seconds
+ whatever last attempt is, so 20.)
But before this PR, the answer was 3 minutes.
## What?
There's a bug (IMO) in `retry`. Instead of waiting for the time you give
it, it counts tries.
For example, `retry(fn, 15_000)` reads as "keep trying for 15 seconds".
However, what it actually was doing is divide 15000 by the 500ms
interval and try 30 times, sleeping 500ms in between every try.
This is a problem because **it wasn't measuring the stall time of each
try itself.**
When a try is cheap, it doesn't matter. But a try that looks for an
element in the browser isn't cheap: if the element isn't there, the
selector waits 5 seconds before giving up.
In the example above, each try sits in the selector for its full 5
seconds, and there are 31 of these intervals, plus the 15 seconds of
sleeping in between. This ends up waiting 170103ms, i.e. three minutes
for a wait that says 15 seconds.
Jest kills the test at its own timeout long before that, and the actual
error gets thrown away. What's left is `Exceeded timeout of 120000 ms`,
which doesn't say which assertion never came true. In the one I measured
it was `page.waitForSelector: Timeout 5000ms exceeded`. My agent lost an
hour trying to make sense of this twice now.
## Fix
We count time now. After the fix, the retry watches the clock and stops
when its time is up. It still always makes at least one try. The
interval no longer has to divide the duration, because nothing counts
steps any more. If the last try is slow, we're gonna wait for that last
try to end (so the timeout is still not *strictly* respected) so that we
can show the error in the correct test.
Cheap waits behave exactly as before. In my local run, a try that had to
repeat cost 251ms at the median, 459ms at the worst. So the only calls
this changes are ones that were already failing, and those now fail with
their own error instead of a test timeout. One flaky dev test that used
to die at 120 seconds with nothing to show now fails in 19.5 seconds
saying `Failed to retry within 15000ms (waited 16022ms)`, next to nine
tests that take 2.2 seconds each.
I added some regression tests. Since it's a test helper, a unit test
seemed fine.
## What this means for existing tests
The catch is that a call whose tries are slow now gets less time than it
used to, and this helper has 2110 call sites, so CI is the real check.
It found one file: the hash scroll tests in `navigation.test.ts` wait
with the default 3 seconds, and each try there costs 150-300ms on a
runner, so they were really getting about 5. They need more than 3, so
they failed on two dev shards. I gave them 10.
If this is green, we can merge it in isolation from the rest of the
stack.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
If we're reasonably confident that a segment can be prefetched
statically without omitting data that would have been included during a
runtime prefetch (e.g. cookies), the client should attempt prefetch the
segment statically instead of going straight to a runtime request.
The decision for whether to do this is based on the
ShouldAttemptStaticPrefetch added in previous steps.
If it turns out the static response is not sufficient, then it will fall
back to a runtime request.
This makes prefetching cheaper for pages that are fully statically
renderable. We can make the optimization better in the future with more
reliable per-segment computation of the ShouldAttemptStaticPrefetch, but
the current approach should at least work for fully static pages, which
is what's most important.
This optimization applies during both the Shell phase and the
Speculative phase of the prefetching algorithm.
This is a refactor of the prefetching algorithm to block the prefetch
task from completing until the response is received.
Currently, a prefetch task is fire-and-forget: once a prefetch's
requests are spawned, the scheduler marks the originating task as
complete regardless of what the server ends up returning. However,
certain planned optimizations require us to verify the response from the
server to determine whether additional work is necessary. For example,
we may attempt to do a static prefetch instead of a runtime prefetch if
we optimistically assume that the static prefetch does not vary on
runtime data. We need to verify that assumption is correct before
marking the task as complete.
Now, whenever a pass over the task spawns a segment request, or
encounters an entry whose response hasn't arrived yet, the task
registers itself on the entry and exits as Blocked instead of Done. When
the entry resolves, the blocked task is pinged and the pass re-runs
against the received data. Only a pass that observes every response it
cares about advances the task to the next phase or completes it.
Rejected entries are handled per segment: a rejection counts as an
observed response, so the pass skips that segment and keeps prefetching
the rest. The task never registers on a rejected entry, since nothing
ever pings one. Retries are governed by the existing per-entry logic.
Also fixes a cache bug this exposed: when a revalidation response is
keyed at a more generic vary path than the stale partial entry that
prompted it (because the segment turned out not to vary on some param),
the stale entry shadowed the result on every lookup — the upgraded data
was unreachable and the revalidation was wasted. With blocking, that
dead end became an infinite loop: each fulfillment re-pinged the blocked
task, which re-read the same stale entry and spawned another
revalidation. Upserting (or re-keying) a segment now deletes settled
entries at more specific vary paths that the incoming entry supersedes.
The router-act test helper's "at least one request initiated" watchdog
is adjusted to account for the new blocking behavior: countable requests
can now be legitimately gated on an in-flight App Shell response (which
the watchdog intentionally doesn't count). On expiry it now proceeds to
response processing if the act scope intercepted a shell request, and
keeps waiting while shell requests are in flight elsewhere on the page,
instead of timing out.
- `detectContentType` test was too aggressive about asserting completion
in 1ms, let's give it several tries.
- We weren't correctly setting up pnpm overrides in tests, which breaks
things whenever canary comes out.
This is a behavior-preserving refactor of the dev-mode Cache Components
validation, ahead of the change that moves it onto a worker thread.
In-process behavior is unchanged.
It narrows the context the validation functions require.
`runValidationInDev` and its helpers (`validateStaticShell`,
`warmupClientModulesForStagedValidation`, `validateStagedShell`,
`validateInstantConfigs`) now take a `ValidationRenderContext` (a `Pick`
of `AppRenderContext` plus the two `renderOpts` fields and the
debug-channel flag they actually read) instead of the full render
context, with `toValidationRenderContext` mapping the in-process and
build-time callers. The worker cannot reconstruct a full
`AppRenderContext` from a serialized snapshot, so narrowing the contract
to what validation genuinely consumes is what lets the same code run
there.
It also separates computing the validation errors from delivering them.
`runValidationInDev` now returns the errors instead of sending them to
the dev overlay inline, and the caller `runDevValidationInBackground`
delivers them via `logMessagesAndSendErrorsToBrowser`. The test-mode
lifecycle markers and delay move into `runWithDevValidationLogging`,
which brackets both the render and the delivery. When validation runs on
the worker, the worker computes the errors but the main thread still
delivers them (delivery needs the response object), so splitting the two
halves is a prerequisite.
Finally, it extracts the test-only validation lifecycle markers into a
shared `dev-validation-events` module. The `<VALIDATION_MESSAGE>`
wrapping and the event shape had been hand-rolled at each emission site
in `app-render.tsx` and duplicated again in the test harness; they now
live in one place as `formatValidationEvent` and an exported
`ValidationEvent` union that the in-process emitters call and the test
utilities import rather than redeclare. The worker emits the same
markers through the same helper once it is added, so a single definition
keeps every producer and the test parser in sync. Build validation's
marker `requestId`, previously the numeric `Date.now()`, is stringified
to fit the shared `string` type; it appears only in the logged marker
payload, which nothing matches on, so this too is unobservable.
We also mitigate the security impact of e2e tests by running everything
in VMs, and by not reading caches in release jobs, but we should still
be using `minimumReleaseAge` in e2e tests.
Ideally we'd also use lockfiles here too, and I'll work on that next,
but that is likely a lot more complicated because we probably need some
way to merge lockfiles, since we can't pin the next.js and react
versions in every test, and we need a reasonable/efficient way to
bulk-update all the lockfiles for tests that need it.
Depends on https://github.com/vercel/next.js/pull/95668 for a bunch of
windows path representation fixes that this ends up exposing in CI,
because adding workspace files causes pnpm to change the absolute path
representation used in the Windows junction points that it creates.
### What?
- Run development instant validation only after the navigation response
has finished.
- Let a newer request supersede stale validation work and yield between
validation render attempts.
- Forward the cancellation signal into React validation prerenders so
asynchronous validation work can stop promptly.
- Add focused scheduler/response tests and assert that development
validation starts after the response finishes.
### Why?
Instant validation was detached from the response promise, but its
additional React renders still ran on the same Node.js event loop. A
subsequent navigation could therefore wait hundreds of milliseconds for
stale diagnostic work from the previous request.
Instant validation still runs when the server is idle. When another
navigation arrives, foreground request work now takes priority and the
obsolete validation is discarded.
### How?
Each Cache Components development request receives a validation
generation signal. Starting a newer request aborts the previous
generation. Validation waits for the Node response `finish` event,
yields through the event-loop poll phase between attempts, and combines
the generation signal with React's existing prerender abort signals.
Tests:
- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest --runTestsByPath
packages/next/src/server/app-render/dev-validation-scheduler.test.ts
packages/next/src/server/app-render/wait-for-response.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/app-dir/instant-validation/suspense-boundaries.test.ts -t
"valid - static prefetch - suspense around runtime and dynamic"`
<!-- NEXT_JS_LLM_PR -->
The idea here is that we waste a bunch of CPU setting up each test suite
by spawning an entirely new Chrome process, but playwright can share the
same browser across many tests. Tests are still isolated at the browser
level, just not at the OS process level.
Hoping to see some performance improvement in e2e tests. Compare to:
https://github.com/vercel/next.js/pull/95617
Somewhat unscientific (single sample) results:
```
Comparing the head-commit build-and-test runs — PR #95589 run [28981702219 ](https://github.com/vercel/next.js/actions/runs/28981702219)vs PR #95617 run [28981686692](https://github.com/vercel/next.js/actions/runs/28981686692). Both ran 101 jobs and both succeeded.
┌─────────────────────────┬────────────────┬───────────────────────┬───────────────────┐
│ Metric │ #95617 control │ #95589 shared browser │ Delta │
├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤
│ Raw wall-time sum │ 616.2 min │ 571.2 min │ −45.0 min (−7.3%) │
├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤
│ Billable (ceil per job) │ 658 min │ 617 min │ −41 min (−6.2%) │
└─────────────────────────┴────────────────┴───────────────────────┴───────────────────┘
The shared-browser experiment is cheaper, and the savings land almost exactly where you'd expect — the browser-driven test suites — while non-browser jobs (rust, lint, unit, windows) are flat within noise:
┌─────────────────────────────────┬─────────┬────────┬───────┐
│ Job category │ control │ shared │ delta │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ test prod │ 130.7 │ 110.5 │ −20.2 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ cache components dev │ 51.5 │ 42.9 │ −8.7 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ turbopack dev │ 75.1 │ 69.5 │ −5.5 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ turbopack production │ 86.3 │ 81.1 │ −5.2 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ test dev │ 110.4 │ 105.5 │ −4.9 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ cache components prod │ 48.7 │ 49.0 │ +0.3 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ firefox and safari │ 5.3 │ 7.3 │ +2.0 │
├─────────────────────────────────┼─────────┼────────┼───────┤
│ flake-detection jobs (combined) │ ~15 │ ~21 │ +~6 │
└─────────────────────────────────┴─────────┴────────┴───────┘
Takeaways:
- Net savings of ~41 billable minutes per run (~6%), concentrated in the Chromium-driven test prod/dev, turbopack, and cache components suites — consistent with reusing one browser process instead of spawning per-suite.
- The small regressions are in firefox and safari and the "new/changed tests for flakes" jobs (+~8 min combined). Worth a glance, though they may just be run-to-run variance.
Caveat: this is a single run per PR, so there's real variance run-to-run. That said, the fact that the deltas track the browser-heavy jobs specifically — and not the Rust/lint/unit jobs — is a good signal the effect is genuine rather than noise. If you want a firmer number, re-running each PR 2–3× and averaging would tighten it up.
```
When rapid edits overlap Server Components HMR refreshes in `next dev`,
only the newest refresh can commit. The client already aborts a
superseded refresh's fetch, which closes its response. We use that
response-close to stop the server work the superseded refresh started,
so the dev server no longer runs a render (and, under Cache Components,
a validation) whose result is discarded.
When the `serverComponentsHmrCancellation` flag is enabled and the
request is an HMR refresh over a Node response, we derive an abort
signal from `signalFromNodeResponse(ctx.res.originalResponse)` and
thread it into the Flight render. This covers both dev RSC render paths:
the Cache Components staged render in
`generateDynamicFlightRenderResultWithStagesInDev` and the
non-Cache-Components render in `generateDynamicFlightRenderResult`.
The render is aborted through `renderToNodeFlightStream`, which calls
`abort()` on the pipeable that `renderToPipeableStream` returns when the
signal fires. React's Node Flight API has no `signal` option, unlike the
Web `renderToReadableStream`, aborting the returned pipeable is the
intended way to stop the render. This also removes the incorrect
`signal` field from the `renderToPipeableStream` type declaration and
derives `FlightRenderOptions` from `renderToReadableStream`'s options,
so the render wrappers are no longer typed as `any`.
Under Cache Components the superseded refresh additionally skips its
detached validation, which only runs on the staged render, so it never
validates a discarded tree, and any error thrown while its background
renders tear down is swallowed. The aborted Flight renders surface as
abort errors that `createReactServerErrorHandler` already ignores, so
nothing reaches the error overlay or the CLI.
The `hmr-rsc-cancellation` suite exercises both render paths, with Cache
Components controlled by the `__NEXT_CACHE_COMPONENTS` test shard rather
than the fixture config. A child component logs when React renders it,
and the test asserts the superseded refresh never logs, since its render
is aborted before reaching the child, while the committed refresh does.
The detached-validation assertions run only when Cache Components is
enabled.
Deploy tests recover the `BUILD_ID`, `DEPLOYMENT_ID` and
`NEXT_SUPPORTS_IMMUTABLE_ASSETS` values by scraping them out of the
deployment's build logs, where the test fixture's `post-build` script
prints them as the final lines of the build. We fetch those logs with
`vercel inspect --logs` after `vercel deploy` returns, but a deployment
can report `Ready` before its full build-log tail has propagated to the
log query API, so a single call sometimes returns a truncated prefix
that stops before the markers. When that happens `parseIdsFromCliOuput`
throws "Failed to get buildId from logs" and fails the whole suite
during setup, which is the intermittent failure we have been seeing
across the deploy jobs.
This change wraps the log fetch in `fetchBuildLogsUntilComplete`, which
re-queries `vercel inspect --logs` until the last-printed marker
(`NEXT_SUPPORTS_IMMUTABLE_ASSETS`) appears before parsing, bounded to
twenty attempts three seconds apart. Gating on the final marker rather
than the first ensures a partial read cannot slip past the loop into the
parser. Streaming the logs from `vercel deploy --logs` as part of the
deploy itself was considered, but it has the same race: the CLI stops
streaming once the deployment reports `Ready`, before the `post-build`
tail flushes, and a single stream cannot re-query for the missing lines.
Implements instant validation for `partialPrefetching`. In this mode,
`<Link>` prefetches an App Shell, which cannot access link data, and we
need to warn for that.
The changes in `instant-validation.tsx` are relatively simple: for an
App Shell, we simply use `ShellRuntime` for all the new segments. We
might also force them into `Runtime` for the purposes of discriminating
dynamic holes. If a hole is present in `ShellRuntime` but disappears in
`Runtime`, then we know it's caused by **link data** (as opposed to
runtime or dynamic data). I've added some new error messages for this
case.
Note that the implementation here is incomplete: it uses the chunks from
the dev render, which resolves static params in the `Static` stage. We
use `ShellRuntime` for validating the App Shell, so as a result, static
params are incorrectly included in it and don't trigger link data
errors. This will be implemented in a follow-up.
Note: It seems like we have some pre-existing bug in build validation
where `fallbackParams` aren't populated, so params resolve statically
when they shouldn't. I've marked two tests with `// TODO(app-shells):
missing fallback params in build validation` so we can follow up and fix
those.
PR #94797 taught `NextStartInstance.start()` to recover from a leftover
`next build` process left behind when a previous test attempt was
interrupted mid-build by the per-test timeout, but `build()` kept
throwing `can not run export while server is running` unconditionally
whenever a child process was still tracked. Tests that only call
`next.build()` (and never `start()`) therefore still failed their
`jest.retryTimes` retry with that misleading error, masking the original
timeout.
This extracts the recovery logic into a shared
`stopLeftoverBuildOrThrow` helper and routes both `start()` and
`build()` through it. When a leftover `next build` process from an
interrupted attempt is found, it is stopped so the retry can continue
from a clean state; a genuinely running server still throws, preserving
the existing guard. Sharing one helper keeps the two call sites in sync.
During `next dev`, certain errors (such as Cache Components validation
errors) are logged directly to the terminal by the dev server and are
also sent to the browser so they can be shown in the dev overlay. The
browser logs them to its own console, and the browser-to-terminal log
forwarding would replay that copy back to the CLI, so the same error
appeared in the terminal twice.
This change marks those errors when the browser receives them, in the
`ERRORS_TO_SHOW_IN_BROWSER` handler, using a non-enumerable symbol.
`forwardErrorLog`, the single function all forwarded error logs pass
through, then skips any error carrying that marker, since it already
originated on the server and was logged there. The marker has to be set
browser-side because it wouldn't survive RSC serialization, which is the
same reason the error code is sent through a side-channel map rather
than on the error itself.
The error still shows in the browser console and the dev overlay; only
the redundant terminal echo is suppressed. This applies to every error
delivered over the errors RSC stream, all of which are logged on the
server before being sent, so the forwarded copy is always a duplicate.
> [!TIP]
> Review the two commits individually to see the before/after change.
When Cache Components is enabled, `next dev` treats a `'use cache'`
value as a miss and renders as if the cache were empty whenever the read
does not resolve right away. Two development configurations triggered
that even for values that were already cached, so warm reloads streamed
slowly instead of serving the cached value: `cacheMaxMemorySize: 0`
replaced the built-in default handler with a no-op stub, so nothing was
cached at all, and custom cache handlers with a slow or remote `get` did
not return in time.
For the size-0 case, development now uses a real in-memory handler
instead of the no-op stub, and the `'use cache'` wrapper forces a
dynamic cache life (`revalidate: 0`, a 5-minute `expire`) for it, the
same treatment private caches already receive, so every read serves the
stale entry and re-warms a fresh one in the background. This also fixes
the dev private handler, which was sized from `cacheMaxMemorySize` and
so degraded to the no-op stub whenever `cacheMaxMemorySize: 0` was set.
Custom cache handlers keep their configured cache life, since their
backing owns it. Instead we put a fast built-in in-memory front handler
in front of the configured one through the new `TieredCacheHandler`,
which serves warm reads from the front, writes through to both tiers,
and reconciles the front against the backing in the background, evicting
the front entry when the backing no longer has it (the handler interface
has no per-key delete, so it overwrites the entry with an
already-expired copy). These dev-only handlers are kept out of the
registered handler set and merged in only where tag operations iterate,
so `revalidateTag` still reaches them.
Everything is gated on `process.env.__NEXT_DEV_SERVER`, so production is
unchanged: `cacheMaxMemorySize: 0` still caches nothing, private entries
are still never persisted, and configured handlers are used directly.
New development test suites cover the size-0 and custom-handler
behavior.
When a `next start` test exceeds the per-test timeout while `next build`
is still running, the build process is left running and tracked by the
next instance, because the build phase of `NextStartInstance.start()`
clears `childProcess` only when the process exits. `jest.retryTimes`
then re-runs the test body in the same process, and `start()`
immediately throws `next already started`, which masks the original
timeout.
`NextStartInstance` now tracks whether a build or a server currently
owns the child process. When `start()` finds a leftover `next build`
process from an interrupted attempt, it stops that process and continues
instead of throwing, so the retry can start from a clean state. The
`next already started` guard is preserved for the case where a server is
genuinely already running.
When App Shells is enabled, prefetching is split into two phases: a
prefetch for the shell, and a separate phase for the per-page content.
This would cause a handful of the existing prefetch-related tests to
fail once App Shells is turned on. However, the fact that the shell is
prefetched separately from the page content is an incidental detail of
how prefetching works. The App Shell is conceptually part of the route,
not part of the "prefetch". Or in other words, App Shell prefetching has
more in common with incremental module loading than it does with
prefetching in the traditional sense used in the context of web apps.
So, this updates router-act to ignore App Shell requests for the
purposes of making assertions about prefetch responses. They're still
captured, fulfilled, and awaited — they just don't participate in the
matching logic. Tests that specifically assert on App Shell behavior can
opt back in with `createRouterAct(page, { includeAppShellRequests: true
})`.
This is groundwork for the next PR, which enables App Shells by default.
With these requests ignored, most prefetch tests pass unchanged
regardless of whether App Shells is on.
<!-- NEXT_JS_LLM_PR -->
When the browser's HTTP cache entry for a back-navigation target has
been evicted between forward visit and back-press — for example with a
long-lived tab, storage pressure, or a manual cache clear — the browser
re-fetches the document fresh from the server. Previously we relied on
`type === 'back_forward'` alone to decide that the document came from
cache, which meant we treated those re-fetches as cache restores too.
The persisted-chunk lookup would miss on the fresh response, and we'd
recover with an unnecessary `location.reload()`.
The same edge case is exposed by the Playwright/WebKit combination in
the upstack PR: bfcache isn't utilized there, so back-navigations always
come over the network, and the old single-signal check classified those
re-fetches as served-from-cache and triggered the same unnecessary
reload.
With this change we split the cache-restore detection into two phases.
At script-execution time we look at `deliveryType` (Chrome ≥109, Safari
≥17) and the navigation entry's `transferSize`/`encodedBodySize` to
decide cache-restore vs fresh-response when those fields are populated.
When the body bytes haven't been measured yet — the common case for
streaming Next.js dev RSC responses on every browser, and also WebKit
leaving both size fields at zero at exec time — we suspend the readable
until `pageshow` and re-check there. On a fresh re-fetch we route
through the live WebSocket-backed channel, which already has the debug
data for the new response, and skip the reload entirely.
The new `bfcache-regression` test exercises this edge case on Chromium
by clearing the browser cache via CDP between the forward and back
navigations, using a new `clearBrowserCache()` helper on the test
browser wrapper. The same exec-time code path is naturally hit by Safari
whenever its navigation entry's size fields are still zero at
script-execution time, but the harness can't force the eviction
deterministically there.
### What?
Fixes an invariant failure (`failed to find source route
/dynamic/[id]/apple-icon.png for prerender
/dynamic/[id]/apple-icon.png`) raised by the build adapter when an app
contains static metadata files (`apple-icon.png`, `icon.png`,
`opengraph-image.png`, `twitter-image.png`, `sitemap.xml`) under a
dynamic segment.
### Why?
Static metadata files under a dynamic segment were treated as full
dynamic prerenders: they ended up in `prerenderManifest.dynamicRoutes`
with `fallbackRouteParams`, but no `.body` file was exported and they
never received an `appOutputMap` entry. The adapter's `dynamicRoutes`
loop then asked `getParentOutput` for a source route that didn't exist
and threw.
These files are content-stable across params — the route handler returns
the same bytes for every URL that matches the dynamic pattern — so they
should prerender once under a canonical pathname rather than be treated
like per-param dynamic routes.
### How?
- New `getStaticMetadataPrerenderPathname` helper that maps
`/dynamic/[id]/apple-icon.png` to `/dynamic/-/apple-icon.png`. This is
the canonical pathname already used by `fillStaticMetadataSegment` for
HTML link emission.
- In `build/utils.ts`, route `APP_ROUTE` entries that are static
metadata files through a new `buildStaticMetadataStaticPaths` (one
prerender path, no fallback params) instead of the regular
`buildAppStaticPaths`.
- In `build/index.ts`:
- Rewrite each prerendered static metadata route to its `-`-placeholder
pathname before splitting into known/unknown buckets, so it lands in
`prerenderManifest.routes` with a `.body` file.
- Skip static metadata entries in the `dynamicPrerenderedRoutes` loop so
they don't get a stray `PRERENDER` manifest entry.
- Defensive sweep before write: drop any leftover bracketed metadata
keys from `prerenderManifest.dynamicRoutes`.
- In the adapter, look up `prerenderManifest.routes` under the canonical
placeholder pathname (with locale prefix when applicable) and skip
static metadata in the `dynamicRoutes` loop.
- Unit tests in `get-metadata-route.test.ts` lock in the placeholder
behavior for the tricky cases: catchall, optional catchall, multi-level
dynamic, literals between dynamics, dynamic + catchall mix.
Reproduction:
`test/e2e/app-dir/metadata-static-file/metadata-static-file-dynamic-route.test.ts`
configured with `adapterPath` previously crashed at
`handleBuildComplete`; all three cases in that file now pass.
Runtime stays the same: arbitrary param URLs continue to be served by
the dynamic route handler shipped with the app route; only the canonical
`-` URL is statically served.
<!-- NEXT_JS_LLM_PR -->
### What?
Migrate remaining direct `next-webdriver` test callers that have a
`NextInstance` to `next.browser()`, and expose the shared `Playwright`
browser type from `e2e-utils`.
### Why?
`NextInstance.browser` should be the supported browser-opening interface
for test fixtures, with `next-webdriver` kept as the private
implementation detail.
### How?
Updated affected development, e2e, and production tests to call
`next.browser()` directly, passing `baseUrl` where tests intentionally
target a manually spawned or proxied server. Shared helpers now receive
browser callbacks from the test context, and browser types import
`Playwright` from `e2e-utils` instead of deriving from `next.browser` or
importing from private paths.
<!-- NEXT_JS_LLM_PR -->
## Summary
### Error overlay shell
- Clean up nav, dialog frame, footer placement, and button styles across
the overlay
- Remove unused `fader` and notch components
- Tighten layout spacing throughout the dialog
### Instant error guidance (fix cards)
- New data-driven fix-card grid for instant errors: each card has a
`group` (Stream / Cache / Block / Static / Dynamic / Client / Defer /
Measure / Prerender), an icon, a color, and an optional docs link
- Whole card is clickable when a docs link is present, with an
always-visible external-link indicator
- Centralized group metadata (label / color / icon) in
`instant-guidance-data.ts` so cards stay consistent across the overlay
and the docs surface
- Outlined SVG icons live in a dedicated `fix-card-icons.tsx`
- Snippets refined: cache cards trimmed to 3 lines, client sync-IO defer
cards use block-body arrow syntax so longer expressions don't overflow
### Code frame
- Restore the red bar / caret on the errored line (parser previously
only matched storybook synthetic frames)
- Fix caret alignment with `codeFrameColumns` output
- Style inline error code snippets
### Storybook & examples
- Add error examples and instant-error fixtures for development/testing
- Align all 11 instant storybook fixtures with canary factory output and
the matching test-app routes
---------
Co-authored-by: Aurora Scharff <aurora.sofie@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com>
### What?
Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.
- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
- `test/e2e/` — 175
- `test/production/` — 130
- `test/development/` — 43
- `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly
### Why?
`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.
The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.
Removing `test/integration/` lets us:
- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)
### How?
Mechanical conversion per suite, with targeted clean-ups:
1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
- `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
- Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`
2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.
3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.
4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.
5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.
6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
The Vercel CLI bundles `@vercel/detect-agent`, which inspects
environment variables that AI coding assistants (Claude Code, Cursor,
Codex, Gemini CLI, GitHub Copilot CLI, and others) set in every shell
they spawn. When the CLI detects one, `vercel deploy` writes a JSON
payload to stdout instead of the plain deployment URL. The harness then
crashes when `new URL(deployRes.stdout)` in `next-deploy.ts` runs, which
breaks every `test-deploy*` run launched from such a shell.
The fix appends `--non-interactive=false` to `vercelFlags` so it is
passed to both `vercel link` and `vercel deploy`. The CLI honors that
flag as an explicit override of the agent-derived default, and the
deploy command's JSON-vs-plain output decision is gated on
`client.nonInteractive` (`const asJson = formatResult.jsonOutput ||
client.nonInteractive` in the deploy source), so a single flag is enough
to restore plain-URL stdout.
The alternative considered was reading `deployment.url` out of the JSON
payload at the single call site that breaks. That is a more localized
change but couples the harness to an undocumented payload shape that
exists only for AI agents, so a future rename of that field would
silently break deploy tests again. Forcing the CLI's output mode avoids
that coupling, which is why the flag won.
With this change, `pnpm test-deploy` and `pnpm test-deploy-turbo` work
uniformly whether they are launched from a human's interactive terminal
or from inside an AI coding assistant.
Nesting TUIs is hard but also a bit excessive since Turborepo's TUI
isn't interactive if `stdin` is ignored.
Co-authored-by: Copilot <copilot@github.com>
### What?
Eliminates the expensive temporary repo directory (`tmpRepoDir`) copy
during isolated test setup by leveraging a Turborepo `pack` task with
caching.
### Why?
The previous test isolation flow copied the entire `packages/` directory
to a temp location, mutated every `package.json` to rewrite workspace
dependency references, and ran `pnpm pack` sequentially for each
package. This added ~10s+ of overhead per test suite. With Turborepo
caching, repeated runs are 500ms (and can still be improved).
### How?
- Adds a `pack-for-isolated-tests` task to `turbo.json` (depends on
`build`, outputs `packed.tgz`)
- Adds a `pack-for-isolated-tests` script (`pnpm pack --out
./packed.tgz`) to every workspace package
- Simplifies `linkPackages` in `repo-setup.js` to scan for pre-built
tarballs instead of copying/rewriting/packing
- Updates `create-next-install.js` to run `pnpm turbo run pack`
(benefits from caching) and use package manager `overrides`
(pnpm/npm/yarn) to resolve transitive workspace deps from local tarballs
- Removes `tmpRepoDir` handling from `base.ts` and `run-tests.js`
**Before:** `createNextInstall` ~16s (copy + sequential pack)
**After:** `createNextInstall` ~5s (first run) / ~4s (cached)
<!-- NEXT_JS_LLM_PR -->
### What?
Skip `.next`, `.next-profiles`, and `.DS_Store` directories in the
`writeFiles` filter when copying test fixture directories.
### Why?
`writeInitialFiles` was taking ~500ms because the `fs.cp` filter only
skipped `package.json`. When a test uses `files: __dirname`, local build
artifacts like `.next` (117MB / 411 files) and `.next-profiles` (147MB)
were being copied into the test directory unnecessarily if a dev/build
run had previously happened on the test app.
This really only applies to working on the repository locally, but it's
a nice win for that case.
### How?
Expanded the `fs.cp` filter to also skip `.next`, `.next-profiles`, and
`.DS_Store`. These are local artifacts never tracked in git.
`node_modules` is intentionally not skipped since some tests include
tracked `node_modules` as fixtures.
<!-- NEXT_JS_LLM_PR -->
Inlining hints (which segments to bundle together) are computed once at
build time by measuring gzip sizes, then persisted to
`prefetch-hints.json`. There are several scenarios where these hints may
not be available at render time. This commit addresses each scenario to
ensure the client always receives correct hints and never enters a bad
state.
### Build-time static pages (stale hints)
The initial RSC payload baked into the HTML is generated before hints
are computed, since hint computation requires at least one completed
render to measure sizes. The server marks these trees with
`InliningHintsStale`. The client immediately expires the route cache
entry so that the next prefetch re-fetches the correct tree from the
`/_tree` response. The segment data doesn't need re-fetching since it
was already cached from the initial HTML payload.
### ISR / revalidation
Hints are always available from the manifest. A missing entry is an
internal error — it means the build pipeline failed to produce hints for
a route that needs them.
### Fully dynamic routes at runtime
No hints exist and none will ever be computed. Every segment gets
`PrefetchDisabled`, which tells the client not to attempt prefetching.
This avoids an infinite re-fetch loop that would occur if the client
kept trying to fetch "correct" hints.
### Unified response format
Also simplifies the segment prefetch response format: every response is
now a `SegmentPrefetchResponse` with a top-level `buildId` and a `data`
array, treating a single-segment response as a bundle that happens to
have length one. This unifies the format so that bundled multi-segment
responses (added in a follow-up) require no additional client parsing
logic.
### What?
Two fixes for the Turbopack build tracing introduced in #90397:
1. **Don't block SSG on Turbopack shutdown**: `workerMain()` no longer awaits the shutdown promise before returning. Trace event collection is deferred to `waitForShutdown()`, which the parent process awaits *after* SSG completes. This allows static generation and Turbopack persistence/cache-flush to run in parallel.
2. **Add persistence spans to `trace-build` allowlist**: `turbopack-build-events`, `turbopack-persistence`, and `turbopack-compaction` are now included in the `to-json-build.ts` allowlist so they appear in `.next/trace-build`.
### Why?
- The `await shutdownPromise` in `workerMain()` was too eager — it prevented the caller from acknowledging the build as complete and starting SSG until Turbopack persistence finished flushing to disk.
- The persistence/compaction spans emitted by Rust (`turbopack-persistence`, `turbopack-compaction`) were not in the `to-json-build.ts` allowlist, so they were silently filtered out of `.next/trace-build`.
### How?
**`impl.ts` (worker)**:
- Removed `await shutdownPromise` from `workerMain()` — it now returns build results immediately
- `waitForShutdown()` now returns `{ debugTraceEvents }` after awaiting shutdown, so trace events are collected only after all compilation events (including persistence spans) have been processed
**`index.ts` (parent)**:
- Moved `recordTraceEvents(debugTraceEvents)` from the `workerMain` result handler into the `shutdownPromise` `.then()` chain, so events are replayed into the parent reporter after shutdown completes
**`to-json-build.ts`**:
- Added `turbopack-build-events`, `turbopack-persistence`, `turbopack-compaction` to the allowlist
**Test updates**:
- Enabled `turbopackFileSystemCacheForBuild: true` in the trace-build test fixture
- Updated the Turbopack inline snapshot to include `turbopack-build-events`
After a successful, Turbopack production build, this writes
`.next/diagnostics/route-bundle-stats.json`
containing First Load JS stats for every route.
## Why?
We used to have route bundle size statistics print after every build. It
was not very high-signal and could be misleading, often using heuristics
to give a best-effort estimate of what sizes may be at runtime. **These
kinds of metrics for route bundle sizes can be misleading, as pages can
be slow to visual/interactive completeness even if they have small first
load bytes.**
However, a minimal gauge of how much JavaScript a route ships during
initial page load can be a useful signal so long as users have
additional context and awareness of their app structure. Let's provide
just this number for now for those that could benefit.
Additionally, this does so in the form of writing a json document to the
build directory, rather than writing a table to stdout, where users have
needed to write extra tools to parse.
## Format
An array of route entries sorted descending by
`firstLoadUncompressedJsBytes`:
```json
[
{
"route": "/blog/[slug]",
"firstLoadUncompressedJsBytes": 124928,
"firstLoadChunkPaths": [
".next/static/chunks/framework.js",
".next/static/chunks/main.js",
".next/static/chunks/pages/blog/[slug].js"
]
}
]
```
`firstLoadUncompressedJsBytes`: sum of uncompressed sizes of all JS
chunks loaded on first navigation to the route (shared chunks included),
for both Pages Router and App Router
`firstLoadChunkPaths`: paths to those chunks relative to the project
root
## Test Plan
Added an integration test. Built with a create-next-app and verified the
json document is written and the numbers align with what Chrome reports
during initial load of a page.