next.js/packages/next/src/server/app-render/create-component-tree.tsx
create-component-tree.tsx1415 lines46.2 KB
import type { ComponentType } from 'react'
import type {
  CacheNodeSeedData,
  LoadingModuleData,
} from '../../shared/lib/app-router-types'
import type { PreloadCallbacks } from './types'
import {
  isClientReference,
  isUseCacheFunction,
} from '../../lib/client-and-server-references'
import { getLayoutOrPageModule } from '../lib/app-dir-module'
import type { LoaderTree } from '../lib/app-dir-module'
import { interopDefault } from './interop-default'
import { parseLoaderTree } from '../../shared/lib/router/utils/parse-loader-tree'
import type { AppRenderContext, GetDynamicParamFromSegment } from './app-render'
import { createComponentStylesAndScripts } from './create-component-styles-and-scripts'
import { getLayerAssets } from './get-layer-assets'
import { hasLoadingComponentInTree } from './has-loading-component-in-tree'
import { validateRevalidate } from '../lib/patch-fetch'
import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../client/components/builtin/default'
import { getTracer } from '../lib/trace/tracer'
import { NextNodeServerSpan } from '../lib/trace/constants'
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
import type { Params } from '../request/params'
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
import {
  createVaryParamsAccumulator,
  emptyVaryParamsAccumulator,
  getVaryParamsThenable,
  type VaryParamsAccumulator,
} from './vary-params'
import type {
  UseCacheLayoutProps,
  UseCachePageProps,
} from '../use-cache/use-cache-wrapper'
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
import {
  BOUNDARY_PREFIX,
  BOUNDARY_SUFFIX,
  BUILTIN_PREFIX,
  getConventionPathByType,
  isNextjsBuiltinFilePath,
} from './segment-explorer-path'
import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config'
import { RenderStage, type StagedRenderingController } from './staged-rendering'

type HTTPAccessErrorStatusCode = 404 | 403 | 401

export type PrerenderHTTPErrorState = {
  boundaryTree: LoaderTree
  triggeredStatus: HTTPAccessErrorStatusCode
}

/**
 * Use the provided loader tree to create the React Component tree.
 */
// TODO convert these arguments to non-object form. the entrypoint doesn't need most of them
export function createComponentTree(props: {
  loaderTree: LoaderTree
  parentParams: Params
  parentOptionalCatchAllParamName: string | null
  parentRuntimePrefetchable: false
  rootLayoutIncluded: boolean
  injectedCSS: Set<string>
  injectedJS: Set<string>
  injectedFontPreloadTags: Set<string>
  ctx: AppRenderContext
  missingSlots?: Set<string>
  preloadCallbacks: PreloadCallbacks
  authInterrupts: boolean
  MetadataOutlet: ComponentType
  prerenderHTTPError?: PrerenderHTTPErrorState
}): Promise<CacheNodeSeedData> {
  return getTracer().trace(
    NextNodeServerSpan.createComponentTree,
    {
      spanName: 'build component tree',
    },
    () => createComponentTreeInternal(props, true)
  )
}

function errorMissingDefaultExport(
  pagePath: string,
  convention: string
): never {
  const normalizedPagePath = pagePath === '/' ? '' : pagePath
  throw new Error(
    `The default export is not a React Component in "${normalizedPagePath}/${convention}"`
  )
}

const cacheNodeKey = 'c'

async function createComponentTreeInternal(
  {
    loaderTree: tree,
    parentParams,
    parentOptionalCatchAllParamName,
    parentRuntimePrefetchable,
    rootLayoutIncluded,
    injectedCSS,
    injectedJS,
    injectedFontPreloadTags,
    ctx,
    missingSlots,
    preloadCallbacks,
    authInterrupts,
    MetadataOutlet,
    prerenderHTTPError,
  }: {
    loaderTree: LoaderTree
    parentParams: Params
    parentOptionalCatchAllParamName: string | null
    parentRuntimePrefetchable: boolean
    rootLayoutIncluded: boolean
    injectedCSS: Set<string>
    injectedJS: Set<string>
    injectedFontPreloadTags: Set<string>
    ctx: AppRenderContext
    missingSlots?: Set<string>
    preloadCallbacks: PreloadCallbacks
    authInterrupts: boolean
    MetadataOutlet: ComponentType | null
    prerenderHTTPError?: PrerenderHTTPErrorState
  },
  isRoot: boolean
): Promise<CacheNodeSeedData> {
  const {
    renderOpts: { nextConfigOutput, experimental, cacheComponents },
    workStore,
    componentMod: {
      createElement,
      Fragment,
      SegmentViewNode,
      HTTPAccessFallbackBoundary,
      LayoutRouter,
      RenderFromTemplateContext,
      ClientPageRoot,
      ClientSegmentRoot,
      createServerSearchParamsForServerPage,
      createPrerenderSearchParamsForClientPage,
      createServerParamsForServerSegment,
      createPrerenderParamsForClientSegment,
      serverHooks: { DynamicServerError },
      Postpone,
    },
    pagePath,
    getDynamicParamFromSegment,
    isPrefetch,
    query,
  } = ctx

  const { page, conventionPath, segment, modules, parallelRoutes } =
    parseLoaderTree(tree)

  const {
    layout,
    template,
    error,
    loading,
    'not-found': notFound,
    forbidden,
    unauthorized,
  } = modules

  const injectedCSSWithCurrentLayout = new Set(injectedCSS)
  const injectedJSWithCurrentLayout = new Set(injectedJS)
  const injectedFontPreloadTagsWithCurrentLayout = new Set(
    injectedFontPreloadTags
  )

  const layerAssets = getLayerAssets({
    preloadCallbacks,
    ctx,
    layoutOrPagePath: conventionPath,
    injectedCSS: injectedCSSWithCurrentLayout,
    injectedJS: injectedJSWithCurrentLayout,
    injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
  })

  const [Template, templateStyles, templateScripts] = template
    ? await createComponentStylesAndScripts({
        ctx,
        filePath: template[1],
        getComponent: template[0],
        injectedCSS: injectedCSSWithCurrentLayout,
        injectedJS: injectedJSWithCurrentLayout,
      })
    : [Fragment]

  const [ErrorComponent, errorStyles, errorScripts] = error
    ? await createComponentStylesAndScripts({
        ctx,
        filePath: error[1],
        getComponent: error[0],
        injectedCSS: injectedCSSWithCurrentLayout,
        injectedJS: injectedJSWithCurrentLayout,
      })
    : []

  const [Loading, loadingStyles, loadingScripts] = loading
    ? await createComponentStylesAndScripts({
        ctx,
        filePath: loading[1],
        getComponent: loading[0],
        injectedCSS: injectedCSSWithCurrentLayout,
        injectedJS: injectedJSWithCurrentLayout,
      })
    : []

  const isLayout = typeof layout !== 'undefined'
  const isPage = typeof page !== 'undefined'
  const { mod: layoutOrPageMod, modType } = await getTracer().trace(
    NextNodeServerSpan.getLayoutOrPageModule,
    {
      hideSpan: !(isLayout || isPage),
      spanName: 'resolve segment modules',
      attributes: {
        'next.segment': segment,
      },
    },
    () => getLayoutOrPageModule(tree)
  )

  /**
   * 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 createComponentStylesAndScripts({
        ctx,
        filePath: notFound[1],
        getComponent: notFound[0],
        injectedCSS: injectedCSSWithCurrentLayout,
        injectedJS: injectedJSWithCurrentLayout,
      })
    : []

  const prefetchConfig = layoutOrPageMod
    ? (layoutOrPageMod as AppSegmentConfig).unstable_prefetch
    : undefined
  const hasRuntimePrefetch = prefetchConfig === 'force-runtime'
  const isRuntimePrefetchable = hasRuntimePrefetch || parentRuntimePrefetchable

  const [Forbidden, forbiddenStyles] =
    authInterrupts && forbidden
      ? await createComponentStylesAndScripts({
          ctx,
          filePath: forbidden[1],
          getComponent: forbidden[0],
          injectedCSS: injectedCSSWithCurrentLayout,
          injectedJS: injectedJSWithCurrentLayout,
        })
      : []

  const [Unauthorized, unauthorizedStyles] =
    authInterrupts && unauthorized
      ? await createComponentStylesAndScripts({
          ctx,
          filePath: unauthorized[1],
          getComponent: unauthorized[0],
          injectedCSS: injectedCSSWithCurrentLayout,
          injectedJS: injectedJSWithCurrentLayout,
        })
      : []

  let dynamic = layoutOrPageMod?.dynamic

  if (nextConfigOutput === 'export') {
    if (!dynamic || dynamic === 'auto') {
      dynamic = 'error'
    } else if (dynamic === 'force-dynamic') {
      // force-dynamic is always incompatible with 'export'. We must interrupt the build
      throw new StaticGenBailoutError(
        `Page with \`dynamic = "force-dynamic"\` couldn't be exported. \`output: "export"\` requires all pages be renderable statically because there is no runtime server to dynamically render routes in this output format. Learn more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports`
      )
    }
  }

  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') {
      workStore.dynamicShouldError = true
    } else if (dynamic === 'force-dynamic') {
      workStore.forceDynamic = true

      // TODO: (PPR) remove this bailout once PPR is the default
      if (workStore.isStaticGeneration && !experimental.isRoutePPREnabled) {
        // If the postpone API isn't available, we can't postpone the render and
        // therefore we can't use the dynamic API.
        const err = new DynamicServerError(
          `Page with \`dynamic = "force-dynamic"\` won't be rendered statically.`
        )
        workStore.dynamicUsageDescription = err.message
        workStore.dynamicUsageStack = err.stack
        throw err
      }
    } else {
      workStore.dynamicShouldError = false
      workStore.forceStatic = dynamic === 'force-static'
    }
  }

  if (typeof layoutOrPageMod?.fetchCache === 'string') {
    workStore.fetchCache = layoutOrPageMod?.fetchCache
  }

  if (typeof layoutOrPageMod?.revalidate !== 'undefined') {
    validateRevalidate(layoutOrPageMod?.revalidate, workStore.route)
  }

  if (typeof layoutOrPageMod?.revalidate === 'number') {
    const defaultRevalidate = layoutOrPageMod.revalidate as number

    const workUnitStore = workUnitAsyncStorage.getStore()

    if (workUnitStore) {
      switch (workUnitStore.type) {
        case 'prerender':
        case 'prerender-runtime':
        case 'prerender-legacy':
        case 'prerender-ppr':
          if (workUnitStore.revalidate > defaultRevalidate) {
            workUnitStore.revalidate = defaultRevalidate
          }
          break
        case 'request':
          // A request store doesn't have a revalidate property.
          break
        // createComponentTree is not called for these stores:
        case 'cache':
        case 'private-cache':
        case 'prerender-client':
        case 'validation-client':
        case 'unstable-cache':
        case 'generate-static-params':
          break
        default:
          workUnitStore satisfies never
      }
    }

    if (
      !workStore.forceStatic &&
      workStore.isStaticGeneration &&
      defaultRevalidate === 0 &&
      // If the postpone API isn't available, we can't postpone the render and
      // therefore we can't use the dynamic API.
      !experimental.isRoutePPREnabled
    ) {
      const dynamicUsageDescription = `revalidate: 0 configured ${segment}`
      workStore.dynamicUsageDescription = dynamicUsageDescription

      throw new DynamicServerError(dynamicUsageDescription)
    }
  }

  // Read unstable_dynamicStaleTime from page modules (not layouts) and track it on
  // the store's stale field. This affects the segment cache stale time via
  // the StaleTimeIterable.
  if (
    isPage &&
    typeof layoutOrPageMod?.unstable_dynamicStaleTime === 'number'
  ) {
    const pageStaleTime = layoutOrPageMod.unstable_dynamicStaleTime
    const workUnitStore = workUnitAsyncStorage.getStore()

    if (workUnitStore) {
      switch (workUnitStore.type) {
        case 'prerender':
        case 'prerender-runtime':
        case 'prerender-legacy':
        case 'prerender-ppr':
          if (workUnitStore.stale > pageStaleTime) {
            workUnitStore.stale = pageStaleTime
          }
          break
        case 'request':
          if (
            workUnitStore.stale === undefined ||
            workUnitStore.stale > pageStaleTime
          ) {
            workUnitStore.stale = pageStaleTime
          }
          break
        // createComponentTree is not called for these stores:
        case 'cache':
        case 'private-cache':
        case 'prerender-client':
        case 'validation-client':
        case 'unstable-cache':
        case 'generate-static-params':
          break
        default:
          workUnitStore satisfies never
      }
    }
  }

  const isStaticGeneration = workStore.isStaticGeneration

  // Assume the segment we're rendering contains only partial data if PPR is
  // enabled and this is a statically generated response. This is used by the
  // client Segment Cache after a prefetch to determine if it can skip the
  // second request to fill in the dynamic data.
  //
  // It's OK for this to be `true` when the data is actually fully static, but
  // it's not OK for this to be `false` when the data possibly contains holes.
  // Although the value here is overly pessimistic, for prefetches, it will be
  // replaced by a more specific value when the data is later processed into
  // per-segment responses (see collect-segment-data.tsx)
  //
  // For dynamic requests, this must always be `false` because dynamic responses
  // are never partial.
  const isPossiblyPartialResponse =
    isStaticGeneration && experimental.isRoutePPREnabled === true

  const LayoutOrPage: ComponentType<any> | undefined = layoutOrPageMod
    ? interopDefault(layoutOrPageMod)
    : undefined

  /**
   * The React Component to render.
   */
  let MaybeComponent = LayoutOrPage

  if (process.env.NODE_ENV === 'development' || isStaticGeneration) {
    const { isValidElementType } =
      require('next/dist/compiled/react-is') as typeof import('next/dist/compiled/react-is')
    if (
      typeof MaybeComponent !== 'undefined' &&
      !isValidElementType(MaybeComponent)
    ) {
      errorMissingDefaultExport(pagePath, modType ?? 'page')
    }

    if (
      typeof ErrorComponent !== 'undefined' &&
      !isValidElementType(ErrorComponent)
    ) {
      errorMissingDefaultExport(pagePath, 'error')
    }

    if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) {
      errorMissingDefaultExport(pagePath, 'loading')
    }

    if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) {
      errorMissingDefaultExport(pagePath, 'not-found')
    }

    if (typeof Forbidden !== 'undefined' && !isValidElementType(Forbidden)) {
      errorMissingDefaultExport(pagePath, 'forbidden')
    }

    if (
      typeof Unauthorized !== 'undefined' &&
      !isValidElementType(Unauthorized)
    ) {
      errorMissingDefaultExport(pagePath, 'unauthorized')
    }
  }

  // Handle dynamic segment params.
  const segmentParam = getDynamicParamFromSegment(tree)

  // Create object holding the parent params and current params
  let currentParams: Params = parentParams
  if (segmentParam && segmentParam.value !== null) {
    currentParams = {
      ...parentParams,
      [segmentParam.param]: segmentParam.value,
    }
  }

  // Track optional catch-all params with no value (e.g., [[...slug]] at /).
  // These params won't exist as properties on the params object, so vary
  // params tracking needs to use a Proxy to detect access. We propagate this
  // through the tree so that child segments (like __PAGE__) also know about
  // the missing param. In practice, this only gets passed down one level —
  // from the optional catch-all layout segment to the page segment — so it's
  // always very close to the leaf of the tree.
  const optionalCatchAllParamName: string | null =
    segmentParam?.type === 'oc' && segmentParam.value === null
      ? segmentParam.param
      : parentOptionalCatchAllParamName

  // Resolve the segment param
  const isSegmentViewEnabled = !!process.env.__NEXT_DEV_SERVER
  const dir =
    (process.env.NEXT_RUNTIME === 'edge'
      ? process.env.__NEXT_EDGE_PROJECT_DIR
      : ctx.renderOpts.dir) || ''

  const [notFoundElement, notFoundFilePath] =
    await createBoundaryConventionElement({
      ctx,
      conventionName: 'not-found',
      Component: NotFound,
      styles: notFoundStyles,
      tree,
    })

  const [forbiddenElement] = await createBoundaryConventionElement({
    ctx,
    conventionName: 'forbidden',
    Component: Forbidden,
    styles: forbiddenStyles,
    tree,
  })

  const [unauthorizedElement] = await createBoundaryConventionElement({
    ctx,
    conventionName: 'unauthorized',
    Component: Unauthorized,
    styles: unauthorizedStyles,
    tree,
  })

  // TODO: Combine this `map` traversal with the loop below that turns the array
  // into an object.
  const parallelRouteMap = await Promise.all(
    Object.keys(parallelRoutes).map(
      async (
        parallelRouteKey
      ): Promise<[string, React.ReactNode, CacheNodeSeedData | null]> => {
        const isChildrenRouteKey = parallelRouteKey === 'children'
        const parallelRoute = parallelRoutes[parallelRouteKey]

        const notFoundComponent = isChildrenRouteKey
          ? notFoundElement
          : undefined

        const forbiddenComponent = isChildrenRouteKey
          ? forbiddenElement
          : undefined

        const unauthorizedComponent = isChildrenRouteKey
          ? unauthorizedElement
          : undefined

        // 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 childCacheNodeSeedData: CacheNodeSeedData | null = null

        if (
          // Before PPR, the way instant navigations work in Next.js is we
          // prefetch everything up to the first route segment that defines a
          // loading.tsx boundary. (We do the same if there's no loading
          // boundary in the entire tree, because we don't want to prefetch too
          // much) The rest of the tree is deferred until the actual navigation.
          // It does not take into account whether the data is dynamic — even if
          // the tree is completely static, it will still defer everything
          // inside the loading boundary.
          //
          // This behavior predates PPR and is only relevant if the
          // PPR flag is not enabled.
          isPrefetch &&
          (Loading || !hasLoadingComponentInTree(parallelRoute)) &&
          // The approach with PPR is different — loading.tsx behaves like a
          // regular Suspense boundary and has no special behavior.
          //
          // With PPR, we prefetch as deeply as possible, and only defer when
          // dynamic data is accessed. If so, we only defer the nearest parent
          // Suspense boundary of the dynamic data access, regardless of whether
          // the boundary is defined by loading.tsx or a normal <Suspense>
          // component in userspace.
          //
          // NOTE: In practice this usually means we'll end up prefetching more
          // than we were before PPR, which may or may not be considered a
          // performance regression by some apps. The plan is to address this
          // before General Availability of PPR by introducing granular
          // per-segment fetching, so we can reuse as much of the tree as
          // possible during both prefetches and dynamic navigations. But during
          // the beta period, we should be clear about this trade off in our
          // communications.
          !experimental.isRoutePPREnabled
        ) {
          // Don't prefetch this child. This will trigger a lazy fetch by the
          // client router.
        } else {
          // Create the child component

          if (process.env.NODE_ENV === 'development' && missingSlots) {
            // When we detect the default fallback (which triggers a 404), we collect the missing slots
            // to provide more helpful debug information during development mode.
            const parsedTree = parseLoaderTree(parallelRoute)
            if (
              parsedTree.conventionPath?.endsWith(PARALLEL_ROUTE_DEFAULT_PATH)
            ) {
              missingSlots.add(parallelRouteKey)
            }
          }

          // The outer prerender catch already found the deepest segment whose
          // HTTP fallback should replace the throwing page. When we reach that
          // segment's `children` slot, render the fallback directly instead of
          // descending back into the subtree that threw during deserialization.

          // Like the other segment-level boundary props below, HTTP access
          // fallbacks are attached to the default `children` slot, not to named
          // parallel routes.
          const shouldRenderPrerenderHTTPFallback =
            prerenderHTTPError?.boundaryTree === tree && isChildrenRouteKey

          if (shouldRenderPrerenderHTTPFallback) {
            let fallbackElement: React.ReactNode | undefined
            switch (prerenderHTTPError.triggeredStatus) {
              case 404:
                fallbackElement = notFoundElement
                break
              case 403:
                fallbackElement = forbiddenElement
                break
              case 401:
                fallbackElement = unauthorizedElement
                break
              default:
                break
            }

            if (fallbackElement) {
              childCacheNodeSeedData = createSeedData(
                ctx,
                fallbackElement,
                {},
                null,
                isPossiblyPartialResponse,
                false,
                emptyVaryParamsAccumulator
              )
            }
          }

          if (childCacheNodeSeedData === null) {
            const seedData = await createComponentTreeInternal(
              {
                loaderTree: parallelRoute,
                parentParams: currentParams,
                parentOptionalCatchAllParamName: optionalCatchAllParamName,
                parentRuntimePrefetchable: isRuntimePrefetchable,
                rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
                injectedCSS: injectedCSSWithCurrentLayout,
                injectedJS: injectedJSWithCurrentLayout,
                injectedFontPreloadTags:
                  injectedFontPreloadTagsWithCurrentLayout,
                ctx,
                missingSlots,
                preloadCallbacks,
                authInterrupts,
                // `StreamingMetadataOutlet` is used to conditionally throw. In the case of parallel routes we will have more than one page
                // but we only want to throw on the first one.
                MetadataOutlet: isChildrenRouteKey ? MetadataOutlet : null,
                prerenderHTTPError,
              },
              false
            )

            childCacheNodeSeedData = seedData
          }
        }

        const templateNode = createElement(
          Template,
          null,
          createElement(RenderFromTemplateContext, null)
        )

        const templateFilePath = getConventionPathByType(tree, dir, 'template')
        const errorFilePath = getConventionPathByType(tree, dir, 'error')
        const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
        const globalErrorFilePath = isRoot
          ? getConventionPathByType(tree, dir, 'global-error')
          : undefined

        const wrappedErrorStyles =
          isSegmentViewEnabled && errorFilePath
            ? createElement(
                SegmentViewNode,
                {
                  type: 'error',
                  pagePath: errorFilePath,
                },
                errorStyles
              )
            : errorStyles

        // Add a suffix to avoid conflict with the segment view node representing rendered file.
        // existence: not-found.tsx@boundary
        // rendered: not-found.tsx
        const fileNameSuffix = BOUNDARY_SUFFIX
        const segmentViewBoundaries = isSegmentViewEnabled
          ? createElement(
              Fragment,
              null,
              notFoundFilePath &&
                createElement(SegmentViewNode, {
                  type: `${BOUNDARY_PREFIX}not-found`,
                  pagePath: notFoundFilePath + fileNameSuffix,
                }),
              loadingFilePath &&
                createElement(SegmentViewNode, {
                  type: `${BOUNDARY_PREFIX}loading`,
                  pagePath: loadingFilePath + fileNameSuffix,
                }),
              errorFilePath &&
                createElement(SegmentViewNode, {
                  type: `${BOUNDARY_PREFIX}error`,
                  pagePath: errorFilePath + fileNameSuffix,
                }),
              globalErrorFilePath &&
                createElement(SegmentViewNode, {
                  type: `${BOUNDARY_PREFIX}global-error`,
                  pagePath: isNextjsBuiltinFilePath(globalErrorFilePath)
                    ? `${BUILTIN_PREFIX}global-error.js${fileNameSuffix}`
                    : globalErrorFilePath,
                })
            )
          : null

        return [
          parallelRouteKey,
          createElement(LayoutRouter, {
            parallelRouterKey: parallelRouteKey,
            error: ErrorComponent,
            errorStyles: wrappedErrorStyles,
            errorScripts: errorScripts,
            template:
              isSegmentViewEnabled && templateFilePath
                ? createElement(
                    SegmentViewNode,
                    {
                      type: 'template',
                      pagePath: templateFilePath,
                    },
                    templateNode
                  )
                : templateNode,
            templateStyles: templateStyles,
            templateScripts: templateScripts,
            notFound: notFoundComponent,
            forbidden: forbiddenComponent,
            unauthorized: unauthorizedComponent,
            ...(isSegmentViewEnabled && {
              segmentViewBoundaries,
            }),
          }),
          childCacheNodeSeedData,
        ]
      }
    )
  )

  // Convert the parallel route map into an object after all promises have been resolved.
  let parallelRouteProps: { [key: string]: React.ReactNode } = {}
  let parallelRouteCacheNodeSeedData: {
    [key: string]: CacheNodeSeedData | null
  } = {}
  for (const parallelRoute of parallelRouteMap) {
    const [parallelRouteKey, parallelRouteProp, flightData] = parallelRoute
    parallelRouteProps[parallelRouteKey] = parallelRouteProp
    parallelRouteCacheNodeSeedData[parallelRouteKey] = flightData
  }

  let loadingElement = Loading
    ? createElement(Loading, {
        key: 'l',
      })
    : null
  const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
  if (isSegmentViewEnabled && loadingElement) {
    if (loadingFilePath) {
      loadingElement = createElement(
        SegmentViewNode,
        {
          key: cacheNodeKey + '-loading',
          type: 'loading',
          pagePath: loadingFilePath,
        },
        loadingElement
      )
    }
  }

  const loadingData: LoadingModuleData = loadingElement
    ? [loadingElement, loadingStyles, loadingScripts]
    : null

  // 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 (!MaybeComponent) {
    return createSeedData(
      ctx,
      createElement(
        Fragment,
        {
          key: cacheNodeKey,
        },
        layerAssets,
        parallelRouteProps.children
      ),
      parallelRouteCacheNodeSeedData,
      loadingData,
      isPossiblyPartialResponse,
      isRuntimePrefetchable,

      // No user-provided component, so no params will be accessed. Use the
      // pre-resolved empty tracker.
      emptyVaryParamsAccumulator
    )
  }

  const Component = MaybeComponent
  // If force-dynamic is used and the current render supports postponing, we
  // replace it with a node that will postpone the render. This ensures that the
  // postpone is invoked during the react render phase and not during the next
  // render phase.
  // @TODO this does not actually do what it seems like it would or should do. The idea is that
  // if we are rendering in a force-dynamic mode and we can postpone we should only make the segments
  // that ask for force-dynamic to be dynamic, allowing other segments to still prerender. However
  // because this comes after the children traversal and the static generation store is mutated every segment
  // along the parent path of a force-dynamic segment will hit this condition effectively making the entire
  // render force-dynamic. We should refactor this function so that we can correctly track which segments
  // need to be dynamic
  if (
    workStore.isStaticGeneration &&
    workStore.forceDynamic &&
    experimental.isRoutePPREnabled
  ) {
    return createSeedData(
      ctx,
      createElement(
        Fragment,
        {
          key: cacheNodeKey,
        },
        createElement(Postpone, {
          reason: 'dynamic = "force-dynamic" was used',
          route: workStore.route,
        }),
        layerAssets
      ),
      parallelRouteCacheNodeSeedData,
      loadingData,
      true,
      isRuntimePrefetchable,

      // force-dynamic postpones without rendering the component, so no params
      // are accessed. The vary params are empty.
      emptyVaryParamsAccumulator
    )
  }

  const isClientComponent = isClientReference(layoutOrPageMod)

  const varyParamsAccumulator =
    isClientComponent && cacheComponents
      ? // Client components with Cache Components enabled don't receive params
        // from the server, so they have an empty vary params set.
        emptyVaryParamsAccumulator
      : createVaryParamsAccumulator()

  if (
    process.env.NODE_ENV === 'development' &&
    'params' in parallelRouteProps
  ) {
    // @TODO consider making this an error and running the check in build as well
    console.error(
      `"params" is a reserved prop in Layouts and Pages and cannot be used as the name of a parallel route in ${segment}`
    )
  }

  if (isPage) {
    const PageComponent = Component

    // Assign searchParams to props if this is a page
    let pageElement: React.ReactNode
    if (isClientComponent) {
      if (cacheComponents) {
        // Params are omitted when Cache Components is enabled
        pageElement = createElement(ClientPageRoot, {
          Component: PageComponent,
          serverProvidedParams: null,
        })
      } else if (isStaticGeneration) {
        const promiseOfParams =
          createPrerenderParamsForClientSegment(currentParams)
        const promiseOfSearchParams = createPrerenderSearchParamsForClientPage()
        pageElement = createElement(ClientPageRoot, {
          Component: PageComponent,
          serverProvidedParams: {
            searchParams: query,
            params: currentParams,
            promises: [promiseOfSearchParams, promiseOfParams],
          },
        })
      } else {
        pageElement = createElement(ClientPageRoot, {
          Component: PageComponent,
          serverProvidedParams: {
            searchParams: query,
            params: currentParams,
            promises: null,
          },
        })
      }
    } else {
      // If we are passing params to a server component Page we need to track
      // their usage in case the current render mode tracks dynamic API usage.
      const params = createServerParamsForServerSegment(
        currentParams,
        optionalCatchAllParamName,
        varyParamsAccumulator,
        isRuntimePrefetchable
      )

      // If we are passing searchParams to a server component Page we need to
      // track their usage in case the current render mode tracks dynamic API
      // usage.
      let searchParams = createServerSearchParamsForServerPage(
        query,
        varyParamsAccumulator,
        isRuntimePrefetchable
      )

      if (isUseCacheFunction(PageComponent)) {
        const UseCachePageComponent: ComponentType<UseCachePageProps> =
          PageComponent

        pageElement = createElement(UseCachePageComponent, {
          params: params,
          searchParams: searchParams,
          $$isPage: true,
        })
      } else {
        pageElement = createElement(PageComponent, {
          params: params,
          searchParams: searchParams,
        })
      }
    }

    const isDefaultSegment = segment === DEFAULT_SEGMENT_KEY
    const pageFilePath =
      getConventionPathByType(tree, dir, 'page') ??
      getConventionPathByType(tree, dir, 'defaultPage')
    const segmentType = isDefaultSegment ? 'default' : 'page'
    const wrappedPageElement =
      isSegmentViewEnabled && pageFilePath
        ? createElement(
            SegmentViewNode,
            {
              key: cacheNodeKey + '-' + segmentType,
              type: segmentType,
              pagePath: pageFilePath,
            },
            pageElement
          )
        : pageElement

    return createSeedData(
      ctx,
      createElement(
        Fragment,
        {
          key: cacheNodeKey,
        },
        wrappedPageElement,
        layerAssets,
        MetadataOutlet ? createElement(MetadataOutlet, null) : null
      ),
      parallelRouteCacheNodeSeedData,
      loadingData,
      isPossiblyPartialResponse,
      isRuntimePrefetchable,

      varyParamsAccumulator
    )
  } else {
    const SegmentComponent = Component
    const isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot =
      rootLayoutAtThisLevel &&
      'children' in parallelRoutes &&
      Object.keys(parallelRoutes).length > 1

    let segmentNode: React.ReactNode

    if (isClientComponent) {
      let clientSegment: React.ReactNode
      if (cacheComponents) {
        // Params are omitted when Cache Components is enabled
        clientSegment = createElement(ClientSegmentRoot, {
          Component: SegmentComponent,
          slots: parallelRouteProps,
          serverProvidedParams: null,
        })
      } else if (isStaticGeneration) {
        const promiseOfParams =
          createPrerenderParamsForClientSegment(currentParams)

        clientSegment = createElement(ClientSegmentRoot, {
          Component: SegmentComponent,
          slots: parallelRouteProps,
          serverProvidedParams: {
            params: currentParams,
            promises: [promiseOfParams],
          },
        })
      } else {
        clientSegment = createElement(ClientSegmentRoot, {
          Component: SegmentComponent,
          slots: parallelRouteProps,
          serverProvidedParams: {
            params: currentParams,
            promises: null,
          },
        })
      }

      if (isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot) {
        let notfoundClientSegment: React.ReactNode
        let forbiddenClientSegment: React.ReactNode
        let unauthorizedClientSegment: React.ReactNode
        // TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
        // This ensures that a `HTTPAccessFallbackBoundary` is available for when that happens,
        // but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
        // We should instead look into handling the fallback behavior differently in development mode so that it doesn't
        // rely on the `NotFound` behavior.
        notfoundClientSegment = createErrorBoundaryClientSegmentRoot({
          ctx,
          ErrorBoundaryComponent: NotFound,
          errorElement: notFoundElement,
          ClientSegmentRoot,
          layerAssets,
          SegmentComponent,
          currentParams,
        })
        forbiddenClientSegment = createErrorBoundaryClientSegmentRoot({
          ctx,
          ErrorBoundaryComponent: Forbidden,
          errorElement: forbiddenElement,
          ClientSegmentRoot,
          layerAssets,
          SegmentComponent,
          currentParams,
        })
        unauthorizedClientSegment = createErrorBoundaryClientSegmentRoot({
          ctx,
          ErrorBoundaryComponent: Unauthorized,
          errorElement: unauthorizedElement,
          ClientSegmentRoot,
          layerAssets,
          SegmentComponent,
          currentParams,
        })
        if (
          notfoundClientSegment ||
          forbiddenClientSegment ||
          unauthorizedClientSegment
        ) {
          segmentNode = createElement(
            HTTPAccessFallbackBoundary,
            {
              key: cacheNodeKey,
              notFound: notfoundClientSegment,
              forbidden: forbiddenClientSegment,
              unauthorized: unauthorizedClientSegment,
            },
            layerAssets,
            clientSegment
          )
        } else {
          segmentNode = createElement(
            Fragment,
            {
              key: cacheNodeKey,
            },
            layerAssets,
            clientSegment
          )
        }
      } else {
        segmentNode = createElement(
          Fragment,
          {
            key: cacheNodeKey,
          },
          layerAssets,
          clientSegment
        )
      }
    } else {
      const params = createServerParamsForServerSegment(
        currentParams,
        optionalCatchAllParamName,
        varyParamsAccumulator,
        isRuntimePrefetchable
      )

      let serverSegment: React.ReactNode

      if (isUseCacheFunction(SegmentComponent)) {
        const UseCacheLayoutComponent: ComponentType<UseCacheLayoutProps> =
          SegmentComponent

        serverSegment = createElement(
          UseCacheLayoutComponent,
          {
            ...parallelRouteProps,
            params: params,
            $$isLayout: true,
          },
          // Force static children here so that they're validated.
          // See https://github.com/facebook/react/pull/34846
          parallelRouteProps.children
        )
      } else {
        serverSegment = createElement(
          SegmentComponent,
          {
            ...parallelRouteProps,
            params: params,
          },
          // Force static children here so that they're validated.
          // See https://github.com/facebook/react/pull/34846
          parallelRouteProps.children
        )
      }

      if (isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot) {
        // TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
        // This ensures that a `HTTPAccessFallbackBoundary` is available for when that happens,
        // but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
        // We should instead look into handling the fallback behavior differently in development mode so that it doesn't
        // rely on the `NotFound` behavior.
        segmentNode = createElement(
          HTTPAccessFallbackBoundary,
          {
            key: cacheNodeKey,
            notFound: notFoundElement
              ? createElement(
                  Fragment,
                  null,
                  layerAssets,
                  createElement(
                    SegmentComponent,
                    {
                      params: params,
                    },
                    notFoundStyles,
                    notFoundElement
                  )
                )
              : undefined,
          },
          layerAssets,
          serverSegment
        )
      } else {
        segmentNode = createElement(
          Fragment,
          {
            key: cacheNodeKey,
          },
          layerAssets,
          serverSegment
        )
      }
    }

    const layoutFilePath = getConventionPathByType(tree, dir, 'layout')
    const wrappedSegmentNode =
      isSegmentViewEnabled && layoutFilePath
        ? createElement(
            SegmentViewNode,
            {
              key: 'layout',
              type: 'layout',
              pagePath: layoutFilePath,
            },
            segmentNode
          )
        : segmentNode

    // For layouts we just render the component
    return createSeedData(
      ctx,
      wrappedSegmentNode,
      parallelRouteCacheNodeSeedData,
      loadingData,
      isPossiblyPartialResponse,
      isRuntimePrefetchable,

      varyParamsAccumulator
    )
  }
}

function createErrorBoundaryClientSegmentRoot({
  ctx,
  ErrorBoundaryComponent,
  errorElement,
  ClientSegmentRoot,
  layerAssets,
  SegmentComponent,
  currentParams,
}: {
  ctx: AppRenderContext
  ErrorBoundaryComponent: ComponentType<any> | undefined
  errorElement: React.ReactNode
  ClientSegmentRoot: ComponentType<any>
  layerAssets: React.ReactNode
  SegmentComponent: ComponentType<any>
  currentParams: Params
}) {
  const {
    componentMod: { createElement, Fragment },
  } = ctx
  if (ErrorBoundaryComponent) {
    const notFoundParallelRouteProps = {
      children: errorElement,
    }
    return createElement(
      Fragment,
      null,
      layerAssets,
      createElement(ClientSegmentRoot, {
        Component: SegmentComponent,
        slots: notFoundParallelRouteProps,
        params: currentParams,
      })
    )
  }
  return null
}

export function getRootParams(
  loaderTree: LoaderTree,
  getDynamicParamFromSegment: GetDynamicParamFromSegment
): Params {
  return getRootParamsImpl({}, loaderTree, getDynamicParamFromSegment)
}

function getRootParamsImpl(
  parentParams: Params,
  loaderTree: LoaderTree,
  getDynamicParamFromSegment: GetDynamicParamFromSegment
): Params {
  const {
    modules: { layout },
    parallelRoutes,
  } = parseLoaderTree(loaderTree)

  const segmentParam = getDynamicParamFromSegment(loaderTree)

  let currentParams: Params = parentParams
  if (segmentParam && segmentParam.value !== null) {
    currentParams = {
      ...parentParams,
      [segmentParam.param]: segmentParam.value,
    }
  }

  const isRootLayout = typeof layout !== 'undefined'

  if (isRootLayout) {
    return currentParams
  } else if (!parallelRoutes.children) {
    // This should really be an error but there are bugs in Turbopack that cause
    // the _not-found LoaderTree to not have any layouts. For rootParams sake
    // this is somewhat irrelevant when you are not customizing the 404 page.
    // If you are customizing 404
    // TODO update rootParams to make all params optional if `/app/not-found.tsx` is defined
    return currentParams
  } else {
    return getRootParamsImpl(
      currentParams,
      // We stop looking for root params as soon as we hit the first layout
      // and it is not possible to use parallel route children above the root layout
      // so every parallelRoutes object that this function can visit will necessarily
      // have a single `children` prop and no others.
      parallelRoutes.children,
      getDynamicParamFromSegment
    )
  }
}

async function createBoundaryConventionElement({
  ctx,
  conventionName,
  Component,
  styles,
  tree,
}: {
  ctx: AppRenderContext
  conventionName:
    | 'not-found'
    | 'error'
    | 'loading'
    | 'forbidden'
    | 'unauthorized'
  Component: ComponentType<any> | undefined
  styles: React.ReactNode | undefined
  tree: LoaderTree
}) {
  const {
    componentMod: { createElement, Fragment },
  } = ctx
  const isSegmentViewEnabled = !!process.env.__NEXT_DEV_SERVER
  const dir =
    (process.env.NEXT_RUNTIME === 'edge'
      ? process.env.__NEXT_EDGE_PROJECT_DIR
      : ctx.renderOpts.dir) || ''
  const { SegmentViewNode } = ctx.componentMod
  const element = Component
    ? createElement(Fragment, null, createElement(Component, null), styles)
    : undefined

  const pagePath = getConventionPathByType(tree, dir, conventionName)

  const wrappedElement =
    isSegmentViewEnabled && element
      ? createElement(
          SegmentViewNode,
          {
            key: cacheNodeKey + '-' + conventionName,
            type: conventionName,
            // TODO: Discovered when moving to `createElement`.
            // `SegmentViewNode` doesn't support undefined `pagePath`
            pagePath: pagePath!,
          },
          element
        )
      : element

  return [wrappedElement, pagePath] as const
}

function createSeedData(
  ctx: AppRenderContext,
  rsc: React.ReactNode,
  parallelRoutes: Record<string, CacheNodeSeedData | null>,
  loading: LoadingModuleData | null,
  isPossiblyPartialResponse: boolean,
  isRuntimePrefetchable: boolean,
  varyParamsAccumulator: VaryParamsAccumulator | null
): CacheNodeSeedData {
  const createElement = ctx.componentMod.createElement

  // When this segment is NOT runtime-prefetchable, delay it until the Static
  // stage by wrapping the node in a promise. This allows runtime-prefetchable
  // segments (the lower tree) to render first during EarlyStatic, so their
  // runtime data resolves in EarlyRuntime where sync IO can be checked.
  // React will suspend on the thenable and resume when the stage advances.
  if (!isRuntimePrefetchable) {
    const workUnitStore = workUnitAsyncStorage.getStore()
    if (workUnitStore) {
      let stagedRendering: StagedRenderingController | null | undefined
      switch (workUnitStore.type) {
        case 'request':
        case 'prerender-runtime':
          stagedRendering = workUnitStore.stagedRendering
          if (stagedRendering) {
            const deferredRsc = rsc
            rsc = stagedRendering
              .waitForStage(RenderStage.Static)
              .then(() => deferredRsc)
          }
          break
        case 'prerender':
        case 'prerender-client':
        case 'validation-client':
        case 'prerender-ppr':
        case 'prerender-legacy':
        case 'cache':
        case 'private-cache':
        case 'unstable-cache':
        case 'generate-static-params':
          break
        default:
          workUnitStore satisfies never
      }
    }
  }

  if (loading !== null) {
    // If a loading.tsx boundary is present, wrap the component data in an
    // additional context provider to pass the loading data to the next
    // set of children.
    // NOTE: The reason this is a separate wrapper from LayoutRouter is because
    // not all segments render a LayoutRouter component, e.g. the root segment.
    const LoadingBoundaryProvider = ctx.componentMod.LoadingBoundaryProvider
    rsc = createElement(LoadingBoundaryProvider, {
      loading: loading,
      children: rsc,
    })
  }
  return [
    rsc,
    parallelRoutes,
    null,
    isPossiblyPartialResponse,
    varyParamsAccumulator ? getVaryParamsThenable(varyParamsAccumulator) : null,
  ]
}
Quest for Codev2.0.0
/
SIGN IN