Skip to content

Commit 8ab259a

Browse files
piehorinokaiserhalp
authored
fix: create cache entries for fallback pages to support next@canary (#2649)
* test: add test case for fallback: true * fix: create cache entries for fallback pages to support next@canary * fix: don't permamently cache fallback html * Update src/build/content/prerendered.ts Co-authored-by: Rob Stanford <[email protected]> * chore: don't use extra fallback manifest and instead store html and boolean wether that is fallback html in single blob * test: add some unit tests about static html blobs and fallbacks --------- Co-authored-by: Rob Stanford <[email protected]> Co-authored-by: Philippe Serhal <[email protected]>
1 parent c77ece7 commit 8ab259a

File tree

13 files changed

+529
-91
lines changed

13 files changed

+529
-91
lines changed

src/build/content/prerendered.ts

+27-6
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,28 @@ const writeCacheEntry = async (
4141
}
4242

4343
/**
44-
* Normalize routes by stripping leading slashes and ensuring root path is index
44+
* Normalize routes by ensuring leading slashes and ensuring root path is /index
4545
*/
46-
const routeToFilePath = (path: string) => (path === '/' ? '/index' : path)
46+
const routeToFilePath = (path: string) => {
47+
if (path === '/') {
48+
return '/index'
49+
}
50+
51+
if (path.startsWith('/')) {
52+
return path
53+
}
54+
55+
return `/${path}`
56+
}
4757

4858
const buildPagesCacheValue = async (
4959
path: string,
5060
shouldUseEnumKind: boolean,
61+
shouldSkipJson = false,
5162
): Promise<NetlifyCachedPageValue> => ({
5263
kind: shouldUseEnumKind ? 'PAGES' : 'PAGE',
5364
html: await readFile(`${path}.html`, 'utf-8'),
54-
pageData: JSON.parse(await readFile(`${path}.json`, 'utf-8')),
65+
pageData: shouldSkipJson ? {} : JSON.parse(await readFile(`${path}.json`, 'utf-8')),
5566
headers: undefined,
5667
status: undefined,
5768
})
@@ -146,8 +157,8 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
146157
})
147158
: false
148159

149-
await Promise.all(
150-
Object.entries(manifest.routes).map(
160+
await Promise.all([
161+
...Object.entries(manifest.routes).map(
151162
([route, meta]): Promise<void> =>
152163
limitConcurrentPrerenderContentHandling(async () => {
153164
const lastModified = meta.initialRevalidateSeconds
@@ -195,7 +206,17 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
195206
await writeCacheEntry(key, value, lastModified, ctx)
196207
}),
197208
),
198-
)
209+
...ctx.getFallbacks(manifest).map(async (route) => {
210+
const key = routeToFilePath(route)
211+
const value = await buildPagesCacheValue(
212+
join(ctx.publishDir, 'server/pages', key),
213+
shouldUseEnumKind,
214+
true, // there is no corresponding json file for fallback, so we are skipping it for this entry
215+
)
216+
217+
await writeCacheEntry(key, value, Date.now(), ctx)
218+
}),
219+
])
199220

200221
// app router 404 pages are not in the prerender manifest
201222
// so we need to check for them manually

src/build/content/static.test.ts

+206-43
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Buffer } from 'node:buffer'
1+
import { readFile } from 'node:fs/promises'
22
import { join } from 'node:path'
33
import { inspect } from 'node:util'
44

55
import type { NetlifyPluginOptions } from '@netlify/build'
66
import glob from 'fast-glob'
7+
import type { PrerenderManifest } from 'next/dist/build/index.js'
78
import { beforeEach, describe, expect, Mock, test, vi } from 'vitest'
89

9-
import { mockFileSystem } from '../../../tests/index.js'
10+
import { decodeBlobKey, encodeBlobKey, mockFileSystem } from '../../../tests/index.js'
1011
import { type FixtureTestContext } from '../../../tests/utils/contexts.js'
1112
import { createFsFixture } from '../../../tests/utils/fixture.js'
1213
import { PluginContext, RequiredServerFilesManifest } from '../plugin-context.js'
@@ -21,7 +22,19 @@ type Context = FixtureTestContext & {
2122
const createFsFixtureWithBasePath = (
2223
fixture: Record<string, string>,
2324
ctx: Omit<Context, 'pluginContext'>,
24-
basePath = '',
25+
26+
{
27+
basePath = '',
28+
// eslint-disable-next-line unicorn/no-useless-undefined
29+
i18n = undefined,
30+
dynamicRoutes = {},
31+
}: {
32+
basePath?: string
33+
i18n?: Pick<NonNullable<RequiredServerFilesManifest['config']['i18n']>, 'locales'>
34+
dynamicRoutes?: {
35+
[route: string]: Pick<PrerenderManifest['dynamicRoutes'][''], 'fallback'>
36+
}
37+
} = {},
2538
) => {
2639
return createFsFixture(
2740
{
@@ -32,8 +45,10 @@ const createFsFixtureWithBasePath = (
3245
appDir: ctx.relativeAppDir,
3346
config: {
3447
distDir: ctx.publishDir,
48+
i18n,
3549
},
3650
} as Pick<RequiredServerFilesManifest, 'relativeAppDir' | 'appDir'>),
51+
[join(ctx.publishDir, 'prerender-manifest.json')]: JSON.stringify({ dynamicRoutes }),
3752
},
3853
ctx,
3954
)
@@ -121,7 +136,7 @@ describe('Regular Repository layout', () => {
121136
'.next/static/sub-dir/test2.js': '',
122137
},
123138
ctx,
124-
'/base/path',
139+
{ basePath: '/base/path' },
125140
)
126141

127142
await copyStaticAssets(pluginContext)
@@ -168,7 +183,7 @@ describe('Regular Repository layout', () => {
168183
'public/another-asset.json': '',
169184
},
170185
ctx,
171-
'/base/path',
186+
{ basePath: '/base/path' },
172187
)
173188

174189
await copyStaticAssets(pluginContext)
@@ -182,26 +197,100 @@ describe('Regular Repository layout', () => {
182197
)
183198
})
184199

185-
test<Context>('should copy the static pages to the publish directory if there are no corresponding JSON files', async ({
186-
pluginContext,
187-
...ctx
188-
}) => {
189-
await createFsFixtureWithBasePath(
190-
{
191-
'.next/server/pages/test.html': '',
192-
'.next/server/pages/test2.html': '',
193-
'.next/server/pages/test3.json': '',
194-
},
195-
ctx,
196-
)
200+
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fallback', () => {
201+
test<Context>('no i18n', async ({ pluginContext, ...ctx }) => {
202+
await createFsFixtureWithBasePath(
203+
{
204+
'.next/server/pages/test.html': '',
205+
'.next/server/pages/test2.html': '',
206+
'.next/server/pages/test3.json': '',
207+
'.next/server/pages/blog/[slug].html': '',
208+
},
209+
ctx,
210+
{
211+
dynamicRoutes: {
212+
'/blog/[slug]': {
213+
fallback: '/blog/[slug].html',
214+
},
215+
},
216+
},
217+
)
197218

198-
await copyStaticContent(pluginContext)
199-
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
219+
await copyStaticContent(pluginContext)
220+
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
221+
222+
const expectedStaticPages = ['blog/[slug].html', 'test.html', 'test2.html']
223+
const expectedFallbacks = new Set(['blog/[slug].html'])
224+
225+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
226+
227+
for (const page of expectedStaticPages) {
228+
const expectedIsFallback = expectedFallbacks.has(page)
229+
230+
const blob = JSON.parse(
231+
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
232+
)
200233

201-
expect(files.map((path) => Buffer.from(path, 'base64').toString('utf-8')).sort()).toEqual([
202-
'test.html',
203-
'test2.html',
204-
])
234+
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
235+
html: '',
236+
isFallback: expectedIsFallback,
237+
})
238+
}
239+
})
240+
241+
test<Context>('with i18n', async ({ pluginContext, ...ctx }) => {
242+
await createFsFixtureWithBasePath(
243+
{
244+
'.next/server/pages/de/test.html': '',
245+
'.next/server/pages/de/test2.html': '',
246+
'.next/server/pages/de/test3.json': '',
247+
'.next/server/pages/de/blog/[slug].html': '',
248+
'.next/server/pages/en/test.html': '',
249+
'.next/server/pages/en/test2.html': '',
250+
'.next/server/pages/en/test3.json': '',
251+
'.next/server/pages/en/blog/[slug].html': '',
252+
},
253+
ctx,
254+
{
255+
dynamicRoutes: {
256+
'/blog/[slug]': {
257+
fallback: '/blog/[slug].html',
258+
},
259+
},
260+
i18n: {
261+
locales: ['en', 'de'],
262+
},
263+
},
264+
)
265+
266+
await copyStaticContent(pluginContext)
267+
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
268+
269+
const expectedStaticPages = [
270+
'de/blog/[slug].html',
271+
'de/test.html',
272+
'de/test2.html',
273+
'en/blog/[slug].html',
274+
'en/test.html',
275+
'en/test2.html',
276+
]
277+
const expectedFallbacks = new Set(['en/blog/[slug].html', 'de/blog/[slug].html'])
278+
279+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
280+
281+
for (const page of expectedStaticPages) {
282+
const expectedIsFallback = expectedFallbacks.has(page)
283+
284+
const blob = JSON.parse(
285+
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
286+
)
287+
288+
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
289+
html: '',
290+
isFallback: expectedIsFallback,
291+
})
292+
}
293+
})
205294
})
206295

207296
test<Context>('should not copy the static pages to the publish directory if there are corresponding JSON files', async ({
@@ -269,7 +358,7 @@ describe('Mono Repository', () => {
269358
'apps/app-1/.next/static/sub-dir/test2.js': '',
270359
},
271360
ctx,
272-
'/base/path',
361+
{ basePath: '/base/path' },
273362
)
274363

275364
await copyStaticAssets(pluginContext)
@@ -316,7 +405,7 @@ describe('Mono Repository', () => {
316405
'apps/app-1/public/another-asset.json': '',
317406
},
318407
ctx,
319-
'/base/path',
408+
{ basePath: '/base/path' },
320409
)
321410

322411
await copyStaticAssets(pluginContext)
@@ -330,26 +419,100 @@ describe('Mono Repository', () => {
330419
)
331420
})
332421

333-
test<Context>('should copy the static pages to the publish directory if there are no corresponding JSON files', async ({
334-
pluginContext,
335-
...ctx
336-
}) => {
337-
await createFsFixtureWithBasePath(
338-
{
339-
'apps/app-1/.next/server/pages/test.html': '',
340-
'apps/app-1/.next/server/pages/test2.html': '',
341-
'apps/app-1/.next/server/pages/test3.json': '',
342-
},
343-
ctx,
344-
)
422+
describe('should copy the static pages to the publish directory if there are no corresponding JSON files and mark wether html file is a fallback', () => {
423+
test<Context>('no i18n', async ({ pluginContext, ...ctx }) => {
424+
await createFsFixtureWithBasePath(
425+
{
426+
'apps/app-1/.next/server/pages/test.html': '',
427+
'apps/app-1/.next/server/pages/test2.html': '',
428+
'apps/app-1/.next/server/pages/test3.json': '',
429+
'apps/app-1/.next/server/pages/blog/[slug].html': '',
430+
},
431+
ctx,
432+
{
433+
dynamicRoutes: {
434+
'/blog/[slug]': {
435+
fallback: '/blog/[slug].html',
436+
},
437+
},
438+
},
439+
)
345440

346-
await copyStaticContent(pluginContext)
347-
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
441+
await copyStaticContent(pluginContext)
442+
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
443+
444+
const expectedStaticPages = ['blog/[slug].html', 'test.html', 'test2.html']
445+
const expectedFallbacks = new Set(['blog/[slug].html'])
446+
447+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
448+
449+
for (const page of expectedStaticPages) {
450+
const expectedIsFallback = expectedFallbacks.has(page)
451+
452+
const blob = JSON.parse(
453+
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
454+
)
348455

349-
expect(files.map((path) => Buffer.from(path, 'base64').toString('utf-8')).sort()).toEqual([
350-
'test.html',
351-
'test2.html',
352-
])
456+
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
457+
html: '',
458+
isFallback: expectedIsFallback,
459+
})
460+
}
461+
})
462+
463+
test<Context>('with i18n', async ({ pluginContext, ...ctx }) => {
464+
await createFsFixtureWithBasePath(
465+
{
466+
'apps/app-1/.next/server/pages/de/test.html': '',
467+
'apps/app-1/.next/server/pages/de/test2.html': '',
468+
'apps/app-1/.next/server/pages/de/test3.json': '',
469+
'apps/app-1/.next/server/pages/de/blog/[slug].html': '',
470+
'apps/app-1/.next/server/pages/en/test.html': '',
471+
'apps/app-1/.next/server/pages/en/test2.html': '',
472+
'apps/app-1/.next/server/pages/en/test3.json': '',
473+
'apps/app-1/.next/server/pages/en/blog/[slug].html': '',
474+
},
475+
ctx,
476+
{
477+
dynamicRoutes: {
478+
'/blog/[slug]': {
479+
fallback: '/blog/[slug].html',
480+
},
481+
},
482+
i18n: {
483+
locales: ['en', 'de'],
484+
},
485+
},
486+
)
487+
488+
await copyStaticContent(pluginContext)
489+
const files = await glob('**/*', { cwd: pluginContext.blobDir, dot: true })
490+
491+
const expectedStaticPages = [
492+
'de/blog/[slug].html',
493+
'de/test.html',
494+
'de/test2.html',
495+
'en/blog/[slug].html',
496+
'en/test.html',
497+
'en/test2.html',
498+
]
499+
const expectedFallbacks = new Set(['en/blog/[slug].html', 'de/blog/[slug].html'])
500+
501+
expect(files.map((path) => decodeBlobKey(path)).sort()).toEqual(expectedStaticPages)
502+
503+
for (const page of expectedStaticPages) {
504+
const expectedIsFallback = expectedFallbacks.has(page)
505+
506+
const blob = JSON.parse(
507+
await readFile(join(pluginContext.blobDir, await encodeBlobKey(page)), 'utf-8'),
508+
)
509+
510+
expect(blob, `${page} should ${expectedIsFallback ? '' : 'not '}be a fallback`).toEqual({
511+
html: '',
512+
isFallback: expectedIsFallback,
513+
})
514+
}
515+
})
353516
})
354517

355518
test<Context>('should not copy the static pages to the publish directory if there are corresponding JSON files', async ({

0 commit comments

Comments
 (0)