363 lines
17 KiB
JavaScript
363 lines
17 KiB
JavaScript
"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 `<Offscreen>` 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 `<html>`
|
|
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 <Offscreen> 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 `<Offscreen>`.
|
|
*/ // TODO-APP: Add handling of `<Offscreen>` 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
|