Skip to content

Commit 4c4a5bc

Browse files
gnofflubieowoce
andauthored
Implement the updated prerender API shape (#84254)
This does not add any validation but it makes the interface for prerender match out intended API shape when validation is landed --------- Co-authored-by: Janka Uryga <[email protected]>
1 parent 86e006b commit 4c4a5bc

File tree

28 files changed

+235
-40
lines changed

28 files changed

+235
-40
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
title: Invalid Prefetch Configuration
3+
---
4+
5+
## Why This Message Occurred
6+
7+
You provided an invalid configuration for `export const unstable_prefetch` in a Layout or Page file.
8+
9+
### Example of Correct Usage
10+
11+
#### Static Prefetching
12+
13+
```tsx filename="app/.../layout.tsx"
14+
export const unstable_prefetch = {
15+
mode: 'static',
16+
}
17+
```
18+
19+
#### Runtime Prefetching
20+
21+
```tsx filename="app/[slug]/page.tsx"
22+
export const unstable_prefetch = {
23+
mode: 'runtime',
24+
samples: [
25+
{
26+
cookies: [{ name: 'experiment', value: 'A' }],
27+
params: { slug: 'example' },
28+
},
29+
],
30+
}
31+
```

packages/next/src/build/segment-config/app/app-segment-config.ts

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,91 @@
11
import { z } from 'next/dist/compiled/zod'
22
import { formatZodError } from '../../../shared/lib/zod'
33

4+
const CookieSchema = z
5+
.object({
6+
name: z.string(),
7+
value: z.string(),
8+
httpOnly: z.boolean().optional(),
9+
path: z.string().optional(),
10+
})
11+
.strict()
12+
13+
const RuntimeSampleSchema = z
14+
.object({
15+
cookies: z.array(CookieSchema).optional(),
16+
headers: z.array(z.tuple([z.string(), z.string()])).optional(),
17+
params: z.record(z.union([z.string(), z.array(z.string())])).optional(),
18+
searchParams: z
19+
.record(z.union([z.string(), z.array(z.string()), z.undefined()]))
20+
.optional(),
21+
})
22+
.strict()
23+
24+
const StaticPrefetchSchema = z
25+
.object({
26+
mode: z.literal('static'),
27+
from: z.array(z.string()).optional(),
28+
expectUnableToVerify: z.boolean().optional(),
29+
})
30+
.strict()
31+
32+
const RuntimePrefetchSchema = z
33+
.object({
34+
mode: z.literal('runtime'),
35+
samples: z.array(RuntimeSampleSchema).min(1),
36+
from: z.array(z.string()).optional(),
37+
expectUnableToVerify: z.boolean().optional(),
38+
})
39+
.strict()
40+
41+
const PrefetchSchema = z.discriminatedUnion('mode', [
42+
StaticPrefetchSchema,
43+
RuntimePrefetchSchema,
44+
])
45+
46+
export type Prefetch = StaticPrefetch | RuntimePrefetch
47+
export type PrefetchForTypeCheckInternal = __GenericPrefetch | Prefetch
48+
// the __GenericPrefetch type is used to avoid type widening issues with
49+
// our choice to make exports the medium for programming a Next.js application
50+
// With exports the type is controlled by the module and all we can do is assert on it
51+
// from a consumer. However with string literals in objects these are by default typed widely
52+
// and thus cannot match the discriminated union type. If we figure out a better way we should
53+
// delete the __GenericPrefetch member.
54+
interface __GenericPrefetch {
55+
mode: string
56+
samples?: Array<WideRuntimeSample>
57+
from?: string[]
58+
expectUnableToVerify?: boolean
59+
}
60+
interface StaticPrefetch {
61+
mode: 'static'
62+
from?: string[]
63+
expectUnableToVerify?: boolean
64+
}
65+
interface RuntimePrefetch {
66+
mode: 'runtime'
67+
samples: Array<RuntimeSample>
68+
from?: string[]
69+
expectUnableToVerify?: boolean
70+
}
71+
type WideRuntimeSample = {
72+
cookies?: RuntimeSample['cookies']
73+
headers?: Array<string[]>
74+
params?: RuntimeSample['params']
75+
searchParams?: RuntimeSample['searchParams']
76+
}
77+
type RuntimeSample = {
78+
cookies?: Array<{
79+
name: string
80+
value: string
81+
httpOnly?: boolean
82+
path?: string
83+
}>
84+
headers?: Array<[string, string]>
85+
params?: { [key: string]: string | string[] }
86+
searchParams?: { [key: string]: string | string[] | undefined }
87+
}
88+
489
/**
590
* The schema for configuration for a page.
691
*/
@@ -43,7 +128,7 @@ const AppSegmentConfigSchema = z.object({
43128
* How this segment should be prefetched.
44129
* (only applicable when `clientSegmentCache` is enabled)
45130
*/
46-
unstable_prefetch: z.enum(['unstable_static', 'unstable_runtime']).optional(),
131+
unstable_prefetch: PrefetchSchema.optional(),
47132

48133
/**
49134
* The preferred region for the page.
@@ -80,11 +165,22 @@ export function parseAppSegmentConfig(
80165
): AppSegmentConfig {
81166
const parsed = AppSegmentConfigSchema.safeParse(data, {
82167
errorMap: (issue, ctx) => {
83-
if (issue.path.length === 1 && issue.path[0] === 'revalidate') {
84-
return {
85-
message: `Invalid revalidate value ${JSON.stringify(
86-
ctx.data
87-
)} on "${route}", must be a non-negative number or false`,
168+
if (issue.path.length === 1) {
169+
switch (issue.path[0]) {
170+
case 'revalidate': {
171+
return {
172+
message: `Invalid revalidate value ${JSON.stringify(
173+
ctx.data
174+
)} on "${route}", must be a non-negative number or false`,
175+
}
176+
}
177+
case 'unstable_prefetch': {
178+
return {
179+
// @TODO replace this link with a link to the docs when they are written
180+
message: `Invalid unstable_prefetch value ${JSON.stringify(ctx.data)} on "${route}", must be an object with a mode of "static" or "runtime". Read more at https://nextjs.org/docs/messages/invalid-prefetch-configuration`,
181+
}
182+
}
183+
default:
88184
}
89185
}
90186

@@ -137,7 +233,7 @@ export type AppSegmentConfig = {
137233
* How this segment should be prefetched.
138234
* (only applicable when `clientSegmentCache` is enabled)
139235
*/
140-
unstable_prefetch?: 'unstable_static' | 'unstable_runtime'
236+
unstable_prefetch?: Prefetch
141237

142238
/**
143239
* The preferred region for the page.

packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ ${
5252
: `import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'`
5353
}
5454
55+
import type { PrefetchForTypeCheckInternal } from 'next/dist/build/segment-config/app/app-segment-config.js'
56+
5557
type TEntry = typeof import('${relativePath}.js')
5658
5759
type SegmentParams<T extends Object = any> = T extends Record<string, any>
@@ -67,7 +69,7 @@ checkFields<Diff<{
6769
}
6870
config?: {}
6971
generateStaticParams?: Function
70-
unstable_prefetch?: 'unstable_static' | 'unstable_runtime'
72+
unstable_prefetch?: PrefetchForTypeCheckInternal
7173
revalidate?: RevalidateRange<TEntry> | false
7274
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
7375
dynamicParams?: boolean

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ async function createComponentTreeInternal(
221221
? (layoutOrPageMod as AppSegmentConfig).unstable_prefetch
222222
: undefined
223223
/** Whether this segment should use a runtime prefetch instead of a static prefetch. */
224-
const hasRuntimePrefetch = prefetchConfig === 'unstable_runtime'
224+
const hasRuntimePrefetch = prefetchConfig?.mode === 'runtime'
225225

226226
const [Forbidden, forbiddenStyles] =
227227
authInterrupts && forbidden

packages/next/src/server/typescript/rules/config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ const API_DOCS: Record<
159159
unstable_prefetch: {
160160
description: `Specifies the default prefetching behavior for this segment. This configuration is currently under development and will change.`,
161161
link: '(docs coming soon)',
162-
options: {
163-
'"unstable_static"':
164-
'Only static and cached parts of the page will be prefetched. (default)',
165-
'"unstable_runtime"':
166-
'Parts of the page that use route params, search params, or cookies will also be prefetched.',
167-
} satisfies DocsOptionsObject<FullAppSegmentConfig['unstable_prefetch']>,
162+
type: 'object',
163+
// TODO: ideally, we'd validate the config object somehow, but this is difficult to do
164+
// with the way this plugin is currently structured.
165+
// For now, since we don't provide an `options` here, we won't do any validation in
166+
// `getSemanticDiagnosticsForExportVariableStatement` below, and only provide hover a tooltip + autocomplete.
167+
insertText: 'unstable_prefetch = { mode: "static" };',
168168
},
169169
}
170170

test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/segment-config/runtime-prefetchable/configured-as-runtime/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { cookies } from 'next/headers'
22
import { Suspense } from 'react'
33

4-
export const unstable_prefetch = 'unstable_runtime'
4+
export const unstable_prefetch = {
5+
mode: 'runtime',
6+
samples: [{ cookies: [] }],
7+
}
58

69
export default function Page() {
710
return (

test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/segment-config/runtime-prefetchable/configured-as-static/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { Suspense } from 'react'
44

55
// Technically, no `export const unstable_prefetch = ...` is needed, because we default to static,
66
// this is just to make sure that we excercise the codepaths for it
7-
export const unstable_prefetch = 'unstable_static'
8-
7+
export const unstable_prefetch = {
8+
mode: 'static',
9+
}
910
export default function Page() {
1011
return (
1112
<main>

test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/segment-config/runtime-prefetchable/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { cookies } from 'next/headers'
22
import { Suspense } from 'react'
33
import { DebugRenderKind } from '../../shared'
44

5-
export const unstable_prefetch = 'unstable_runtime'
6-
5+
export const unstable_prefetch = {
6+
mode: 'runtime',
7+
samples: [{ cookies: [] }],
8+
}
79
export default async function Layout({ children }) {
810
return (
911
<main>

test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/fully-static/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
// but it's useful to exercise this codepath.
44
// In the future, this test can be used to check whether we correctly
55
// *skip* a runtime prefetch if a page was prerendered as static.
6-
export const unstable_prefetch = 'unstable_runtime'
6+
export const unstable_prefetch = {
7+
mode: 'runtime',
8+
samples: [{ cookies: [] }],
9+
}
710

811
export default async function Page() {
912
return (

test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies-only/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { cookies } from 'next/headers'
22
import { Suspense } from 'react'
33
import { cachedDelay, DebugRenderKind } from '../../../shared'
44

5-
export const unstable_prefetch = 'unstable_runtime'
5+
export const unstable_prefetch = {
6+
mode: 'runtime',
7+
samples: [{ cookies: [{ name: 'testCookie', value: 'testValue' }] }],
8+
}
69

710
export default async function Page() {
811
return (

0 commit comments

Comments
 (0)