@ossido-labs/ossido-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/esm/components/CriticalCss.d.ts +13 -0
- package/dist/esm/components/CriticalCss.js +22 -0
- package/dist/esm/components/CriticalCss.js.map +1 -0
- package/dist/esm/components/Link.d.ts +30 -0
- package/dist/esm/components/Link.js +47 -0
- package/dist/esm/components/Link.js.map +1 -0
- package/dist/esm/components/Matches.d.ts +7 -0
- package/dist/esm/components/Matches.js +22 -0
- package/dist/esm/components/Matches.js.map +1 -0
- package/dist/esm/components/NotFound.d.ts +5 -0
- package/dist/esm/components/NotFound.js +22 -0
- package/dist/esm/components/NotFound.js.map +1 -0
- package/dist/esm/components/NotFoundDefaultContent.d.ts +7 -0
- package/dist/esm/components/NotFoundDefaultContent.js +29 -0
- package/dist/esm/components/NotFoundDefaultContent.js.map +1 -0
- package/dist/esm/components/OssidoErrorBoundary.d.ts +39 -0
- package/dist/esm/components/OssidoErrorBoundary.js +48 -0
- package/dist/esm/components/OssidoErrorBoundary.js.map +1 -0
- package/dist/esm/components/Redirect.d.ts +11 -0
- package/dist/esm/components/Redirect.js +21 -0
- package/dist/esm/components/Redirect.js.map +1 -0
- package/dist/esm/components/RouteDataLoader.d.ts +21 -0
- package/dist/esm/components/RouteDataLoader.js +27 -0
- package/dist/esm/components/RouteDataLoader.js.map +1 -0
- package/dist/esm/components/RouteMatch.d.ts +15 -0
- package/dist/esm/components/RouteMatch.js +81 -0
- package/dist/esm/components/RouteMatch.js.map +1 -0
- package/dist/esm/components/RouterContext.d.ts +56 -0
- package/dist/esm/components/RouterContext.js +32 -0
- package/dist/esm/components/RouterContext.js.map +1 -0
- package/dist/esm/components/RouterContextProvider.d.ts +10 -0
- package/dist/esm/components/RouterContextProvider.js +122 -0
- package/dist/esm/components/RouterContextProvider.js.map +1 -0
- package/dist/esm/components/RouterProvider.d.ts +15 -0
- package/dist/esm/components/RouterProvider.js +28 -0
- package/dist/esm/components/RouterProvider.js.map +1 -0
- package/dist/esm/data/resourceCache.d.ts +75 -0
- package/dist/esm/data/resourceCache.js +148 -0
- package/dist/esm/data/resourceCache.js.map +1 -0
- package/dist/esm/hooks/useRoute.d.ts +14 -0
- package/dist/esm/hooks/useRoute.js +22 -0
- package/dist/esm/hooks/useRoute.js.map +1 -0
- package/dist/esm/hooks/useRouter.d.ts +47 -0
- package/dist/esm/hooks/useRouter.js +49 -0
- package/dist/esm/hooks/useRouter.js.map +1 -0
- package/dist/esm/hot.d.ts +9 -0
- package/dist/esm/hot.js +32 -0
- package/dist/esm/hot.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/route.d.ts +70 -0
- package/dist/esm/route.js +50 -0
- package/dist/esm/route.js.map +1 -0
- package/dist/esm/router.d.ts +27 -0
- package/dist/esm/router.js +52 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/types.d.ts +63 -0
- package/dist/esm/utils/from-url-to-parsed-location.d.ts +2 -0
- package/dist/esm/utils/from-url-to-parsed-location.js +15 -0
- package/dist/esm/utils/from-url-to-parsed-location.js.map +1 -0
- package/dist/esm/utils/match-route.d.ts +16 -0
- package/dist/esm/utils/match-route.js +72 -0
- package/dist/esm/utils/match-route.js.map +1 -0
- package/dist/esm/utils/preload-route-chain.d.ts +16 -0
- package/dist/esm/utils/preload-route-chain.js +31 -0
- package/dist/esm/utils/preload-route-chain.js.map +1 -0
- package/dist/esm/utils/view-transition.d.ts +18 -0
- package/dist/esm/utils/view-transition.js +35 -0
- package/dist/esm/utils/view-transition.js.map +1 -0
- package/dist/esm/utils.d.ts +6 -0
- package/dist/esm/utils.js +20 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Schofield
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { Mode } from '../types';
|
|
3
|
+
interface CriticalCssProps {
|
|
4
|
+
routeFilePath?: string;
|
|
5
|
+
mode?: Mode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns the critical CSS for the given route
|
|
9
|
+
* This is required in order to avoid FOUC during development
|
|
10
|
+
* since vite does not support CSS injection without JS waterfall
|
|
11
|
+
*/
|
|
12
|
+
export declare function CriticalCss({ routeFilePath, mode, }: CriticalCssProps): JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/components/CriticalCss.tsx
|
|
4
|
+
const CRITICAL_CSS_PATH = "/vite-server/ossido_internal__critical_css";
|
|
5
|
+
const CRITICAL_CSS_ENABLED = typeof __OSSIDO_CRITICAL_CSS__ === "undefined" || __OSSIDO_CRITICAL_CSS__;
|
|
6
|
+
/**
|
|
7
|
+
* Returns the critical CSS for the given route
|
|
8
|
+
* This is required in order to avoid FOUC during development
|
|
9
|
+
* since vite does not support CSS injection without JS waterfall
|
|
10
|
+
*/
|
|
11
|
+
function CriticalCss({ routeFilePath, mode }) {
|
|
12
|
+
if (!CRITICAL_CSS_ENABLED || !routeFilePath || mode !== "Dev") return null;
|
|
13
|
+
return /* @__PURE__ */ jsx("link", {
|
|
14
|
+
href: `${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`,
|
|
15
|
+
precedence: "high",
|
|
16
|
+
rel: "stylesheet"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { CriticalCss };
|
|
22
|
+
//# sourceMappingURL=CriticalCss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CriticalCss.js","names":[],"sources":["../../../src/components/CriticalCss.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport type { Mode } from '../types';\n\nconst VITE_PROXY_PATH = '/vite-server';\nconst CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/ossido_internal__critical_css';\n\n/**\n * Baked in by the vite `define` in ossido's build config, mirroring the\n * `dev.criticalCss` option. When the user disables critical CSS the vite\n * endpoint is disabled too, so the links must not render (they would be dead,\n * render-blocking requests). The `typeof` guard keeps this safe under\n * vitest/tsdown where the define is not applied.\n */\ndeclare const __OSSIDO_CRITICAL_CSS__: boolean;\nconst CRITICAL_CSS_ENABLED =\n typeof __OSSIDO_CRITICAL_CSS__ === 'undefined' || __OSSIDO_CRITICAL_CSS__;\n\ninterface CriticalCssProps {\n routeFilePath?: string;\n mode?: Mode;\n}\n\n/**\n * Returns the critical CSS for the given route\n * This is required in order to avoid FOUC during development\n * since vite does not support CSS injection without JS waterfall\n */\nexport function CriticalCss({\n routeFilePath,\n mode,\n}: CriticalCssProps): JSX.Element | null {\n if (!CRITICAL_CSS_ENABLED || !routeFilePath || mode !== 'Dev') {\n return null;\n }\n\n return (\n <link\n href={`${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`}\n precedence=\"high\"\n rel=\"stylesheet\"\n />\n );\n}\n"],"mappings":";;;AAKA,MAAM,oBAAoB;AAU1B,MAAM,uBACJ,OAAO,4BAA4B,eAAe;;;;;;AAYpD,SAAgB,YAAY,EAC1B,eACA,QACuC;CACvC,IAAI,CAAC,wBAAwB,CAAC,iBAAiB,SAAS,OACtD,OAAO;CAGT,OACE,oBAAC,QAAD;EACE,MAAM,GAAG,kBAAkB,eAAe;EAC1C,YAAW;EACX,KAAI;CACL;AAEL"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { JSX, Ref } from 'react';
|
|
2
|
+
interface OssidoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
3
|
+
/**
|
|
4
|
+
* If "true" the route gets loaded when the link enters the viewport.
|
|
5
|
+
* @default true
|
|
6
|
+
*/
|
|
7
|
+
preload?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* If "false" the scroll offset will be kept across page navigation.
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
scroll?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* If "true" the history entry will be replaced instead of pushed.
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
replace?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Override the app's `viewTransitions` config for this link: `true` forces a
|
|
20
|
+
* View Transition, `false` skips it. Defaults to the config value.
|
|
21
|
+
*/
|
|
22
|
+
viewTransition?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Forwarded to the underlying `<a>`, merged with the internal preload ref.
|
|
25
|
+
* Lets wrappers (e.g. react-aria's `render`) connect their own ref.
|
|
26
|
+
*/
|
|
27
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
28
|
+
}
|
|
29
|
+
export declare function Link(componentProps: OssidoLinkProps): JSX.Element;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useRoute } from "../hooks/useRoute.js";
|
|
2
|
+
import { useRouter } from "../hooks/useRouter.js";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useInView } from "react-intersection-observer";
|
|
6
|
+
|
|
7
|
+
//#region src/components/Link.tsx
|
|
8
|
+
function isEventModifierKeyActiveAndTargetDifferentFromSelf(event) {
|
|
9
|
+
const target = event.currentTarget.getAttribute("target");
|
|
10
|
+
return target && target !== "_self" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
11
|
+
}
|
|
12
|
+
function Link(componentProps) {
|
|
13
|
+
const { preload = true, scroll = true, children, href, replace, viewTransition, onClick, ref: forwardedRef, ...rest } = componentProps;
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const route = useRoute(href);
|
|
16
|
+
const { ref: inViewRef } = useInView({
|
|
17
|
+
onChange(inView) {
|
|
18
|
+
if (inView && preload) route?.component.preload?.();
|
|
19
|
+
},
|
|
20
|
+
triggerOnce: true
|
|
21
|
+
});
|
|
22
|
+
const setRef = useCallback((node) => {
|
|
23
|
+
inViewRef(node);
|
|
24
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
25
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
26
|
+
}, [inViewRef, forwardedRef]);
|
|
27
|
+
const handleTransition = (event) => {
|
|
28
|
+
onClick?.(event);
|
|
29
|
+
if (href?.startsWith("#") || isEventModifierKeyActiveAndTargetDifferentFromSelf(event)) return;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
router[replace ? "replace" : "push"](href || "", {
|
|
32
|
+
scroll,
|
|
33
|
+
viewTransition
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
return /* @__PURE__ */ jsx("a", {
|
|
37
|
+
...rest,
|
|
38
|
+
href,
|
|
39
|
+
ref: setRef,
|
|
40
|
+
onClick: handleTransition,
|
|
41
|
+
children
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { Link };
|
|
47
|
+
//# sourceMappingURL=Link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Link.js","names":[],"sources":["../../../src/components/Link.tsx"],"sourcesContent":["import type { JSX, MouseEvent, Ref } from 'react';\nimport { useCallback } from 'react';\nimport { useInView } from 'react-intersection-observer';\n\nimport { useRouter } from '../hooks/useRouter';\nimport { useRoute } from '../hooks/useRoute';\n\ninterface OssidoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n /**\n * If \"true\" the route gets loaded when the link enters the viewport.\n * @default true\n */\n preload?: boolean;\n\n /**\n * If \"false\" the scroll offset will be kept across page navigation.\n * @default true\n */\n scroll?: boolean;\n\n /**\n * If \"true\" the history entry will be replaced instead of pushed.\n * @default false\n */\n replace?: boolean;\n\n /**\n * Override the app's `viewTransitions` config for this link: `true` forces a\n * View Transition, `false` skips it. Defaults to the config value.\n */\n viewTransition?: boolean;\n\n /**\n * Forwarded to the underlying `<a>`, merged with the internal preload ref.\n * Lets wrappers (e.g. react-aria's `render`) connect their own ref.\n */\n ref?: Ref<HTMLAnchorElement>;\n}\n\nfunction isEventModifierKeyActiveAndTargetDifferentFromSelf(\n event: MouseEvent<HTMLAnchorElement>,\n): boolean {\n const target = event.currentTarget.getAttribute('target');\n return (\n (target && target !== '_self') ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey // triggers resource download\n );\n}\n\nexport function Link(componentProps: OssidoLinkProps): JSX.Element {\n const {\n preload = true,\n scroll = true,\n children,\n href,\n replace,\n viewTransition,\n onClick,\n ref: forwardedRef,\n ...rest\n } = componentProps;\n\n const router = useRouter();\n const route = useRoute(href);\n const { ref: inViewRef } = useInView({\n onChange(inView) {\n if (inView && preload) route?.component.preload?.();\n },\n triggerOnce: true,\n });\n\n // Merge the caller's ref (e.g. from react-aria's `render`) with the internal\n // in-view ref, so both receive the underlying `<a>` node. Memoised so the\n // callback ref identity is stable across renders.\n const setRef = useCallback(\n (node: HTMLAnchorElement | null): void => {\n inViewRef(node);\n if (typeof forwardedRef === 'function') {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [inViewRef, forwardedRef],\n );\n\n const handleTransition: React.MouseEventHandler<HTMLAnchorElement> = (\n event,\n ) => {\n onClick?.(event);\n\n if (\n href?.startsWith('#') ||\n // If the user is pressing a modifier key or using the target attribute,\n // we fall back to default behaviour of `a` tag\n isEventModifierKeyActiveAndTargetDifferentFromSelf(event)\n ) {\n return;\n }\n\n event.preventDefault();\n\n const method = replace ? 'replace' : 'push';\n\n router[method](href || '', { scroll, viewTransition });\n };\n\n return (\n <a {...rest} href={href} ref={setRef} onClick={handleTransition}>\n {children}\n </a>\n );\n}\n"],"mappings":";;;;;;;AAuCA,SAAS,mDACP,OACS;CACT,MAAM,SAAS,MAAM,cAAc,aAAa,QAAQ;CACxD,OACG,UAAU,WAAW,WACtB,MAAM,WACN,MAAM,WACN,MAAM,YACN,MAAM;AAEV;AAEA,SAAgB,KAAK,gBAA8C;CACjE,MAAM,EACJ,UAAU,MACV,SAAS,MACT,UACA,MACA,SACA,gBACA,SACA,KAAK,cACL,GAAG,SACD;CAEJ,MAAM,SAAS,UAAU;CACzB,MAAM,QAAQ,SAAS,IAAI;CAC3B,MAAM,EAAE,KAAK,cAAc,UAAU;EACnC,SAAS,QAAQ;GACf,IAAI,UAAU,SAAS,OAAO,UAAU,UAAU;EACpD;EACA,aAAa;CACf,CAAC;CAKD,MAAM,SAAS,aACZ,SAAyC;EACxC,UAAU,IAAI;EACd,IAAI,OAAO,iBAAiB,YAC1B,aAAa,IAAI;OACZ,IAAI,cACT,aAAa,UAAU;CAE3B,GACA,CAAC,WAAW,YAAY,CAC1B;CAEA,MAAM,oBACJ,UACG;EACH,UAAU,KAAK;EAEf,IACE,MAAM,WAAW,GAAG,KAGpB,mDAAmD,KAAK,GAExD;EAGF,MAAM,eAAe;EAIrB,OAFe,UAAU,YAAY,OAEvB,CAAC,QAAQ,IAAI;GAAE;GAAQ;EAAe,CAAC;CACvD;CAEA,OACE,oBAAC,KAAD;EAAG,GAAI;EAAY;EAAM,KAAK;EAAQ,SAAS;EAC5C;CACA;AAEP"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useRouterContext } from "./RouterContext.js";
|
|
2
|
+
import { useRoute } from "../hooks/useRoute.js";
|
|
3
|
+
import { useRouteHotVersion } from "../hot.js";
|
|
4
|
+
import { RouteMatch } from "./RouteMatch.js";
|
|
5
|
+
import { NotFound } from "./NotFound.js";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/components/Matches.tsx
|
|
9
|
+
function Matches({ mode }) {
|
|
10
|
+
const { location } = useRouterContext();
|
|
11
|
+
useRouteHotVersion();
|
|
12
|
+
const route = useRoute(location.pathname);
|
|
13
|
+
if (!route) return /* @__PURE__ */ jsx(NotFound, { mode });
|
|
14
|
+
return /* @__PURE__ */ jsx(RouteMatch, {
|
|
15
|
+
route,
|
|
16
|
+
mode
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { Matches };
|
|
22
|
+
//# sourceMappingURL=Matches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Matches.js","names":[],"sources":["../../../src/components/Matches.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { useRoute } from '../hooks/useRoute';\n\nimport type { Mode } from '../types';\nimport { useRouteHotVersion } from '../hot';\n\nimport { RouteMatch } from './RouteMatch';\nimport { NotFound } from './NotFound';\nimport { useRouterContext } from './RouterContext';\n\ninterface MatchesProps {\n mode?: Mode;\n}\n\nexport function Matches({ mode }: MatchesProps): JSX.Element {\n const { location } = useRouterContext();\n\n // Dev-only: re-render the whole match tree when a route component is\n // hot-swapped (see `../hot`). Inert in prod — the store is never bumped.\n useRouteHotVersion();\n\n const route = useRoute(location.pathname);\n\n if (!route) {\n return <NotFound mode={mode} />;\n }\n\n return <RouteMatch route={route} mode={mode} />;\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,QAAQ,EAAE,QAAmC;CAC3D,MAAM,EAAE,aAAa,iBAAiB;CAItC,mBAAmB;CAEnB,MAAM,QAAQ,SAAS,SAAS,QAAQ;CAExC,IAAI,CAAC,OACH,OAAO,oBAAC,UAAD,EAAgB,KAAO;CAGhC,OAAO,oBAAC,YAAD;EAAmB;EAAa;CAAO;AAChD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useRouterContext } from "./RouterContext.js";
|
|
2
|
+
import { CriticalCss } from "./CriticalCss.js";
|
|
3
|
+
import { ROOT_ROUTE_ID } from "../route.js";
|
|
4
|
+
import { NotFoundDefaultContent } from "./NotFoundDefaultContent.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/components/NotFound.tsx
|
|
8
|
+
function NotFound({ mode }) {
|
|
9
|
+
const { router } = useRouterContext();
|
|
10
|
+
const rootRoute = router.routesById[ROOT_ROUTE_ID];
|
|
11
|
+
const RootLayout = rootRoute?.component;
|
|
12
|
+
if (!RootLayout) return null;
|
|
13
|
+
const CustomNotFound = rootRoute?.options.notFoundComponent;
|
|
14
|
+
return /* @__PURE__ */ jsxs(RootLayout, { children: [/* @__PURE__ */ jsx(CriticalCss, {
|
|
15
|
+
routeFilePath: "__root__",
|
|
16
|
+
mode
|
|
17
|
+
}), CustomNotFound ? /* @__PURE__ */ jsx(CustomNotFound, {}) : /* @__PURE__ */ jsx(NotFoundDefaultContent, {})] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { NotFound };
|
|
22
|
+
//# sourceMappingURL=NotFound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotFound.js","names":[],"sources":["../../../src/components/NotFound.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { useRouterContext } from '../components/RouterContext';\nimport { ROOT_ROUTE_ID } from '../route';\n\nimport type { Mode } from '../types';\n\nimport { NotFoundDefaultContent } from './NotFoundDefaultContent';\nimport { CriticalCss } from './CriticalCss';\n\nexport function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {\n const { router } = useRouterContext();\n\n const rootRoute = router.routesById[ROOT_ROUTE_ID];\n const RootLayout = rootRoute?.component;\n\n if (!RootLayout) return null;\n\n // The global not-found is the root `not-found.tsx`, rendered inside the root\n // layout (Next.js semantics). Falls back to the framework default.\n const CustomNotFound = rootRoute?.options.notFoundComponent;\n\n return (\n <RootLayout>\n <CriticalCss routeFilePath=\"__root__\" mode={mode} />\n {CustomNotFound ? <CustomNotFound /> : <NotFoundDefaultContent />}\n </RootLayout>\n );\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,SAAS,EAAE,QAA6C;CACtE,MAAM,EAAE,WAAW,iBAAiB;CAEpC,MAAM,YAAY,OAAO,WAAW;CACpC,MAAM,aAAa,WAAW;CAE9B,IAAI,CAAC,YAAY,OAAO;CAIxB,MAAM,iBAAiB,WAAW,QAAQ;CAE1C,OACE,qBAAC,YAAD,aACE,oBAAC,aAAD;EAAa,eAAc;EAAiB;CAAO,IAClD,iBAAiB,oBAAC,gBAAD,CAAiB,KAAI,oBAAC,wBAAD,CAAyB,EACtD;AAEhB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Framework default 404 page, shown when no route matches and the app provides
|
|
4
|
+
* no `/404` route. Shares the {@link DefaultScreen} shell with
|
|
5
|
+
* {@link DefaultError} so the two default screens look consistent.
|
|
6
|
+
*/
|
|
7
|
+
export declare function NotFoundDefaultContent(): JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Link } from "./Link.js";
|
|
2
|
+
import { DefaultScreen } from "@ossido-labs/ossido-ui";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/components/NotFoundDefaultContent.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Framework default 404 page, shown when no route matches and the app provides
|
|
8
|
+
* no `/404` route. Shares the {@link DefaultScreen} shell with
|
|
9
|
+
* {@link DefaultError} so the two default screens look consistent.
|
|
10
|
+
*/
|
|
11
|
+
function NotFoundDefaultContent() {
|
|
12
|
+
return /* @__PURE__ */ jsxs(DefaultScreen, {
|
|
13
|
+
role: "status",
|
|
14
|
+
badge: "404",
|
|
15
|
+
title: "Page not found",
|
|
16
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
17
|
+
className: "ossido-screen-text",
|
|
18
|
+
children: "The page you’re looking for doesn’t exist or may have moved."
|
|
19
|
+
}), /* @__PURE__ */ jsx(Link, {
|
|
20
|
+
href: "/",
|
|
21
|
+
className: "ossido-screen-action",
|
|
22
|
+
children: "Return to homepage"
|
|
23
|
+
})]
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { NotFoundDefaultContent };
|
|
29
|
+
//# sourceMappingURL=NotFoundDefaultContent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotFoundDefaultContent.js","names":[],"sources":["../../../src/components/NotFoundDefaultContent.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { DefaultScreen } from '@ossido-labs/ossido-ui';\n\nimport { Link } from './Link';\n\n/**\n * Framework default 404 page, shown when no route matches and the app provides\n * no `/404` route. Shares the {@link DefaultScreen} shell with\n * {@link DefaultError} so the two default screens look consistent.\n */\nexport function NotFoundDefaultContent(): JSX.Element {\n return (\n <DefaultScreen role=\"status\" badge=\"404\" title=\"Page not found\">\n <p className=\"ossido-screen-text\">\n The page you’re looking for doesn’t exist or may have moved.\n </p>\n <Link href=\"/\" className=\"ossido-screen-action\">\n Return to homepage\n </Link>\n </DefaultScreen>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,yBAAsC;CACpD,OACE,qBAAC,eAAD;EAAe,MAAK;EAAS,OAAM;EAAM,OAAM;YAA/C,CACE,oBAAC,KAAD;GAAG,WAAU;aAAqB;EAE/B,IACH,oBAAC,MAAD;GAAM,MAAK;GAAI,WAAU;aAAuB;EAE1C,EACO;;AAEnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { ErrorComponent } from '../types';
|
|
4
|
+
interface OssidoErrorBoundaryProps {
|
|
5
|
+
fallback: ErrorComponent;
|
|
6
|
+
/**
|
|
7
|
+
* Changes per navigation/retry (the route's data-resource key). When it
|
|
8
|
+
* changes the boundary clears any caught error and re-renders its children —
|
|
9
|
+
* a reset without remounting. Not remounting matters: the surrounding
|
|
10
|
+
* `<Suspense>` stays mounted, so a navigation (whose data + code are already
|
|
11
|
+
* prefetched) swaps straight to the new page instead of flashing the fallback.
|
|
12
|
+
*/
|
|
13
|
+
resetKey: string;
|
|
14
|
+
/**
|
|
15
|
+
* Called by the fallback's `reset`. It bumps the navigation id (which changes
|
|
16
|
+
* `resetKey`), clearing the error and creating a fresh data resource.
|
|
17
|
+
*/
|
|
18
|
+
onReset: () => void;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
interface OssidoErrorBoundaryState {
|
|
22
|
+
error: Error | null;
|
|
23
|
+
resetKey: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Renders the nearest `error.tsx` (or the framework default) when a descendant
|
|
27
|
+
* throws — including a rejected data resource surfaced through `use()`.
|
|
28
|
+
*
|
|
29
|
+
* Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from
|
|
30
|
+
* the caller, so its children re-render (not remount) on navigation.
|
|
31
|
+
*/
|
|
32
|
+
export declare class OssidoErrorBoundary extends Component<OssidoErrorBoundaryProps, OssidoErrorBoundaryState> {
|
|
33
|
+
state: OssidoErrorBoundaryState;
|
|
34
|
+
static getDerivedStateFromError(error: Error): Partial<OssidoErrorBoundaryState>;
|
|
35
|
+
static getDerivedStateFromProps(props: OssidoErrorBoundaryProps, state: OssidoErrorBoundaryState): Partial<OssidoErrorBoundaryState> | null;
|
|
36
|
+
reset: () => void;
|
|
37
|
+
render(): ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/components/OssidoErrorBoundary.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Renders the nearest `error.tsx` (or the framework default) when a descendant
|
|
7
|
+
* throws — including a rejected data resource surfaced through `use()`.
|
|
8
|
+
*
|
|
9
|
+
* Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from
|
|
10
|
+
* the caller, so its children re-render (not remount) on navigation.
|
|
11
|
+
*/
|
|
12
|
+
var OssidoErrorBoundary = class extends Component {
|
|
13
|
+
constructor(..._args) {
|
|
14
|
+
super(..._args);
|
|
15
|
+
this.state = {
|
|
16
|
+
error: null,
|
|
17
|
+
resetKey: this.props.resetKey
|
|
18
|
+
};
|
|
19
|
+
this.reset = () => {
|
|
20
|
+
this.props.onReset();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
static getDerivedStateFromError(error) {
|
|
24
|
+
return { error };
|
|
25
|
+
}
|
|
26
|
+
static getDerivedStateFromProps(props, state) {
|
|
27
|
+
if (props.resetKey !== state.resetKey) return {
|
|
28
|
+
error: null,
|
|
29
|
+
resetKey: props.resetKey
|
|
30
|
+
};
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
const { error } = this.state;
|
|
35
|
+
if (error) {
|
|
36
|
+
const Fallback = this.props.fallback;
|
|
37
|
+
return /* @__PURE__ */ jsx(Fallback, {
|
|
38
|
+
error,
|
|
39
|
+
reset: this.reset
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return this.props.children;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { OssidoErrorBoundary };
|
|
48
|
+
//# sourceMappingURL=OssidoErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OssidoErrorBoundary.js","names":[],"sources":["../../../src/components/OssidoErrorBoundary.tsx"],"sourcesContent":["import { Component } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { ErrorComponent } from '../types';\n\ninterface OssidoErrorBoundaryProps {\n fallback: ErrorComponent;\n /**\n * Changes per navigation/retry (the route's data-resource key). When it\n * changes the boundary clears any caught error and re-renders its children —\n * a reset without remounting. Not remounting matters: the surrounding\n * `<Suspense>` stays mounted, so a navigation (whose data + code are already\n * prefetched) swaps straight to the new page instead of flashing the fallback.\n */\n resetKey: string;\n /**\n * Called by the fallback's `reset`. It bumps the navigation id (which changes\n * `resetKey`), clearing the error and creating a fresh data resource.\n */\n onReset: () => void;\n children: ReactNode;\n}\n\ninterface OssidoErrorBoundaryState {\n error: Error | null;\n resetKey: string;\n}\n\n/**\n * Renders the nearest `error.tsx` (or the framework default) when a descendant\n * throws — including a rejected data resource surfaced through `use()`.\n *\n * Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from\n * the caller, so its children re-render (not remount) on navigation.\n */\nexport class OssidoErrorBoundary extends Component<\n OssidoErrorBoundaryProps,\n OssidoErrorBoundaryState\n> {\n state: OssidoErrorBoundaryState = {\n error: null,\n resetKey: this.props.resetKey,\n };\n\n static getDerivedStateFromError(\n error: Error,\n ): Partial<OssidoErrorBoundaryState> {\n return { error };\n }\n\n static getDerivedStateFromProps(\n props: OssidoErrorBoundaryProps,\n state: OssidoErrorBoundaryState,\n ): Partial<OssidoErrorBoundaryState> | null {\n // A new navigation/retry: drop the caught error and try the children again.\n if (props.resetKey !== state.resetKey) {\n return { error: null, resetKey: props.resetKey };\n }\n return null;\n }\n\n reset = (): void => {\n // Bumps the navigation id → `resetKey` changes → the error is cleared and a\n // fresh data resource is created (a refetch).\n this.props.onReset();\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (error) {\n const Fallback = this.props.fallback;\n return <Fallback error={error} reset={this.reset} />;\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;AAmCA,IAAa,sBAAb,cAAyC,UAGvC;;;eACkC;GAChC,OAAO;GACP,UAAU,KAAK,MAAM;EACvB;qBAmBoB;GAGlB,KAAK,MAAM,QAAQ;EACrB;;CArBA,OAAO,yBACL,OACmC;EACnC,OAAO,EAAE,MAAM;CACjB;CAEA,OAAO,yBACL,OACA,OAC0C;EAE1C,IAAI,MAAM,aAAa,MAAM,UAC3B,OAAO;GAAE,OAAO;GAAM,UAAU,MAAM;EAAS;EAEjD,OAAO;CACT;CAQA,SAAoB;EAClB,MAAM,EAAE,UAAU,KAAK;EACvB,IAAI,OAAO;GACT,MAAM,WAAW,KAAK,MAAM;GAC5B,OAAO,oBAAC,UAAD;IAAiB;IAAO,OAAO,KAAK;GAAQ;EACrD;EACA,OAAO,KAAK,MAAM;CACpB;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface RedirectProps {
|
|
2
|
+
to: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Performs a client-side redirect (returned from a route's data load) in an
|
|
6
|
+
* effect — never during render, since navigation is a side effect and the app
|
|
7
|
+
* renders under `StrictMode`. Uses `replace` so the redirecting URL does not
|
|
8
|
+
* become a back-button trap.
|
|
9
|
+
*/
|
|
10
|
+
export declare function Redirect({ to }: RedirectProps): null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useRouter } from "../hooks/useRouter.js";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components/Redirect.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Performs a client-side redirect (returned from a route's data load) in an
|
|
7
|
+
* effect — never during render, since navigation is a side effect and the app
|
|
8
|
+
* renders under `StrictMode`. Uses `replace` so the redirecting URL does not
|
|
9
|
+
* become a back-button trap.
|
|
10
|
+
*/
|
|
11
|
+
function Redirect({ to }) {
|
|
12
|
+
const { replace } = useRouter();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
replace(to);
|
|
15
|
+
}, [replace, to]);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { Redirect };
|
|
21
|
+
//# sourceMappingURL=Redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Redirect.js","names":[],"sources":["../../../src/components/Redirect.tsx"],"sourcesContent":["import { useEffect } from 'react';\n\nimport { useRouter } from '../hooks/useRouter';\n\ninterface RedirectProps {\n to: string;\n}\n\n/**\n * Performs a client-side redirect (returned from a route's data load) in an\n * effect — never during render, since navigation is a side effect and the app\n * renders under `StrictMode`. Uses `replace` so the redirecting URL does not\n * become a back-button trap.\n */\nexport function Redirect({ to }: RedirectProps): null {\n const { replace } = useRouter();\n\n useEffect(() => {\n replace(to);\n }, [replace, to]);\n\n return null;\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,SAAS,EAAE,MAA2B;CACpD,MAAM,EAAE,YAAY,UAAU;CAE9B,gBAAgB;EACd,QAAQ,EAAE;CACZ,GAAG,CAAC,SAAS,EAAE,CAAC;CAEhB,OAAO;AACT"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { Route } from '../route';
|
|
3
|
+
import type { ParsedLocation } from './RouterContext';
|
|
4
|
+
interface RouteDataLoaderProps {
|
|
5
|
+
route: Route;
|
|
6
|
+
resourceKey: string;
|
|
7
|
+
location: ParsedLocation;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads the route's server data resource via `use()`. On the initial render the
|
|
11
|
+
* resource is pre-seeded (SSR data) so this resolves synchronously; on client
|
|
12
|
+
* navigation it suspends until the fetch settles, showing the `<Suspense>`
|
|
13
|
+
* fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and
|
|
14
|
+
* caught by the surrounding error boundary (`error.tsx`).
|
|
15
|
+
*
|
|
16
|
+
* The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE
|
|
17
|
+
* the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet
|
|
18
|
+
* inside the boundary would defer its streamed reveal.
|
|
19
|
+
*/
|
|
20
|
+
export declare function RouteDataLoader({ route, resourceKey, location, }: RouteDataLoaderProps): JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getOrCreateResource } from "../data/resourceCache.js";
|
|
2
|
+
import { Redirect } from "./Redirect.js";
|
|
3
|
+
import { use } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/components/RouteDataLoader.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Reads the route's server data resource via `use()`. On the initial render the
|
|
9
|
+
* resource is pre-seeded (SSR data) so this resolves synchronously; on client
|
|
10
|
+
* navigation it suspends until the fetch settles, showing the `<Suspense>`
|
|
11
|
+
* fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and
|
|
12
|
+
* caught by the surrounding error boundary (`error.tsx`).
|
|
13
|
+
*
|
|
14
|
+
* The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE
|
|
15
|
+
* the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet
|
|
16
|
+
* inside the boundary would defer its streamed reveal.
|
|
17
|
+
*/
|
|
18
|
+
function RouteDataLoader({ route, resourceKey, location }) {
|
|
19
|
+
const result = use(getOrCreateResource(resourceKey, route, location));
|
|
20
|
+
if (result.kind === "redirect") return /* @__PURE__ */ jsx(Redirect, { to: result.destination });
|
|
21
|
+
const Component = route.component;
|
|
22
|
+
return /* @__PURE__ */ jsx(Component, { ...result.props });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { RouteDataLoader };
|
|
27
|
+
//# sourceMappingURL=RouteDataLoader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteDataLoader.js","names":[],"sources":["../../../src/components/RouteDataLoader.tsx"],"sourcesContent":["import { use } from 'react';\nimport type { JSX } from 'react';\n\nimport type { Route } from '../route';\nimport { getOrCreateResource } from '../data/resourceCache';\n\nimport type { ParsedLocation } from './RouterContext';\nimport { Redirect } from './Redirect';\n\ninterface RouteDataLoaderProps {\n route: Route;\n resourceKey: string;\n location: ParsedLocation;\n}\n\n/**\n * Reads the route's server data resource via `use()`. On the initial render the\n * resource is pre-seeded (SSR data) so this resolves synchronously; on client\n * navigation it suspends until the fetch settles, showing the `<Suspense>`\n * fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and\n * caught by the surrounding error boundary (`error.tsx`).\n *\n * The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE\n * the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet\n * inside the boundary would defer its streamed reveal.\n */\nexport function RouteDataLoader({\n route,\n resourceKey,\n location,\n}: RouteDataLoaderProps): JSX.Element {\n const result = use(getOrCreateResource(resourceKey, route, location));\n\n if (result.kind === 'redirect') {\n return <Redirect to={result.destination} />;\n }\n\n const Component = route.component;\n\n // Server data is spread directly as the page component's props.\n return <Component {...result.props} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,SAAgB,gBAAgB,EAC9B,OACA,aACA,YACoC;CACpC,MAAM,SAAS,IAAI,oBAAoB,aAAa,OAAO,QAAQ,CAAC;CAEpE,IAAI,OAAO,SAAS,YAClB,OAAO,oBAAC,UAAD,EAAU,IAAI,OAAO,YAAc;CAG5C,MAAM,YAAY,MAAM;CAGxB,OAAO,oBAAC,WAAD,EAAW,GAAI,OAAO,MAAQ;AACvC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { Mode } from '../types';
|
|
3
|
+
import type { Route } from '../route';
|
|
4
|
+
interface RouteMatchProps {
|
|
5
|
+
route: Route;
|
|
6
|
+
mode?: Mode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders the matched route: its parent layouts wrap an error boundary +
|
|
10
|
+
* `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by
|
|
11
|
+
* the data-resource key so a navigation (including same-route param changes)
|
|
12
|
+
* remounts it and shows the loading fallback, while the layouts persist.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RouteMatch: ({ route, mode }: RouteMatchProps) => JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildResourceKey, readLayoutData } from "../data/resourceCache.js";
|
|
2
|
+
import { useRouterContext } from "./RouterContext.js";
|
|
3
|
+
import { CriticalCss } from "./CriticalCss.js";
|
|
4
|
+
import { RouteDataLoader } from "./RouteDataLoader.js";
|
|
5
|
+
import { OssidoErrorBoundary } from "./OssidoErrorBoundary.js";
|
|
6
|
+
import { Suspense, memo, useMemo } from "react";
|
|
7
|
+
import { DefaultError, DefaultLoading, DevErrorReporter } from "@ossido-labs/ossido-ui";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
|
|
10
|
+
//#region src/components/RouteMatch.tsx
|
|
11
|
+
/**
|
|
12
|
+
* Renders the matched route: its parent layouts wrap an error boundary +
|
|
13
|
+
* `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by
|
|
14
|
+
* the data-resource key so a navigation (including same-route param changes)
|
|
15
|
+
* remounts it and shows the loading fallback, while the layouts persist.
|
|
16
|
+
*/
|
|
17
|
+
const RouteMatch = ({ route, mode }) => {
|
|
18
|
+
const { location, navigationId, retry } = useRouterContext();
|
|
19
|
+
const routes = useMemo(() => loadParentComponents(route), [route.id]);
|
|
20
|
+
const resourceKey = buildResourceKey(navigationId, location);
|
|
21
|
+
const LoadingComponent = route.options.loadingComponent ?? DefaultLoading;
|
|
22
|
+
const ErrorComponent = route.options.errorComponent ?? (mode === "Dev" ? DevErrorReporter : DefaultError);
|
|
23
|
+
return /* @__PURE__ */ jsxs(TraverseRootComponents, {
|
|
24
|
+
routes,
|
|
25
|
+
mode,
|
|
26
|
+
children: [/* @__PURE__ */ jsx(CriticalCss, {
|
|
27
|
+
routeFilePath: route.filePath,
|
|
28
|
+
mode
|
|
29
|
+
}), /* @__PURE__ */ jsx(Suspense, {
|
|
30
|
+
fallback: /* @__PURE__ */ jsx(LoadingComponent, {}),
|
|
31
|
+
children: /* @__PURE__ */ jsx(OssidoErrorBoundary, {
|
|
32
|
+
resetKey: resourceKey,
|
|
33
|
+
fallback: ErrorComponent,
|
|
34
|
+
onReset: retry,
|
|
35
|
+
children: /* @__PURE__ */ jsx(RouteDataLoader, {
|
|
36
|
+
route,
|
|
37
|
+
resourceKey,
|
|
38
|
+
location
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
})]
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Renders the layout (`layout`) components that wrap the selected route.
|
|
46
|
+
* Layouts receive only `children` now — they live OUTSIDE the keyed Suspense
|
|
47
|
+
* boundary, so they persist across navigation while only the page area shows
|
|
48
|
+
* the loading fallback. Memoized so navigation does not re-render layouts.
|
|
49
|
+
*/
|
|
50
|
+
const TraverseRootComponents = memo(({ routes, index = 0, mode, children }) => {
|
|
51
|
+
if (routes.length > index) {
|
|
52
|
+
const route = routes[index];
|
|
53
|
+
const Parent = route.component;
|
|
54
|
+
const routeFilePath = route.filePath || route.id;
|
|
55
|
+
const layoutProps = route.options.hasHandler && route.options.dataKey ? readLayoutData(route.options.dataKey) : void 0;
|
|
56
|
+
return /* @__PURE__ */ jsxs(Parent, {
|
|
57
|
+
...layoutProps,
|
|
58
|
+
children: [/* @__PURE__ */ jsx(CriticalCss, {
|
|
59
|
+
routeFilePath,
|
|
60
|
+
mode
|
|
61
|
+
}), /* @__PURE__ */ jsx(TraverseRootComponents, {
|
|
62
|
+
routes,
|
|
63
|
+
index: index + 1,
|
|
64
|
+
mode,
|
|
65
|
+
children
|
|
66
|
+
})]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
70
|
+
});
|
|
71
|
+
TraverseRootComponents.displayName = "TraverseRootComponents";
|
|
72
|
+
const loadParentComponents = (route, loader = []) => {
|
|
73
|
+
const parentComponent = route.options.getParentRoute?.();
|
|
74
|
+
loader.push(parentComponent);
|
|
75
|
+
if (!parentComponent.isRoot) return loadParentComponents(parentComponent, loader);
|
|
76
|
+
return loader.reverse();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { RouteMatch };
|
|
81
|
+
//# sourceMappingURL=RouteMatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteMatch.js","names":[],"sources":["../../../src/components/RouteMatch.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { memo, Suspense, useMemo } from 'react';\n\nimport { DefaultLoading, DefaultError, DevErrorReporter } from '@ossido-labs/ossido-ui';\n\nimport type { Mode } from '../types';\nimport type { Route } from '../route';\nimport { buildResourceKey, readLayoutData } from '../data/resourceCache';\n\nimport { useRouterContext } from './RouterContext';\nimport { CriticalCss } from './CriticalCss';\nimport { RouteDataLoader } from './RouteDataLoader';\nimport { OssidoErrorBoundary } from './OssidoErrorBoundary';\n\ninterface RouteMatchProps {\n route: Route;\n mode?: Mode;\n}\n\n/**\n * Renders the matched route: its parent layouts wrap an error boundary +\n * `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by\n * the data-resource key so a navigation (including same-route param changes)\n * remounts it and shows the loading fallback, while the layouts persist.\n */\nexport const RouteMatch = ({ route, mode }: RouteMatchProps): JSX.Element => {\n const { location, navigationId, retry } = useRouterContext();\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const routes = useMemo(() => loadParentComponents(route), [route.id]);\n\n const resourceKey = buildResourceKey(navigationId, location);\n\n const LoadingComponent = route.options.loadingComponent ?? DefaultLoading;\n // In dev the boundary reports the error to the shared store (rendered by the\n // floating DevErrorOverlayHost); production falls back to a detail-free page\n // so source/stack are never leaked to end users.\n const ErrorComponent =\n route.options.errorComponent ??\n (mode === 'Dev' ? DevErrorReporter : DefaultError);\n\n return (\n <TraverseRootComponents routes={routes} mode={mode}>\n {/* The leaf route's critical CSS must stay OUTSIDE the <Suspense> below:\n React 19 ties a boundary's reveal to any `precedence` stylesheet\n rendered inside it, which would push the streamed initial content into\n an out-of-order late chunk (empty shell paints first — a flash on\n every cold load). Rendered here it still hoists into the shell's\n <head> as render-blocking, but the boundary streams inline. */}\n <CriticalCss routeFilePath={route.filePath} mode={mode} />\n {/* The <Suspense> is not keyed by the resource key, so navigation\n re-renders (rather than remounts) the subtree. With the destination's\n data + code prefetched by `updateLocation`, that lets the new page\n appear in place with no fallback flash. The error boundary resets via\n `resetKey` instead of a key change. */}\n <Suspense fallback={<LoadingComponent />}>\n <OssidoErrorBoundary\n resetKey={resourceKey}\n fallback={ErrorComponent}\n onReset={retry}\n >\n <RouteDataLoader\n route={route}\n resourceKey={resourceKey}\n location={location}\n />\n </OssidoErrorBoundary>\n </Suspense>\n </TraverseRootComponents>\n );\n};\n\ninterface TraverseRootComponentsProps {\n routes: Array<Route>;\n children?: React.ReactNode;\n index?: number;\n mode?: Mode;\n}\n\n/**\n * Renders the layout (`layout`) components that wrap the selected route.\n * Layouts receive only `children` now — they live OUTSIDE the keyed Suspense\n * boundary, so they persist across navigation while only the page area shows\n * the loading fallback. Memoized so navigation does not re-render layouts.\n */\nconst TraverseRootComponents = memo(\n ({\n routes,\n index = 0,\n mode,\n children,\n }: TraverseRootComponentsProps): React.JSX.Element => {\n if (routes.length > index) {\n const route = routes[index] as Route;\n const Parent = route.component;\n\n // Fallback to the route id if the filePath is not defined\n // as is the case for the root route\n const routeFilePath = route.filePath || route.id;\n\n // A `layout.rs` handler's data is seeded (by SSR or the page's data fetch)\n // and read synchronously here, then spread as the layout's props alongside\n // `children`. Layouts without a data handler receive only `children`.\n const layoutProps =\n route.options.hasHandler && route.options.dataKey\n ? readLayoutData(route.options.dataKey)\n : undefined;\n\n return (\n <Parent {...layoutProps}>\n <CriticalCss routeFilePath={routeFilePath} mode={mode} />\n <TraverseRootComponents routes={routes} index={index + 1} mode={mode}>\n {children}\n </TraverseRootComponents>\n </Parent>\n );\n }\n\n return <>{children}</>;\n },\n);\nTraverseRootComponents.displayName = 'TraverseRootComponents';\n\nconst loadParentComponents = (\n route: Route,\n loader: Array<Route> = [],\n): Array<Route> => {\n const parentComponent = route.options.getParentRoute?.() as Route;\n\n loader.push(parentComponent);\n\n if (!parentComponent.isRoot) {\n return loadParentComponents(parentComponent, loader);\n }\n\n return loader.reverse();\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,MAAa,cAAc,EAAE,OAAO,WAAyC;CAC3E,MAAM,EAAE,UAAU,cAAc,UAAU,iBAAiB;CAG3D,MAAM,SAAS,cAAc,qBAAqB,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC;CAEpE,MAAM,cAAc,iBAAiB,cAAc,QAAQ;CAE3D,MAAM,mBAAmB,MAAM,QAAQ,oBAAoB;CAI3D,MAAM,iBACJ,MAAM,QAAQ,mBACb,SAAS,QAAQ,mBAAmB;CAEvC,OACE,qBAAC,wBAAD;EAAgC;EAAc;YAA9C,CAOE,oBAAC,aAAD;GAAa,eAAe,MAAM;GAAgB;EAAO,IAMzD,oBAAC,UAAD;GAAU,UAAU,oBAAC,kBAAD,CAAmB;aACrC,oBAAC,qBAAD;IACE,UAAU;IACV,UAAU;IACV,SAAS;cAET,oBAAC,iBAAD;KACS;KACM;KACH;IACX;GACkB;EACb,EACY;;AAE5B;;;;;;;AAeA,MAAM,yBAAyB,MAC5B,EACC,QACA,QAAQ,GACR,MACA,eACoD;CACpD,IAAI,OAAO,SAAS,OAAO;EACzB,MAAM,QAAQ,OAAO;EACrB,MAAM,SAAS,MAAM;EAIrB,MAAM,gBAAgB,MAAM,YAAY,MAAM;EAK9C,MAAM,cACJ,MAAM,QAAQ,cAAc,MAAM,QAAQ,UACtC,eAAe,MAAM,QAAQ,OAAO,IACpC;EAEN,OACE,qBAAC,QAAD;GAAQ,GAAI;aAAZ,CACE,oBAAC,aAAD;IAA4B;IAAqB;GAAO,IACxD,oBAAC,wBAAD;IAAgC;IAAQ,OAAO,QAAQ;IAAS;IAC7D;GACqB,EAClB;;CAEZ;CAEA,OAAO,gCAAG,SAAW;AACvB,CACF;AACA,uBAAuB,cAAc;AAErC,MAAM,wBACJ,OACA,SAAuB,CAAC,MACP;CACjB,MAAM,kBAAkB,MAAM,QAAQ,iBAAiB;CAEvD,OAAO,KAAK,eAAe;CAE3B,IAAI,CAAC,gBAAgB,QACnB,OAAO,qBAAqB,iBAAiB,MAAM;CAGrD,OAAO,OAAO,QAAQ;AACxB"}
|