## Summary
App Router execution intent was previously inferred during WorkStore
construction from response capabilities and request state. App Page also
carried that intent through its handler context and lazy render API
before selecting the rendering operation inside app-render.
This change moves those decisions to entrypoints where the work is
known:
- Reusable output, including build-time generation, fallback shells, and
runtime revalidation, selects prerendering.
- Request-specific and resumed rendering selects request rendering.
- App Page exposes distinct render and prerender operations, so
execution mode no longer travels through `AppPageRouteHandlerContext` or
the lazy render API.
- App Route execution derives its intent from whether the response has
an ISR cache key.
WorkStore still consumes the explicit mode internally as a transitional
seam. Follow-up work can migrate downstream behavior to the active
WorkUnitStore and remove the mode from WorkStore entirely.
Fixes#96519
## Verification
- `pnpm --filter=next types`
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/static-shell-vary-params-regression/static-shell-vary-params-regression.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/segment-cache/static-shell-vary-params-regression/static-shell-vary-params-regression.test.ts`
<!-- NEXT_JS_LLM -->
## Summary
`create-component-tree` currently combines `WorkStore.executionMode`
with route-level PPR configuration at several leaf call sites to
determine whether it can postpone dynamic subtrees and whether the
resulting response may be partial.
This introduces `AppRenderCapabilities`, derived once at the App Render
boundary, containing:
- `canPostpone`
- `isPossiblyPartialResponse`
- `supportsPerSegmentPrefetching`
Component tree construction consumes the explicit rendering capabilities
instead of independently interpreting the PPR configuration. RSC payload
generation likewise consumes `supportsPerSegmentPrefetching` instead of
re-deriving the client `S` flag from execution mode and Cache
Components, keeping the protocol decision consistent across dynamic,
initial, and error payloads.
## Summary
- derive an explicit missing-prefetch-hint policy at the App Render
boundary
- pass that policy through the Flight router-state builders instead of
exposing execution mode and related render options
Stacked on #96570.
## Summary
- replace `WorkStore.isStaticGeneration` with an explicit `'prerender' |
'request'` execution mode
- preserve the existing derivation and behavior while making the
render/prerender distinction explicit
- document that prerendering includes both build-time generation and
runtime revalidation
- establish groundwork for moving the render decision higher in the
stack and replacing downstream mode checks with purpose-specific state
Stacked on #96564.
Previously, this returned `{} as T` which was unsafe. Now it properly
returns `undefined` instead and the caller can decide what the fallback
value should be
## Summary
- preserve `htmlLimitedBots` substring semantics when generating the PPR
cache-bypass user-agent rule
- allow surrounding user-agent text so bots such as `googleweblight`
correctly bypass the prerender shell
- add deployment coverage confirming blocking metadata for bot
identifiers embedded in full user-agent strings, including custom
`htmlLimitedBots` patterns
<!-- NEXT_JS_LLM -->
## Summary
Cache Components routes support per-segment prefetching, but dynamic
Flight responses only advertised this capability during static
generation. This could cause a subsequent `router.prefetch()` call to
fall back to loading-boundary prefetching and issue an unnecessary
request.
In other words, this logic was correctly determined
[here](https://github.com/vercel/next.js/blob/0db9e5f994ddf144bef72640d5be9bb17d77fe30/packages/next/src/server/app-render/app-render.tsx#L2204)
but not in the initial payload.
## Verification
- `pnpm test-start-turbo
test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts`
## Summary
- rename the `isStaticGeneration` stream option to `waitForAllReady`
- derive the value once from `supportsDynamicResponse` and reuse it
across render paths
- clarify that the option controls waiting for React’s `allReady`,
rather than representing the broader static-generation mode
Stacked on #96563.
## Summary
- Removes the unused `RenderOptsPartial.isStaticGeneration` option and
its debug-render write. `WorkStore` derives this value independently.
- Removes `shouldGenerateStaticFlightData` and its redundant guards.
These checks only occur inside `prerenderToStream`, which is exclusively
entered when `isStaticGeneration` is true.
<!-- NEXT_JS_LLM -->
Navigating between sibling pages of an optional catch-all could leave
the URL updated but the content stuck on the route's index page, with no
request made at all. On nextjs.org that is every /docs/* sidebar link,
once /docs itself has been prefetched.
The App Shell of a segment is the part that doesn't depend on params, so
the cache keeps one copy and reuses it for every param value. The client
decides it has that shell when a response arrives without a shell/full
split.
But a response arrives without a split whenever the render had no
dynamic hole, and that is also what a page fully prerendered at concrete
params looks like. The split alone can't tell the two apart, and the
second is not param-independent at all — its content is the params. So
the /docs page was stored as the shell for every slug. Because a payload
that needs nothing further is also recorded complete, sibling slugs read
it and stopped there: no prefetch, and no dynamic request during
navigation to correct it.
Treat a payload as param-independent only on evidence. The server
already reports which params each segment depends on, so use that.
Without it, assume every param varies, except for a shell variant, which
reduces param-dependent content to param fallbacks and so is good for
any value of them.
The new e2e suite prefetches a fully static catch-all index, navigates
there, then prefetches and navigates to a sibling slug. It fails on
canary with a prefetch that never fires, and is verified with vary
params both enabled and disabled.
Previously, adding a truthy `instant` config to a route segment (e.g.
`export const instant = true`) did two things: it enabled instant
navigation validation, and it implicitly opted the route into Partial
Prefetching.
Now those concerns are fully separate. `instant` only controls
validation, so enabling it no longer silently changes how a route is
prefetched. Partial Prefetching is enabled with the `prefetch` segment
config (`'partial'` or `'unstable_eager'`), or globally with the
`partialPrefetching` option in next.config.
For routes whose only opt-in was a truthy `instant`, `<Link
prefetch={true}>` goes back to performing a legacy full prefetch instead
of being downgraded to a partial one. `instant = false` still opts a
route out of the dev-time `prefetch={true}` warning.
Adds a regression test asserting that a route that only exports `instant
= true` keeps the full-prefetch behavior.
## 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 -->
## Summary
- Treat the root `scroll-padding-top` as the lower boundary of the
usable viewport during App Router navigations, so content obscured by a
sticky header is not incorrectly considered visible.
- Preserve the merged Fragment scroll-ownership state machine: empty
Fragments remain unavailable targets, while real targets are classified
against the padding-aware viewport.
- Resolve pixel and percentage values and use the same visibility rule
in both the legacy element handler and the Fragment-ref handler.
- Add regression coverage showing that Next scrolls when the destination
is hidden inside the padding boundary, while preserving the current
scroll position when the destination is genuinely visible below it.
## Performance
The root computed style is resolved lazily, only after the candidate
produces client rects. Empty Fragments and hash navigations do not
perform this lookup. For a real route-scroll target, the resolved value
is cached locally and reused by the second geometry check after
`scrollTop = 0`.
This does not add work to scroll events or every render, and keeping the
value local allows responsive CSS, root classes, and custom properties
to change between navigations. In a 50-iteration dirty-style Chromium
probe, both the existing and updated paths reported 50 style
recalculations and 50 layouts, indicating that the lookup reused the
style/layout update already required by the handler in that test.
## Related work
#96342 is now merged into `canary` and handles empty Fragment scroll
ownership. This PR composes with its `NoClientRects` / `InViewport` /
`OutOfViewport` state machine by changing only the visible-region
boundary for real targets.
## Verification
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=false pnpm
test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/navigation-focus/navigation-focus.test.ts` (5/5)
- Manually compared the same sticky-header reproduction before and after
the change.
<!-- NEXT_JS_LLM -->
Closes#96339
Caches that started filling after a prerender was aborted would produce
an empty stream, because an aborted `renderSignal` made [the signal we
pass to `prerender()` inside `use-cache-wrapper`] be aborted
immediately. So we produced an empty entry and saved it, essentially
poisoning the cache.
Fixed by removing `renderSignal` from the `AbortSignal.any(...)` we pass
in and returning a rejected promise before we get to the cache filling
codepath. This should not be observable to userspace because the
prerender is already aborted.
## Summary
Enable `experimental.turbopackFileSystemCacheForBuild` by default in all
environments, including generic CI. Explicitly setting the option to
`false` remains the opt-out. Update the focused coverage and
documentation to match.
## Verification
- `IS_TURBOPACK_TEST=1 pnpm test-start-turbo
test/e2e/filesystem-cache/build-cache-default.test.ts`
- `pnpm prettier --with-node-modules --ignore-path .prettierignore
--check packages/next/src/server/config-shared.ts
test/e2e/filesystem-cache/build-cache-default.test.ts
docs/01-app/03-api-reference/08-turbopack.mdx
docs/01-app/03-api-reference/05-config/01-next-config-js/turbopackFileSystemCache.mdx`
- Not run: `pnpm --filter=next build` (the checkout has 11 pre-existing
TypeScript errors in unrelated files)
<!-- NEXT_JS_LLM -->
## Summary
When Pages Router and App Router routes coexist, a Pages Router route
that returns `notFound` will internally hand rendering to the App Router
not-found page. Next.js [prefers the App Router `/_not-found` entry for
404
responses](https://github.com/vercel/next.js/blob/5f5e4773bbb54f19697063ea093e8d89ca7d081a/packages/next/src/server/base-server.ts#L2965-L2980)
before falling back to a Pages Router `/404` page.
When deployed via an adapter, this means it would have attempted to
resume the Pages entry, which wouldn't contain the relevant prerendered
shell or postponed state. If the not-found page suspends, its response
cannot be resumed using the metadata associated with the Pages Router
output.
For adapter requests, this marks the handoff with an empty postponed
state. This uses the existing resume protocol to perform a complete,
non-cacheable dynamic render. `next start` continues to use the normal
PPR path, where it can load and resume the App Router not-found output
directly.
This trades PPR shell reuse for correctness only during this
cross-router not-found handoff. Longer term, adapters should have an
explicit contract for cross-output handoffs so they can select and
resume the App Router not-found output normally, without necessarily
declaring the pages router output PPR-capable.
The change also documents the empty postponed-state contract and adds
deployment regression coverage.
## Verification
- Added an end-to-end test that verifies a Pages Router `notFound`
response includes the dynamic App Router not-found content when
deployed.
- `pnpm testheadless test/e2e/app-dir/pages-router-app-not-found`
- `pnpm --filter=next types`
<!-- NEXT_JS_LLM -->
### What?
Makes `experimental.turbopackFileSystemCacheForBuild` (Turbopack's
on-disk
filesystem cache for `next build`) enabled by default, matching the
dev-mode
flag. It stays disabled by default in non-Vercel CI environments, and
can
always be turned off explicitly with
`experimental.turbopackFileSystemCacheForBuild: false`.
### Why?
The build filesystem cache greatly speeds up warm builds, but was
previously
opt-in (only auto-enabled for canary builds on the Vercel builder).
Defaulting
it on gives everyone faster subsequent builds, while keeping it off in
non-Vercel CI where the cache is unlikely to persist between builds.
### How?
- `turbopackFileSystemCacheForBuildDefault()` now returns `true` except
when
`isCI && !NOW_BUILDER` (removed the `isStableBuild()` gate).
- The explicit `false` opt-out already works via the existing
`config.experimental?.turbopackFileSystemCacheForBuild || false` read.
- Adds an e2e test (`test/e2e/app-dir/turbopack-fs-cache-build-default`)
asserting `.next/cache/turbopack` is written by default, empty when the
flag
is `false`, and empty in non-Vercel CI.
- Updates the `turbopackFileSystemCache` / Turbopack docs.
<!-- NEXT_JS_LLM -->
---------
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>
An unhandled rejection was previously logged by up to three independent
process listeners at once: the render runtime's crash-prevention handler
in `process-error-handlers.ts` (a bare `console.error`), the router
server's `Log.error('unhandledRejection: ', err)`, and the dev server's
`logErrorWithOriginalStack`. The runtime handler must exist on every
deployment target (#77997), but on self-hosted `next start`/`next dev`
it shares a process with the router server's and dev server's listeners,
so a single rejection was logged multiple times in different formats.
THe first commits adds a test showing the current behavior where we log
multiple times.
The second commit introduces `registerUnhandledRejectionListener` and
`isUnhandledRejectionListenerRegistered` in `process-error-handlers.ts`,
and converts the router server and dev server to check-then-register
instead of installing their own rejection loggers:
- The listener function is shared via a `Symbol.for` key on
`globalThis`, so multiple copies of the module (e.g. in the pre-compiled
server bundle and in a route module bundle) register and detect a single
listener instance.
- The registration check queries
`process.listeners('unhandledRejection')` instead of a module-global
flag, so it stays accurate even after external code calls
`process.removeAllListeners`. `installProcessErrorHandlers` therefore
calls the register function unconditionally.
The `uncaughtException` handlers are left as they are; they have the
same duplication and could be consolidated the same way in a follow-up.
## Summary
Fix the build output for App Router `not-found` routes that get
partially prerendered when cacheComponents is enabled.
Previously, Next.js treated any statically generated `/_not-found` route
as a complete static 404, even when it contained postponed state. It
copied the partial HTML shell to `server/pages/404.html` and omitted the
corresponding `/_not-found` prerender from adapter outputs.
This change distinguishes fully static not-found routes from partially
prerendered ones. Fully static routes retain the existing `404.html`
behavior, while PPR routes remain resumable prerender outputs.
This allows a `not-found.tsx` that suspends under Cache Components to
render its dynamic content when deployed.
Closes https://github.com/vercel/next.js/pull/96399
## Testing
Expanded `not-found-non-document-dynamic` coverage to verify that:
- dynamic not-found content renders for ordinary missing paths
- dynamic not-found content renders when `/_not-found` is selected by a
rewrite
- a PPR not-found does not emit `server/pages/404.html`
- its adapter output includes `PARTIALLY_STATIC` metadata, postponed
state, and a resume chain
Also verified that fully static App Router not-found routes continue to
emit and serve `pages/404.html`.
- `__NEXT_CACHE_COMPONENTS=true pnpm test-start-turbo
test/e2e/app-dir/not-found-non-document-dynamic/not-found-non-document-dynamic.test.ts`
- `pnpm test-start-turbo test/e2e/app-dir/not-found/basic/index.test.ts`
Built using @sokra's Fleet! This re-arranges all of our chunking
experimental features under `experimental.turbopackChunking` and adds
the following options:
```javascript
/**
* Avoid creating more than one chunk smaller than this size, in bytes. Smaller
* chunks are merged into bigger ones to avoid that. Defaults to `50000` (50 KB).
*/
minChunkSize?: number
/**
* Avoid creating more than this number of chunks per chunk group. Chunks are
* merged into bigger ones to avoid that. Defaults to `40`.
*/
maxChunkCountPerGroup?: number
/**
* Never merge chunks bigger than this size, in bytes, with other chunks. This keeps code
* in big chunks from being duplicated across multiple chunks. Defaults to `200000` (200 KB).
*/
maxMergeChunkSize?: number
/**
* Minimum size, in bytes, for a component chunk to be emitted on its own when
* `generateComponentChunks` is enabled. Component chunks smaller than this are folded into a
* single remainder chunk. Defaults to `20000` (20 KB).
*/
minComponentChunkSize?: number
```
When a static fallback shell is served as-is — e.g. from a CDN in a
deployed environment — the client can't hydrate from inline Flight data;
it fetches the full RSC payload separately and reconstructs the initial
payload on the client. During that reconstruction, the head's partiality
flag was accidentally set to the head itself. Since the head is always
truthy, the head was unconditionally treated as partial.
This happens to be unobservable today: the only flows that reach the
reconstruction are fallback-shell prerenders, where the server
pessimistically marks the head partial anyway, and when Cache Components
is enabled the client ignores the per-payload flag entirely in favor of
response-level partiality. But we're about to rework the RSC response
format so that head partiality has a single, load-bearing source of
truth, so the flag needs to survive the reconstruction correctly rather
than by coincidence.
While auditing the payload consumers, I also fixed the payload shape
check in segment prefetch generation (it used `&&` where `||` was
intended, so it accepted any shape) and deleted
`should-hard-navigate.ts`, which no longer has any callers on the
client.
Adds e2e coverage asserting that metadata survives hydrating a fallback
shell page: the title is correct after hydration, the head is included
in prefetched data, and a return navigation is served entirely from the
cache that was populated during initial hydration. In deployed
environments this exercises the client resume path, which is where the
reconstruction runs.
## Summary
`shouldWaitOnAllReady` previously allowed an otherwise dynamic App
Router render to wait for every Suspense boundary before sending the
response. After [#96367](https://github.com/vercel/next.js/pull/96367),
no caller enables this behavior anymore.
This removes the unused option and makes `supportsDynamicResponse` the
sole signal for whether App Router generates static HTML. Static
generation still waits for `allReady`, while dynamic responses continue
streaming.
<!-- NEXT_JS_LLM -->
## Summary
DOM-capable crawlers can consume streamed metadata, but with
cacheComponents enabled, it treated them like HTML-limited bots and
forced a full dynamic render. This meant capable crawlers could not use
the prerendered shell even when they were not matched by
`htmlLimitedBots`.
This change lets DOM-capable crawlers follow the normal PPR shell and
streaming-metadata path. `htmlLimitedBots` remains the source of truth
for requests that need blocking metadata, including when a custom
pattern explicitly matches a DOM-capable crawler.
## Test Plan
Tests were added that contained a Suspense boundary that intentionally
never resolves. The test reads the response stream until it receives the
fallback, then verifies that blocking metadata has already been emitted
while the unresolved page content has not. A fully buffered response
would never return, so this avoids relying on timing or arbitrary
delays.
This PR is stacked on #96366.
## Summary
- treat Fragment instances without client rects as unavailable route
scroll targets
- leave the shared scroll signal unconsumed so another changed segment
can handle an empty route Fragment
- avoid focus and scroll side effects when an empty segment cannot claim
ownership
- consume and clear an explicit hash intent when no matching target
exists, without falling back to route content
- cover empty and visible intercepted routes across both scroll
handlers, including focus preservation and real hash scrolling
This PR is stacked on #96323, which provides the minimal reproduction.
## Verification
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/navigation-focus/navigation-focus.test.ts` (5/5)
<!-- NEXT_JS_LLM -->
Closes NAR-854
### TLDR
New approach to #94949. We can work around the problem of debug info
getting clamped when re-serializing by doing more work, which should be
good enough unless we can figure out the correct way to adjust the
clamping behavior in react.
### The problem
Before this PR, instant validation works like this:
1. deserialize the full page payload (in stages)
2. reserialize each segment (in stages)
3. reassemble the segments into a combined payload, varying the stage
per segment as needed
Unfortunately, in step 2, we lose timing debug info that tells us when
IO operations started/ended:
https://github.com/facebook/react/blob/c235eb40f1c3121798d233bf992fd8f8c8558d06/packages/react-server/src/ReactFlightServer.js#L5550-L5553
This means that even if we provide an appropriate `endTime` during step
3, no IO info will be omitted by `filterDebugInfo`, because it was all
re-timed to when the render occurred. In practice, this means that when
we report a blocking dynamic hole, it might point to a different await
than the one that was actually blocking in the stage that we rendered
the segment in.
### The solution
(h/t to @gnoff for coming up with this trick)
We can work around this problem with no changes in React. We just have
to apply an `endTime` when deserializing the payload for the first time,
before reserializing each segments. However, this means that we can no
longer disassemble the payload into segments in one pass -- e.g. if we
trim the debug info to what it'd be in the `Static` stage, we can't get
good data for the `Runtime` stage anymore. So we have to run the segment
disassembly separately for each stage we care about.
The debugInfo can now vary depending on the stage, we need to change the
shape of `SegmentCacheItem`. Previously, it was like this:
```
'/foo':
chunks:
Static: [...]
Runtime: [...]
Dynamic: [...]
debugChunks: [...]
```
But to account for the varying debug info, we now need to store the
partial chunks, the complete chunks, and the debug chunks separately for
each stage:
```
'/foo':
Static:
chunks: [...]
allChunks: [...]
debugChunks: [...]
Runtime:
chunks: [...]
allChunks: [...]
debugChunks: [...]
Dynamic:
...
```
Most of the changes in the PR come from adjusting the code to
- create separate entries for each segmentPath + stage in the new way
outlined above, using an `endTime` based on the stage
- consume the cache entry appropriately
## Summary
When PPR is enabled, requests matching a custom `htmlLimitedBots`
pattern could still use the prerendered route path. As a result, the
configured blocking-metadata behavior was not applied consistently.
Use the existing streaming-metadata decision when selecting the PPR
render path. Built-in bots retain their existing fully buffered
behavior, custom HTML-limited bots can continue streaming body content
after metadata resolves, and regular user agents remain unchanged.
## Verification
- HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/html-limited-bots-ppr/html-limited-bots-ppr.test.ts
- HEADLESS=true pnpm test-start-webpack
test/e2e/app-dir/html-limited-bots-ppr/html-limited-bots-ppr.test.ts
Co-authored-by: Sebastian Silbermann <sebastian.silbermann@vercel.com>
Note that https://github.com/advisories/GHSA-6g55-p6wh-862q does not
affect Next.js users unless they build from untrusted source code (which
would have more severe security implications). We're merging this to
reduce noise from security scanners.
- 8.5.21 is 50h+ old so it can be installed
- 8.5.22 is still under 48h - valid at 2026-07-24 08:48:15 UTC
Closes: https://github.com/vercel/next.js/issues/96349
A HTTP request can render the same React tree in multiple passes with
different semantics for `connection()`: the prospective and final
prerenders of a runtime prefetch, or a navigation's dynamic render and
the runtime prerender that is spawned from it to refresh the client's
prefetch cache. All of these passes previously resolved `headers()` to
the single sealed headers object created lazily by the request store, so
userland caches keyed on the identity of `await headers()` (any
per-request memoization that treats the headers object as "the request")
leaked promises across passes.
With this change, every render pass resolves `headers()` to a distinct
object over the same underlying data, making "the headers object
identifies the request within one render pass" a real contract.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
In #96074 I put the `partialFallback` behavior behind the
`partialPrefetching` flag, so that we don't risk an explosion in ISR
costs for apps that haven't opted into Partial Prefetching. But it turns
out I only handled this for deploy mode — i.e. the build output consumed
by the Vercel adapter. The behavior was still unconditionally on for
`next start`.
The reason it was easy to miss is that `next start` and the adapter
express partial fallback shells through completely separate mechanisms.
The adapter emits a `partialFallback` flag into the build output and
lets the platform's ISR layer perform the shell upgrade. `next start`
has no such flag; the server performs the upgrade itself, in the
compiled page runtime. So the gate I added to the adapter output had no
effect on the self-hosted path.
To understand how the `next start` path was configured, I looked at the
PR that removed the original top-level `partialFallbacks` config flag
(#93859). Before that PR, the behavior was gated in the runtime; when
the flag was removed those gates were deleted and the behavior became
always-on.
The important subtlety is that only the *upgrade* should be gated, not
the shell machinery as a whole. On `next start`, the value that decides
whether a shell can be specialized (`remainingPrerenderableParams`) also
drives core Cache Components behavior — build-time partial prerendering
of params and serving build-time sub-shells — which must stay on
regardless of `partialPrefetching`. So this gates only the two things
that make up the "upgrade on first request" cost risk: the background
ISR revalidation that specializes a shell per request, and the
client-facing `isFallbackUpgradeable` signal that tells the client to
retry a prefetch waiting for that upgrade.
Adds coverage with a `partialPrefetching`-disabled fixture asserting
that fallback shells stay shared rather than specializing per request.
This lets next detect if it is in a worktree and find the main repo and
copy turbopack caches for build or dev to make initial build faster. I
wasn't sure about commiting tests for this and how exactly we'd won't to
go about this with the git requirement. I also could do some unit tests,
but following the library conventions there wasn't a nice abstracted out
file system. So I did various tests locally. If anyone has good ideas on
what kind of testing totally opened to it.
Also left a comment that I don't look in other worktrees but instead the
root project. Not sure if this is the right answer or not. But works for
the patterns I think people typically follow.