next.js/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx
walk-tree-with-flight-router-state.tsx428 lines13.7 KB
import type {
  FlightDataPath,
  FlightDataSegment,
  FlightRouterState,
  PrefetchHints,
  Segment,
  HeadData,
} from '../../shared/lib/app-router-types'
import type { PreloadCallbacks } from './types'
import { matchSegment } from '../../client/components/match-segments'
import type { LoaderTree } from '../lib/app-dir-module'
import { getLinkAndScriptTags } from './get-css-inlined-link-tags'
import { getPreloadableFonts } from './get-preloadable-fonts'
import {
  createFlightRouterStateFromLoaderTree,
  createRouteTreePrefetch,
} from './create-flight-router-state-from-loader-tree'
import type { AppRenderContext } from './app-render'
import { hasLoadingComponentInTree } from './has-loading-component-in-tree'
import { addSearchParamsIfPageSegment } from '../../shared/lib/segment'
import { createComponentTree } from './create-component-tree'
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param'

/**
 * 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.
 */
export async function walkTreeWithFlightRouterState({
  loaderTreeToFilter,
  parentParams,
  flightRouterState,
  parentIsInsideSharedLayout,
  rscHead,
  injectedCSS,
  injectedJS,
  injectedFontPreloadTags,
  rootLayoutIncluded,
  ctx,
  preloadCallbacks,
  MetadataOutlet,
  hintTree,
}: {
  loaderTreeToFilter: LoaderTree
  parentParams: { [key: string]: string | string[] }
  flightRouterState?: FlightRouterState
  rscHead: HeadData
  parentIsInsideSharedLayout?: boolean
  injectedCSS: Set<string>
  injectedJS: Set<string>
  injectedFontPreloadTags: Set<string>
  rootLayoutIncluded: boolean
  ctx: AppRenderContext
  preloadCallbacks: PreloadCallbacks
  MetadataOutlet: React.ComponentType
  hintTree: PrefetchHints | null
}): Promise<FlightDataPath[]> {
  const {
    renderOpts: { nextFontManifest, experimental },
    query,
    isPrefetch,
    getDynamicParamFromSegment,
    parsedRequestHeaders,
    workStore,
  } = ctx
  const prefetchInliningEnabled = Boolean(experimental.prefetchInlining)
  const cacheComponents = ctx.renderOpts.cacheComponents
  const isStaticGeneration = workStore.isStaticGeneration
  const isBuildTimePrerendering =
    ctx.renderOpts.isBuildTimePrerendering ?? false

  const [segment, parallelRoutes, modules] = loaderTreeToFilter

  const parallelRoutesKeys = Object.keys(parallelRoutes)

  const { layout } = modules
  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(loaderTreeToFilter)
  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]) ||
    // Explicit refresh
    flightRouterState[3] === 'refetch'

  // Pre-PPR, the `loading` component signals to the router how deep to render the component tree
  // to ensure prefetches are quick and inexpensive. If there's no `loading` component anywhere in the tree being rendered,
  // the prefetch will be short-circuited to avoid requesting a potentially very expensive subtree. If there's a `loading`
  // somewhere in the tree, we'll recursively render the component tree up until we encounter that loading component, and then stop.

  // Check if we're inside the "new" part of the navigation — inside the
  // shared layout. In the case of a prefetch, this can be true even if the
  // segment matches, because the client might send a matching segment to
  // indicate that it already has the data in its cache. But in order to find
  // the correct loading boundary, we still need to track where the shared
  // layout begins.
  //
  // TODO: We should rethink the protocol for dynamic requests. It might not
  // make sense for the client to send a FlightRouterState, since that type is
  // overloaded with other concerns.
  const isInsideSharedLayout =
    renderComponentsOnThisLevel ||
    parentIsInsideSharedLayout ||
    flightRouterState[3] === 'inside-shared-layout'

  if (
    isInsideSharedLayout &&
    !experimental.isRoutePPREnabled &&
    // If PPR is disabled, and this is a request for the route tree, then we
    // never render any components. Only send the router state.
    (parsedRequestHeaders.isRouteTreePrefetchRequest ||
      // Otherwise, check for the presence of a `loading` component.
      (isPrefetch &&
        !Boolean(modules.loading) &&
        !hasLoadingComponentInTree(loaderTreeToFilter)))
  ) {
    // Send only the router state.
    // TODO: Even for a dynamic route, we should cache these responses,
    // because they do not contain any render data (neither segment data nor
    // the head). They can be made even more cacheable once we move the route
    // params into a separate data structure.
    const overriddenSegment =
      flightRouterState &&
      // TODO: Why does canSegmentBeOverridden exist? Why don't we always just
      // use `actualSegment`? Is it to avoid overwriting some state that's
      // tracked by the client? Dig deeper to see if we can simplify this.
      canSegmentBeOverridden(actualSegment, flightRouterState[0])
        ? flightRouterState[0]
        : actualSegment

    const routerState = parsedRequestHeaders.isRouteTreePrefetchRequest
      ? // Route tree prefetch requests contain some extra information
        await createRouteTreePrefetch(
          loaderTreeToFilter,
          hintTree,
          prefetchInliningEnabled,
          cacheComponents,
          isStaticGeneration,
          isBuildTimePrerendering,
          getDynamicParamFromSegment
        )
      : await createFlightRouterStateFromLoaderTree(
          loaderTreeToFilter,
          hintTree,
          prefetchInliningEnabled,
          cacheComponents,
          isStaticGeneration,
          isBuildTimePrerendering,
          getDynamicParamFromSegment,
          query
        )

    return [
      [
        overriddenSegment,
        routerState,
        null,
        [null, null],
        true,
      ] satisfies FlightDataSegment,
    ]
  }

  // Similar to the previous branch. This flag is sent by the client to request
  // only the metadata for a page. No segment data.
  if (flightRouterState && flightRouterState[3] === 'metadata-only') {
    const overriddenSegment =
      flightRouterState &&
      canSegmentBeOverridden(actualSegment, flightRouterState[0])
        ? flightRouterState[0]
        : actualSegment
    const routerState = parsedRequestHeaders.isRouteTreePrefetchRequest
      ? await createRouteTreePrefetch(
          loaderTreeToFilter,
          hintTree,
          prefetchInliningEnabled,
          cacheComponents,
          isStaticGeneration,
          isBuildTimePrerendering,
          getDynamicParamFromSegment
        )
      : await createFlightRouterStateFromLoaderTree(
          loaderTreeToFilter,
          hintTree,
          prefetchInliningEnabled,
          cacheComponents,
          isStaticGeneration,
          isBuildTimePrerendering,
          getDynamicParamFromSegment,
          query
        )
    return [
      [
        overriddenSegment,
        routerState,
        null,
        rscHead,
        false,
      ] satisfies FlightDataSegment,
    ]
  }

  if (renderComponentsOnThisLevel) {
    const overriddenSegment =
      flightRouterState &&
      // TODO: Why does canSegmentBeOverridden exist? Why don't we always just
      // use `actualSegment`? Is it to avoid overwriting some state that's
      // tracked by the client? Dig deeper to see if we can simplify this.
      canSegmentBeOverridden(actualSegment, flightRouterState[0])
        ? flightRouterState[0]
        : actualSegment

    const routerState = await createFlightRouterStateFromLoaderTree(
      // Create router state using the slice of the loaderTree
      loaderTreeToFilter,
      hintTree,
      prefetchInliningEnabled,
      cacheComponents,
      isStaticGeneration,
      isBuildTimePrerendering,
      getDynamicParamFromSegment,
      query
    )

    // Create component tree using the slice of the loaderTree
    const seedData = await createComponentTree(
      // This ensures flightRouterPath is valid and filters down the tree
      {
        ctx,
        loaderTree: loaderTreeToFilter,
        parentParams: currentParams,
        parentOptionalCatchAllParamName: null,
        parentRuntimePrefetchable: false,
        injectedCSS,
        injectedJS,
        injectedFontPreloadTags,
        // This is intentionally not "rootLayoutIncludedAtThisLevelOrAbove" as createComponentTree starts at the current level and does a check for "rootLayoutAtThisLevel" too.
        rootLayoutIncluded,
        preloadCallbacks,
        authInterrupts: experimental.authInterrupts,
        MetadataOutlet,
      }
    )

    return [
      [
        overriddenSegment,
        routerState,
        seedData,
        rscHead,
        false,
      ] satisfies FlightDataSegment,
    ]
  }

  // 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 injectedJSWithCurrentLayout = new Set(injectedJS)
  const injectedFontPreloadTagsWithCurrentLayout = new Set(
    injectedFontPreloadTags
  )
  if (layoutPath) {
    getLinkAndScriptTags(
      layoutPath,
      injectedCSSWithCurrentLayout,
      injectedJSWithCurrentLayout,
      true
    )
    getPreloadableFonts(
      nextFontManifest,
      layoutPath,
      injectedFontPreloadTagsWithCurrentLayout
    )
  }

  const paths: FlightDataPath[] = []

  // Walk through all parallel routes.
  for (const parallelRouteKey of parallelRoutesKeys) {
    const parallelRoute = parallelRoutes[parallelRouteKey]

    const subPaths = await walkTreeWithFlightRouterState({
      ctx,
      loaderTreeToFilter: parallelRoute,
      parentParams: currentParams,
      flightRouterState:
        flightRouterState && flightRouterState[1][parallelRouteKey],
      parentIsInsideSharedLayout: isInsideSharedLayout,
      rscHead,
      injectedCSS: injectedCSSWithCurrentLayout,
      injectedJS: injectedJSWithCurrentLayout,
      injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
      rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
      preloadCallbacks,
      MetadataOutlet,
      hintTree: hintTree?.slots?.[parallelRouteKey] ?? null,
    })

    for (const subPath of subPaths) {
      paths.push([actualSegment, parallelRouteKey, ...subPath])
    }
  }

  return paths
}

/**
 * A simplified version of `walkTreeWithFlightRouterState` that doesn't skip any layouts
 * but returns a result of the same shape.
 * Intended to be used for instant validation, where we need the complete tree.
 */
export async function createFullTreeFlightDataForNavigation({
  loaderTree,
  rscHead,
  injectedCSS,
  injectedJS,
  injectedFontPreloadTags,
  ctx,
  preloadCallbacks,
  MetadataOutlet,
}: {
  loaderTree: LoaderTree
  flightRouterState?: FlightRouterState
  rscHead: HeadData
  injectedCSS: Set<string>
  injectedJS: Set<string>
  injectedFontPreloadTags: Set<string>
  ctx: AppRenderContext
  preloadCallbacks: PreloadCallbacks
  MetadataOutlet: React.ComponentType
}): Promise<[rootSegment: FlightDataPath]> {
  const {
    renderOpts: { experimental },
    query,
    getDynamicParamFromSegment,
    pagePath,
    workStore: workStoreForInitialRender,
  } = ctx

  const hintTreeForInitialRender =
    ctx.renderOpts.prefetchHints?.[pagePath] ?? null

  const routerState = await createFlightRouterStateFromLoaderTree(
    loaderTree,
    hintTreeForInitialRender,
    Boolean(experimental.prefetchInlining),
    ctx.renderOpts.cacheComponents,
    workStoreForInitialRender.isStaticGeneration,
    ctx.renderOpts.isBuildTimePrerendering ?? false,
    getDynamicParamFromSegment,
    query
  )
  const rootSegment = routerState[0]

  const seedData = await createComponentTree({
    ctx,
    loaderTree,
    parentParams: {},
    parentOptionalCatchAllParamName: null,
    parentRuntimePrefetchable: false,
    injectedCSS,
    injectedJS,
    injectedFontPreloadTags,
    rootLayoutIncluded: false,
    preloadCallbacks,
    authInterrupts: experimental.authInterrupts,
    MetadataOutlet,
  })

  return [
    [
      // TODO: app-render slices this Segment off.
      // why is that valid, and why are we including it in the first place?
      rootSegment,
      routerState,
      seedData,
      rscHead,
      false,
    ] satisfies FlightDataSegment,
  ]
}

/*
 * This function is used to determine if an existing segment can be overridden
 * by the incoming segment.
 */
const canSegmentBeOverridden = (
  existingSegment: Segment,
  segment: Segment
): boolean => {
  if (Array.isArray(existingSegment) || !Array.isArray(segment)) {
    return false
  }

  return getSegmentParam(existingSegment)?.paramName === segment[0]
}
Quest for Codev2.0.0
/
SIGN IN