mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Initial version of build-time instant validation.
During prerendering, if we have `unstable_instant` configs, we perform a
complete staged render of the page (including dynamic data). This render
mocks request data (`params`/`searchParams`/`cookies`/`headers`) using
the `samples` provided in `unstable_instant`. Then, we use the staged
chunks to run instant validation in mostly the same way that we would in
dev. Most changes in the validation flow are related to samples, which
also need to be passed into the client render to mock APIs like
`useParams`.
### Providing Mock data
We run everything in a fresh workStore+workUnitStore to (hopefully)
avoid any data from the prerender from sneaking in. This is quite
involved.
A big part of the trickiness is that if you do e.g. `(await
cookies()).get('myCookie')` but the `samples` don't contain `myCookie`,
we want the validation to error and require explicitly saying what state
`myCookie` is in.
If it's available, use
```ts
export const unstable_instant = {
// ...
samples: { cookies: [{ name: "myCookie", value: "someValue" }] }
}
```
If it's not available, use
```ts
export const unstable_instant = {
// ...
samples: { cookies: [{ name: "myCookie", value: null }] }
}
```
This explicitness makes it harder to e.g. unintentionally validate a
logged-out state.
Note that even validating a `prefetch: 'static'` page might require
samples, because we need the shared parent for the navigation to be
(mostly) complete. We might be able to relax this in the future, but
it's not straightforward.
Another issue is that a segment might access a missing sample in a
"benign" way, i.e. where it wouldn't block validation (or be inside a
suspense boundary). Ideally we wouldn't require providing a sample for
usages like that, but currently we do.
### Instrumenting APIs to detect missing samples
All request APIs are instrumented in this way.
- `params` throw when accessing a property that was not declared in
`samples`. same for the object from `useParams()`. `in` is allowed
because we know the shape of the params object statically, based on the
routing structure.
- `searchParams` throw when accessing a property that was not declared
in `samples` and `in` checks.
- objects returned from `cookies()`, `headers()`, `useSearchPrams()`
throw on `get()` and `has()`.
There's a lot of tests for this behavior in `instant-validation-build`.
One gap is that iteration/enumeration is currently not instrumented.
We're punting on it for now and will revisit in a follow-up.
### Relation to prerender
We kick off the validation after the prerender, in
`renderToHTMLOrFlightImpl`. Ideally, we should kick this off somewhere
higher up, separately from the prerender itself, but we're leaving that
for future work.
Note that if we do multiple prerenders for the same page (e.g. due to
`generateStaticParams`, we'll only run validation for the first
prerender for that route. `generateStaticParams` is not currently
integrated into the validation at all, i.e. we don't use those params to
validate, so this is safe. All params need to be provided via `samples`.
This will likely change in the future, but we're not sure how yet.
### Disabling build validation
Build-time instant validation is experimental, and has more potential
problems to deal with than dev validation. To allow incremental rollout
and testing, we allow disabling it per instant config:
```ts
export const unstable_instant = {
unstable_disableBuildValidation: true
// ...
}
```
for parity, there's also `unstable_disableBuildValidation`, but we don't
expect that to see much use.
71 lines
2.0 KiB
TypeScript
71 lines
2.0 KiB
TypeScript
/**
|
|
* This helper is used for testing code that should throw, without swallowing the error.
|
|
*
|
|
* It calls `callback` and checks if it throws.
|
|
* If it did, we rethrow the error.
|
|
* If it didn't throw, we throw a AssertionError/ClientAssertionError.
|
|
* */
|
|
export function ensureThrows(callback: () => any, message?: string): never {
|
|
let thrown: { value: unknown } | null = null
|
|
try {
|
|
callback()
|
|
} catch (err) {
|
|
thrown = { value: err }
|
|
}
|
|
|
|
if (thrown) {
|
|
// The callback threw, as expected.
|
|
throw thrown.value
|
|
} else {
|
|
// The callback didn't throw like it should have.
|
|
message ??= 'Expected callback to throw'
|
|
if (typeof window === 'undefined') {
|
|
const { fail } =
|
|
require('node:assert/strict') as typeof import('node:assert/strict')
|
|
fail(message)
|
|
} else {
|
|
throw new Error('This code should never run in the browser')
|
|
}
|
|
}
|
|
|
|
// For some reason, typescript doesn't believe that all codepaths above throw
|
|
throw new Error('Unreachable')
|
|
}
|
|
|
|
/**
|
|
* This helper is used for testing code that should throw, without swallowing the error.
|
|
*
|
|
* iT calls `callback` and checks if it rejected.
|
|
* If it did, we rethrow the error.
|
|
* If it didn't throw, we throw a AssertionError/ClientAssertionError.
|
|
* */
|
|
export async function ensureRejects(
|
|
callback: () => Promise<any>,
|
|
message?: string
|
|
): Promise<never> {
|
|
let thrown: { value: unknown } | null = null
|
|
try {
|
|
await callback()
|
|
} catch (err) {
|
|
thrown = { value: err }
|
|
}
|
|
|
|
if (thrown) {
|
|
// The callback rejected, as expected.
|
|
throw thrown.value
|
|
} else {
|
|
// The callback didn't reject like it should have.
|
|
message ??= 'Expected callback to reject'
|
|
if (typeof window === 'undefined') {
|
|
const { fail } =
|
|
require('node:assert/strict') as typeof import('node:assert/strict')
|
|
fail(message)
|
|
} else {
|
|
throw new Error('This code should never run in the browser')
|
|
}
|
|
}
|
|
|
|
// For some reason, typescript doesn't believe that all codepaths above throw
|
|
throw new Error('Unreachable')
|
|
}
|