Replicate production prefetch shells for instant navigations in dev (#95067)
The Instant Navigation lock makes a development render reflect the
route's prefetched state: while the lock is held, a navigation shows
only what was prefetched and keeps navigation-time data deferred. Both
the `instant()` testing API from `@next/playwright` and the Instant
Navigation devtools rely on it. The client side of that behavior,
restricting the navigation read to the prefetched shell unless the link
opts into a runtime prefetch and ignoring cache entries acquired before
the lock, landed separately in #95150, which this change builds on. What
remained were two ways the development app render diverged from what a
production build serves, both in `app-page.ts`, so this PR contains no
client-side router changes.
While the lock is held, the app render now permits an empty static
shell. A route that reads dynamic data such as `cookies()` outside any
`<Suspense>` boundary has no static shell, so the on-demand render
previously threw a static generation bailout, served an error page, and
entered a `/_tree` redirect loop that committed the blocked data.
Permitting an empty shell lets the render emit the shell instead. The
override is scoped to the prefetch render made while the lock is held, a
document request or a `'1'` static prefetch; a regular dynamic
navigation, including the one that commits once the lock releases, runs
without it. So the validation that flags a blocking route is not
weakened: production validation runs at build time, and the development
validation still runs and is shown in the dev overlay.
The development fallback-shell render now reads the per-URL
`fallbackParams` request meta that base-server derives for the requested
URL, so the instant shell defers exactly the params a production
prefetch would: `generateStaticParams`-covered params resolve in the
shell and only the uncovered ones are deferred. When the URL is fully
covered the meta is absent and nothing is deferred. That per-URL
computation landed in #95066, which this change depends on.
This suite now runs with App Shells enabled, which is the default under
Cache Components. #94516 had temporarily forced it back to `false` here
while the prefetch behavior settled and left the migration as a
follow-up; re-enabling it is what lets these tests exercise the
app-shell prefetch path the changes above target.
New end-to-end coverage exercises the cases this render path affects. A
deeper-segment blocking navigation must stay parked on the committed
parent while the lock is held. Two mixed routes pair a
`generateStaticParams`-covered param with an uncovered one: a plain
route where a normal navigation surfaces only the covered param while
the uncovered param and a request-time `connection()` sibling stay
deferred, and an `allow-runtime` route where a `prefetch={true}` link
additionally surfaces the uncovered param from the runtime prefetch. A
blocking route that reads request data outside any `<Suspense>` boundary
cannot be built for production, so it lives in a development-only
fixture, guarded so the production job registers only a placeholder. The
default fixture moves the dev-tools indicator to `bottom-right` so the
Instant Navigation panel does not overlap the left-aligned test links
during a navigation.
One client-navigation cookie case is marked `it.failing`: on a
non-partial route the speculative static prefetch is fuller than the
app-shell render and supersedes it in the segment cache, so the cookie
that only the app shell carries never reaches the instant shell.
#95150's shell handling only engages under partial prefetching. Closing
this needs a separate server-side change so that a route reading
`cookies()` during app-shell generation opts into either partial
prefetching, where only the app shell is fetched and nothing fuller can
supersede it, or a runtime prefetch, where the speculative prefetch
carries the cookie and no longer regresses what the app shell initially
showed.