"use client"; import _extends from "@swc/helpers/src/_extends.mjs"; import React, { useContext, useEffect, use } from 'react'; import ReactDOM from 'react-dom'; import { CacheStates, LayoutRouterContext, GlobalLayoutRouterContext, TemplateContext } from '../../shared/lib/app-router-context'; import { fetchServerResponse } from './app-router'; import { createInfinitePromise } from './infinite-promise'; import { ErrorBoundary } from './error-boundary'; import { matchSegment } from './match-segments'; import { useRouter } from './navigation'; /** * Add refetch marker to router state at the point of the current layout segment. * This ensures the response returned is not further down than the current layout segment. */ function walkAddRefetch(segmentPathToWalk, treeToRecreate) { if (segmentPathToWalk) { const [segment, parallelRouteKey] = segmentPathToWalk; const isLast = segmentPathToWalk.length === 2; if (matchSegment(treeToRecreate[0], segment)) { if (treeToRecreate[1].hasOwnProperty(parallelRouteKey)) { if (isLast) { const subTree = walkAddRefetch(undefined, treeToRecreate[1][parallelRouteKey]); return [ treeToRecreate[0], _extends({}, treeToRecreate[1], { [parallelRouteKey]: [ subTree[0], subTree[1], subTree[2], 'refetch', ] }), ]; } return [ treeToRecreate[0], _extends({}, treeToRecreate[1], { [parallelRouteKey]: walkAddRefetch(segmentPathToWalk.slice(2), treeToRecreate[1][parallelRouteKey]) }), ]; } } } return treeToRecreate; } // TODO-APP: Replace with new React API for finding dom nodes without a `ref` when available /** * Wraps ReactDOM.findDOMNode with additional logic to hide React Strict Mode warning */ function findDOMNode(instance) { // Tree-shake for server bundle if (typeof window === undefined) return null; // Only apply strict mode warning when not in production if (process.env.NODE_ENV !== 'production') { const originalConsoleError = console.error; try { console.error = (...messages)=>{ // Ignore strict mode warning for the findDomNode call below if (!messages[0].includes('Warning: %s is deprecated in StrictMode.')) { originalConsoleError(...messages); } }; return ReactDOM.findDOMNode(instance); } finally{ console.error = originalConsoleError; } } return ReactDOM.findDOMNode(instance); } /** * Check if the top of the HTMLElement is in the viewport. */ function topOfElementInViewport(element) { const rect = element.getBoundingClientRect(); return rect.top >= 0; } class ScrollAndFocusHandler extends React.Component { componentDidMount() { // Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed. const { focusAndScrollRef } = this.props; const domNode = findDOMNode(this); if (focusAndScrollRef.apply && domNode instanceof HTMLElement) { // State is mutated to ensure that the focus and scroll is applied only once. focusAndScrollRef.apply = false; // Set focus on the element domNode.focus(); // Only scroll into viewport when the layout is not visible currently. if (!topOfElementInViewport(domNode)) { const htmlElement = document.documentElement; const existing = htmlElement.style.scrollBehavior; htmlElement.style.scrollBehavior = 'auto'; // In Chrome-based browsers we need to force reflow before calling `scrollTo`. // Otherwise it will not pickup the change in scrollBehavior // More info here: https://github.com/vercel/next.js/issues/40719#issuecomment-1336248042 htmlElement.getClientRects(); domNode.scrollIntoView(); htmlElement.style.scrollBehavior = existing; } } } render() { return this.props.children; } } /** * InnerLayoutRouter handles rendering the provided segment based on the cache. */ export function InnerLayoutRouter({ parallelRouterKey , url , childNodes , childProp , segmentPath , tree , // TODO-APP: implement `` when available. // isActive, path , rootLayoutIncluded }) { const context = useContext(GlobalLayoutRouterContext); if (!context) { throw new Error('invariant global layout router not mounted'); } const { changeByServerResponse , tree: fullTree , focusAndScrollRef } = context; // Read segment path from the parallel router cache node. let childNode = childNodes.get(path); // If childProp is available this means it's the Flight / SSR case. if (childProp && // TODO-APP: verify if this can be null based on user code childProp.current !== null) { if (childNode && childNode.status === CacheStates.LAZY_INITIALIZED) { // @ts-expect-error TODO-APP: handle changing of the type childNode.status = CacheStates.READY; // @ts-expect-error TODO-APP: handle changing of the type childNode.subTreeData = childProp.current; // Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache. childProp.current = null; } else { // Add the segment's subTreeData to the cache. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode. childNodes.set(path, { status: CacheStates.READY, data: null, subTreeData: childProp.current, parallelRoutes: new Map() }); // Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache. childProp.current = null; // In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again. childNode = childNodes.get(path); } } // When childNode is not available during rendering client-side we need to fetch it from the server. if (!childNode || childNode.status === CacheStates.LAZY_INITIALIZED) { /** * Router state with refetch marker added */ // TODO-APP: remove '' const refetchTree = walkAddRefetch([ '', ...segmentPath ], fullTree); /** * Flight data fetch kicked off during render and put into the cache. */ childNodes.set(path, { status: CacheStates.DATA_FETCH, data: fetchServerResponse(new URL(url, location.origin), refetchTree), subTreeData: null, head: childNode && childNode.status === CacheStates.LAZY_INITIALIZED ? childNode.head : undefined, parallelRoutes: childNode && childNode.status === CacheStates.LAZY_INITIALIZED ? childNode.parallelRoutes : new Map() }); // In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again. childNode = childNodes.get(path); } // This case should never happen so it throws an error. It indicates there's a bug in the Next.js. if (!childNode) { throw new Error('Child node should always exist'); } // This case should never happen so it throws an error. It indicates there's a bug in the Next.js. if (childNode.subTreeData && childNode.data) { throw new Error('Child node should not have both subTreeData and data'); } // If cache node has a data request we have to unwrap response by `use` and update the cache. if (childNode.data) { /** * Flight response data */ // When the data has not resolved yet `use` will suspend here. const [flightData, overrideCanonicalUrl] = use(childNode.data); // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { window.location.href = url; return null; } // segmentPath from the server does not match the layout's segmentPath childNode.data = null; // setTimeout is used to start a new transition during render, this is an intentional hack around React. setTimeout(()=>{ // @ts-ignore startTransition exists React.startTransition(()=>{ changeByServerResponse(fullTree, flightData, overrideCanonicalUrl); }); }); // Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered. use(createInfinitePromise()); } // If cache node has no subTreeData and no data request we have to infinitely suspend as the data will likely flow in from another place. // TODO-APP: double check users can't return null in a component that will kick in here. if (!childNode.subTreeData) { use(createInfinitePromise()); } const subtree = // The layout router context narrows down tree and childNodes at each level. /*#__PURE__*/ React.createElement(LayoutRouterContext.Provider, { value: { tree: tree[1][parallelRouterKey], childNodes: childNode.parallelRoutes, // TODO-APP: overriding of url for parallel routes url: url } }, childNode.subTreeData); // Ensure root layout is not wrapped in a div as the root layout renders `` return rootLayoutIncluded ? /*#__PURE__*/ React.createElement(ScrollAndFocusHandler, { focusAndScrollRef: focusAndScrollRef }, subtree) : subtree; } /** * Renders suspense boundary with the provided "loading" property as the fallback. * If no loading property is provided it renders the children without a suspense boundary. */ function LoadingBoundary({ children , loading , loadingStyles , hasLoading }) { if (hasLoading) { return /*#__PURE__*/ React.createElement(React.Suspense, { fallback: /*#__PURE__*/ React.createElement(React.Fragment, null, loadingStyles, loading) }, children); } return /*#__PURE__*/ React.createElement(React.Fragment, null, children); } function HandleRedirect({ redirect }) { const router = useRouter(); useEffect(()=>{ router.replace(redirect, {}); }, [ redirect, router ]); return null; } class RedirectErrorBoundary extends React.Component { static getDerivedStateFromError(error) { var ref; if (error == null ? void 0 : (ref = error.digest) == null ? void 0 : ref.startsWith('NEXT_REDIRECT')) { const url = error.digest.split(';')[1]; return { redirect: url }; } // Re-throw if error is not for redirect throw error; } render() { const redirect = this.state.redirect; if (redirect !== null) { return /*#__PURE__*/ React.createElement(HandleRedirect, { redirect: redirect }); } return this.props.children; } constructor(props){ super(props); this.state = { redirect: null }; } } function RedirectBoundary({ children }) { const router = useRouter(); return /*#__PURE__*/ React.createElement(RedirectErrorBoundary, { router: router }, children); } class NotFoundErrorBoundary extends React.Component { static getDerivedStateFromError(error) { if ((error == null ? void 0 : error.digest) === 'NEXT_NOT_FOUND') { return { notFoundTriggered: true }; } // Re-throw if error is not for 404 throw error; } render() { if (this.state.notFoundTriggered) { return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement("meta", { name: "robots", content: "noindex" }), this.props.notFoundStyles, this.props.notFound); } return this.props.children; } constructor(props){ super(props); this.state = { notFoundTriggered: false }; } } function NotFoundBoundary({ notFound , notFoundStyles , children }) { return notFound ? /*#__PURE__*/ React.createElement(NotFoundErrorBoundary, { notFound: notFound, notFoundStyles: notFoundStyles }, children) : /*#__PURE__*/ React.createElement(React.Fragment, null, children); } /** * OuterLayoutRouter handles the current segment as well as rendering of other segments. * It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes. */ export default function OuterLayoutRouter({ parallelRouterKey , segmentPath , childProp , error , errorStyles , templateStyles , loading , loadingStyles , hasLoading , template , notFound , notFoundStyles , rootLayoutIncluded }) { const context = useContext(LayoutRouterContext); if (!context) { throw new Error('invariant expected layout router to be mounted'); } const { childNodes , tree , url } = context; // Get the current parallelRouter cache node let childNodesForParallelRouter = childNodes.get(parallelRouterKey); // If the parallel router cache node does not exist yet, create it. // This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode. if (!childNodesForParallelRouter) { childNodes.set(parallelRouterKey, new Map()); childNodesForParallelRouter = childNodes.get(parallelRouterKey); } // Get the active segment in the tree // The reason arrays are used in the data format is that these are transferred from the server to the browser so it's optimized to save bytes. const treeSegment = tree[1][parallelRouterKey][0]; const childPropSegment = Array.isArray(childProp.segment) ? childProp.segment[1] : childProp.segment; // If segment is an array it's a dynamic route and we want to read the dynamic route value as the segment to get from the cache. const currentChildSegment = Array.isArray(treeSegment) ? treeSegment[1] : treeSegment; /** * Decides which segments to keep rendering, all segments that are not active will be wrapped in ``. */ // TODO-APP: Add handling of `` when it's available. const preservedSegments = [ currentChildSegment ]; return /*#__PURE__*/ React.createElement(React.Fragment, null, preservedSegments.map((preservedSegment)=>{ return(/* - Error boundary - Only renders error boundary if error component is provided. - Rendered for each segment to ensure they have their own error state. - Loading boundary - Only renders suspense boundary if loading components is provided. - Rendered for each segment to ensure they have their own loading state. - Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch. */ /*#__PURE__*/ React.createElement(TemplateContext.Provider, { key: preservedSegment, value: /*#__PURE__*/ React.createElement(ErrorBoundary, { errorComponent: error, errorStyles: errorStyles }, /*#__PURE__*/ React.createElement(LoadingBoundary, { hasLoading: hasLoading, loading: loading, loadingStyles: loadingStyles }, /*#__PURE__*/ React.createElement(NotFoundBoundary, { notFound: notFound, notFoundStyles: notFoundStyles }, /*#__PURE__*/ React.createElement(RedirectBoundary, null, /*#__PURE__*/ React.createElement(InnerLayoutRouter, { parallelRouterKey: parallelRouterKey, url: url, tree: tree, childNodes: childNodesForParallelRouter, childProp: childPropSegment === preservedSegment ? childProp : null, segmentPath: segmentPath, path: preservedSegment, isActive: currentChildSegment === preservedSegment, rootLayoutIncluded: rootLayoutIncluded }))))) }, /*#__PURE__*/ React.createElement(React.Fragment, null, templateStyles, template))); })); }; //# sourceMappingURL=layout-router.js.map