372 lines
18 KiB
JavaScript
372 lines
18 KiB
JavaScript
"use client";
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.default = OuterLayoutRouter;
|
|
exports.InnerLayoutRouter = InnerLayoutRouter;
|
|
var _extends = require("@swc/helpers/lib/_extends.js").default;
|
|
var _interop_require_default = require("@swc/helpers/lib/_interop_require_default.js").default;
|
|
var _interop_require_wildcard = require("@swc/helpers/lib/_interop_require_wildcard.js").default;
|
|
var _react = _interop_require_wildcard(require("react"));
|
|
var _reactDom = _interop_require_default(require("react-dom"));
|
|
var _appRouterContext = require("../../shared/lib/app-router-context");
|
|
var _appRouter = require("./app-router");
|
|
var _infinitePromise = require("./infinite-promise");
|
|
var _errorBoundary = require("./error-boundary");
|
|
var _matchSegments = require("./match-segments");
|
|
var _navigation = require("./navigation");
|
|
function OuterLayoutRouter({ parallelRouterKey , segmentPath , childProp , error , errorStyles , templateStyles , loading , loadingStyles , hasLoading , template , notFound , notFoundStyles , rootLayoutIncluded }) {
|
|
const context = (0, _react).useContext(_appRouterContext.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.default.createElement(_react.default.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.default.createElement(_appRouterContext.TemplateContext.Provider, {
|
|
key: preservedSegment,
|
|
value: /*#__PURE__*/ _react.default.createElement(_errorBoundary.ErrorBoundary, {
|
|
errorComponent: error,
|
|
errorStyles: errorStyles
|
|
}, /*#__PURE__*/ _react.default.createElement(LoadingBoundary, {
|
|
hasLoading: hasLoading,
|
|
loading: loading,
|
|
loadingStyles: loadingStyles
|
|
}, /*#__PURE__*/ _react.default.createElement(NotFoundBoundary, {
|
|
notFound: notFound,
|
|
notFoundStyles: notFoundStyles
|
|
}, /*#__PURE__*/ _react.default.createElement(RedirectBoundary, null, /*#__PURE__*/ _react.default.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.default.createElement(_react.default.Fragment, null, templateStyles, template)));
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* 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 ((0, _matchSegments).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.default.findDOMNode(instance);
|
|
} finally{
|
|
console.error = originalConsoleError;
|
|
}
|
|
}
|
|
return _reactDom.default.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.default.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;
|
|
}
|
|
}
|
|
function InnerLayoutRouter({ parallelRouterKey , url , childNodes , childProp , segmentPath , tree , // TODO-APP: implement `<Offscreen>` when available.
|
|
// isActive,
|
|
path , rootLayoutIncluded }) {
|
|
const context = (0, _react).useContext(_appRouterContext.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 === _appRouterContext.CacheStates.LAZY_INITIALIZED) {
|
|
// @ts-expect-error TODO-APP: handle changing of the type
|
|
childNode.status = _appRouterContext.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: _appRouterContext.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 === _appRouterContext.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: _appRouterContext.CacheStates.DATA_FETCH,
|
|
data: (0, _appRouter).fetchServerResponse(new URL(url, location.origin), refetchTree),
|
|
subTreeData: null,
|
|
head: childNode && childNode.status === _appRouterContext.CacheStates.LAZY_INITIALIZED ? childNode.head : undefined,
|
|
parallelRoutes: childNode && childNode.status === _appRouterContext.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] = (0, _react).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.default.startTransition(()=>{
|
|
changeByServerResponse(fullTree, flightData, overrideCanonicalUrl);
|
|
});
|
|
});
|
|
// Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
|
|
(0, _react).use((0, _infinitePromise).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) {
|
|
(0, _react).use((0, _infinitePromise).createInfinitePromise());
|
|
}
|
|
const subtree = // The layout router context narrows down tree and childNodes at each level.
|
|
/*#__PURE__*/ _react.default.createElement(_appRouterContext.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.default.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.default.createElement(_react.default.Suspense, {
|
|
fallback: /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, loadingStyles, loading)
|
|
}, children);
|
|
}
|
|
return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
|
|
}
|
|
function HandleRedirect({ redirect }) {
|
|
const router = (0, _navigation).useRouter();
|
|
(0, _react).useEffect(()=>{
|
|
router.replace(redirect, {});
|
|
}, [
|
|
redirect,
|
|
router
|
|
]);
|
|
return null;
|
|
}
|
|
class RedirectErrorBoundary extends _react.default.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.default.createElement(HandleRedirect, {
|
|
redirect: redirect
|
|
});
|
|
}
|
|
return this.props.children;
|
|
}
|
|
constructor(props){
|
|
super(props);
|
|
this.state = {
|
|
redirect: null
|
|
};
|
|
}
|
|
}
|
|
function RedirectBoundary({ children }) {
|
|
const router = (0, _navigation).useRouter();
|
|
return /*#__PURE__*/ _react.default.createElement(RedirectErrorBoundary, {
|
|
router: router
|
|
}, children);
|
|
}
|
|
class NotFoundErrorBoundary extends _react.default.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.default.createElement(_react.default.Fragment, null, /*#__PURE__*/ _react.default.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.default.createElement(NotFoundErrorBoundary, {
|
|
notFound: notFound,
|
|
notFoundStyles: notFoundStyles
|
|
}, children) : /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, children);
|
|
}
|
|
|
|
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
|
|
Object.defineProperty(exports.default, '__esModule', { value: true });
|
|
Object.assign(exports.default, exports);
|
|
module.exports = exports.default;
|
|
}
|
|
|
|
//# sourceMappingURL=layout-router.js.map
|