Skip to content

Commit 8bc5716

Browse files
committed
fix: support ppr dynamic shells
1 parent 4585d6b commit 8bc5716

File tree

4 files changed

+105
-13
lines changed

4 files changed

+105
-13
lines changed

src/build/content/prerendered.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,30 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
209209
await writeCacheEntry(key, value, lastModified, ctx)
210210
}),
211211
),
212-
...ctx.getFallbacks(manifest).map(async (route) => {
213-
const key = routeToFilePath(route)
214-
const value = await buildPagesCacheValue(
215-
join(ctx.publishDir, 'server/pages', key),
216-
undefined,
217-
shouldUseEnumKind,
218-
true, // there is no corresponding json file for fallback, so we are skipping it for this entry
219-
)
212+
...ctx.getFallbacks(manifest).map((route) =>
213+
limitConcurrentPrerenderContentHandling(async () => {
214+
const key = routeToFilePath(route)
215+
const value = await buildPagesCacheValue(
216+
join(ctx.publishDir, 'server/pages', key),
217+
undefined,
218+
shouldUseEnumKind,
219+
true, // there is no corresponding json file for fallback, so we are skipping it for this entry
220+
)
220221

221-
await writeCacheEntry(key, value, Date.now(), ctx)
222-
}),
222+
await writeCacheEntry(key, value, Date.now(), ctx)
223+
}),
224+
),
225+
...ctx.getShells(manifest).map((route) =>
226+
limitConcurrentPrerenderContentHandling(async () => {
227+
const key = routeToFilePath(route)
228+
const value = await buildAppCacheValue(
229+
join(ctx.publishDir, 'server/app', key),
230+
shouldUseAppPageKind,
231+
)
232+
233+
await writeCacheEntry(key, value, Date.now(), ctx)
234+
}),
235+
),
223236
])
224237

225238
// app router 404 pages are not in the prerender manifest

src/build/plugin-context.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import type {
1010
NetlifyPluginOptions,
1111
NetlifyPluginUtils,
1212
} from '@netlify/build'
13-
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js'
1413
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
1514
import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manifest-plugin.js'
1615
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
16+
import type {
17+
PrerenderManifest,
18+
RoutesManifest,
19+
} from 'next-with-cache-handler-v2/dist/build/index.js'
1720
import { satisfies } from 'semver'
1821

1922
const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
@@ -337,7 +340,7 @@ export class PluginContext {
337340

338341
#fallbacks: string[] | null = null
339342
/**
340-
* Get an array of localized fallback routes
343+
* Get an array of localized fallback routes for Pages Router
341344
*
342345
* Example return value for non-i18n site: `['blog/[slug]']`
343346
*
@@ -356,7 +359,7 @@ export class PluginContext {
356359
// - `string` - when user use pages router with `fallback: true`, and then it's html file path
357360
// - `null` - when user use pages router with `fallback: 'block'` or app router with `export const dynamicParams = true`
358361
// - `false` - when user use pages router with `fallback: false` or app router with `export const dynamicParams = false`
359-
if (typeof meta.fallback === 'string') {
362+
if (typeof meta.fallback === 'string' && meta.renderingMode !== 'PARTIALLY_STATIC') {
360363
for (const locale of locales) {
361364
const localizedRoute = posixJoin(locale, route.replace(/^\/+/g, ''))
362365
fallbacks.push(localizedRoute)
@@ -400,6 +403,26 @@ export class PluginContext {
400403
return this.#fullyStaticHtmlPages
401404
}
402405

406+
#shells: string[] | null = null
407+
/**
408+
* Get an array of static shells for App Router's PPR dynamic routes
409+
*/
410+
getShells(prerenderManifest: PrerenderManifest): string[] {
411+
if (!this.#shells) {
412+
this.#shells = Object.entries(prerenderManifest.dynamicRoutes).reduce(
413+
(shells, [route, meta]) => {
414+
if (typeof meta.fallback === 'string' && meta.renderingMode === 'PARTIALLY_STATIC') {
415+
shells.push(route)
416+
}
417+
return shells
418+
},
419+
[] as string[],
420+
)
421+
}
422+
423+
return this.#shells
424+
}
425+
403426
/** Fails a build with a message and an optional error */
404427
failBuild(message: string, error?: unknown): never {
405428
return this.utils.build.failBuild(message, error instanceof Error ? { error } : undefined)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Suspense } from 'react'
2+
import { connection } from 'next/server'
3+
4+
export async function generateStaticParams() {
5+
return [{ dynamic: '1' }, { dynamic: '2' }]
6+
}
7+
8+
async function getData(params) {
9+
await connection()
10+
const res = await fetch(`https://api.tvmaze.com/shows/${params.id}`, {
11+
next: {
12+
tags: [`show-${params.id}`],
13+
},
14+
})
15+
await new Promise((res) => setTimeout(res, 3000))
16+
return res.json()
17+
}
18+
19+
async function Content(params) {
20+
const data = await getData(params)
21+
22+
return (
23+
<dl>
24+
<dt>Show</dt>
25+
<dd>{data.name}</dd>
26+
<dt>Param</dt>
27+
<dd>{params.id}</dd>
28+
<dt>Time</dt>
29+
<dd data-testid="date-now">{new Date().toISOString()}</dd>
30+
</dl>
31+
)
32+
}
33+
34+
export default async function DynamicPage({ params }) {
35+
const { dynamic } = await params
36+
37+
return (
38+
<main>
39+
<h1>Dynamic Page: {dynamic}</h1>
40+
<Suspense fallback={<div>loading...</div>}>
41+
<Content id={dynamic} />
42+
</Suspense>
43+
</main>
44+
)
45+
}

tests/integration/simple-app.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,10 @@ test.skipIf(process.env.NEXT_VERSION !== 'canary')<FixtureTestContext>(
394394
const blobEntries = await getBlobEntries(ctx)
395395
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
396396
[
397+
'/1',
398+
'/2',
397399
'/404',
400+
'/[dynamic]',
398401
shouldHaveAppRouterGlobalErrorInPrerenderManifest() ? '/_global-error' : undefined,
399402
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
400403
'/index',
@@ -407,6 +410,14 @@ test.skipIf(process.env.NEXT_VERSION !== 'canary')<FixtureTestContext>(
407410
const home = await invokeFunction(ctx)
408411
expect(home.statusCode).toBe(200)
409412
expect(load(home.body)('h1').text()).toBe('Home')
413+
414+
const dynamicPrerendered = await invokeFunction(ctx, { url: '/1' })
415+
expect(dynamicPrerendered.statusCode).toBe(200)
416+
expect(load(dynamicPrerendered.body)('h1').text()).toBe('Dynamic Page: 1')
417+
418+
const dynamicNotPrerendered = await invokeFunction(ctx, { url: '/3' })
419+
expect(dynamicNotPrerendered.statusCode).toBe(200)
420+
expect(load(dynamicNotPrerendered.body)('h1').text()).toBe('Dynamic Page: 3')
410421
},
411422
)
412423

0 commit comments

Comments
 (0)