[Instant] Instant validation in Dev (#89077)
This PR implements the initial version of dev-time validation for
`export const instant = ...`
```tsx
export const instant = { prefetch: 'static' }
```
```tsx
export const instant = { prefetch: 'runtime', ... }
```
```tsx
export const instant = false
```
(the config is currently expected to be named `unstable_instant`, but
i'll be shortening it to `instant` below.)
When a segment specifies an instant config with `prefetch: 'static'` or
`prefetch: 'runtime'`, we'll validate that all (prefetched) navigations
into that segment will render instant UI, i.e. that the navigation won't
block. On the other hand, if a segment is allowed to block, it must be
marked with `export const instant = false`.
Note that our existing static shell validation is a special case of
this, but for now, we're keeping both. A future PR will reimplement
static shell validation as a special case of instant validation.
Like static shell validation, these validations currently run in dev
whenever we render the full page (i.e. no segments are omitted). This
means it'll happen on an initial load (or refresh) and HMR.
### Implementation notes
#### Validation approach
The goal is to simulate what the browser would display for a prefetched
client navigation. We do this by re-assembling the segments extracted
from the original stream for the page.
- The outer (shared) segments should already fully resolved (i.e. in the
Dynamic stage), to represent the fact that the browser loaded them
before
- The new segments are either in Static or Runtime stage, depending on
how each segment would have been prefetched.
Similar to static shell validation, we then perform a Fizz prerender on
the combined payload, abort it, and track the locations of `onError`
calls. The difference here is that unlike static shell validation, we
require a Suspense boundary *inside* the new subtree. This is detected
by adding a `InstantValidationBoundary` around the new subtree when we
construct the payload, and checking if there's a suspense below that in
the component stack.
Currently, for each `instant` config we find, we take each of its parent
layouts as a possible navigation parent (unless it's marked as
blocking). So if we have a page with parent layouts like this:
```
/layout.tsx
/foo/layout.tsx
/foo/bar/layout.tsx
/foo/bar/page.tsx <- `export const instant = { prefetch: 'static' }`
```
Then we'll check for navigations where each of `/layout.tsx`,
`/foo/layout.tsx` and `/foo/bar/layout.tsx` is a fully resolved shared
parent.
If a validation fails, we do a "discriminated error message" flow
analogous to static shell validation. Static segments are replaced with
Runtime segments to see if the error goes away, which lets us determine
if the hole is caused by runtime or dynamic data.
#### Building the combined payload
First, we need to separate the full stream into segments. The process is
similar to `collectSegmentData` -- we need to deserialize the payload
from the stream and then re-serialize each segment separately. The
complexity here comes from the fact that the full stream is separated
into stages, and we need each segment to be in staged form as well. This
lets us pick and choose which stage the segment is in to simulate what
we'd show for a static or runtime prefetch.
The other complex part is the "late release trick" (as implemented in
`createNodeStreamWithLateRelease`). This was already done for static
shell validation, but it's more complex here.
Debug info for dynamic holes is usually delayed until a further stage.
So, in the static stage we only see an unresolved promise (or lazy
reference), and then in the runtime stage we get the debugInfo telling
us that it was caused by `await cookies()`. To ensure that this debug
info is available for purposes on error reporting, we do a "late
release" -- before we abort, we advance the each segment to the dynamic
stage, which won't cause any new content to render, but will provide
Fizz with debug info and thus give us precise error locations in
`onError`. If you see `releaseSignal`, that's what it's for.
#### `instant` on layouts
We support specifying `instant` on layouts. The validation principle is
the same, but note that in the current mode (validating a single page in
dev) we can't enforce that *all* navigations into that layout satisfy
the constraint, we can only check if that's satisfied for the current
page.
#### Blocking segments
A segment can be marked as blocking with
```tsx
export const instant = false
```
This is meant to signal that this segment deliberately does not have
instant UI.
Currently, this is affects validation as follows:
If there's no `instant` config in parent segments, skip validating all
navigations where this layout would be new (because we know it'd block),
i.e. only validate navigations where it's a shared parent. Children of
this segment can still have `instant` configs of their own, and those
will be validated. This allows structures where a layout is blocking,
but once it's loaded, navigations within it should have instant UI.
If there is an `instant` config in a parent segment, the validation of
that parent is *not* skipped. The reasoning here is that if a layout
asserts that navigations into it should have static UI, then a child
with `instant = false` should not violate that. This essentially
requires that the child have a `loading.js` or that the layout has to
wrap the slot with Suspense.
Note that `instant = false` can also be used to opt segements out of
static shell validation without requiring a Suspense above body.
### Limitations and planned follow-ups
- `instant` configs in parallel routes (i.e. `@slot`) are not currently
validated.
- `expectUnableToVerify` is not implemented yet, i.e. there's no way to
say "this cannot be validated using SSR". The shared segments are not
expected to suspend. For now, `export const instant = { prefetch: ...,
unstable_disableValidation: true }` can be used to bypass a segment if
validating it causes problems.
- static shell validation is a special case of instant validation, but
is not currently implemented as such.
- we should be able to validate during client navigations, not just
full-page loads and HMR, but we currently can't because we don't have
all the segments when we do that, so we can't assemble a proper combined
payload.