From 057d33964d307dcd3c65799d3fd4e807549f66a6 Mon Sep 17 00:00:00 2001 From: pieh Date: Tue, 8 Oct 2024 15:04:46 +0200 Subject: [PATCH] fix: create cache entries for fallback pages to support next@canary --- src/build/content/prerendered.ts | 58 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/build/content/prerendered.ts b/src/build/content/prerendered.ts index 6c45a7447..e7dce0d61 100644 --- a/src/build/content/prerendered.ts +++ b/src/build/content/prerendered.ts @@ -1,6 +1,7 @@ import { existsSync } from 'node:fs' import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' +import { join as posixJoin } from 'node:path/posix' import { trace } from '@opentelemetry/api' import { wrapTracer } from '@opentelemetry/api/experimental' @@ -41,17 +42,28 @@ const writeCacheEntry = async ( } /** - * Normalize routes by stripping leading slashes and ensuring root path is index + * Normalize routes by ensuring leading slashes and ensuring root path is index */ -const routeToFilePath = (path: string) => (path === '/' ? '/index' : path) +const routeToFilePath = (path: string) => { + if (path === '/') { + return '/index' + } + + if (path.startsWith('/')) { + return path + } + + return `/${path}` +} const buildPagesCacheValue = async ( path: string, shouldUseEnumKind: boolean, + shouldSkipJson = false, ): Promise => ({ kind: shouldUseEnumKind ? 'PAGES' : 'PAGE', html: await readFile(`${path}.html`, 'utf-8'), - pageData: JSON.parse(await readFile(`${path}.json`, 'utf-8')), + pageData: shouldSkipJson ? {} : JSON.parse(await readFile(`${path}.json`, 'utf-8')), headers: undefined, status: undefined, }) @@ -146,8 +158,8 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise }) : false - await Promise.all( - Object.entries(manifest.routes).map( + await Promise.all([ + ...Object.entries(manifest.routes).map( ([route, meta]): Promise => limitConcurrentPrerenderContentHandling(async () => { const lastModified = meta.initialRevalidateSeconds @@ -195,7 +207,41 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise await writeCacheEntry(key, value, lastModified, ctx) }), ), - ) + ...Object.entries(manifest.dynamicRoutes).map(async ([route, meta]) => { + // fallback can be `string | false | null` + // - `string` - when user use pages router with `fallback: true`, and then it's html file path + // - `null` - when user use pages router with `fallback: 'block'` or app router with `export const dynamicParams = true` + // - `false` - when user use pages router with `fallback: false` or app router with `export const dynamicParams = false` + if (typeof meta.fallback === 'string') { + // https://github.com/vercel/next.js/pull/68603 started using route cache to serve fallbacks + // so we have to seed blobs with fallback entries + + // create cache entry for pages router with `fallback: true` case + await limitConcurrentPrerenderContentHandling(async () => { + // dynamic routes don't have entries for each locale so we have to generate them + // ourselves. If i18n is not used we use empty string as "locale" to be able to use + // same handling wether i18n is used or not + const locales = ctx.buildConfig.i18n?.locales ?? [''] + + const lastModified = Date.now() + for (const locale of locales) { + const key = routeToFilePath(posixJoin(locale, route)) + const value = await buildPagesCacheValue( + join(ctx.publishDir, 'server/pages', key), + shouldUseEnumKind, + true, // there is no corresponding json file for fallback, so we are skipping it for this entry + ) + // Netlify Forms are not support and require a workaround + if (value.kind === 'PAGE' || value.kind === 'PAGES' || value.kind === 'APP_PAGE') { + verifyNetlifyForms(ctx, value.html) + } + + await writeCacheEntry(key, value, lastModified, ctx) + } + }) + } + }), + ]) // app router 404 pages are not in the prerender manifest // so we need to check for them manually