diff --git a/.vscode/settings.json b/.vscode/settings.json index c8298e95c1..c0d580944b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -66,6 +66,7 @@ "napi", "nextjs", "opentelemetry", + "prerendered", "Threadsafe", "Turbopack", "zipkin" diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 2f4fa5511b..82703f782d 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1807,6 +1807,7 @@ export default async function build( runtime: pageRuntime, pageDuration: undefined, ssgPageDurations: undefined, + hasEmptyPrelude: undefined, }) }) }) @@ -2237,8 +2238,16 @@ export default async function build( const { revalidate = appConfig.revalidate ?? false, metadata = {}, + hasEmptyPrelude, + hasPostponed, } = exportResult.byPath.get(route) ?? {} + pageInfos.set(route, { + ...(pageInfos.get(route) as PageInfo), + hasPostponed, + hasEmptyPrelude, + }) + if (revalidate !== 0) { const normalizedRoute = normalizePagePath(route) const dataRoute = isRouteHandler @@ -2299,6 +2308,11 @@ export default async function build( const normalizedRoute = normalizePagePath(page) const dataRoute = path.posix.join(`${normalizedRoute}.rsc`) + pageInfos.set(page, { + ...(pageInfos.get(page) as PageInfo), + isDynamicAppRoute: true, + }) + // TODO: create a separate manifest to allow enforcing // dynamicParams for non-static paths? finalDynamicRoutes[page] = { diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index ec1f57c012..09b4813e1d 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -337,6 +337,9 @@ export interface PageInfo { pageDuration: number | undefined ssgPageDurations: number[] | undefined runtime: ServerRuntime + hasEmptyPrelude?: boolean + hasPostponed?: boolean + isDynamicAppRoute?: boolean } export async function printTreeView( @@ -450,18 +453,28 @@ export async function printTreeView( (pageInfo?.pageDuration || 0) + (pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0) - const symbol = - item === '/_app' || item === '/_app.server' - ? ' ' - : isEdgeRuntime(pageInfo?.runtime) - ? 'ℇ' - : pageInfo?.isPPR - ? '◐' - : pageInfo?.isStatic - ? '○' - : pageInfo?.isSSG - ? '●' - : 'λ' + let symbol: string + + if (item === '/_app' || item === '/_app.server') { + symbol = ' ' + } else if (isEdgeRuntime(pageInfo?.runtime)) { + symbol = 'ℇ' + } else if (pageInfo?.isPPR) { + // If the page has an empty prelude, then it's equivalent to a static page. + if (pageInfo?.hasEmptyPrelude) { + symbol = 'λ' + } else if (!pageInfo?.hasPostponed && !pageInfo.isDynamicAppRoute) { + symbol = '○' + } else { + symbol = '◐' + } + } else if (pageInfo?.isStatic) { + symbol = '○' + } else if (pageInfo?.isSSG) { + symbol = '●' + } else { + symbol = 'λ' + } usedSymbols.add(symbol) @@ -667,34 +680,11 @@ export async function printTreeView( print( textTable( [ - usedSymbols.has('◐') && [ - '◐', - '(Partially Pre-Rendered)', - 'static parts of the page were pre-rendered and the dynamic parts will be streamed', - ], - usedSymbols.has('ℇ') && [ - 'ℇ', - '(Streaming)', - `server-side renders with streaming (uses React 18 SSR streaming or Server Components)`, - ], - usedSymbols.has('λ') && [ - 'λ', - '(Server)', - `server-side renders at runtime (uses ${cyan( - 'getInitialProps' - )} or ${cyan('getServerSideProps')})`, - ], - usedSymbols.has('○') && [ - '○', - '(Static)', - 'automatically rendered as static HTML (uses no initial props)', - ], + usedSymbols.has('○') && ['○', '(Static)', 'prerendered as static HTML'], usedSymbols.has('●') && [ '●', '(SSG)', - `automatically generated as static HTML + JSON (uses ${cyan( - 'getStaticProps' - )})`, + `prerendered as static HTML (uses ${cyan('getStaticProps')})`, ], usedSymbols.has('ISR') && [ '', @@ -703,6 +693,21 @@ export async function printTreeView( 'getStaticProps' )})`, ], + usedSymbols.has('◐') && [ + '◐', + '(Partial Prerender)', + 'prerendered as static HTML with dynamic server-streamed content', + ], + usedSymbols.has('λ') && [ + 'λ', + '(Dynamic)', + `server-rendered on demand using Node.js`, + ], + usedSymbols.has('ℇ') && [ + 'ℇ', + '(Edge Runtime)', + `server-rendered on demand using the Edge Runtime`, + ], ].filter((x) => x) as [string, string, string][], { align: ['l', 'l', 'l'], diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 0466870a9e..c7a6a56a99 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -746,6 +746,15 @@ export async function exportAppImpl( if (typeof result.metadata !== 'undefined') { info.metadata = result.metadata } + + if (typeof result.hasEmptyPrelude !== 'undefined') { + info.hasEmptyPrelude = result.hasEmptyPrelude + } + + if (typeof result.hasPostponed !== 'undefined') { + info.hasPostponed = result.hasPostponed + } + collector.byPath.set(path, info) // Update not found. diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index 9615910a32..5da68b1588 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -160,7 +160,12 @@ export async function exportAppPage( ) // Writing the request metadata to a file. - const meta: RouteMetadata = { status: undefined, headers, postponed } + const meta: RouteMetadata = { + status: undefined, + headers, + postponed, + } + await fileWriter( ExportedAppPageFiles.META, htmlFilepath.replace(/\.html$/, '.meta'), @@ -177,6 +182,8 @@ export async function exportAppPage( return { // Only include the metadata if the environment has next support. metadata: hasNextSupport ? meta : undefined, + hasEmptyPrelude: Boolean(postponed) && html === '', + hasPostponed: Boolean(postponed), revalidate, } } catch (err: any) { diff --git a/packages/next/src/export/types.ts b/packages/next/src/export/types.ts index fc31a4a300..1f94d9ce80 100644 --- a/packages/next/src/export/types.ts +++ b/packages/next/src/export/types.ts @@ -73,6 +73,8 @@ export type ExportRouteResult = headers?: OutgoingHttpHeaders } ssgNotFound?: boolean + hasEmptyPrelude?: boolean + hasPostponed?: boolean } | { error: boolean @@ -135,6 +137,14 @@ export type ExportAppResult = { * The metadata for the page. */ metadata?: { status?: number; headers?: OutgoingHttpHeaders } + /** + * If the page has an empty prelude when using PPR. + */ + hasEmptyPrelude?: boolean + /** + * If the page has postponed when using PPR. + */ + hasPostponed?: boolean } > diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index cfed198f0b..3f221db6f6 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -360,5 +360,7 @@ export default async function exportPage( revalidate: result.revalidate, metadata: result.metadata, ssgNotFound: result.ssgNotFound, + hasEmptyPrelude: result.hasEmptyPrelude, + hasPostponed: result.hasPostponed, } }