@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/esm/components/CriticalCss.d.ts +13 -0
  4. package/dist/esm/components/CriticalCss.js +22 -0
  5. package/dist/esm/components/CriticalCss.js.map +1 -0
  6. package/dist/esm/components/Link.d.ts +30 -0
  7. package/dist/esm/components/Link.js +47 -0
  8. package/dist/esm/components/Link.js.map +1 -0
  9. package/dist/esm/components/Matches.d.ts +7 -0
  10. package/dist/esm/components/Matches.js +22 -0
  11. package/dist/esm/components/Matches.js.map +1 -0
  12. package/dist/esm/components/NotFound.d.ts +5 -0
  13. package/dist/esm/components/NotFound.js +22 -0
  14. package/dist/esm/components/NotFound.js.map +1 -0
  15. package/dist/esm/components/NotFoundDefaultContent.d.ts +7 -0
  16. package/dist/esm/components/NotFoundDefaultContent.js +29 -0
  17. package/dist/esm/components/NotFoundDefaultContent.js.map +1 -0
  18. package/dist/esm/components/OssidoErrorBoundary.d.ts +39 -0
  19. package/dist/esm/components/OssidoErrorBoundary.js +48 -0
  20. package/dist/esm/components/OssidoErrorBoundary.js.map +1 -0
  21. package/dist/esm/components/Redirect.d.ts +11 -0
  22. package/dist/esm/components/Redirect.js +21 -0
  23. package/dist/esm/components/Redirect.js.map +1 -0
  24. package/dist/esm/components/RouteDataLoader.d.ts +21 -0
  25. package/dist/esm/components/RouteDataLoader.js +27 -0
  26. package/dist/esm/components/RouteDataLoader.js.map +1 -0
  27. package/dist/esm/components/RouteMatch.d.ts +15 -0
  28. package/dist/esm/components/RouteMatch.js +81 -0
  29. package/dist/esm/components/RouteMatch.js.map +1 -0
  30. package/dist/esm/components/RouterContext.d.ts +56 -0
  31. package/dist/esm/components/RouterContext.js +32 -0
  32. package/dist/esm/components/RouterContext.js.map +1 -0
  33. package/dist/esm/components/RouterContextProvider.d.ts +10 -0
  34. package/dist/esm/components/RouterContextProvider.js +122 -0
  35. package/dist/esm/components/RouterContextProvider.js.map +1 -0
  36. package/dist/esm/components/RouterProvider.d.ts +15 -0
  37. package/dist/esm/components/RouterProvider.js +28 -0
  38. package/dist/esm/components/RouterProvider.js.map +1 -0
  39. package/dist/esm/data/resourceCache.d.ts +75 -0
  40. package/dist/esm/data/resourceCache.js +148 -0
  41. package/dist/esm/data/resourceCache.js.map +1 -0
  42. package/dist/esm/hooks/useRoute.d.ts +14 -0
  43. package/dist/esm/hooks/useRoute.js +22 -0
  44. package/dist/esm/hooks/useRoute.js.map +1 -0
  45. package/dist/esm/hooks/useRouter.d.ts +47 -0
  46. package/dist/esm/hooks/useRouter.js +49 -0
  47. package/dist/esm/hooks/useRouter.js.map +1 -0
  48. package/dist/esm/hot.d.ts +9 -0
  49. package/dist/esm/hot.js +32 -0
  50. package/dist/esm/hot.js.map +1 -0
  51. package/dist/esm/index.d.ts +9 -0
  52. package/dist/esm/index.js +9 -0
  53. package/dist/esm/route.d.ts +70 -0
  54. package/dist/esm/route.js +50 -0
  55. package/dist/esm/route.js.map +1 -0
  56. package/dist/esm/router.d.ts +27 -0
  57. package/dist/esm/router.js +52 -0
  58. package/dist/esm/router.js.map +1 -0
  59. package/dist/esm/types.d.ts +63 -0
  60. package/dist/esm/utils/from-url-to-parsed-location.d.ts +2 -0
  61. package/dist/esm/utils/from-url-to-parsed-location.js +15 -0
  62. package/dist/esm/utils/from-url-to-parsed-location.js.map +1 -0
  63. package/dist/esm/utils/match-route.d.ts +16 -0
  64. package/dist/esm/utils/match-route.js +72 -0
  65. package/dist/esm/utils/match-route.js.map +1 -0
  66. package/dist/esm/utils/preload-route-chain.d.ts +16 -0
  67. package/dist/esm/utils/preload-route-chain.js +31 -0
  68. package/dist/esm/utils/preload-route-chain.js.map +1 -0
  69. package/dist/esm/utils/view-transition.d.ts +18 -0
  70. package/dist/esm/utils/view-transition.js +35 -0
  71. package/dist/esm/utils/view-transition.js.map +1 -0
  72. package/dist/esm/utils.d.ts +6 -0
  73. package/dist/esm/utils.js +20 -0
  74. package/dist/esm/utils.js.map +1 -0
  75. package/package.json +64 -0
@@ -0,0 +1,56 @@
1
+ import type { Router } from '../router';
2
+ import type { ServerInitialLocation } from '../types';
3
+ /** How to reflect a committed navigation in the browser (history + scroll). */
4
+ export interface NavigationCommitOptions {
5
+ history?: {
6
+ type: 'pushState' | 'replaceState';
7
+ path: string;
8
+ };
9
+ scroll?: boolean;
10
+ /**
11
+ * Override the app's `viewTransitions` config for this navigation: `true`
12
+ * forces a view transition, `false` skips it. Defaults to the config value.
13
+ */
14
+ viewTransition?: boolean;
15
+ }
16
+ export interface ParsedLocation {
17
+ href: string;
18
+ pathname: string;
19
+ search: Record<string, string>;
20
+ searchStr: string;
21
+ hash: string;
22
+ }
23
+ export interface RouterContextValue {
24
+ router: Router;
25
+ location: ParsedLocation;
26
+ /**
27
+ * Incremented on every navigation (and on error retry / a manual
28
+ * `refetchProps`). Combined with the
29
+ * pathname it forms the data-resource key, so each navigation gets a fresh
30
+ * resource (a refetch).
31
+ *
32
+ * Initialized to `0` identically on server and client and never changed
33
+ * during the initial mount — otherwise the boundary would remount and flash
34
+ * the loading fallback over server-rendered content (hydration mismatch).
35
+ */
36
+ navigationId: number;
37
+ /**
38
+ * Start a navigation to `loc`. Unless the destination has a `loading.tsx`,
39
+ * its data is prefetched first and the navigation is only committed once the
40
+ * data is ready — so the current page stays until then (no blank flash), even
41
+ * across a layout change. `options` carries the browser history/scroll update
42
+ * to apply at commit time.
43
+ */
44
+ updateLocation: (loc: ParsedLocation, options?: NavigationCommitOptions) => void;
45
+ /**
46
+ * Re-run the current route's data load (a refetch). Used by the error
47
+ * boundary `reset` and by `useRouter().refetchProps`.
48
+ */
49
+ retry: () => void;
50
+ }
51
+ export declare const RouterContext: import("react").Context<RouterContextValue>;
52
+ export declare function getInitialLocation(serverPayloadLocation: ServerInitialLocation): ParsedLocation;
53
+ /**
54
+ * @warning THIS SHOULD NOT BE EXPOSED TO USERLAND
55
+ */
56
+ export declare function useRouterContext(): RouterContextValue;
@@ -0,0 +1,32 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ //#region src/components/RouterContext.tsx
4
+ const isServerSide = typeof window === "undefined";
5
+ const RouterContext = createContext({});
6
+ function getInitialLocation(serverPayloadLocation) {
7
+ if (isServerSide) return {
8
+ pathname: serverPayloadLocation.pathname || "",
9
+ hash: "",
10
+ href: serverPayloadLocation.href || "",
11
+ searchStr: serverPayloadLocation.searchStr || "",
12
+ search: Object.fromEntries(new URLSearchParams(serverPayloadLocation.searchStr))
13
+ };
14
+ const { pathname, hash, href, search } = window.location;
15
+ return {
16
+ pathname,
17
+ hash,
18
+ href,
19
+ searchStr: search,
20
+ search: Object.fromEntries(new URLSearchParams(search))
21
+ };
22
+ }
23
+ /**
24
+ * @warning THIS SHOULD NOT BE EXPOSED TO USERLAND
25
+ */
26
+ function useRouterContext() {
27
+ return useContext(RouterContext);
28
+ }
29
+
30
+ //#endregion
31
+ export { RouterContext, getInitialLocation, useRouterContext };
32
+ //# sourceMappingURL=RouterContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterContext.js","names":[],"sources":["../../../src/components/RouterContext.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport type { Router } from '../router';\nimport type { ServerInitialLocation } from '../types';\n\n/** How to reflect a committed navigation in the browser (history + scroll). */\nexport interface NavigationCommitOptions {\n history?: { type: 'pushState' | 'replaceState'; path: string };\n scroll?: boolean;\n /**\n * Override the app's `viewTransitions` config for this navigation: `true`\n * forces a view transition, `false` skips it. Defaults to the config value.\n */\n viewTransition?: boolean;\n}\n\nconst isServerSide = typeof window === 'undefined';\n\nexport interface ParsedLocation {\n href: string;\n pathname: string;\n search: Record<string, string>;\n searchStr: string;\n hash: string;\n}\n\nexport interface RouterContextValue {\n router: Router;\n location: ParsedLocation;\n /**\n * Incremented on every navigation (and on error retry / a manual\n * `refetchProps`). Combined with the\n * pathname it forms the data-resource key, so each navigation gets a fresh\n * resource (a refetch).\n *\n * Initialized to `0` identically on server and client and never changed\n * during the initial mount — otherwise the boundary would remount and flash\n * the loading fallback over server-rendered content (hydration mismatch).\n */\n navigationId: number;\n /**\n * Start a navigation to `loc`. Unless the destination has a `loading.tsx`,\n * its data is prefetched first and the navigation is only committed once the\n * data is ready — so the current page stays until then (no blank flash), even\n * across a layout change. `options` carries the browser history/scroll update\n * to apply at commit time.\n */\n updateLocation: (\n loc: ParsedLocation,\n options?: NavigationCommitOptions,\n ) => void;\n /**\n * Re-run the current route's data load (a refetch). Used by the error\n * boundary `reset` and by `useRouter().refetchProps`.\n */\n retry: () => void;\n}\n\nexport const RouterContext = createContext({} as RouterContextValue);\n\nexport function getInitialLocation(\n serverPayloadLocation: ServerInitialLocation,\n): ParsedLocation {\n if (isServerSide) {\n return {\n pathname: serverPayloadLocation.pathname || '',\n hash: '',\n href: serverPayloadLocation.href || '',\n searchStr: serverPayloadLocation.searchStr || '',\n search: Object.fromEntries(\n new URLSearchParams(serverPayloadLocation.searchStr),\n ),\n };\n }\n\n const { pathname, hash, href, search } = window.location;\n return {\n pathname,\n hash,\n href,\n searchStr: search,\n search: Object.fromEntries(new URLSearchParams(search)),\n };\n}\n\n/**\n * @warning THIS SHOULD NOT BE EXPOSED TO USERLAND\n */\nexport function useRouterContext(): RouterContextValue {\n return useContext(RouterContext);\n}\n"],"mappings":";;;AAgBA,MAAM,eAAe,OAAO,WAAW;AA0CvC,MAAa,gBAAgB,cAAc,CAAC,CAAuB;AAEnE,SAAgB,mBACd,uBACgB;CAChB,IAAI,cACF,OAAO;EACL,UAAU,sBAAsB,YAAY;EAC5C,MAAM;EACN,MAAM,sBAAsB,QAAQ;EACpC,WAAW,sBAAsB,aAAa;EAC9C,QAAQ,OAAO,YACb,IAAI,gBAAgB,sBAAsB,SAAS,CACrD;CACF;CAGF,MAAM,EAAE,UAAU,MAAM,MAAM,WAAW,OAAO;CAChD,OAAO;EACL;EACA;EACA;EACA,WAAW;EACX,QAAQ,OAAO,YAAY,IAAI,gBAAgB,MAAM,CAAC;CACxD;AACF;;;;AAKA,SAAgB,mBAAuC;CACrD,OAAO,WAAW,aAAa;AACjC"}
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Router } from '../router';
3
+ import type { ServerInitialLocation } from '../types';
4
+ interface RouterContextProviderProps {
5
+ router: Router;
6
+ serverInitialLocation: ServerInitialLocation;
7
+ children: ReactNode;
8
+ }
9
+ export declare function RouterContextProvider({ router, serverInitialLocation, children, }: RouterContextProviderProps): ReactNode;
10
+ export {};
@@ -0,0 +1,122 @@
1
+ import { buildResourceKey, getOrCreateResource } from "../data/resourceCache.js";
2
+ import { RouterContext, getInitialLocation } from "./RouterContext.js";
3
+ import { fromUrlToParsedLocation } from "../utils/from-url-to-parsed-location.js";
4
+ import { matchRoute } from "../utils/match-route.js";
5
+ import { VIEW_TRANSITIONS_ENABLED, runCommit } from "../utils/view-transition.js";
6
+ import { useCallback, useEffect, useMemo, useState } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+
9
+ //#region src/components/RouterContextProvider.tsx
10
+ const isServerSide = typeof window === "undefined";
11
+ /**
12
+ * For a route with a `loading.tsx`, wait up to this long for the destination to
13
+ * become ready before committing (and thereby showing the loading fallback). A
14
+ * navigation that resolves within this window commits with its data already
15
+ * resolved — so `use()` reads it synchronously and no fallback flashes. Only
16
+ * genuinely slow navigations fall through and show the loading screen.
17
+ */
18
+ const FALLBACK_DELAY_MS = 100;
19
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
+ const CRITICAL_CSS_PATH = "/vite-server/ossido_internal__critical_css";
21
+ const CRITICAL_CSS_LINK_SELECTOR = `link[href*="${CRITICAL_CSS_PATH.split("/").pop()}"]`;
22
+ /**
23
+ * Warm a route's critical CSS before navigating to it, so the
24
+ * `<link rel="stylesheet" precedence>` that renders on arrival doesn't suspend
25
+ * (and flash the loading fallback) while the stylesheet downloads. Resolves once
26
+ * the resource is cached; never rejects.
27
+ */
28
+ function preloadCriticalCss(componentId) {
29
+ const href = `${CRITICAL_CSS_PATH}?componentId=${componentId}`;
30
+ if (document.querySelector(`link[rel="preload"][href="${href}"]`)) return Promise.resolve();
31
+ return new Promise((resolve) => {
32
+ const link = document.createElement("link");
33
+ link.rel = "preload";
34
+ link.as = "style";
35
+ link.href = href;
36
+ link.onload = () => resolve();
37
+ link.onerror = () => resolve();
38
+ document.head.appendChild(link);
39
+ });
40
+ }
41
+ function RouterContextProvider({ router, serverInitialLocation, children }) {
42
+ const [location, setLocation] = useState(() => getInitialLocation(serverInitialLocation));
43
+ const [navigationId, setNavigationId] = useState(0);
44
+ const updateLocation = useCallback((newLocation, options = {}) => {
45
+ const useViewTransition = !(newLocation.pathname === location.pathname && newLocation.searchStr === location.searchStr) && (options.viewTransition ?? VIEW_TRANSITIONS_ENABLED);
46
+ const commit = () => {
47
+ runCommit(() => {
48
+ setNavigationId((id) => id + 1);
49
+ setLocation(newLocation);
50
+ if (options.history) {
51
+ const { type, path } = options.history;
52
+ window.history[type](path, "", path);
53
+ }
54
+ if (options.scroll) window.scroll(0, 0);
55
+ }, useViewTransition);
56
+ };
57
+ const targetRoute = matchRoute(router.routesById, newLocation.pathname);
58
+ if (!targetRoute || isServerSide) {
59
+ commit();
60
+ return;
61
+ }
62
+ const pending = [];
63
+ if (targetRoute.options.hasHandler) pending.push(getOrCreateResource(buildResourceKey(navigationId + 1, newLocation), targetRoute, newLocation));
64
+ const criticalCssEnabled = !!document.querySelector(CRITICAL_CSS_LINK_SELECTOR);
65
+ for (let node = targetRoute; node; node = node.isRoot ? void 0 : node.options.getParentRoute?.()) {
66
+ const preloadComponent = node.component.preload;
67
+ if (preloadComponent) pending.push(preloadComponent());
68
+ const componentId = node.filePath || node.id;
69
+ if (criticalCssEnabled && componentId) pending.push(preloadCriticalCss(componentId));
70
+ }
71
+ if (pending.length === 0) {
72
+ commit();
73
+ return;
74
+ }
75
+ const ready = Promise.all(pending);
76
+ if (targetRoute.options.loadingComponent) Promise.race([ready, wait(FALLBACK_DELAY_MS)]).then(commit, commit);
77
+ else ready.then(commit, commit);
78
+ }, [
79
+ router,
80
+ navigationId,
81
+ location
82
+ ]);
83
+ const retry = useCallback(() => {
84
+ setNavigationId((id) => id + 1);
85
+ }, []);
86
+ /**
87
+ * Listen browser navigation events. The browser has already updated the URL,
88
+ * so this only mirrors it into router state (and bumps the navigation id so
89
+ * the route refetches — preserving back/forward data loads).
90
+ */
91
+ useEffect(() => {
92
+ const updateLocationOnPopStateChange = ({ target }) => {
93
+ const { location: targetLocation } = target;
94
+ updateLocation(fromUrlToParsedLocation(targetLocation.href));
95
+ };
96
+ window.addEventListener("popstate", updateLocationOnPopStateChange);
97
+ return () => {
98
+ window.removeEventListener("popstate", updateLocationOnPopStateChange);
99
+ };
100
+ }, [updateLocation]);
101
+ const contextValue = useMemo(() => ({
102
+ router,
103
+ location,
104
+ navigationId,
105
+ updateLocation,
106
+ retry
107
+ }), [
108
+ location,
109
+ router,
110
+ navigationId,
111
+ updateLocation,
112
+ retry
113
+ ]);
114
+ return /* @__PURE__ */ jsx(RouterContext.Provider, {
115
+ value: contextValue,
116
+ children
117
+ });
118
+ }
119
+
120
+ //#endregion
121
+ export { RouterContextProvider };
122
+ //# sourceMappingURL=RouterContextProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterContextProvider.js","names":[],"sources":["../../../src/components/RouterContextProvider.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useMemo } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { Router } from '../router';\nimport type { Route } from '../route';\nimport type { ServerInitialLocation } from '../types';\nimport { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location';\nimport { matchRoute } from '../utils/match-route';\nimport { runCommit, VIEW_TRANSITIONS_ENABLED } from '../utils/view-transition';\nimport { buildResourceKey, getOrCreateResource } from '../data/resourceCache';\n\nimport {\n RouterContext,\n getInitialLocation,\n type ParsedLocation,\n type NavigationCommitOptions,\n type RouterContextValue,\n} from './RouterContext';\n\nconst isServerSide = typeof window === 'undefined';\n\n/**\n * For a route with a `loading.tsx`, wait up to this long for the destination to\n * become ready before committing (and thereby showing the loading fallback). A\n * navigation that resolves within this window commits with its data already\n * resolved — so `use()` reads it synchronously and no fallback flashes. Only\n * genuinely slow navigations fall through and show the loading screen.\n */\nconst FALLBACK_DELAY_MS = 100;\n\nconst wait = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n// Kept in sync with `CriticalCss`. Its `<link rel=\"stylesheet\" precedence>` is\n// dev-only, so the presence of one also tells us we're in dev.\nconst CRITICAL_CSS_PATH = '/vite-server/ossido_internal__critical_css';\nconst CRITICAL_CSS_LINK_SELECTOR = `link[href*=\"${CRITICAL_CSS_PATH.split('/').pop()}\"]`;\n\n/**\n * Warm a route's critical CSS before navigating to it, so the\n * `<link rel=\"stylesheet\" precedence>` that renders on arrival doesn't suspend\n * (and flash the loading fallback) while the stylesheet downloads. Resolves once\n * the resource is cached; never rejects.\n */\nfunction preloadCriticalCss(componentId: string): Promise<void> {\n const href = `${CRITICAL_CSS_PATH}?componentId=${componentId}`;\n // Already warmed (or warming) for this route — don't stack up <link>s.\n if (document.querySelector(`link[rel=\"preload\"][href=\"${href}\"]`)) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const link = document.createElement('link');\n link.rel = 'preload';\n link.as = 'style';\n link.href = href;\n link.onload = (): void => resolve();\n link.onerror = (): void => resolve();\n document.head.appendChild(link);\n });\n}\n\ninterface RouterContextProviderProps {\n router: Router;\n serverInitialLocation: ServerInitialLocation;\n children: ReactNode;\n}\n\nexport function RouterContextProvider({\n router,\n serverInitialLocation,\n children,\n}: RouterContextProviderProps): ReactNode {\n const [location, setLocation] = useState<ParsedLocation>(() =>\n getInitialLocation(serverInitialLocation),\n );\n const [navigationId, setNavigationId] = useState<number>(0);\n\n const updateLocation = useCallback(\n (\n newLocation: ParsedLocation,\n options: NavigationCommitOptions = {},\n ): void => {\n // Apply the navigation's state change. Wrapped by `runCommit` so — when\n // view transitions are enabled (globally or per-navigation) and supported\n // — it animates via `document.startViewTransition`. Every commit path\n // (immediate, loading-race, ready.then) goes through this `commit`.\n //\n // A same-page anchor navigation — the pathname and search are unchanged, so\n // only the URL hash (e.g. `#tuono`) differs — is a scroll to an id, not a\n // page change: never run a view transition for it.\n const isSamePageAnchor =\n newLocation.pathname === location.pathname &&\n newLocation.searchStr === location.searchStr;\n const useViewTransition =\n !isSamePageAnchor &&\n (options.viewTransition ?? VIEW_TRANSITIONS_ENABLED);\n const commit = (): void => {\n runCommit(() => {\n setNavigationId((id) => id + 1);\n setLocation(newLocation);\n if (options.history) {\n const { type, path } = options.history;\n window.history[type](path, '', path);\n }\n if (options.scroll) {\n window.scroll(0, 0);\n }\n }, useViewTransition);\n };\n\n const targetRoute = matchRoute(router.routesById, newLocation.pathname);\n\n // On the server, or for an unknown route (which renders the not-found\n // fallback), commit immediately.\n if (!targetRoute || isServerSide) {\n commit();\n return;\n }\n\n const pending: Array<PromiseLike<unknown>> = [];\n\n // 1. The route's server data (the key the destination render will read —\n // navigationId is bumped by one on commit — so it reads it synchronously).\n if (targetRoute.options.hasHandler) {\n pending.push(\n getOrCreateResource(\n buildResourceKey(navigationId + 1, newLocation),\n targetRoute,\n newLocation,\n ),\n );\n }\n\n // In dev the route's critical CSS is a `<link rel=\"stylesheet\" precedence>`\n // (see `CriticalCss`), which React suspends on until it loads. Detect dev by\n // the presence of one for the current page.\n const criticalCssEnabled = !!document.querySelector(\n CRITICAL_CSS_LINK_SELECTOR,\n );\n\n // Everything else the destination needs to render without suspending:\n // - the code chunk of the matched route and its lazy ancestor layouts\n // (otherwise it suspends on the lazy `import()` after commit), and\n // - the critical CSS of each (otherwise React suspends on the stylesheet).\n for (\n let node: Route | undefined = targetRoute;\n node;\n node = node.isRoot ? undefined : node.options.getParentRoute?.()\n ) {\n const preloadComponent = node.component.preload;\n if (preloadComponent) pending.push(preloadComponent());\n\n const componentId = node.filePath || node.id;\n if (criticalCssEnabled && componentId) {\n pending.push(preloadCriticalCss(componentId));\n }\n }\n\n if (pending.length === 0) {\n commit();\n return;\n }\n\n // A rejected resource still navigates so the error boundary can surface it.\n const ready = Promise.all(pending);\n\n if (targetRoute.options.loadingComponent) {\n // The route has a loading fallback: commit as soon as the destination is\n // ready, or after a short grace period — whichever comes first. Fast\n // navigations commit with data resolved (no fallback flash); slow ones\n // commit early and the loading component shows while data finishes.\n Promise.race([ready, wait(FALLBACK_DELAY_MS)]).then(commit, commit);\n } else {\n // No loading fallback: wait for everything so the destination appears\n // fully (the current page stays visible until then, never a blank).\n ready.then(commit, commit);\n }\n },\n [router, navigationId, location],\n );\n\n const retry = useCallback((): void => {\n setNavigationId((id) => id + 1);\n }, []);\n\n /**\n * Listen browser navigation events. The browser has already updated the URL,\n * so this only mirrors it into router state (and bumps the navigation id so\n * the route refetches — preserving back/forward data loads).\n */\n useEffect(() => {\n const updateLocationOnPopStateChange = ({\n target,\n }: PopStateEvent): void => {\n const { location: targetLocation } = target as typeof window;\n updateLocation(fromUrlToParsedLocation(targetLocation.href));\n };\n\n window.addEventListener('popstate', updateLocationOnPopStateChange);\n\n return (): void => {\n window.removeEventListener('popstate', updateLocationOnPopStateChange);\n };\n }, [updateLocation]);\n\n const contextValue: RouterContextValue = useMemo(\n () => ({\n router,\n location,\n navigationId,\n updateLocation,\n retry,\n }),\n [location, router, navigationId, updateLocation, retry],\n );\n\n return (\n <RouterContext.Provider value={contextValue}>\n {children}\n </RouterContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;;AAmBA,MAAM,eAAe,OAAO,WAAW;;;;;;;;AASvC,MAAM,oBAAoB;AAE1B,MAAM,QAAQ,OACZ,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AAIlD,MAAM,oBAAoB;AAC1B,MAAM,6BAA6B,eAAe,kBAAkB,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE;;;;;;;AAQrF,SAAS,mBAAmB,aAAoC;CAC9D,MAAM,OAAO,GAAG,kBAAkB,eAAe;CAEjD,IAAI,SAAS,cAAc,6BAA6B,KAAK,GAAG,GAC9D,OAAO,QAAQ,QAAQ;CAEzB,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,OAAO,SAAS,cAAc,MAAM;EAC1C,KAAK,MAAM;EACX,KAAK,KAAK;EACV,KAAK,OAAO;EACZ,KAAK,eAAqB,QAAQ;EAClC,KAAK,gBAAsB,QAAQ;EACnC,SAAS,KAAK,YAAY,IAAI;CAChC,CAAC;AACH;AAQA,SAAgB,sBAAsB,EACpC,QACA,uBACA,YACwC;CACxC,MAAM,CAAC,UAAU,eAAe,eAC9B,mBAAmB,qBAAqB,CAC1C;CACA,MAAM,CAAC,cAAc,mBAAmB,SAAiB,CAAC;CAE1D,MAAM,iBAAiB,aAEnB,aACA,UAAmC,CAAC,MAC3B;EAYT,MAAM,oBACJ,EAHA,YAAY,aAAa,SAAS,YAClC,YAAY,cAAc,SAAS,eAGlC,QAAQ,kBAAkB;EAC7B,MAAM,eAAqB;GACzB,gBAAgB;IACd,iBAAiB,OAAO,KAAK,CAAC;IAC9B,YAAY,WAAW;IACvB,IAAI,QAAQ,SAAS;KACnB,MAAM,EAAE,MAAM,SAAS,QAAQ;KAC/B,OAAO,QAAQ,KAAK,CAAC,MAAM,IAAI,IAAI;IACrC;IACA,IAAI,QAAQ,QACV,OAAO,OAAO,GAAG,CAAC;GAEtB,GAAG,iBAAiB;EACtB;EAEA,MAAM,cAAc,WAAW,OAAO,YAAY,YAAY,QAAQ;EAItE,IAAI,CAAC,eAAe,cAAc;GAChC,OAAO;GACP;EACF;EAEA,MAAM,UAAuC,CAAC;EAI9C,IAAI,YAAY,QAAQ,YACtB,QAAQ,KACN,oBACE,iBAAiB,eAAe,GAAG,WAAW,GAC9C,aACA,WACF,CACF;EAMF,MAAM,qBAAqB,CAAC,CAAC,SAAS,cACpC,0BACF;EAMA,KACE,IAAI,OAA0B,aAC9B,MACA,OAAO,KAAK,SAAS,SAAY,KAAK,QAAQ,iBAAiB,GAC/D;GACA,MAAM,mBAAmB,KAAK,UAAU;GACxC,IAAI,kBAAkB,QAAQ,KAAK,iBAAiB,CAAC;GAErD,MAAM,cAAc,KAAK,YAAY,KAAK;GAC1C,IAAI,sBAAsB,aACxB,QAAQ,KAAK,mBAAmB,WAAW,CAAC;EAEhD;EAEA,IAAI,QAAQ,WAAW,GAAG;GACxB,OAAO;GACP;EACF;EAGA,MAAM,QAAQ,QAAQ,IAAI,OAAO;EAEjC,IAAI,YAAY,QAAQ,kBAKtB,QAAQ,KAAK,CAAC,OAAO,KAAK,iBAAiB,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,MAAM;OAIlE,MAAM,KAAK,QAAQ,MAAM;CAE7B,GACA;EAAC;EAAQ;EAAc;CAAQ,CACjC;CAEA,MAAM,QAAQ,kBAAwB;EACpC,iBAAiB,OAAO,KAAK,CAAC;CAChC,GAAG,CAAC,CAAC;;;;;;CAOL,gBAAgB;EACd,MAAM,kCAAkC,EACtC,aACyB;GACzB,MAAM,EAAE,UAAU,mBAAmB;GACrC,eAAe,wBAAwB,eAAe,IAAI,CAAC;EAC7D;EAEA,OAAO,iBAAiB,YAAY,8BAA8B;EAElE,aAAmB;GACjB,OAAO,oBAAoB,YAAY,8BAA8B;EACvE;CACF,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,eAAmC,eAChC;EACL;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAU;EAAQ;EAAc;EAAgB;CAAK,CACxD;CAEA,OACE,oBAAC,cAAc,UAAf;EAAwB,OAAO;EAC5B;CACqB;AAE5B"}
@@ -0,0 +1,15 @@
1
+ import type { JSX } from 'react';
2
+ import type { ServerInitialLocation, Mode, ServerErrorPayload } from '../types';
3
+ import type { Router } from '../router';
4
+ interface RouterProviderProps {
5
+ router: Router;
6
+ serverInitialLocation: ServerInitialLocation;
7
+ serverInitialData: unknown;
8
+ /** Wrapping layouts' server data, keyed by each layout's `dataKey`. */
9
+ serverInitialLayoutData?: Record<string, unknown>;
10
+ /** Set when the initial route's Rust handler panicked (dev mode). */
11
+ serverInitialError?: ServerErrorPayload;
12
+ mode?: Mode;
13
+ }
14
+ export declare function RouterProvider({ router, serverInitialLocation, serverInitialData, serverInitialLayoutData, serverInitialError, mode, }: RouterProviderProps): JSX.Element;
15
+ export {};
@@ -0,0 +1,28 @@
1
+ import { buildResourceKey, seedErrorResource, seedLayoutData, seedResource, toDataResult } from "../data/resourceCache.js";
2
+ import { getInitialLocation } from "./RouterContext.js";
3
+ import { RouterContextProvider } from "./RouterContextProvider.js";
4
+ import { Matches } from "./Matches.js";
5
+ import { useState } from "react";
6
+ import { DevErrorOverlayHost } from "@ossido-labs/ossido-ui";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+
9
+ //#region src/components/RouterProvider.tsx
10
+ function RouterProvider({ router, serverInitialLocation, serverInitialData, serverInitialLayoutData, serverInitialError, mode }) {
11
+ useState(() => {
12
+ const initialLocation = getInitialLocation(serverInitialLocation);
13
+ const resourceKey = buildResourceKey(0, initialLocation);
14
+ if (serverInitialError) seedErrorResource(resourceKey, serverInitialError);
15
+ else seedResource(resourceKey, toDataResult(serverInitialData));
16
+ if (serverInitialLayoutData) seedLayoutData(serverInitialLayoutData);
17
+ return null;
18
+ });
19
+ return /* @__PURE__ */ jsxs(RouterContextProvider, {
20
+ router,
21
+ serverInitialLocation,
22
+ children: [/* @__PURE__ */ jsx(Matches, { mode }), import.meta.env.DEV && mode === "Dev" && /* @__PURE__ */ jsx(DevErrorOverlayHost, {})]
23
+ });
24
+ }
25
+
26
+ //#endregion
27
+ export { RouterProvider };
28
+ //# sourceMappingURL=RouterProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterProvider.js","names":[],"sources":["../../../src/components/RouterProvider.tsx"],"sourcesContent":["import { useState } from 'react';\nimport type { JSX } from 'react';\nimport { DevErrorOverlayHost } from '@ossido-labs/ossido-ui';\n\nimport type { ServerInitialLocation, Mode, ServerErrorPayload } from '../types';\nimport type { Router } from '../router';\nimport {\n buildResourceKey,\n seedResource,\n seedErrorResource,\n seedLayoutData,\n toDataResult,\n} from '../data/resourceCache';\n\nimport { getInitialLocation } from './RouterContext';\nimport { RouterContextProvider } from './RouterContextProvider';\nimport { Matches } from './Matches';\n\ninterface RouterProviderProps {\n router: Router;\n serverInitialLocation: ServerInitialLocation;\n serverInitialData: unknown;\n /** Wrapping layouts' server data, keyed by each layout's `dataKey`. */\n serverInitialLayoutData?: Record<string, unknown>;\n /** Set when the initial route's Rust handler panicked (dev mode). */\n serverInitialError?: ServerErrorPayload;\n mode?: Mode;\n}\n\nexport function RouterProvider({\n router,\n serverInitialLocation,\n serverInitialData,\n serverInitialLayoutData,\n serverInitialError,\n mode,\n}: RouterProviderProps): JSX.Element {\n // Seed the initial route's data resource synchronously, during render, once,\n // on server AND client (a `useState` lazy initializer — not an effect, which\n // would run too late and make the first render suspend → hydration mismatch).\n // navigationId starts at 0, matching the key RouteMatch builds on first render.\n useState(() => {\n const initialLocation = getInitialLocation(serverInitialLocation);\n const resourceKey = buildResourceKey(0, initialLocation);\n // A handler panic seeds a rejected resource so the boundary/overlay render;\n // otherwise seed the fulfilled server data.\n if (serverInitialError) {\n seedErrorResource(resourceKey, serverInitialError);\n } else {\n seedResource(resourceKey, toDataResult(serverInitialData));\n }\n // Seed the wrapping layouts' data so they render synchronously (SSR + first\n // client render) without a fetch.\n if (serverInitialLayoutData) {\n seedLayoutData(serverInitialLayoutData);\n }\n return null;\n });\n\n return (\n <RouterContextProvider\n router={router}\n serverInitialLocation={serverInitialLocation}\n >\n <Matches mode={mode} />\n {/* Dev-only: the floating overlay host that surfaces every kind of dev\n error (render/SSR panics, uncaught errors, rejections, Vite build\n errors). Renders null until something is reported.\n\n `import.meta.env.DEV` is a build-time constant (true only in the dev\n bundle), so the prod build eliminates this branch — the overlay host is\n never rendered in prod. The heavy Shiki syntax-highlighter it uses is\n reached lazily (via the dev error reporter), so the prod build\n code-splits it into chunks that a real prod visitor never fetches (the\n runtime `mode` check gates them). The `mode` check is also what keeps\n the overlay working in the dev bundle. */}\n {import.meta.env.DEV && mode === 'Dev' && <DevErrorOverlayHost />}\n </RouterContextProvider>\n );\n}\n"],"mappings":";;;;;;;;;AA6BA,SAAgB,eAAe,EAC7B,QACA,uBACA,mBACA,yBACA,oBACA,QACmC;CAKnC,eAAe;EACb,MAAM,kBAAkB,mBAAmB,qBAAqB;EAChE,MAAM,cAAc,iBAAiB,GAAG,eAAe;EAGvD,IAAI,oBACF,kBAAkB,aAAa,kBAAkB;OAEjD,aAAa,aAAa,aAAa,iBAAiB,CAAC;EAI3D,IAAI,yBACF,eAAe,uBAAuB;EAExC,OAAO;CACT,CAAC;CAED,OACE,qBAAC,uBAAD;EACU;EACe;YAFzB,CAIE,oBAAC,SAAD,EAAe,KAAO,IAYrB,YAAY,IAAI,OAAO,SAAS,SAAS,oBAAC,qBAAD,CAAsB,EAC3C;;AAE3B"}
@@ -0,0 +1,75 @@
1
+ import type { Route } from '../route';
2
+ import type { ServerErrorPayload } from '../types';
3
+ /**
4
+ * Fulfilled value of a data resource. A redirect is a normal (non-error)
5
+ * outcome so it must NOT reject — rejecting would trigger the error boundary.
6
+ */
7
+ export type RouteDataResult = {
8
+ kind: 'data';
9
+ props: Record<string, unknown>;
10
+ } | {
11
+ kind: 'redirect';
12
+ destination: string;
13
+ };
14
+ /**
15
+ * A promise annotated with `status`/`value`/`reason` so React's `use()` reads
16
+ * settled resources synchronously (no suspend). This is what lets the SSR and
17
+ * hydration first render resolve pre-seeded data without suspending.
18
+ */
19
+ export interface DataResource extends Promise<RouteDataResult> {
20
+ status: 'pending' | 'fulfilled' | 'rejected';
21
+ value?: RouteDataResult;
22
+ reason?: unknown;
23
+ }
24
+ interface LocationKeyParts {
25
+ pathname: string;
26
+ searchStr: string;
27
+ }
28
+ /**
29
+ * Rebuild a `ServerErrorPayload` (from the Rust server) into a real `Error` so
30
+ * the error boundary / dev overlay treats a backend panic exactly like a JS
31
+ * error, preserving the panic message and backtrace.
32
+ */
33
+ export declare function serverErrorToError(payload: ServerErrorPayload): Error;
34
+ /**
35
+ * Single source of truth for a resource key — used by both the seeder and the
36
+ * loader. Includes `searchStr` so search-param-only navigations create a
37
+ * distinct resource (and therefore refetch).
38
+ */
39
+ export declare function buildResourceKey(navigationId: number, { pathname, searchStr }: LocationKeyParts): string;
40
+ /** Normalize user server data into a `data` result for seeding. */
41
+ export declare function toDataResult(props: unknown): RouteDataResult;
42
+ /**
43
+ * Seed the wrapping layouts' server data (from a page's SSR payload or data
44
+ * fetch), keyed by each layout's `dataKey`.
45
+ */
46
+ export declare function seedLayoutData(layoutData: Record<string, unknown>): void;
47
+ /**
48
+ * Read a layout's seeded server data synchronously (no fetch, no suspend): a
49
+ * layout's data always arrives via the page it wraps. Returns empty props when
50
+ * nothing is seeded yet (e.g. a loading-fallback navigation's first frame).
51
+ */
52
+ export declare function readLayoutData(dataKey: string): Record<string, unknown>;
53
+ /**
54
+ * Insert a pre-fulfilled resource — used to seed the SSR initial data so the
55
+ * first render (server and client) reads it synchronously.
56
+ */
57
+ export declare function seedResource(key: string, value: RouteDataResult): DataResource;
58
+ /**
59
+ * Insert a pre-rejected resource carrying a server error — used to seed a
60
+ * handler panic (dev) so `use()` re-throws it into the error boundary on the
61
+ * first render (server and client), rendering the error overlay.
62
+ */
63
+ export declare function seedErrorResource(key: string, payload: ServerErrorPayload): DataResource;
64
+ /**
65
+ * Return the cached resource for `key`, creating one on demand. Idempotent
66
+ * (StrictMode-safe): at most one fetch is started per key.
67
+ *
68
+ * The server never fetches (the ossido_ssr V8 runtime has no `fetch`); every
69
+ * server-rendered route is pre-seeded, and any un-seeded server lookup resolves
70
+ * to empty props rather than suspending.
71
+ */
72
+ export declare function getOrCreateResource(key: string, route: Route, location: LocationKeyParts): DataResource;
73
+ /** Drop a resource so the next lookup refetches (used by error `reset`). */
74
+ export declare function invalidateResource(key: string): void;
75
+ export {};
@@ -0,0 +1,148 @@
1
+ //#region src/data/resourceCache.ts
2
+ const isServerSide = typeof window === "undefined";
3
+ const IS_STATIC_EXPORT = typeof __OSSIDO_STATIC__ !== "undefined" && __OSSIDO_STATIC__;
4
+ /**
5
+ * The data endpoint URL for a location. In a static export it targets the
6
+ * pre-rendered `.json` file matching what `ossido build --static` writes (root is
7
+ * `/__ossido/data.json` to avoid a `data/.json` dotfile); otherwise the live
8
+ * server's extensionless route, including search params.
9
+ */
10
+ function dataEndpointUrl({ pathname, searchStr }) {
11
+ if (IS_STATIC_EXPORT) return pathname === "/" ? "/__ossido/data.json" : `/__ossido/data${pathname}.json`;
12
+ return `/__ossido/data${pathname}${searchStr}`;
13
+ }
14
+ /**
15
+ * Rebuild a `ServerErrorPayload` (from the Rust server) into a real `Error` so
16
+ * the error boundary / dev overlay treats a backend panic exactly like a JS
17
+ * error, preserving the panic message and backtrace.
18
+ */
19
+ function serverErrorToError(payload) {
20
+ const error = new Error(payload.message);
21
+ error.name = payload.name;
22
+ if (payload.stack) error.stack = payload.stack;
23
+ if (payload.source) error.ossidoServerSource = payload.source;
24
+ return error;
25
+ }
26
+ const CACHE_LIMIT = 50;
27
+ const cache = /* @__PURE__ */ new Map();
28
+ /**
29
+ * Single source of truth for a resource key — used by both the seeder and the
30
+ * loader. Includes `searchStr` so search-param-only navigations create a
31
+ * distinct resource (and therefore refetch).
32
+ */
33
+ function buildResourceKey(navigationId, { pathname, searchStr }) {
34
+ return `${navigationId}::${pathname}${searchStr}`;
35
+ }
36
+ /** Normalize user server data into a `data` result for seeding. */
37
+ function toDataResult(props) {
38
+ return {
39
+ kind: "data",
40
+ props: props ?? {}
41
+ };
42
+ }
43
+ /**
44
+ * Resource key for a `layout.rs` handler's data. Keyed by the layout's `dataKey`
45
+ * alone (not the navigation) so its data persists across navigations under the
46
+ * same layout and is simply overwritten by each page's data fetch — matching how
47
+ * layouts persist in the tree.
48
+ */
49
+ function buildLayoutResourceKey(dataKey) {
50
+ return `layout::${dataKey}`;
51
+ }
52
+ /**
53
+ * Seed the wrapping layouts' server data (from a page's SSR payload or data
54
+ * fetch), keyed by each layout's `dataKey`.
55
+ */
56
+ function seedLayoutData(layoutData) {
57
+ for (const [dataKey, props] of Object.entries(layoutData)) seedResource(buildLayoutResourceKey(dataKey), toDataResult(props));
58
+ }
59
+ /**
60
+ * Read a layout's seeded server data synchronously (no fetch, no suspend): a
61
+ * layout's data always arrives via the page it wraps. Returns empty props when
62
+ * nothing is seeded yet (e.g. a loading-fallback navigation's first frame).
63
+ */
64
+ function readLayoutData(dataKey) {
65
+ const resource = cache.get(buildLayoutResourceKey(dataKey));
66
+ if (resource?.status === "fulfilled" && resource.value?.kind === "data") return resource.value.props;
67
+ return {};
68
+ }
69
+ function annotate(promise) {
70
+ const resource = promise;
71
+ resource.status = "pending";
72
+ resource.then((value) => {
73
+ resource.status = "fulfilled";
74
+ resource.value = value;
75
+ }, (reason) => {
76
+ resource.status = "rejected";
77
+ resource.reason = reason;
78
+ });
79
+ return resource;
80
+ }
81
+ async function fetchRouteData(location) {
82
+ const res = await fetch(dataEndpointUrl(location));
83
+ const body = await res.json().catch(() => null);
84
+ if (body?.info.serverError) throw serverErrorToError(body.info.serverError);
85
+ if (body?.info.redirect_destination) return {
86
+ kind: "redirect",
87
+ destination: body.info.redirect_destination
88
+ };
89
+ if (!res.ok || !body) throw new Error(`Failed to load server data for "${location.pathname}" (status ${res.status})`);
90
+ if (body.layoutData) seedLayoutData(body.layoutData);
91
+ return toDataResult(body.data);
92
+ }
93
+ /**
94
+ * Insert a pre-fulfilled resource — used to seed the SSR initial data so the
95
+ * first render (server and client) reads it synchronously.
96
+ */
97
+ function seedResource(key, value) {
98
+ const resource = Promise.resolve(value);
99
+ resource.status = "fulfilled";
100
+ resource.value = value;
101
+ cache.set(key, resource);
102
+ return resource;
103
+ }
104
+ /**
105
+ * Insert a pre-rejected resource carrying a server error — used to seed a
106
+ * handler panic (dev) so `use()` re-throws it into the error boundary on the
107
+ * first render (server and client), rendering the error overlay.
108
+ */
109
+ function seedErrorResource(key, payload) {
110
+ const reason = serverErrorToError(payload);
111
+ const resource = Promise.reject(reason);
112
+ resource.catch(() => void 0);
113
+ resource.status = "rejected";
114
+ resource.reason = reason;
115
+ cache.set(key, resource);
116
+ return resource;
117
+ }
118
+ /**
119
+ * Return the cached resource for `key`, creating one on demand. Idempotent
120
+ * (StrictMode-safe): at most one fetch is started per key.
121
+ *
122
+ * The server never fetches (the ossido_ssr V8 runtime has no `fetch`); every
123
+ * server-rendered route is pre-seeded, and any un-seeded server lookup resolves
124
+ * to empty props rather than suspending.
125
+ */
126
+ function getOrCreateResource(key, route, location) {
127
+ const existing = cache.get(key);
128
+ if (existing) return existing;
129
+ if (isServerSide || !route.options.hasHandler) return seedResource(key, {
130
+ kind: "data",
131
+ props: {}
132
+ });
133
+ const resource = annotate(fetchRouteData(location));
134
+ cache.set(key, resource);
135
+ evictStaleEntries(key);
136
+ return resource;
137
+ }
138
+ function evictStaleEntries(currentKey) {
139
+ if (cache.size <= CACHE_LIMIT) return;
140
+ for (const key of cache.keys()) {
141
+ if (cache.size <= CACHE_LIMIT) break;
142
+ if (key !== currentKey) cache.delete(key);
143
+ }
144
+ }
145
+
146
+ //#endregion
147
+ export { buildResourceKey, getOrCreateResource, readLayoutData, seedErrorResource, seedLayoutData, seedResource, serverErrorToError, toDataResult };
148
+ //# sourceMappingURL=resourceCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resourceCache.js","names":[],"sources":["../../../src/data/resourceCache.ts"],"sourcesContent":["import type { Route } from '../route';\nimport type { ServerErrorPayload, OssidoErrorWithSource } from '../types';\n\nconst isServerSide = typeof window === 'undefined';\n\n/**\n * Baked to `true` by `ossido build --static` (via the vite `define`); the\n * `typeof` guard keeps this safe under vitest/SSR where the define isn't\n * applied. In a static export there is no server to resolve the extensionless\n * `/__ossido/data{path}` route, so data is pre-rendered to `.json` files, keyed\n * by path only (no query variants).\n */\ndeclare const __OSSIDO_STATIC__: boolean;\nconst IS_STATIC_EXPORT =\n typeof __OSSIDO_STATIC__ !== 'undefined' && __OSSIDO_STATIC__;\n\n/**\n * The data endpoint URL for a location. In a static export it targets the\n * pre-rendered `.json` file matching what `ossido build --static` writes (root is\n * `/__ossido/data.json` to avoid a `data/.json` dotfile); otherwise the live\n * server's extensionless route, including search params.\n */\nfunction dataEndpointUrl({ pathname, searchStr }: LocationKeyParts): string {\n if (IS_STATIC_EXPORT) {\n return pathname === '/'\n ? '/__ossido/data.json'\n : `/__ossido/data${pathname}.json`;\n }\n return `/__ossido/data${pathname}${searchStr}`;\n}\n\n/**\n * Fulfilled value of a data resource. A redirect is a normal (non-error)\n * outcome so it must NOT reject — rejecting would trigger the error boundary.\n */\nexport type RouteDataResult =\n | { kind: 'data'; props: Record<string, unknown> }\n | { kind: 'redirect'; destination: string };\n\n/**\n * A promise annotated with `status`/`value`/`reason` so React's `use()` reads\n * settled resources synchronously (no suspend). This is what lets the SSR and\n * hydration first render resolve pre-seeded data without suspending.\n */\nexport interface DataResource extends Promise<RouteDataResult> {\n status: 'pending' | 'fulfilled' | 'rejected';\n value?: RouteDataResult;\n reason?: unknown;\n}\n\ninterface LocationKeyParts {\n pathname: string;\n searchStr: string;\n}\n\ninterface OssidoApiResponse {\n data?: unknown;\n // Server data for the `layout.rs` handlers wrapping this page, keyed by each\n // layout's `dataKey`. Present only when a wrapping layout has a data handler.\n layoutData?: Record<string, unknown>;\n info: {\n redirect_destination?: string;\n // Present (dev only) when the Rust handler panicked — see `error_json` in\n // crates/ossido/src/response.rs.\n serverError?: ServerErrorPayload;\n };\n}\n\n/**\n * Rebuild a `ServerErrorPayload` (from the Rust server) into a real `Error` so\n * the error boundary / dev overlay treats a backend panic exactly like a JS\n * error, preserving the panic message and backtrace.\n */\nexport function serverErrorToError(payload: ServerErrorPayload): Error {\n const error: OssidoErrorWithSource = new Error(payload.message);\n error.name = payload.name;\n if (payload.stack) error.stack = payload.stack;\n // Carry the panic-site source through to the overlay for a highlighted\n // excerpt (Rust has no sourcemap the client could resolve one from).\n if (payload.source) error.ossidoServerSource = payload.source;\n return error;\n}\n\nconst CACHE_LIMIT = 50;\nconst cache = new Map<string, DataResource>();\n\n/**\n * Single source of truth for a resource key — used by both the seeder and the\n * loader. Includes `searchStr` so search-param-only navigations create a\n * distinct resource (and therefore refetch).\n */\nexport function buildResourceKey(\n navigationId: number,\n { pathname, searchStr }: LocationKeyParts,\n): string {\n return `${navigationId}::${pathname}${searchStr}`;\n}\n\n/** Normalize user server data into a `data` result for seeding. */\nexport function toDataResult(props: unknown): RouteDataResult {\n return { kind: 'data', props: (props ?? {}) as Record<string, unknown> };\n}\n\n/**\n * Resource key for a `layout.rs` handler's data. Keyed by the layout's `dataKey`\n * alone (not the navigation) so its data persists across navigations under the\n * same layout and is simply overwritten by each page's data fetch — matching how\n * layouts persist in the tree.\n */\nfunction buildLayoutResourceKey(dataKey: string): string {\n return `layout::${dataKey}`;\n}\n\n/**\n * Seed the wrapping layouts' server data (from a page's SSR payload or data\n * fetch), keyed by each layout's `dataKey`.\n */\nexport function seedLayoutData(layoutData: Record<string, unknown>): void {\n for (const [dataKey, props] of Object.entries(layoutData)) {\n seedResource(buildLayoutResourceKey(dataKey), toDataResult(props));\n }\n}\n\n/**\n * Read a layout's seeded server data synchronously (no fetch, no suspend): a\n * layout's data always arrives via the page it wraps. Returns empty props when\n * nothing is seeded yet (e.g. a loading-fallback navigation's first frame).\n */\nexport function readLayoutData(dataKey: string): Record<string, unknown> {\n const resource = cache.get(buildLayoutResourceKey(dataKey));\n if (resource?.status === 'fulfilled' && resource.value?.kind === 'data') {\n return resource.value.props;\n }\n return {};\n}\n\nfunction annotate(promise: Promise<RouteDataResult>): DataResource {\n const resource = promise as DataResource;\n resource.status = 'pending';\n resource.then(\n (value) => {\n resource.status = 'fulfilled';\n resource.value = value;\n },\n (reason) => {\n resource.status = 'rejected';\n resource.reason = reason;\n },\n );\n return resource;\n}\n\nasync function fetchRouteData(\n location: LocationKeyParts,\n): Promise<RouteDataResult> {\n const res = await fetch(dataEndpointUrl(location));\n // Read the body before the `res.ok` check: a panicked handler responds with a\n // 500 that still carries the structured error under `info.serverError` (dev).\n const body = (await res.json().catch(() => null)) as OssidoApiResponse | null;\n\n if (body?.info.serverError) {\n throw serverErrorToError(body.info.serverError);\n }\n // A redirecting handler responds with a 308 whose destination lives in the\n // body (there is no `Location` header — this is a data envelope, not an HTTP\n // redirect the browser should follow). Handle it before the ok-status check,\n // since 308 is not an \"ok\" status.\n if (body?.info.redirect_destination) {\n return { kind: 'redirect', destination: body.info.redirect_destination };\n }\n\n if (!res.ok || !body) {\n throw new Error(\n `Failed to load server data for \"${location.pathname}\" (status ${res.status})`,\n );\n }\n\n // A page's data fetch also carries its wrapping layouts' data — seed those so\n // the layout components read them synchronously.\n if (body.layoutData) seedLayoutData(body.layoutData);\n\n return toDataResult(body.data);\n}\n\n/**\n * Insert a pre-fulfilled resource — used to seed the SSR initial data so the\n * first render (server and client) reads it synchronously.\n */\nexport function seedResource(\n key: string,\n value: RouteDataResult,\n): DataResource {\n const resource = Promise.resolve(value) as DataResource;\n resource.status = 'fulfilled';\n resource.value = value;\n cache.set(key, resource);\n return resource;\n}\n\n/**\n * Insert a pre-rejected resource carrying a server error — used to seed a\n * handler panic (dev) so `use()` re-throws it into the error boundary on the\n * first render (server and client), rendering the error overlay.\n */\nexport function seedErrorResource(\n key: string,\n payload: ServerErrorPayload,\n): DataResource {\n const reason = serverErrorToError(payload);\n const resource = Promise.reject(reason) as DataResource;\n // The rejection is consumed synchronously by `use()`, but attach a no-op\n // catch so it is never reported as an unhandled rejection.\n resource.catch(() => undefined);\n resource.status = 'rejected';\n resource.reason = reason;\n cache.set(key, resource);\n return resource;\n}\n\n/**\n * Return the cached resource for `key`, creating one on demand. Idempotent\n * (StrictMode-safe): at most one fetch is started per key.\n *\n * The server never fetches (the ossido_ssr V8 runtime has no `fetch`); every\n * server-rendered route is pre-seeded, and any un-seeded server lookup resolves\n * to empty props rather than suspending.\n */\nexport function getOrCreateResource(\n key: string,\n route: Route,\n location: LocationKeyParts,\n): DataResource {\n const existing = cache.get(key);\n if (existing) return existing;\n\n if (isServerSide || !route.options.hasHandler) {\n return seedResource(key, { kind: 'data', props: {} });\n }\n\n const resource = annotate(fetchRouteData(location));\n cache.set(key, resource);\n evictStaleEntries(key);\n return resource;\n}\n\n/** Drop a resource so the next lookup refetches (used by error `reset`). */\nexport function invalidateResource(key: string): void {\n cache.delete(key);\n}\n\nfunction evictStaleEntries(currentKey: string): void {\n if (cache.size <= CACHE_LIMIT) return;\n // Map preserves insertion order, so this drops the oldest entries first and\n // never evicts the entry currently being rendered.\n for (const key of cache.keys()) {\n if (cache.size <= CACHE_LIMIT) break;\n if (key !== currentKey) cache.delete(key);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Route } from '../route';
2
+ import { sanitizePathname } from '../utils/match-route';
3
+ export { sanitizePathname };
4
+ /**
5
+ * Returns the route that matches the given pathname.
6
+ *
7
+ * This hook is also implemented on server side to match the bundle
8
+ * file to load at the first rendering.
9
+ *
10
+ * File: crates/ossido/src/payload.rs
11
+ *
12
+ * Optimizations should occur on both
13
+ */
14
+ export declare function useRoute(pathname?: string): Route | undefined;
@@ -0,0 +1,22 @@
1
+ import { useRouterContext } from "../components/RouterContext.js";
2
+ import { matchRoute, sanitizePathname } from "../utils/match-route.js";
3
+
4
+ //#region src/hooks/useRoute.ts
5
+ /**
6
+ * Returns the route that matches the given pathname.
7
+ *
8
+ * This hook is also implemented on server side to match the bundle
9
+ * file to load at the first rendering.
10
+ *
11
+ * File: crates/ossido/src/payload.rs
12
+ *
13
+ * Optimizations should occur on both
14
+ */
15
+ function useRoute(pathname) {
16
+ const { router: { routesById } } = useRouterContext();
17
+ return matchRoute(routesById, pathname);
18
+ }
19
+
20
+ //#endregion
21
+ export { useRoute };
22
+ //# sourceMappingURL=useRoute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRoute.js","names":[],"sources":["../../../src/hooks/useRoute.ts"],"sourcesContent":["import type { Route } from '../route';\n\nimport { useRouterContext } from '../components/RouterContext';\nimport { matchRoute, sanitizePathname } from '../utils/match-route';\n\nexport { sanitizePathname };\n\n/**\n * Returns the route that matches the given pathname.\n *\n * This hook is also implemented on server side to match the bundle\n * file to load at the first rendering.\n *\n * File: crates/ossido/src/payload.rs\n *\n * Optimizations should occur on both\n */\nexport function useRoute(pathname?: string): Route | undefined {\n const {\n router: { routesById },\n } = useRouterContext();\n\n return matchRoute(routesById, pathname);\n}\n"],"mappings":";;;;;;;;;;;;;;AAiBA,SAAgB,SAAS,UAAsC;CAC7D,MAAM,EACJ,QAAQ,EAAE,iBACR,iBAAiB;CAErB,OAAO,WAAW,YAAY,QAAQ;AACxC"}