diff --git a/src/build/content/prerendered.ts b/src/build/content/prerendered.ts index a05fc7db79..cfe6909597 100644 --- a/src/build/content/prerendered.ts +++ b/src/build/content/prerendered.ts @@ -5,6 +5,7 @@ import { join } from 'node:path' import { trace } from '@opentelemetry/api' import { wrapTracer } from '@opentelemetry/api/experimental' import { glob } from 'fast-glob' +import type { RouteMetadata } from 'next-with-cache-handler-v2/dist/export/routes/types.js' import pLimit from 'p-limit' import { satisfies } from 'semver' @@ -69,21 +70,44 @@ const buildPagesCacheValue = async ( revalidate: initialRevalidateSeconds, }) +const RSC_SEGMENTS_DIR_SUFFIX = '.segments' +const RSC_SEGMENT_SUFFIX = '.segment.rsc' + const buildAppCacheValue = async ( path: string, shouldUseAppPageKind: boolean, ): Promise => { - const meta = JSON.parse(await readFile(`${path}.meta`, 'utf-8')) + const meta = JSON.parse(await readFile(`${path}.meta`, 'utf-8')) as RouteMetadata const html = await readFile(`${path}.html`, 'utf-8') // supporting both old and new cache kind for App Router pages - https://github.com/vercel/next.js/pull/65988 if (shouldUseAppPageKind) { + // segments are normalized and outputted separately for each segment, we denormalize it here and stitch + // fully constructed segmentData to avoid data fetch waterfalls later in cache handler at runtime + // https://github.com/vercel/next.js/blob/def2c6ba75dff754767379afb44c26c30bd3d96b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts#L185 + let segmentData: NetlifyCachedAppPageValue['segmentData'] + if (meta.segmentPaths) { + const segmentsDir = path + RSC_SEGMENTS_DIR_SUFFIX + + segmentData = Object.fromEntries( + await Promise.all( + meta.segmentPaths.map(async (segmentPath: string) => { + const segmentDataFilePath = segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX + + const segmentContent = await readFile(segmentDataFilePath, 'base64') + return [segmentPath, segmentContent] + }), + ), + ) + } + return { kind: 'APP_PAGE', html, rscData: await readFile(`${path}.rsc`, 'base64').catch(() => readFile(`${path}.prefetch.rsc`, 'base64'), ), + segmentData, ...meta, } } @@ -97,7 +121,10 @@ const buildAppCacheValue = async ( if ( !meta.status && rsc.includes('NEXT_NOT_FOUND') && - !meta.headers['x-next-cache-tags'].includes('/@') + !( + typeof meta.headers?.['x-next-cache-tags'] === 'string' && + meta.headers?.['x-next-cache-tags'].includes('/@') + ) ) { meta.status = 404 } diff --git a/src/run/handlers/cache.cts b/src/run/handlers/cache.cts index bfd7815635..1baa0c3957 100644 --- a/src/run/handlers/cache.cts +++ b/src/run/handlers/cache.cts @@ -364,7 +364,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions { requestContext.isCacheableAppPage = true } - const { revalidate, rscData, ...restOfPageValue } = blob.value + const { revalidate, rscData, segmentData, ...restOfPageValue } = blob.value span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl }) @@ -375,6 +375,14 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions { value: { ...restOfPageValue, rscData: rscData ? Buffer.from(rscData, 'base64') : undefined, + segmentData: segmentData + ? new Map( + Object.entries(segmentData).map(([segmentPath, base64EncodedSegment]) => [ + segmentPath, + Buffer.from(base64EncodedSegment, 'base64'), + ]), + ) + : undefined, }, } } @@ -416,6 +424,14 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions { revalidate: context.revalidate ?? context.cacheControl?.revalidate, cacheControl: context.cacheControl, rscData: data.rscData?.toString('base64'), + segmentData: data.segmentData + ? Object.fromEntries( + [...data.segmentData.entries()].map(([segmentPath, base64EncodedSegment]) => [ + segmentPath, + base64EncodedSegment.toString('base64'), + ]), + ) + : undefined, } } diff --git a/src/shared/cache-types.cts b/src/shared/cache-types.cts index defba8e6fa..620211dc5f 100644 --- a/src/shared/cache-types.cts +++ b/src/shared/cache-types.cts @@ -9,6 +9,7 @@ import type { IncrementalCachedPageValue, IncrementalCacheValue, } from 'next/dist/server/response-cache/types.js' +import type { IncrementalCachedAppPageValue as IncrementalCachedAppPageValueForNewVersions } from 'next-with-cache-handler-v2/dist/server/response-cache/types.js' export type { CacheHandlerContext } from 'next/dist/server/lib/incremental-cache/index.js' @@ -44,17 +45,19 @@ type IncrementalCachedAppPageValueForMultipleVersions = Omit< 'kind' > & { kind: 'APP_PAGE' -} +} & Pick /** * Used for storing in blobs and reading from blobs */ export type NetlifyCachedAppPageValue = Omit< IncrementalCachedAppPageValueForMultipleVersions, - 'rscData' + 'rscData' | 'segmentData' > & { - // Next.js stores rscData as buffer, while we store it as base64 encoded string + // Next.js stores rscData as Buffer, while we store it as base64 encoded string rscData: string | undefined + // Next.js stores segmentData as Map, while we store it as Record, where value is base64 encoded string + segmentData: Record | undefined revalidate?: Parameters[2]['revalidate'] cacheControl?: CacheControl }