## Summary
- migrate Cache Components, PPR, prefetching, prerendering,
revalidation, and SSG deployment exclusions to `@force-gate`
- remove the corresponding `skipDeployment` options and `skipped`
control flow while preserving each suite's rationale
This keeps caching-related exclusions together so their deployed
semantics can be reviewed by the same owners.
## Verification
- verified on the combined top-of-stack tree with `pnpm typescript`
- compared ordinary-mode collection before and after: all 4,670 existing
test names matched
- collected all 510 affected files in deploy mode: 4,578 skipped tests,
zero failures
<!-- NEXT_JS_LLM -->
The fix implemented in #95665 (`params/searchParams` not hanging
correctly in Runtime App Shells during prospective prerenders) was
incomplete. We started using `makeHanging[Search]Params` in the
prospective prerender (if it was an app shell and wouldn't resolve
them), but not during the final prerender. This meant that, when
encoding the cache key, we would only fire `dynamicAccessAbortSignal` in
the prospective prerender:
https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2160-L2174
...but the final prerender wasn't using `makeHanging[Search]Params`, so
it would not hit this early exit. The hanging input abort signal would
still correctly abort the key encoding (because it accounts for
`stageController.finalStage`) so the key would still be partial, as
expected. We would continue on, and attempt to look the key up in the
Resume Data Cache. But the prospective prerender exited early and never
put anything under that key, so we would incorrectly end up here, and
log:
https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2730-L2734
This is fixed by consistently using `makeHanging[Search]Params` if the
runtime prerender won't reach the stage where they resolve, i.e. if it's
an App Shell. This means we'll trigger `dynamicAccessAbortSignal` while
encoding the cache key in *both* renders, so we'll do the early exit
consistently and avoid the log.
(This feels a bit unsatisfying, because ideally this would all just work
based on stages, which is what I idealistically thought when writing the
code originally. But unfortunately our usage of
`dynamicAccessAbortSignal` for `params/searchParams` requires these
special cases to work consistently. Maybe this can be improved in the
future.)
### Tests
This scenario is covered by the existing `prefetch-runtime` tests -- i
discovered the bug by running them and seeing the `Unexpected cache miss
after cache warming phase` warning. We now assert that the warning is
not logged. The assertion is negative and would also pass if the error
message changed, so i've exported the message as a constant to ensure
`use-cache-wrapper` and the tests stay in sync.
The handing for page components marked with `use cache: private` was
assuming that searchParams can never hang in `prerender-runtime`, so it
passed the serialized `innerSearchParams` to the page, which we expected
to just be a resolved promise. This *used to* be correct when runtime
prerenders always had search params available, because the serialized
search params were always equivalent to `outerSearchParams`.
However, for `partialPrefetching` we started using `prerender-runtime`
for runtime shells, where search params are not available, and if a
private cache awaits them, it needs to abort filling and become dynamic,
same as when it awaits params. So we need to trigger
`dynamicAccessAbortController` when `searchParams` is awaited:
```tsx
export default async function Page({ searchParams }) {
'use cache: private'
// this should trigger `dynamicAccessAbortController.abort()`, so we need the original instrumented
// searchParams object instead of the serialized one
await searchParams
}
```
This is fixed by using the outer searchParams object in the
`isPageSegmentFunction` codepath of `use-cache-wrapper` -- we're
preserving the instrumented promise, so the cache prerender will abort
as expected.
(before this fix, a shell prefetch of such a page would get a
deserialized hanging promise for `searchParams` and hang until it times
out)
2 of the 3 newly added tests ("params in a public cache" and "params in
a private cache") were already passing, because `params` was already
being preserved, but i'm adding them here to prevent regressions. The
"search params in a private cache" one was failing due to the cache
timing out, and is now passing. I'm not covering "search params in
public cache" because that's an error and is already tested elsewhere.
### Long story short
Fixes an analogous bug to #95665, but affecting
`prefetch()/navigation()`. Those two were accidentally always resolving
in the **prospective** runtime prerender, so we were warming content
that isn't needed for the final runtime prerender. We now guard them
properly.
Also, I replaced `PrerenderStoreModernRuntime.isSessionShell` with
`finalStage` (the same stage we're gonna use for the final prerender),
so we can include/exclude `navigation` properly -- the boolean only
distinguished shell/prefetch, not navigation.
### Long story long
We use runtime prerenders for three things: runtime shells, runtime
prefetches, and embedding a prefetch in navigations. These are aborted
after `ShellRuntime`, `Runtime`, and `NavigationRuntime` respectively
(accessible in `stageController.finalStage`). If we're doing the
**final** prerender for a shell, promises that would resolve in e.g.
`NavigationRuntime` would never resolve, because we abort the prerender
before we get there.
However, the **prospective** runtime prerender has no
`StagedRenderingController` and needs to model this separately (so that
the same APIs hang in both). This was [previously done via
`prerenderStore.isSessionShell`](https://github.com/vercel/next.js/pull/95665)
which we checked in URL data apis i.e. `params/searchParams` and the
metadata`pathname` to do this. `isSessionShell: true` meant they should
hang because it's a shell, and `isSessionShell: false` meant they should
resolve.
However, I forgot about this case when implementing
`navigation`/`prefetch`, and made them resolve unconditionally in the
prospective prerender. This means we'd potentially warm content that
would not actually be reached in the final prerender. In other words,
```ts
await navigation()
return <Expensive />
```
would needlessly render `<Expensive />` during the prospective render
but not in the final one. This kinda defeats `navigation`'s main
purpose, i.e. avoiding expensive work.
This PR fixes the bug by replacing `isSessionShell: boolean` with
`finalStage: AdvanceableRenderStage`, which is set to the same value we
put in `stageController.finalStage`. This lets us keep the logic of what
hangs and what resolves relatively close in both prerenders.
---
On a meta level, the fact that the same bug happened twice tells me that
having a `StageRenderingController` that is sometimes `null` is
error-prone, and we should figure out a better solution, but i'm gonna
keep this fix targeted and figure out a more holistic solution in a
follow-up