diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index fcc5089e693c9..d45e52ab2801a 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -73,6 +73,8 @@ import { appendMutableCookies } from '../web/spec-extension/adapters/request-coo import { createServerInsertedHTML } from './server-inserted-html' import { getRequiredScripts } from './required-scripts' import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix' +import { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external' +import { RequestStore } from '../../client/components/request-async-storage.external' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -151,24 +153,21 @@ function hasLoadingComponentInTree(tree: LoaderTree): boolean { ) as boolean } -export type AppPageRender = ( +type AppRenderContext = { + staticGenerationStore: StaticGenerationStore + requestStore: RequestStore +} + +const wrappedRender = async ( req: IncomingMessage, res: ServerResponse, pagePath: string, query: NextParsedUrlQuery, - renderOpts: RenderOpts -) => Promise - -export const renderToHTMLOrFlight: AppPageRender = ( - req, - res, - pagePath, - query, - renderOpts + renderOpts: RenderOpts, + ctx: AppRenderContext ) => { const isFlight = req.headers[RSC.toLowerCase()] !== undefined const isNotFoundPath = pagePath === '/404' - const pathname = validateURL(req.url) // A unique request timestamp used by development to ensure that it's // consistent and won't change during this request. This is important to @@ -189,6 +188,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( buildId, deploymentId, appDirDevErrorLogger, + assetPrefix = '', } = renderOpts // We need to expose the bundled `require` API globally for @@ -205,6 +205,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( const appUsingSizeAdjust = !!nextFontManifest?.appUsingSizeAdjust + // TODO: fix this typescript const clientReferenceManifest = renderOpts.clientReferenceManifest! const capturedErrors: Error[] = [] @@ -252,8 +253,6 @@ export const renderToHTMLOrFlight: AppPageRender = ( // Pull out the hooks/references from the component. const { - staticGenerationAsyncStorage, - requestAsyncStorage, staticGenerationBailout, LayoutRouter, RenderFromTemplateContext, @@ -270,626 +269,598 @@ export const renderToHTMLOrFlight: AppPageRender = ( preloadStyle, } = ComponentMod - // we wrap the render in an AsyncLocalStorage context - const wrappedRender = async () => { - const staticGenerationStore = staticGenerationAsyncStorage.getStore() - if (!staticGenerationStore) { - throw new Error( - `Invariant: Render expects to have staticGenerationAsyncStorage, none found` - ) - } - - staticGenerationStore.fetchMetrics = [] - extraRenderResultMeta.fetchMetrics = staticGenerationStore.fetchMetrics + const { staticGenerationStore, requestStore } = ctx + const { urlPathname } = staticGenerationStore - const requestStore = requestAsyncStorage.getStore() - if (!requestStore) { - throw new Error( - `Invariant: Render expects to have requestAsyncStorage, none found` - ) - } + staticGenerationStore.fetchMetrics = [] + extraRenderResultMeta.fetchMetrics = staticGenerationStore.fetchMetrics - // don't modify original query object - query = { ...query } - stripInternalQueries(query) + // don't modify original query object + query = { ...query } + stripInternalQueries(query) - const isPrefetch = - req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] !== undefined + const isPrefetch = + req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] !== undefined - /** - * Router state provided from the client-side router. Used to handle rendering from the common layout down. - */ - let providedFlightRouterState = isFlight - ? parseAndValidateFlightRouterState( - req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] - ) - : undefined + /** + * Router state provided from the client-side router. Used to handle rendering from the common layout down. + */ + let providedFlightRouterState = isFlight + ? parseAndValidateFlightRouterState( + req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] + ) + : undefined - /** - * The metadata items array created in next-app-loader with all relevant information - * that we need to resolve the final metadata. - */ + /** + * The metadata items array created in next-app-loader with all relevant information + * that we need to resolve the final metadata. + */ - let requestId: string + let requestId: string - if (process.env.NEXT_RUNTIME === 'edge') { - requestId = crypto.randomUUID() - } else { - requestId = require('next/dist/compiled/nanoid').nanoid() - } + if (process.env.NEXT_RUNTIME === 'edge') { + requestId = crypto.randomUUID() + } else { + requestId = require('next/dist/compiled/nanoid').nanoid() + } - const isStaticGeneration = staticGenerationStore.isStaticGeneration - // During static generation we need to call the static generation bailout when reading searchParams - const providedSearchParams = isStaticGeneration - ? createSearchParamsBailoutProxy() - : query - const searchParamsProps = { searchParams: providedSearchParams } + const isStaticGeneration = staticGenerationStore.isStaticGeneration + // During static generation we need to call the static generation bailout when reading searchParams + const providedSearchParams = isStaticGeneration + ? createSearchParamsBailoutProxy() + : query + const searchParamsProps = { searchParams: providedSearchParams } - /** - * Server Context is specifically only available in Server Components. - * It has to hold values that can't change while rendering from the common layout down. - * An example of this would be that `headers` are available but `searchParams` are not because that'd mean we have to render from the root layout down on all requests. - */ + /** + * Server Context is specifically only available in Server Components. + * It has to hold values that can't change while rendering from the common layout down. + * An example of this would be that `headers` are available but `searchParams` are not because that'd mean we have to render from the root layout down on all requests. + */ - const serverContexts: Array<[string, any]> = [ - ['WORKAROUND', null], // TODO-APP: First value has a bug currently where the value is not set on the second request: https://github.com/facebook/react/issues/24849 - ] + const serverContexts: Array<[string, any]> = [ + ['WORKAROUND', null], // TODO-APP: First value has a bug currently where the value is not set on the second request: https://github.com/facebook/react/issues/24849 + ] - type CreateSegmentPath = (child: FlightSegmentPath) => FlightSegmentPath + type CreateSegmentPath = (child: FlightSegmentPath) => FlightSegmentPath - /** - * Dynamic parameters. E.g. when you visit `/dashboard/vercel` which is rendered by `/dashboard/[slug]` the value will be {"slug": "vercel"}. - */ - const params = renderOpts.params ?? {} + /** + * Dynamic parameters. E.g. when you visit `/dashboard/vercel` which is rendered by `/dashboard/[slug]` the value will be {"slug": "vercel"}. + */ + const params = renderOpts.params ?? {} - /** - * Parse the dynamic segment and return the associated value. - */ - const getDynamicParamFromSegment: GetDynamicParamFromSegment = ( - // [slug] / [[slug]] / [...slug] - segment: string - ) => { - const segmentParam = getSegmentParam(segment) - if (!segmentParam) { - return null - } + /** + * Parse the dynamic segment and return the associated value. + */ + const getDynamicParamFromSegment: GetDynamicParamFromSegment = ( + // [slug] / [[slug]] / [...slug] + segment: string + ) => { + const segmentParam = getSegmentParam(segment) + if (!segmentParam) { + return null + } - const key = segmentParam.param + const key = segmentParam.param - let value = params[key] + let value = params[key] - // this is a special marker that will be present for interception routes - if (value === '__NEXT_EMPTY_PARAM__') { - value = undefined - } + // this is a special marker that will be present for interception routes + if (value === '__NEXT_EMPTY_PARAM__') { + value = undefined + } - if (Array.isArray(value)) { - value = value.map((i) => encodeURIComponent(i)) - } else if (typeof value === 'string') { - value = encodeURIComponent(value) - } + if (Array.isArray(value)) { + value = value.map((i) => encodeURIComponent(i)) + } else if (typeof value === 'string') { + value = encodeURIComponent(value) + } - if (!value) { - // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` - if (segmentParam.type === 'optional-catchall') { - const type = dynamicParamTypes[segmentParam.type] - return { - param: key, - value: null, - type: type, - // This value always has to be a string. - treeSegment: [key, '', type], - } + if (!value) { + // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` + if (segmentParam.type === 'optional-catchall') { + const type = dynamicParamTypes[segmentParam.type] + return { + param: key, + value: null, + type: type, + // This value always has to be a string. + treeSegment: [key, '', type], } - return findDynamicParamFromRouterState( - providedFlightRouterState, - segment - ) } + return findDynamicParamFromRouterState(providedFlightRouterState, segment) + } - const type = getShortDynamicParamType(segmentParam.type) + const type = getShortDynamicParamType(segmentParam.type) - return { - param: key, - // The value that is passed to user code. - value: value, - // The value that is rendered in the router tree. - treeSegment: [ - key, - Array.isArray(value) ? value.join('/') : value, - type, - ], - type: type, - } + return { + param: key, + // The value that is passed to user code. + value: value, + // The value that is rendered in the router tree. + treeSegment: [key, Array.isArray(value) ? value.join('/') : value, type], + type: type, } + } - let defaultRevalidate: false | undefined | number = false - - const assetPrefix = renderOpts.assetPrefix || '' + let defaultRevalidate: false | undefined | number = false - const getAssetQueryString = (addTimestamp: boolean) => { - const isDev = process.env.NODE_ENV === 'development' - let qs = '' + const getAssetQueryString = (addTimestamp: boolean) => { + const isDev = process.env.NODE_ENV === 'development' + let qs = '' - if (isDev && addTimestamp) { - qs += `?v=${DEV_REQUEST_TS}` - } + if (isDev && addTimestamp) { + qs += `?v=${DEV_REQUEST_TS}` + } - if (deploymentId) { - qs += `${isDev ? '&' : '?'}dpl=${deploymentId}` - } - return qs + if (deploymentId) { + qs += `${isDev ? '&' : '?'}dpl=${deploymentId}` } + return qs + } - const createComponentAndStyles = async ({ + const createComponentAndStyles = async ({ + filePath, + getComponent, + injectedCSS, + }: { + filePath: string + getComponent: () => any + injectedCSS: Set + }): Promise => { + const cssHrefs = getCssInlinedLinkTags( + clientReferenceManifest, filePath, - getComponent, - injectedCSS, - }: { - filePath: string - getComponent: () => any - injectedCSS: Set - }): Promise => { - const cssHrefs = getCssInlinedLinkTags( - clientReferenceManifest, - filePath, - injectedCSS - ) + injectedCSS + ) - const styles = cssHrefs - ? cssHrefs.map((href, index) => { - // In dev, Safari and Firefox will cache the resource during HMR: - // - https://github.com/vercel/next.js/issues/5860 - // - https://bugs.webkit.org/show_bug.cgi?id=187726 - // Because of this, we add a `?v=` query to bypass the cache during - // development. We need to also make sure that the number is always - // increasing. - const fullHref = `${assetPrefix}/_next/${href}${getAssetQueryString( - true - )}` - - // `Precedence` is an opt-in signal for React to handle resource - // loading and deduplication, etc. It's also used as the key to sort - // resources so they will be injected in the correct order. - // During HMR, it's critical to use different `precedence` values - // for different stylesheets, so their order will be kept. - // https://github.com/facebook/react/pull/25060 - const precedence = - process.env.NODE_ENV === 'development' ? 'next_' + href : 'next' + const styles = cssHrefs + ? cssHrefs.map((href, index) => { + // In dev, Safari and Firefox will cache the resource during HMR: + // - https://github.com/vercel/next.js/issues/5860 + // - https://bugs.webkit.org/show_bug.cgi?id=187726 + // Because of this, we add a `?v=` query to bypass the cache during + // development. We need to also make sure that the number is always + // increasing. + const fullHref = `${assetPrefix}/_next/${href}${getAssetQueryString( + true + )}` - return ( - - ) - }) - : null + // `Precedence` is an opt-in signal for React to handle resource + // loading and deduplication, etc. It's also used as the key to sort + // resources so they will be injected in the correct order. + // During HMR, it's critical to use different `precedence` values + // for different stylesheets, so their order will be kept. + // https://github.com/facebook/react/pull/25060 + const precedence = + process.env.NODE_ENV === 'development' ? 'next_' + href : 'next' - const Comp = interopDefault(await getComponent()) + return ( + + ) + }) + : null - return [Comp, styles] - } + const Comp = interopDefault(await getComponent()) - const getLayerAssets = ({ - layoutOrPagePath, - injectedCSS: injectedCSSWithCurrentLayout, - injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, - }: { - layoutOrPagePath: string | undefined - injectedCSS: Set - injectedFontPreloadTags: Set - }): React.ReactNode => { - const stylesheets: string[] = layoutOrPagePath - ? getCssInlinedLinkTags( - clientReferenceManifest, - layoutOrPagePath, - injectedCSSWithCurrentLayout, - true - ) - : [] + return [Comp, styles] + } - const preloadedFontFiles = layoutOrPagePath - ? getPreloadableFonts( - nextFontManifest, - layoutOrPagePath, - injectedFontPreloadTagsWithCurrentLayout - ) - : null - - if (preloadedFontFiles) { - if (preloadedFontFiles.length) { - for (let i = 0; i < preloadedFontFiles.length; i++) { - const fontFilename = preloadedFontFiles[i] - const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1] - const type = `font/${ext}` - const href = `${assetPrefix}/_next/${fontFilename}` - preloadFont(href, type, renderOpts.crossOrigin) - } - } else { - try { - let url = new URL(assetPrefix) - preconnect(url.origin, 'anonymous') - } catch (error) { - // assetPrefix must not be a fully qualified domain name. We assume - // we should preconnect to same origin instead - preconnect('/', 'anonymous') - } + const getLayerAssets = ({ + layoutOrPagePath, + injectedCSS: injectedCSSWithCurrentLayout, + injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, + }: { + layoutOrPagePath: string | undefined + injectedCSS: Set + injectedFontPreloadTags: Set + }): React.ReactNode => { + const stylesheets: string[] = layoutOrPagePath + ? getCssInlinedLinkTags( + clientReferenceManifest, + layoutOrPagePath, + injectedCSSWithCurrentLayout, + true + ) + : [] + + const preloadedFontFiles = layoutOrPagePath + ? getPreloadableFonts( + nextFontManifest, + layoutOrPagePath, + injectedFontPreloadTagsWithCurrentLayout + ) + : null + + if (preloadedFontFiles) { + if (preloadedFontFiles.length) { + for (let i = 0; i < preloadedFontFiles.length; i++) { + const fontFilename = preloadedFontFiles[i] + const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1] + const type = `font/${ext}` + const href = `${assetPrefix}/_next/${fontFilename}` + preloadFont(href, type, renderOpts.crossOrigin) + } + } else { + try { + let url = new URL(assetPrefix) + preconnect(url.origin, 'anonymous') + } catch (error) { + // assetPrefix must not be a fully qualified domain name. We assume + // we should preconnect to same origin instead + preconnect('/', 'anonymous') } } + } - const styles = stylesheets - ? stylesheets.map((href, index) => { - // In dev, Safari and Firefox will cache the resource during HMR: - // - https://github.com/vercel/next.js/issues/5860 - // - https://bugs.webkit.org/show_bug.cgi?id=187726 - // Because of this, we add a `?v=` query to bypass the cache during - // development. We need to also make sure that the number is always - // increasing. - const fullHref = `${assetPrefix}/_next/${href}${getAssetQueryString( - true - )}` - - // `Precedence` is an opt-in signal for React to handle resource - // loading and deduplication, etc. It's also used as the key to sort - // resources so they will be injected in the correct order. - // During HMR, it's critical to use different `precedence` values - // for different stylesheets, so their order will be kept. - // https://github.com/facebook/react/pull/25060 - const precedence = - process.env.NODE_ENV === 'development' ? 'next_' + href : 'next' - - preloadStyle(fullHref, renderOpts.crossOrigin) + const styles = stylesheets + ? stylesheets.map((href, index) => { + // In dev, Safari and Firefox will cache the resource during HMR: + // - https://github.com/vercel/next.js/issues/5860 + // - https://bugs.webkit.org/show_bug.cgi?id=187726 + // Because of this, we add a `?v=` query to bypass the cache during + // development. We need to also make sure that the number is always + // increasing. + const fullHref = `${assetPrefix}/_next/${href}${getAssetQueryString( + true + )}` - return ( - - ) - }) - : null + // `Precedence` is an opt-in signal for React to handle resource + // loading and deduplication, etc. It's also used as the key to sort + // resources so they will be injected in the correct order. + // During HMR, it's critical to use different `precedence` values + // for different stylesheets, so their order will be kept. + // https://github.com/facebook/react/pull/25060 + const precedence = + process.env.NODE_ENV === 'development' ? 'next_' + href : 'next' - return styles - } + preloadStyle(fullHref, renderOpts.crossOrigin) - const parseLoaderTree = (tree: LoaderTree) => { - const [segment, parallelRoutes, components] = tree - const { layout } = components - let { page } = components - // a __DEFAULT__ segment means that this route didn't match any of the - // segments in the route, so we should use the default page + return ( + + ) + }) + : null - page = segment === '__DEFAULT__' ? components.defaultPage : page + return styles + } - const layoutOrPagePath = layout?.[1] || page?.[1] + const parseLoaderTree = (tree: LoaderTree) => { + const [segment, parallelRoutes, components] = tree + const { layout } = components + let { page } = components + // a __DEFAULT__ segment means that this route didn't match any of the + // segments in the route, so we should use the default page - return { - page, - segment, - components, - layoutOrPagePath, - parallelRoutes, - } - } + page = segment === '__DEFAULT__' ? components.defaultPage : page - /** - * Use the provided loader tree to create the React Component tree. - */ - const createComponentTree = async ({ - createSegmentPath, - loaderTree: tree, - parentParams, - firstItem, - rootLayoutIncluded, - injectedCSS, - injectedFontPreloadTags, - asNotFound, - metadataOutlet, - }: { - createSegmentPath: CreateSegmentPath - loaderTree: LoaderTree - parentParams: { [key: string]: any } - rootLayoutIncluded: boolean - firstItem?: boolean - injectedCSS: Set - injectedFontPreloadTags: Set - asNotFound?: boolean - metadataOutlet?: React.ReactNode - }): Promise<{ - Component: React.ComponentType - styles: React.ReactNode - }> => { - const { page, layoutOrPagePath, segment, components, parallelRoutes } = - parseLoaderTree(tree) - - const { - layout, - template, - error, - loading, - 'not-found': notFound, - } = components + const layoutOrPagePath = layout?.[1] || page?.[1] - const injectedCSSWithCurrentLayout = new Set(injectedCSS) - const injectedFontPreloadTagsWithCurrentLayout = new Set( - injectedFontPreloadTags - ) + return { + page, + segment, + components, + layoutOrPagePath, + parallelRoutes, + } + } - const styles = getLayerAssets({ - layoutOrPagePath, - injectedCSS: injectedCSSWithCurrentLayout, - injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, - }) + /** + * Use the provided loader tree to create the React Component tree. + */ + const createComponentTree = async ({ + createSegmentPath, + loaderTree: tree, + parentParams, + firstItem, + rootLayoutIncluded, + injectedCSS, + injectedFontPreloadTags, + asNotFound, + metadataOutlet, + }: { + createSegmentPath: CreateSegmentPath + loaderTree: LoaderTree + parentParams: { [key: string]: any } + rootLayoutIncluded: boolean + firstItem?: boolean + injectedCSS: Set + injectedFontPreloadTags: Set + asNotFound?: boolean + metadataOutlet?: React.ReactNode + }): Promise<{ + Component: React.ComponentType + styles: React.ReactNode + }> => { + const { page, layoutOrPagePath, segment, components, parallelRoutes } = + parseLoaderTree(tree) + + const { + layout, + template, + error, + loading, + 'not-found': notFound, + } = components + + const injectedCSSWithCurrentLayout = new Set(injectedCSS) + const injectedFontPreloadTagsWithCurrentLayout = new Set( + injectedFontPreloadTags + ) - const [Template, templateStyles] = template - ? await createComponentAndStyles({ - filePath: template[1], - getComponent: template[0], - injectedCSS: injectedCSSWithCurrentLayout, - }) - : [React.Fragment] + const styles = getLayerAssets({ + layoutOrPagePath, + injectedCSS: injectedCSSWithCurrentLayout, + injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, + }) - const [ErrorComponent, errorStyles] = error - ? await createComponentAndStyles({ - filePath: error[1], - getComponent: error[0], - injectedCSS: injectedCSSWithCurrentLayout, - }) - : [] + const [Template, templateStyles] = template + ? await createComponentAndStyles({ + filePath: template[1], + getComponent: template[0], + injectedCSS: injectedCSSWithCurrentLayout, + }) + : [React.Fragment] - const [Loading, loadingStyles] = loading - ? await createComponentAndStyles({ - filePath: loading[1], - getComponent: loading[0], - injectedCSS: injectedCSSWithCurrentLayout, - }) - : [] + const [ErrorComponent, errorStyles] = error + ? await createComponentAndStyles({ + filePath: error[1], + getComponent: error[0], + injectedCSS: injectedCSSWithCurrentLayout, + }) + : [] - const isLayout = typeof layout !== 'undefined' - const isPage = typeof page !== 'undefined' - const [layoutOrPageMod] = await getLayoutOrPageModule(tree) + const [Loading, loadingStyles] = loading + ? await createComponentAndStyles({ + filePath: loading[1], + getComponent: loading[0], + injectedCSS: injectedCSSWithCurrentLayout, + }) + : [] - /** - * Checks if the current segment is a root layout. - */ - const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded - /** - * Checks if the current segment or any level above it has a root layout. - */ - const rootLayoutIncludedAtThisLevelOrAbove = - rootLayoutIncluded || rootLayoutAtThisLevel + const isLayout = typeof layout !== 'undefined' + const isPage = typeof page !== 'undefined' + const [layoutOrPageMod] = await getLayoutOrPageModule(tree) - const [NotFound, notFoundStyles] = notFound - ? await createComponentAndStyles({ - filePath: notFound[1], - getComponent: notFound[0], - injectedCSS: injectedCSSWithCurrentLayout, - }) - : [] - - let dynamic = layoutOrPageMod?.dynamic - - if (nextConfigOutput === 'export') { - if (!dynamic || dynamic === 'auto') { - dynamic = 'error' - } else if (dynamic === 'force-dynamic') { - staticGenerationStore.forceDynamic = true - staticGenerationStore.dynamicShouldError = true - staticGenerationBailout(`output: export`, { - dynamic, - link: 'https://nextjs.org/docs/advanced-features/static-html-export', - }) - } + /** + * Checks if the current segment is a root layout. + */ + const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded + /** + * Checks if the current segment or any level above it has a root layout. + */ + const rootLayoutIncludedAtThisLevelOrAbove = + rootLayoutIncluded || rootLayoutAtThisLevel + + const [NotFound, notFoundStyles] = notFound + ? await createComponentAndStyles({ + filePath: notFound[1], + getComponent: notFound[0], + injectedCSS: injectedCSSWithCurrentLayout, + }) + : [] + + let dynamic = layoutOrPageMod?.dynamic + + if (nextConfigOutput === 'export') { + if (!dynamic || dynamic === 'auto') { + dynamic = 'error' + } else if (dynamic === 'force-dynamic') { + staticGenerationStore.forceDynamic = true + staticGenerationStore.dynamicShouldError = true + staticGenerationBailout(`output: export`, { + dynamic, + link: 'https://nextjs.org/docs/advanced-features/static-html-export', + }) } + } - if (typeof dynamic === 'string') { - // the nested most config wins so we only force-static - // if it's configured above any parent that configured - // otherwise - if (dynamic === 'error') { - staticGenerationStore.dynamicShouldError = true - } else if (dynamic === 'force-dynamic') { - staticGenerationStore.forceDynamic = true - staticGenerationBailout(`force-dynamic`, { dynamic }) + if (typeof dynamic === 'string') { + // the nested most config wins so we only force-static + // if it's configured above any parent that configured + // otherwise + if (dynamic === 'error') { + staticGenerationStore.dynamicShouldError = true + } else if (dynamic === 'force-dynamic') { + staticGenerationStore.forceDynamic = true + staticGenerationBailout(`force-dynamic`, { dynamic }) + } else { + staticGenerationStore.dynamicShouldError = false + if (dynamic === 'force-static') { + staticGenerationStore.forceStatic = true } else { - staticGenerationStore.dynamicShouldError = false - if (dynamic === 'force-static') { - staticGenerationStore.forceStatic = true - } else { - staticGenerationStore.forceStatic = false - } + staticGenerationStore.forceStatic = false } } + } - if (typeof layoutOrPageMod?.fetchCache === 'string') { - staticGenerationStore.fetchCache = layoutOrPageMod?.fetchCache - } + if (typeof layoutOrPageMod?.fetchCache === 'string') { + staticGenerationStore.fetchCache = layoutOrPageMod?.fetchCache + } - if (typeof layoutOrPageMod?.revalidate === 'number') { - defaultRevalidate = layoutOrPageMod.revalidate as number + if (typeof layoutOrPageMod?.revalidate === 'number') { + defaultRevalidate = layoutOrPageMod.revalidate as number - if ( - typeof staticGenerationStore.revalidate === 'undefined' || - (typeof staticGenerationStore.revalidate === 'number' && - staticGenerationStore.revalidate > defaultRevalidate) - ) { - staticGenerationStore.revalidate = defaultRevalidate - } + if ( + typeof staticGenerationStore.revalidate === 'undefined' || + (typeof staticGenerationStore.revalidate === 'number' && + staticGenerationStore.revalidate > defaultRevalidate) + ) { + staticGenerationStore.revalidate = defaultRevalidate + } - if ( - staticGenerationStore.isStaticGeneration && - defaultRevalidate === 0 - ) { - const dynamicUsageDescription = `revalidate: 0 configured ${segment}` - staticGenerationStore.dynamicUsageDescription = - dynamicUsageDescription + if (staticGenerationStore.isStaticGeneration && defaultRevalidate === 0) { + const dynamicUsageDescription = `revalidate: 0 configured ${segment}` + staticGenerationStore.dynamicUsageDescription = dynamicUsageDescription - throw new DynamicServerError(dynamicUsageDescription) - } + throw new DynamicServerError(dynamicUsageDescription) } + } - if (staticGenerationStore?.dynamicUsageErr) { - throw staticGenerationStore.dynamicUsageErr - } + if (staticGenerationStore?.dynamicUsageErr) { + throw staticGenerationStore.dynamicUsageErr + } - const LayoutOrPage = layoutOrPageMod - ? interopDefault(layoutOrPageMod) - : undefined + const LayoutOrPage = layoutOrPageMod + ? interopDefault(layoutOrPageMod) + : undefined - /** - * The React Component to render. - */ - let Component = LayoutOrPage - const parallelKeys = Object.keys(parallelRoutes) - const hasSlotKey = parallelKeys.length > 1 - - if (hasSlotKey && rootLayoutAtThisLevel) { - Component = (componentProps: any) => { - const NotFoundComponent = NotFound - const RootLayoutComponent = LayoutOrPage - return ( - - {styles} - - {notFoundStyles} - - - - } - > - - - ) - } + /** + * The React Component to render. + */ + let Component = LayoutOrPage + const parallelKeys = Object.keys(parallelRoutes) + const hasSlotKey = parallelKeys.length > 1 + + if (hasSlotKey && rootLayoutAtThisLevel) { + Component = (componentProps: any) => { + const NotFoundComponent = NotFound + const RootLayoutComponent = LayoutOrPage + return ( + + {styles} + + {notFoundStyles} + + + + } + > + + + ) } + } - if (dev) { - const { isValidElementType } = require('next/dist/compiled/react-is') - if ( - (isPage || typeof Component !== 'undefined') && - !isValidElementType(Component) - ) { - throw new Error( - `The default export is not a React Component in page: "${pagePath}"` - ) - } - - if ( - typeof ErrorComponent !== 'undefined' && - !isValidElementType(ErrorComponent) - ) { - throw new Error( - `The default export of error is not a React Component in page: ${segment}` - ) - } - - if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) { - throw new Error( - `The default export of loading is not a React Component in ${segment}` - ) - } + if (dev) { + const { isValidElementType } = require('next/dist/compiled/react-is') + if ( + (isPage || typeof Component !== 'undefined') && + !isValidElementType(Component) + ) { + throw new Error( + `The default export is not a React Component in page: "${pagePath}"` + ) + } - if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) { - throw new Error( - `The default export of notFound is not a React Component in ${segment}` - ) - } + if ( + typeof ErrorComponent !== 'undefined' && + !isValidElementType(ErrorComponent) + ) { + throw new Error( + `The default export of error is not a React Component in page: ${segment}` + ) } - // Handle dynamic segment params. - const segmentParam = getDynamicParamFromSegment(segment) - /** - * Create object holding the parent params and current params - */ - const currentParams = - // Handle null case where dynamic param is optional - segmentParam && segmentParam.value !== null - ? { - ...parentParams, - [segmentParam.param]: segmentParam.value, - } - : // Pass through parent params to children - parentParams - // Resolve the segment param - const actualSegment = segmentParam ? segmentParam.treeSegment : segment - - // This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down - const parallelRouteMap = await Promise.all( - Object.keys(parallelRoutes).map( - async (parallelRouteKey): Promise<[string, React.ReactNode]> => { - const isChildrenRouteKey = parallelRouteKey === 'children' - const currentSegmentPath: FlightSegmentPath = firstItem - ? [parallelRouteKey] - : [actualSegment, parallelRouteKey] + if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) { + throw new Error( + `The default export of loading is not a React Component in ${segment}` + ) + } - const parallelRoute = parallelRoutes[parallelRouteKey] + if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) { + throw new Error( + `The default export of notFound is not a React Component in ${segment}` + ) + } + } - const childSegment = parallelRoute[0] - const childSegmentParam = getDynamicParamFromSegment(childSegment) - const notFoundComponent = - NotFound && isChildrenRouteKey ? : undefined - - function getParallelRoutePair( - currentChildProp: ChildProp, - currentStyles: React.ReactNode - ): [string, React.ReactNode] { - // This is turned back into an object below. - return [ - parallelRouteKey, - : undefined} - loadingStyles={loadingStyles} - // TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning. - hasLoading={Boolean(Loading)} - error={ErrorComponent} - errorStyles={errorStyles} - template={ - - } - templateStyles={templateStyles} - notFound={notFoundComponent} - notFoundStyles={notFoundStyles} - childProp={currentChildProp} - styles={currentStyles} - />, - ] - } + // Handle dynamic segment params. + const segmentParam = getDynamicParamFromSegment(segment) + /** + * Create object holding the parent params and current params + */ + const currentParams = + // Handle null case where dynamic param is optional + segmentParam && segmentParam.value !== null + ? { + ...parentParams, + [segmentParam.param]: segmentParam.value, + } + : // Pass through parent params to children + parentParams + // Resolve the segment param + const actualSegment = segmentParam ? segmentParam.treeSegment : segment + + // This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down + const parallelRouteMap = await Promise.all( + Object.keys(parallelRoutes).map( + async (parallelRouteKey): Promise<[string, React.ReactNode]> => { + const isChildrenRouteKey = parallelRouteKey === 'children' + const currentSegmentPath: FlightSegmentPath = firstItem + ? [parallelRouteKey] + : [actualSegment, parallelRouteKey] + + const parallelRoute = parallelRoutes[parallelRouteKey] + + const childSegment = parallelRoute[0] + const childSegmentParam = getDynamicParamFromSegment(childSegment) + const notFoundComponent = + NotFound && isChildrenRouteKey ? : undefined + + function getParallelRoutePair( + currentChildProp: ChildProp, + currentStyles: React.ReactNode + ): [string, React.ReactNode] { + // This is turned back into an object below. + return [ + parallelRouteKey, + : undefined} + loadingStyles={loadingStyles} + // TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning. + hasLoading={Boolean(Loading)} + error={ErrorComponent} + errorStyles={errorStyles} + template={ + + } + templateStyles={templateStyles} + notFound={notFoundComponent} + notFoundStyles={notFoundStyles} + childProp={currentChildProp} + styles={currentStyles} + />, + ] + } - // if we're prefetching and that there's a Loading component, we bail out - // otherwise we keep rendering for the prefetch. - // We also want to bail out if there's no Loading component in the tree. - let currentStyles = undefined - let childElement = null - const childPropSegment = addSearchParamsIfPageSegment( - childSegmentParam ? childSegmentParam.treeSegment : childSegment, - query + // if we're prefetching and that there's a Loading component, we bail out + // otherwise we keep rendering for the prefetch. + // We also want to bail out if there's no Loading component in the tree. + let currentStyles = undefined + let childElement = null + const childPropSegment = addSearchParamsIfPageSegment( + childSegmentParam ? childSegmentParam.treeSegment : childSegment, + query + ) + if ( + !( + isPrefetch && + (Loading || !hasLoadingComponentInTree(parallelRoute)) ) - if ( - !( - isPrefetch && - (Loading || !hasLoadingComponentInTree(parallelRoute)) - ) - ) { - // Create the child component - const { - Component: ChildComponent, - styles: childComponentStyles, - } = await createComponentTree({ + ) { + // Create the child component + const { Component: ChildComponent, styles: childComponentStyles } = + await createComponentTree({ createSegmentPath: (child) => { return createSegmentPath([...currentSegmentPath, ...child]) }, @@ -903,924 +874,935 @@ export const renderToHTMLOrFlight: AppPageRender = ( metadataOutlet, }) - currentStyles = childComponentStyles - childElement = - } + currentStyles = childComponentStyles + childElement = + } - const childProp: ChildProp = { - current: childElement, - segment: childPropSegment, - } - - return getParallelRoutePair(childProp, currentStyles) + const childProp: ChildProp = { + current: childElement, + segment: childPropSegment, } - ) - ) - // Convert the parallel route map into an object after all promises have been resolved. - const parallelRouteComponents = parallelRouteMap.reduce( - (list, [parallelRouteKey, Comp]) => { - list[parallelRouteKey] = Comp - return list - }, - {} as { [key: string]: React.ReactNode } + return getParallelRoutePair(childProp, currentStyles) + } ) + ) - // When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component - if (!Component) { - return { - Component: () => <>{parallelRouteComponents.children}, - styles, - } + // Convert the parallel route map into an object after all promises have been resolved. + const parallelRouteComponents = parallelRouteMap.reduce( + (list, [parallelRouteKey, Comp]) => { + list[parallelRouteKey] = Comp + return list + }, + {} as { [key: string]: React.ReactNode } + ) + + // When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component + if (!Component) { + return { + Component: () => <>{parallelRouteComponents.children}, + styles, } + } - const isClientComponent = isClientReference(layoutOrPageMod) + const isClientComponent = isClientReference(layoutOrPageMod) + + // If it's a not found route, and we don't have any matched parallel + // routes, we try to render the not found component if it exists. + let notFoundComponent = {} + if ( + NotFound && + asNotFound && + // In development, it could hit the parallel-route-default not found, so we only need to check the segment. + // Or if there's no parallel routes means it reaches the end. + !parallelRouteMap.length + ) { + notFoundComponent = { + children: ( + <> + + {process.env.NODE_ENV === 'development' && ( + + )} + {notFoundStyles} + + + ), + } + } - // If it's a not found route, and we don't have any matched parallel - // routes, we try to render the not found component if it exists. - let notFoundComponent = {} - if ( - NotFound && - asNotFound && - // In development, it could hit the parallel-route-default not found, so we only need to check the segment. - // Or if there's no parallel routes means it reaches the end. - !parallelRouteMap.length - ) { - notFoundComponent = { - children: ( - <> - - {process.env.NODE_ENV === 'development' && ( - - )} - {notFoundStyles} - - - ), + const props = { + ...parallelRouteComponents, + ...notFoundComponent, + // TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list. + // Params are always the current params that apply to the layout + // If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down. + params: currentParams, + // Query is only provided to page + ...(() => { + if (isClientComponent && isStaticGeneration) { + return {} } - } - const props = { - ...parallelRouteComponents, - ...notFoundComponent, - // TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list. - // Params are always the current params that apply to the layout - // If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down. - params: currentParams, - // Query is only provided to page - ...(() => { - if (isClientComponent && isStaticGeneration) { - return {} - } + if (isPage) { + return searchParamsProps + } + })(), + } - if (isPage) { - return searchParamsProps - } - })(), - } + // Eagerly execute layout/page component to trigger fetches early. + if (!isClientComponent) { + Component = await Promise.resolve().then(() => + preloadComponent(Component, props) + ) + } - // Eagerly execute layout/page component to trigger fetches early. - if (!isClientComponent) { - Component = await Promise.resolve().then(() => - preloadComponent(Component, props) + return { + Component: () => { + return ( + <> + {isPage ? metadataOutlet : null} + {/* needs to be the first element because we use `findDOMNode` in layout router to locate it. */} + {isPage && isClientComponent ? ( + + ) : ( + + )} + {/* This null is currently critical. The wrapped Component can render null and if there was not fragment + surrounding it this would look like a pending tree data state on the client which will cause an errror + and break the app. Long-term we need to move away from using null as a partial tree identifier since it + is a valid return type for the components we wrap. Once we make this change we can safely remove the + fragment. The reason the extra null here is required is that fragments which only have 1 child are elided. + If the Component above renders null the actual treedata will look like `[null, null]`. If we remove the extra + null it will look like `null` (the array is elided) and this is what confuses the client router. + TODO-APP update router to use a Symbol for partial tree detection */} + {null} + ) - } - - return { - Component: () => { - return ( - <> - {isPage ? metadataOutlet : null} - {/* needs to be the first element because we use `findDOMNode` in layout router to locate it. */} - {isPage && isClientComponent ? ( - - ) : ( - - )} - {/* This null is currently critical. The wrapped Component can render null and if there was not fragment - surrounding it this would look like a pending tree data state on the client which will cause an errror - and break the app. Long-term we need to move away from using null as a partial tree identifier since it - is a valid return type for the components we wrap. Once we make this change we can safely remove the - fragment. The reason the extra null here is required is that fragments which only have 1 child are elided. - If the Component above renders null the actual treedata will look like `[null, null]`. If we remove the extra - null it will look like `null` (the array is elided) and this is what confuses the client router. - TODO-APP update router to use a Symbol for partial tree detection */} - {null} - - ) - }, - styles, - } + }, + styles, } + } - // Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`. - const generateFlight = async (options?: { - actionResult: ActionResult - skipFlight: boolean + // Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`. + const generateFlight = async (options?: { + actionResult: ActionResult + skipFlight: boolean + asNotFound?: boolean + }): Promise => { + /** + * Use router state to decide at what common layout to render the page. + * This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree. + */ + const walkTreeWithFlightRouterState = async ({ + createSegmentPath, + loaderTreeToFilter, + parentParams, + isFirst, + flightRouterState, + parentRendered, + rscPayloadHead, + injectedCSS, + injectedFontPreloadTags, + rootLayoutIncluded, + asNotFound, + metadataOutlet, + }: { + createSegmentPath: CreateSegmentPath + loaderTreeToFilter: LoaderTree + parentParams: { [key: string]: string | string[] } + isFirst: boolean + flightRouterState?: FlightRouterState + parentRendered?: boolean + rscPayloadHead: React.ReactNode + injectedCSS: Set + injectedFontPreloadTags: Set + rootLayoutIncluded: boolean asNotFound?: boolean - }): Promise => { + metadataOutlet: React.ReactNode + }): Promise => { + const [segment, parallelRoutes, components] = loaderTreeToFilter + + const parallelRoutesKeys = Object.keys(parallelRoutes) + + const { layout } = components + const isLayout = typeof layout !== 'undefined' + /** - * Use router state to decide at what common layout to render the page. - * This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree. + * Checks if the current segment is a root layout. */ - const walkTreeWithFlightRouterState = async ({ - createSegmentPath, - loaderTreeToFilter, - parentParams, - isFirst, - flightRouterState, - parentRendered, - rscPayloadHead, - injectedCSS, - injectedFontPreloadTags, - rootLayoutIncluded, - asNotFound, - metadataOutlet, - }: { - createSegmentPath: CreateSegmentPath - loaderTreeToFilter: LoaderTree - parentParams: { [key: string]: string | string[] } - isFirst: boolean - flightRouterState?: FlightRouterState - parentRendered?: boolean - rscPayloadHead: React.ReactNode - injectedCSS: Set - injectedFontPreloadTags: Set - rootLayoutIncluded: boolean - asNotFound?: boolean - metadataOutlet: React.ReactNode - }): Promise => { - const [segment, parallelRoutes, components] = loaderTreeToFilter - - const parallelRoutesKeys = Object.keys(parallelRoutes) - - const { layout } = components - const isLayout = typeof layout !== 'undefined' - - /** - * Checks if the current segment is a root layout. - */ - const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded - /** - * Checks if the current segment or any level above it has a root layout. - */ - const rootLayoutIncludedAtThisLevelOrAbove = - rootLayoutIncluded || rootLayoutAtThisLevel - - // Because this function walks to a deeper point in the tree to start rendering we have to track the dynamic parameters up to the point where rendering starts - const segmentParam = getDynamicParamFromSegment(segment) - const currentParams = - // Handle null case where dynamic param is optional - segmentParam && segmentParam.value !== null - ? { - ...parentParams, - [segmentParam.param]: segmentParam.value, - } - : parentParams - const actualSegment: Segment = addSearchParamsIfPageSegment( - segmentParam ? segmentParam.treeSegment : segment, - query - ) + const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded + /** + * Checks if the current segment or any level above it has a root layout. + */ + const rootLayoutIncludedAtThisLevelOrAbove = + rootLayoutIncluded || rootLayoutAtThisLevel - /** - * Decide if the current segment is where rendering has to start. - */ - const renderComponentsOnThisLevel = - // No further router state available - !flightRouterState || - // Segment in router state does not match current segment - !matchSegment(actualSegment, flightRouterState[0]) || - // Last item in the tree - parallelRoutesKeys.length === 0 || - // Explicit refresh - flightRouterState[3] === 'refetch' - - const shouldSkipComponentTree = - isPrefetch && - !Boolean(components.loading) && - (flightRouterState || - // If there is no flightRouterState, we need to check the entire loader tree, as otherwise we'll be only checking the root - !hasLoadingComponentInTree(loaderTree)) - - if (!parentRendered && renderComponentsOnThisLevel) { - const overriddenSegment = - flightRouterState && - canSegmentBeOverridden(actualSegment, flightRouterState[0]) - ? flightRouterState[0] - : null - - return [ - [ - overriddenSegment ?? actualSegment, - createFlightRouterStateFromLoaderTree( - // Create router state using the slice of the loaderTree - loaderTreeToFilter, - getDynamicParamFromSegment, - query - ), - shouldSkipComponentTree - ? null - : // Create component tree using the slice of the loaderTree - // @ts-expect-error TODO-APP: fix async component type - React.createElement(async () => { - const { Component } = await createComponentTree( - // This ensures flightRouterPath is valid and filters down the tree - { - createSegmentPath, - loaderTree: loaderTreeToFilter, - parentParams: currentParams, - firstItem: isFirst, - injectedCSS, - injectedFontPreloadTags, - // This is intentionally not "rootLayoutIncludedAtThisLevelOrAbove" as createComponentTree starts at the current level and does a check for "rootLayoutAtThisLevel" too. - rootLayoutIncluded, - asNotFound, - metadataOutlet, - } - ) - - return - }), - shouldSkipComponentTree - ? null - : (() => { - const { layoutOrPagePath } = - parseLoaderTree(loaderTreeToFilter) - - const styles = getLayerAssets({ - layoutOrPagePath, - injectedCSS: new Set(injectedCSS), - injectedFontPreloadTags: new Set(injectedFontPreloadTags), - }) - - return ( - <> - {styles} - {rscPayloadHead} - - ) - })(), - ], - ] - } + // Because this function walks to a deeper point in the tree to start rendering we have to track the dynamic parameters up to the point where rendering starts + const segmentParam = getDynamicParamFromSegment(segment) + const currentParams = + // Handle null case where dynamic param is optional + segmentParam && segmentParam.value !== null + ? { + ...parentParams, + [segmentParam.param]: segmentParam.value, + } + : parentParams + const actualSegment: Segment = addSearchParamsIfPageSegment( + segmentParam ? segmentParam.treeSegment : segment, + query + ) + + /** + * Decide if the current segment is where rendering has to start. + */ + const renderComponentsOnThisLevel = + // No further router state available + !flightRouterState || + // Segment in router state does not match current segment + !matchSegment(actualSegment, flightRouterState[0]) || + // Last item in the tree + parallelRoutesKeys.length === 0 || + // Explicit refresh + flightRouterState[3] === 'refetch' + + const shouldSkipComponentTree = + isPrefetch && + !Boolean(components.loading) && + (flightRouterState || + // If there is no flightRouterState, we need to check the entire loader tree, as otherwise we'll be only checking the root + !hasLoadingComponentInTree(loaderTree)) + + if (!parentRendered && renderComponentsOnThisLevel) { + const overriddenSegment = + flightRouterState && + canSegmentBeOverridden(actualSegment, flightRouterState[0]) + ? flightRouterState[0] + : null + + return [ + [ + overriddenSegment ?? actualSegment, + createFlightRouterStateFromLoaderTree( + // Create router state using the slice of the loaderTree + loaderTreeToFilter, + getDynamicParamFromSegment, + query + ), + shouldSkipComponentTree + ? null + : // Create component tree using the slice of the loaderTree + // @ts-expect-error TODO-APP: fix async component type + React.createElement(async () => { + const { Component } = await createComponentTree( + // This ensures flightRouterPath is valid and filters down the tree + { + createSegmentPath, + loaderTree: loaderTreeToFilter, + parentParams: currentParams, + firstItem: isFirst, + injectedCSS, + injectedFontPreloadTags, + // This is intentionally not "rootLayoutIncludedAtThisLevelOrAbove" as createComponentTree starts at the current level and does a check for "rootLayoutAtThisLevel" too. + rootLayoutIncluded, + asNotFound, + metadataOutlet, + } + ) + + return + }), + shouldSkipComponentTree + ? null + : (() => { + const { layoutOrPagePath } = + parseLoaderTree(loaderTreeToFilter) + + const styles = getLayerAssets({ + layoutOrPagePath, + injectedCSS: new Set(injectedCSS), + injectedFontPreloadTags: new Set(injectedFontPreloadTags), + }) + + return ( + <> + {styles} + {rscPayloadHead} + + ) + })(), + ], + ] + } - // If we are not rendering on this level we need to check if the current - // segment has a layout. If so, we need to track all the used CSS to make - // the result consistent. - const layoutPath = layout?.[1] - const injectedCSSWithCurrentLayout = new Set(injectedCSS) - const injectedFontPreloadTagsWithCurrentLayout = new Set( - injectedFontPreloadTags + // If we are not rendering on this level we need to check if the current + // segment has a layout. If so, we need to track all the used CSS to make + // the result consistent. + const layoutPath = layout?.[1] + const injectedCSSWithCurrentLayout = new Set(injectedCSS) + const injectedFontPreloadTagsWithCurrentLayout = new Set( + injectedFontPreloadTags + ) + if (layoutPath) { + getCssInlinedLinkTags( + clientReferenceManifest, + layoutPath, + injectedCSSWithCurrentLayout, + true ) - if (layoutPath) { - getCssInlinedLinkTags( - clientReferenceManifest, - layoutPath, - injectedCSSWithCurrentLayout, - true - ) - getPreloadableFonts( - nextFontManifest, - layoutPath, - injectedFontPreloadTagsWithCurrentLayout - ) - } + getPreloadableFonts( + nextFontManifest, + layoutPath, + injectedFontPreloadTagsWithCurrentLayout + ) + } - // Walk through all parallel routes. - const paths: FlightDataPath[] = ( - await Promise.all( - parallelRoutesKeys.map(async (parallelRouteKey) => { - // for (const parallelRouteKey of parallelRoutesKeys) { - const parallelRoute = parallelRoutes[parallelRouteKey] + // Walk through all parallel routes. + const paths: FlightDataPath[] = ( + await Promise.all( + parallelRoutesKeys.map(async (parallelRouteKey) => { + // for (const parallelRouteKey of parallelRoutesKeys) { + const parallelRoute = parallelRoutes[parallelRouteKey] - const currentSegmentPath: FlightSegmentPath = isFirst - ? [parallelRouteKey] - : [actualSegment, parallelRouteKey] + const currentSegmentPath: FlightSegmentPath = isFirst + ? [parallelRouteKey] + : [actualSegment, parallelRouteKey] - const path = await walkTreeWithFlightRouterState({ - createSegmentPath: (child) => { - return createSegmentPath([...currentSegmentPath, ...child]) - }, - loaderTreeToFilter: parallelRoute, - parentParams: currentParams, - flightRouterState: - flightRouterState && flightRouterState[1][parallelRouteKey], - parentRendered: parentRendered || renderComponentsOnThisLevel, - isFirst: false, - rscPayloadHead, - injectedCSS: injectedCSSWithCurrentLayout, - injectedFontPreloadTags: - injectedFontPreloadTagsWithCurrentLayout, - rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, - asNotFound, - metadataOutlet, + const path = await walkTreeWithFlightRouterState({ + createSegmentPath: (child) => { + return createSegmentPath([...currentSegmentPath, ...child]) + }, + loaderTreeToFilter: parallelRoute, + parentParams: currentParams, + flightRouterState: + flightRouterState && flightRouterState[1][parallelRouteKey], + parentRendered: parentRendered || renderComponentsOnThisLevel, + isFirst: false, + rscPayloadHead, + injectedCSS: injectedCSSWithCurrentLayout, + injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, + rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, + asNotFound, + metadataOutlet, + }) + + return path + .map((item) => { + // we don't need to send over default routes in the flight data + // because they are always ignored by the client, unless it's a refetch + if ( + item[0] === '__DEFAULT__' && + flightRouterState && + !!flightRouterState[1][parallelRouteKey][0] && + flightRouterState[1][parallelRouteKey][3] !== 'refetch' + ) { + return null + } + return [actualSegment, parallelRouteKey, ...item] }) + .filter(Boolean) as FlightDataPath[] + }) + ) + ).flat() - return path - .map((item) => { - // we don't need to send over default routes in the flight data - // because they are always ignored by the client, unless it's a refetch - if ( - item[0] === '__DEFAULT__' && - flightRouterState && - !!flightRouterState[1][parallelRouteKey][0] && - flightRouterState[1][parallelRouteKey][3] !== 'refetch' - ) { - return null - } - return [actualSegment, parallelRouteKey, ...item] - }) - .filter(Boolean) as FlightDataPath[] - }) - ) - ).flat() + return paths + } - return paths + // Flight data that is going to be passed to the browser. + // Currently a single item array but in the future multiple patches might be combined in a single request. + + let flightData: FlightData | null = null + if (!options?.skipFlight) { + const [MetadataTree, MetadataOutlet] = createMetadataComponents({ + tree: loaderTree, + pathname: urlPathname, + searchParams: providedSearchParams, + getDynamicParamFromSegment, + appUsingSizeAdjust, + }) + flightData = ( + await walkTreeWithFlightRouterState({ + createSegmentPath: (child) => child, + loaderTreeToFilter: loaderTree, + parentParams: {}, + flightRouterState: providedFlightRouterState, + isFirst: true, + // For flight, render metadata inside leaf page + rscPayloadHead: ( + // Adding requestId as react key to make metadata remount for each render + + ), + injectedCSS: new Set(), + injectedFontPreloadTags: new Set(), + rootLayoutIncluded: false, + asNotFound: isNotFoundPath || options?.asNotFound, + metadataOutlet: , + }) + ).map((path) => path.slice(1)) // remove the '' (root) segment + } + + const buildIdFlightDataPair = [buildId, flightData] + + // For app dir, use the bundled version of Flight server renderer (renderToReadableStream) + // which contains the subset React. + const flightReadableStream = renderToReadableStream( + options + ? [options.actionResult, buildIdFlightDataPair] + : buildIdFlightDataPair, + clientReferenceManifest.clientModules, + { + context: serverContexts, + onError: flightDataRendererErrorHandler, } + ).pipeThrough(createBufferedTransformStream()) - // Flight data that is going to be passed to the browser. - // Currently a single item array but in the future multiple patches might be combined in a single request. + return new FlightRenderResult(flightReadableStream) + } - let flightData: FlightData | null = null - if (!options?.skipFlight) { - const [MetadataTree, MetadataOutlet] = createMetadataComponents({ - tree: loaderTree, - pathname, - searchParams: providedSearchParams, - getDynamicParamFromSegment, - appUsingSizeAdjust, - }) - flightData = ( - await walkTreeWithFlightRouterState({ - createSegmentPath: (child) => child, - loaderTreeToFilter: loaderTree, - parentParams: {}, - flightRouterState: providedFlightRouterState, - isFirst: true, - // For flight, render metadata inside leaf page - rscPayloadHead: ( - // Adding requestId as react key to make metadata remount for each render - + if (isFlight && !staticGenerationStore.isStaticGeneration) { + return generateFlight() + } + + // Get the nonce from the incoming request if it has one. + const csp = req.headers['content-security-policy'] + let nonce: string | undefined + if (csp && typeof csp === 'string') { + nonce = getScriptNonceFromHeader(csp) + } + + const serverComponentsRenderOpts = { + inlinedDataTransformStream: new TransformStream(), + clientReferenceManifest, + serverContexts, + formState: null, + } + + const validateRootLayout = dev + ? { + validateRootLayout: { + assetPrefix: renderOpts.assetPrefix, + getTree: () => + createFlightRouterStateFromLoaderTree( + loaderTree, + getDynamicParamFromSegment, + query ), - injectedCSS: new Set(), - injectedFontPreloadTags: new Set(), - rootLayoutIncluded: false, - asNotFound: isNotFoundPath || options?.asNotFound, - metadataOutlet: , - }) - ).map((path) => path.slice(1)) // remove the '' (root) segment + }, } + : {} - const buildIdFlightDataPair = [buildId, flightData] - - // For app dir, use the bundled version of Flight server renderer (renderToReadableStream) - // which contains the subset React. - const flightReadableStream = renderToReadableStream( - options - ? [options.actionResult, buildIdFlightDataPair] - : buildIdFlightDataPair, - clientReferenceManifest.clientModules, - { - context: serverContexts, - onError: flightDataRendererErrorHandler, - } - ).pipeThrough(createBufferedTransformStream()) + /** + * A new React Component that renders the provided React Component + * using Flight which can then be rendered to HTML. + */ + const createServerComponentsRenderer = ( + loaderTreeToRender: LoaderTree, + preinitScripts: () => void, + formState: null | any + ) => + createServerComponentRenderer<{ + asNotFound: boolean + }>( + async (props) => { + preinitScripts() + // Create full component tree from root to leaf. + const injectedCSS = new Set() + const injectedFontPreloadTags = new Set() + const initialTree = createFlightRouterStateFromLoaderTree( + loaderTreeToRender, + getDynamicParamFromSegment, + query + ) - return new FlightRenderResult(flightReadableStream) - } + const [MetadataTree, MetadataOutlet] = createMetadataComponents({ + tree: loaderTreeToRender, + errorType: props.asNotFound ? 'not-found' : undefined, + pathname: urlPathname, + searchParams: providedSearchParams, + getDynamicParamFromSegment: getDynamicParamFromSegment, + appUsingSizeAdjust: appUsingSizeAdjust, + }) - if (isFlight && !staticGenerationStore.isStaticGeneration) { - return generateFlight() - } + const { Component: ComponentTree, styles } = await createComponentTree({ + createSegmentPath: (child) => child, + loaderTree: loaderTreeToRender, + parentParams: {}, + firstItem: true, + injectedCSS, + injectedFontPreloadTags, + rootLayoutIncluded: false, + asNotFound: props.asNotFound, + metadataOutlet: , + }) - // Get the nonce from the incoming request if it has one. - const csp = req.headers['content-security-policy'] - let nonce: string | undefined - if (csp && typeof csp === 'string') { - nonce = getScriptNonceFromHeader(csp) - } + return ( + <> + {styles} + + {res.statusCode > 400 && ( + + )} + {/* Adding requestId as react key to make metadata remount for each render */} + + + } + globalErrorComponent={GlobalError} + > + + + + ) + }, + ComponentMod, + { ...serverComponentsRenderOpts, formState }, + serverComponentsErrorHandler, + nonce + ) - const serverComponentsRenderOpts = { - inlinedDataTransformStream: new TransformStream(), - clientReferenceManifest, - serverContexts, - formState: null, - } + const { HeadManagerContext } = + require('../../shared/lib/head-manager-context.shared-runtime') as typeof import('../../shared/lib/head-manager-context.shared-runtime') + + // On each render, create a new `ServerInsertedHTML` context to capture + // injected nodes from user code (`useServerInsertedHTML`). + const { ServerInsertedHTMLProvider, renderServerInsertedHTML } = + createServerInsertedHTML() + + getTracer().getRootSpanAttributes()?.set('next.route', pagePath) + const bodyResult = getTracer().wrap( + AppRenderSpan.getBodyResult, + { + spanName: `render route (app) ${pagePath}`, + attributes: { + 'next.route': pagePath, + }, + }, + async ({ + asNotFound, + tree, + formState, + }: { + /** + * This option is used to indicate that the page should be rendered as + * if it was not found. When it's enabled, instead of rendering the + * page component, it renders the not-found segment. + * + */ + asNotFound: boolean + tree: LoaderTree + formState: any + }) => { + const polyfills: JSX.IntrinsicElements['script'][] = + buildManifest.polyfillFiles + .filter( + (polyfill) => + polyfill.endsWith('.js') && !polyfill.endsWith('.module.js') + ) + .map((polyfill) => ({ + src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString( + false + )}`, + integrity: subresourceIntegrityManifest?.[polyfill], + crossOrigin: renderOpts.crossOrigin, + noModule: true, + nonce, + })) + + const [preinitScripts, bootstrapScript] = getRequiredScripts( + buildManifest, + assetPrefix, + renderOpts.crossOrigin, + subresourceIntegrityManifest, + getAssetQueryString(true), + nonce + ) + const ServerComponentsRenderer = createServerComponentsRenderer( + tree, + preinitScripts, + formState + ) + const content = ( + + + + + + ) - const validateRootLayout = dev - ? { - validateRootLayout: { - assetPrefix: renderOpts.assetPrefix, - getTree: () => - createFlightRouterStateFromLoaderTree( - loaderTree, - getDynamicParamFromSegment, - query - ), - }, + let polyfillsFlushed = false + let flushedErrorMetaTagsUntilIndex = 0 + const getServerInsertedHTML = (serverCapturedErrors: Error[]) => { + // Loop through all the errors that have been captured but not yet + // flushed. + const errorMetaTags = [] + for ( + ; + flushedErrorMetaTagsUntilIndex < serverCapturedErrors.length; + flushedErrorMetaTagsUntilIndex++ + ) { + const error = serverCapturedErrors[flushedErrorMetaTagsUntilIndex] + + if (isNotFoundError(error)) { + errorMetaTags.push( + , + process.env.NODE_ENV === 'development' ? ( + + ) : null + ) + } else if (isRedirectError(error)) { + const redirectUrl = getURLFromRedirectError(error) + const isPermanent = + getRedirectStatusCodeFromError(error) === 308 ? true : false + if (redirectUrl) { + errorMetaTags.push( + + ) + } + } } - : {} - /** - * A new React Component that renders the provided React Component - * using Flight which can then be rendered to HTML. - */ - const createServerComponentsRenderer = ( - loaderTreeToRender: LoaderTree, - preinitScripts: () => void, - formState: null | any - ) => - createServerComponentRenderer<{ - asNotFound: boolean - }>( - async (props) => { - preinitScripts() - // Create full component tree from root to leaf. - const injectedCSS = new Set() - const injectedFontPreloadTags = new Set() - const initialTree = createFlightRouterStateFromLoaderTree( - loaderTreeToRender, - getDynamicParamFromSegment, - query - ) + const flushed = renderToString({ + ReactDOMServer: require('react-dom/server.edge'), + element: ( + <> + {polyfillsFlushed + ? null + : polyfills?.map((polyfill) => { + return