@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type NavigationFn = (path: string, opts?: NavigationOptions) => void;
|
|
2
|
+
interface NavigationOptions {
|
|
3
|
+
/**
|
|
4
|
+
* If "false" the scroll offset will be kept across page navigation. Default "true"
|
|
5
|
+
*/
|
|
6
|
+
scroll?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Override the app's `viewTransitions` config for this navigation: `true`
|
|
9
|
+
* forces a View Transition, `false` skips it. Defaults to the config value.
|
|
10
|
+
*/
|
|
11
|
+
viewTransition?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface UseRouterResult {
|
|
14
|
+
/**
|
|
15
|
+
* Redirects to the path passed as argument updating the browser history.
|
|
16
|
+
*/
|
|
17
|
+
push: NavigationFn;
|
|
18
|
+
/**
|
|
19
|
+
* Redirects to the path passed as argument replacing the current history
|
|
20
|
+
* entry.
|
|
21
|
+
*/
|
|
22
|
+
replace: NavigationFn;
|
|
23
|
+
/**
|
|
24
|
+
* This object contains all the query params of the current route
|
|
25
|
+
*/
|
|
26
|
+
query: Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the current pathname
|
|
29
|
+
*/
|
|
30
|
+
pathname: string;
|
|
31
|
+
/**
|
|
32
|
+
* Re-fetch the current route's server props — its `page.rs` `#[handler]` data
|
|
33
|
+
* — and re-render the page with the fresh values. Use it when something has
|
|
34
|
+
* changed the data the page was server-rendered with (e.g. after a mutation)
|
|
35
|
+
* and you want the page to reflect it without a full navigation.
|
|
36
|
+
*
|
|
37
|
+
* The current page stays on screen while the refetch is in flight (no
|
|
38
|
+
* `loading.tsx` flash); read `isRefetching` to reflect the pending state.
|
|
39
|
+
*/
|
|
40
|
+
refetchProps: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* `true` while a `refetchProps()` call from this hook is in flight.
|
|
43
|
+
*/
|
|
44
|
+
isRefetching: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const useRouter: () => UseRouterResult;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRouterContext } from "../components/RouterContext.js";
|
|
2
|
+
import { useCallback, useTransition } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/useRouter.ts
|
|
5
|
+
const useRouter = () => {
|
|
6
|
+
const { location, updateLocation, retry } = useRouterContext();
|
|
7
|
+
const [isRefetching, startRefetch] = useTransition();
|
|
8
|
+
const navigate = useCallback((type, path, opts) => {
|
|
9
|
+
const { scroll = true, viewTransition } = opts || {};
|
|
10
|
+
const url = new URL(path, window.location.origin);
|
|
11
|
+
updateLocation({
|
|
12
|
+
href: url.href,
|
|
13
|
+
pathname: url.pathname,
|
|
14
|
+
search: Object.fromEntries(url.searchParams),
|
|
15
|
+
searchStr: url.search,
|
|
16
|
+
hash: url.hash
|
|
17
|
+
}, {
|
|
18
|
+
history: {
|
|
19
|
+
type,
|
|
20
|
+
path
|
|
21
|
+
},
|
|
22
|
+
scroll,
|
|
23
|
+
viewTransition
|
|
24
|
+
});
|
|
25
|
+
}, [updateLocation]);
|
|
26
|
+
const push = useCallback((path, opts) => {
|
|
27
|
+
navigate("pushState", path, opts);
|
|
28
|
+
}, [navigate]);
|
|
29
|
+
const replace = useCallback((path, opts) => {
|
|
30
|
+
navigate("replaceState", path, opts);
|
|
31
|
+
}, [navigate]);
|
|
32
|
+
const refetchProps = useCallback(() => {
|
|
33
|
+
startRefetch(() => {
|
|
34
|
+
retry();
|
|
35
|
+
});
|
|
36
|
+
}, [retry]);
|
|
37
|
+
return {
|
|
38
|
+
push,
|
|
39
|
+
replace,
|
|
40
|
+
query: location.search,
|
|
41
|
+
pathname: location.pathname,
|
|
42
|
+
refetchProps,
|
|
43
|
+
isRefetching
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { useRouter };
|
|
49
|
+
//# sourceMappingURL=useRouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRouter.js","names":[],"sources":["../../../src/hooks/useRouter.ts"],"sourcesContent":["import { useCallback, useTransition } from 'react';\n\nimport { useRouterContext } from '../components/RouterContext';\n\ntype NavigationType = 'pushState' | 'replaceState';\ntype NavigationFn = (path: string, opts?: NavigationOptions) => void;\n\ninterface NavigationOptions {\n /**\n * If \"false\" the scroll offset will be kept across page navigation. Default \"true\"\n */\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\ninterface UseRouterResult {\n /**\n * Redirects to the path passed as argument updating the browser history.\n */\n push: NavigationFn;\n\n /**\n * Redirects to the path passed as argument replacing the current history\n * entry.\n */\n replace: NavigationFn;\n\n /**\n * This object contains all the query params of the current route\n */\n query: Record<string, string>;\n\n /**\n * Returns the current pathname\n */\n pathname: string;\n\n /**\n * Re-fetch the current route's server props — its `page.rs` `#[handler]` data\n * — and re-render the page with the fresh values. Use it when something has\n * changed the data the page was server-rendered with (e.g. after a mutation)\n * and you want the page to reflect it without a full navigation.\n *\n * The current page stays on screen while the refetch is in flight (no\n * `loading.tsx` flash); read `isRefetching` to reflect the pending state.\n */\n refetchProps: () => void;\n\n /**\n * `true` while a `refetchProps()` call from this hook is in flight.\n */\n isRefetching: boolean;\n}\n\nexport const useRouter = (): UseRouterResult => {\n const { location, updateLocation, retry } = useRouterContext();\n const [isRefetching, startRefetch] = useTransition();\n\n const navigate = useCallback(\n (type: NavigationType, path: string, opts?: NavigationOptions): void => {\n const { scroll = true, viewTransition } = opts || {};\n const url = new URL(path, window.location.origin);\n\n // The history/scroll update is applied by `updateLocation` when the\n // navigation actually commits — which, for a route without `loading.tsx`,\n // is after its data has been prefetched (so the URL doesn't change while\n // the current page is still showing).\n updateLocation(\n {\n href: url.href,\n pathname: url.pathname,\n search: Object.fromEntries(url.searchParams),\n searchStr: url.search,\n hash: url.hash,\n },\n { history: { type, path }, scroll, viewTransition },\n );\n },\n [updateLocation],\n );\n\n const push = useCallback(\n (path: string, opts?: NavigationOptions): void => {\n navigate('pushState', path, opts);\n },\n [navigate],\n );\n\n const replace = useCallback(\n (path: string, opts?: NavigationOptions): void => {\n navigate('replaceState', path, opts);\n },\n [navigate],\n );\n\n const refetchProps = useCallback((): void => {\n // `retry` bumps the navigation id, which changes the data-resource key and\n // so triggers a refetch of the current route (same mechanism as the error\n // boundary's reset). Wrapped in a transition so React keeps the current page\n // rendered until the fresh props resolve, instead of flashing the loading\n // fallback.\n startRefetch(() => {\n retry();\n });\n }, [retry]);\n\n return {\n push,\n replace,\n query: location.search,\n pathname: location.pathname,\n refetchProps,\n isRefetching,\n };\n};\n"],"mappings":";;;;AA0DA,MAAa,kBAAmC;CAC9C,MAAM,EAAE,UAAU,gBAAgB,UAAU,iBAAiB;CAC7D,MAAM,CAAC,cAAc,gBAAgB,cAAc;CAEnD,MAAM,WAAW,aACd,MAAsB,MAAc,SAAmC;EACtE,MAAM,EAAE,SAAS,MAAM,mBAAmB,QAAQ,CAAC;EACnD,MAAM,MAAM,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM;EAMhD,eACE;GACE,MAAM,IAAI;GACV,UAAU,IAAI;GACd,QAAQ,OAAO,YAAY,IAAI,YAAY;GAC3C,WAAW,IAAI;GACf,MAAM,IAAI;EACZ,GACA;GAAE,SAAS;IAAE;IAAM;GAAK;GAAG;GAAQ;EAAe,CACpD;CACF,GACA,CAAC,cAAc,CACjB;CAEA,MAAM,OAAO,aACV,MAAc,SAAmC;EAChD,SAAS,aAAa,MAAM,IAAI;CAClC,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,UAAU,aACb,MAAc,SAAmC;EAChD,SAAS,gBAAgB,MAAM,IAAI;CACrC,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,eAAe,kBAAwB;EAM3C,mBAAmB;GACjB,MAAM;EACR,CAAC;CACH,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO;EACL;EACA;EACA,OAAO,SAAS;EAChB,UAAU,SAAS;EACnB;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Bump the store and notify subscribers. Dev-only in practice. */
|
|
2
|
+
export declare function notifyRouteHotUpdate(): void;
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe the calling component to route hot-swaps. Returns a value that
|
|
5
|
+
* changes whenever {@link notifyRouteHotUpdate} fires, forcing a re-render so
|
|
6
|
+
* freshly swapped `route.component`s are picked up. Inert outside dev, where
|
|
7
|
+
* nothing bumps the store.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useRouteHotVersion(): number;
|
package/dist/esm/hot.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/hot.ts
|
|
4
|
+
let version = 0;
|
|
5
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
/** Bump the store and notify subscribers. Dev-only in practice. */
|
|
7
|
+
function notifyRouteHotUpdate() {
|
|
8
|
+
version += 1;
|
|
9
|
+
for (const listener of listeners) listener();
|
|
10
|
+
}
|
|
11
|
+
function subscribe(listener) {
|
|
12
|
+
listeners.add(listener);
|
|
13
|
+
return () => {
|
|
14
|
+
listeners.delete(listener);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function getSnapshot() {
|
|
18
|
+
return version;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe the calling component to route hot-swaps. Returns a value that
|
|
22
|
+
* changes whenever {@link notifyRouteHotUpdate} fires, forcing a re-render so
|
|
23
|
+
* freshly swapped `route.component`s are picked up. Inert outside dev, where
|
|
24
|
+
* nothing bumps the store.
|
|
25
|
+
*/
|
|
26
|
+
function useRouteHotVersion() {
|
|
27
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { notifyRouteHotUpdate, useRouteHotVersion };
|
|
32
|
+
//# sourceMappingURL=hot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hot.js","names":[],"sources":["../../src/hot.ts"],"sourcesContent":["import { useSyncExternalStore } from 'react';\n\n// Dev-only HMR bridge for route components.\n//\n// A route module edit that React Fast Refresh CAN apply in place (the module\n// only exports components) never reaches here — vite contains it at that\n// module's own boundary. But an edit that breaks the boundary (a non-component\n// export, or an anonymous default export) makes react-refresh call\n// `import.meta.hot.invalidate()`, which would otherwise propagate to the client\n// entry and force a full page reload — refetching the route's server data.\n//\n// Instead, the generated route tree accepts its route-module deps and swaps the\n// affected `route.component`s, then bumps this store so the match tree\n// re-renders with them. That downgrades a full reload to a route-subtree\n// remount. Prod builds never call `notifyRouteHotUpdate` (the generated hot\n// block is dead code under `if (import.meta.hot)` and is eliminated), so the\n// subscription is inert.\nlet version = 0;\nconst listeners = new Set<() => void>();\n\n/** Bump the store and notify subscribers. Dev-only in practice. */\nexport function notifyRouteHotUpdate(): void {\n version += 1;\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction getSnapshot(): number {\n return version;\n}\n\n/**\n * Subscribe the calling component to route hot-swaps. Returns a value that\n * changes whenever {@link notifyRouteHotUpdate} fires, forcing a re-render so\n * freshly swapped `route.component`s are picked up. Inert outside dev, where\n * nothing bumps the store.\n */\nexport function useRouteHotVersion(): number {\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n"],"mappings":";;;AAiBA,IAAI,UAAU;AACd,MAAM,4BAAY,IAAI,IAAgB;;AAGtC,SAAgB,uBAA6B;CAC3C,WAAW;CACX,KAAK,MAAM,YAAY,WAAW,SAAS;AAC7C;AAEA,SAAS,UAAU,UAAkC;CACnD,UAAU,IAAI,QAAQ;CACtB,aAAa;EACX,UAAU,OAAO,QAAQ;CAC3B;AACF;AAEA,SAAS,cAAsB;CAC7B,OAAO;AACT;;;;;;;AAQA,SAAgB,qBAA6B;CAC3C,OAAO,qBAAqB,WAAW,aAAa,WAAW;AACjE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { RouterProvider } from './components/RouterProvider';
|
|
2
|
+
export { Link } from './components/Link';
|
|
3
|
+
export { createRouter } from './router';
|
|
4
|
+
export type { RouterInstanceType } from './router';
|
|
5
|
+
export { createRoute, createRootRoute } from './route';
|
|
6
|
+
export { useRouter } from './hooks/useRouter';
|
|
7
|
+
export { preloadRouteChain } from './utils/preload-route-chain';
|
|
8
|
+
export { notifyRouteHotUpdate, useRouteHotVersion } from './hot';
|
|
9
|
+
export type { RouteProps, RouteComponent, OssidoErrorProps, ServerErrorPayload, } from './types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { notifyRouteHotUpdate, useRouteHotVersion } from "./hot.js";
|
|
2
|
+
import { useRouter } from "./hooks/useRouter.js";
|
|
3
|
+
import { createRootRoute, createRoute } from "./route.js";
|
|
4
|
+
import { Link } from "./components/Link.js";
|
|
5
|
+
import { RouterProvider } from "./components/RouterProvider.js";
|
|
6
|
+
import { createRouter } from "./router.js";
|
|
7
|
+
import { preloadRouteChain } from "./utils/preload-route-chain.js";
|
|
8
|
+
|
|
9
|
+
export { Link, RouterProvider, createRootRoute, createRoute, createRouter, notifyRouteHotUpdate, preloadRouteChain, useRouteHotVersion, useRouter };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { RouteComponent, LoadingComponent, ErrorComponent, NotFoundComponent } from './types';
|
|
2
|
+
interface RouteOptions {
|
|
3
|
+
id?: string;
|
|
4
|
+
isRoot?: boolean;
|
|
5
|
+
getParentRoute?: () => Route;
|
|
6
|
+
path?: string;
|
|
7
|
+
filePath?: string;
|
|
8
|
+
/**
|
|
9
|
+
* The route's source file path (e.g. `/about/page`, `/blog/layout`), matching
|
|
10
|
+
* the Rust route key. Used to seed/read this route's server data — for pages
|
|
11
|
+
* and, crucially, for `layout.rs` data (keyed by `dataKey` in the payload's
|
|
12
|
+
* `layoutData`).
|
|
13
|
+
*/
|
|
14
|
+
dataKey?: string;
|
|
15
|
+
component: RouteComponent;
|
|
16
|
+
hasHandler?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Nearest-ancestor `loading.tsx`, resolved at generation time. Rendered as
|
|
19
|
+
* the `<Suspense>` fallback while this route's server data loads on client
|
|
20
|
+
* navigation. Falls back to a framework default when absent.
|
|
21
|
+
*/
|
|
22
|
+
loadingComponent?: LoadingComponent;
|
|
23
|
+
/**
|
|
24
|
+
* Nearest-ancestor `error.tsx`, resolved at generation time. Rendered by the
|
|
25
|
+
* route error boundary. Falls back to a framework default when absent.
|
|
26
|
+
*/
|
|
27
|
+
errorComponent?: ErrorComponent;
|
|
28
|
+
/**
|
|
29
|
+
* Nearest-ancestor `not-found.tsx`, resolved at generation time. On the root
|
|
30
|
+
* route it is the global not-found UI, rendered when no route matches. Falls
|
|
31
|
+
* back to a framework default when absent.
|
|
32
|
+
*/
|
|
33
|
+
notFoundComponent?: NotFoundComponent;
|
|
34
|
+
}
|
|
35
|
+
export declare function createRoute(options: RouteOptions): Route;
|
|
36
|
+
export declare const ROOT_ROUTE_ID = "__root__";
|
|
37
|
+
export declare class Route {
|
|
38
|
+
options: RouteOptions;
|
|
39
|
+
/**
|
|
40
|
+
* The route id is used to identify the route in the router
|
|
41
|
+
* and is used to match the route with the URL.
|
|
42
|
+
*
|
|
43
|
+
* For now is the `path`
|
|
44
|
+
*/
|
|
45
|
+
id?: string;
|
|
46
|
+
isRoot: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Used for identify the route by matching the URL
|
|
49
|
+
*/
|
|
50
|
+
path?: string;
|
|
51
|
+
fullPath: string;
|
|
52
|
+
/**
|
|
53
|
+
* Utility to identify the route in the file system
|
|
54
|
+
* Used i.e. for finding the criticalCss to load
|
|
55
|
+
*
|
|
56
|
+
* The path does not include the file extension
|
|
57
|
+
*/
|
|
58
|
+
filePath?: string;
|
|
59
|
+
children?: Array<Route>;
|
|
60
|
+
parentRoute?: Route;
|
|
61
|
+
originalIndex?: number;
|
|
62
|
+
component: RouteComponent;
|
|
63
|
+
'$$typeof': symbol;
|
|
64
|
+
constructor(options: RouteOptions);
|
|
65
|
+
init: (originalIndex: number) => void;
|
|
66
|
+
addChildren(routes: Array<Route>): this;
|
|
67
|
+
update: (options: Partial<RouteOptions>) => this;
|
|
68
|
+
}
|
|
69
|
+
export declare function createRootRoute(options: RouteOptions): Route;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { joinPaths, trimPathLeft } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/route.ts
|
|
4
|
+
function createRoute(options) {
|
|
5
|
+
return new Route(options);
|
|
6
|
+
}
|
|
7
|
+
const ROOT_ROUTE_ID = "__root__";
|
|
8
|
+
var Route = class {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.init = (originalIndex) => {
|
|
11
|
+
this.originalIndex = originalIndex;
|
|
12
|
+
this.parentRoute = this.options.getParentRoute?.();
|
|
13
|
+
const isRoot = !this.parentRoute && !this.options.path && !this.options.id;
|
|
14
|
+
if (isRoot) this.path = ROOT_ROUTE_ID;
|
|
15
|
+
let path = isRoot ? ROOT_ROUTE_ID : this.options.path;
|
|
16
|
+
if (path && path !== "/") path = trimPathLeft(path);
|
|
17
|
+
const customId = this.options.id || path || this.options.filePath;
|
|
18
|
+
let id = isRoot ? ROOT_ROUTE_ID : joinPaths([customId]);
|
|
19
|
+
if (path === "__root__") path = "/";
|
|
20
|
+
if (id !== "__root__") id = joinPaths(["/", id]);
|
|
21
|
+
this.filePath = this.options.filePath;
|
|
22
|
+
this.path = path;
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.fullPath = path || "";
|
|
25
|
+
};
|
|
26
|
+
this.update = (options) => {
|
|
27
|
+
Object.assign(this.options, options);
|
|
28
|
+
this.isRoot = options.isRoot || !options.getParentRoute;
|
|
29
|
+
return this;
|
|
30
|
+
};
|
|
31
|
+
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== "function";
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.$$typeof = Symbol.for("react.memo");
|
|
34
|
+
this.component = options.component;
|
|
35
|
+
}
|
|
36
|
+
addChildren(routes) {
|
|
37
|
+
this.children = routes;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function createRootRoute(options) {
|
|
42
|
+
return new Route({
|
|
43
|
+
...options,
|
|
44
|
+
isRoot: true
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ROOT_ROUTE_ID, Route, createRootRoute, createRoute };
|
|
50
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.js","names":[],"sources":["../../src/route.ts"],"sourcesContent":["import type {\n RouteComponent,\n LoadingComponent,\n ErrorComponent,\n NotFoundComponent,\n} from './types';\nimport { trimPathLeft, joinPaths } from './utils';\n\ninterface RouteOptions {\n id?: string;\n isRoot?: boolean;\n getParentRoute?: () => Route;\n path?: string;\n filePath?: string;\n /**\n * The route's source file path (e.g. `/about/page`, `/blog/layout`), matching\n * the Rust route key. Used to seed/read this route's server data — for pages\n * and, crucially, for `layout.rs` data (keyed by `dataKey` in the payload's\n * `layoutData`).\n */\n dataKey?: string;\n component: RouteComponent;\n hasHandler?: boolean;\n /**\n * Nearest-ancestor `loading.tsx`, resolved at generation time. Rendered as\n * the `<Suspense>` fallback while this route's server data loads on client\n * navigation. Falls back to a framework default when absent.\n */\n loadingComponent?: LoadingComponent;\n /**\n * Nearest-ancestor `error.tsx`, resolved at generation time. Rendered by the\n * route error boundary. Falls back to a framework default when absent.\n */\n errorComponent?: ErrorComponent;\n /**\n * Nearest-ancestor `not-found.tsx`, resolved at generation time. On the root\n * route it is the global not-found UI, rendered when no route matches. Falls\n * back to a framework default when absent.\n */\n notFoundComponent?: NotFoundComponent;\n}\n\nexport function createRoute(options: RouteOptions): Route {\n return new Route(options);\n}\n\nexport const ROOT_ROUTE_ID = '__root__';\n\nexport class Route {\n options: RouteOptions;\n\n /**\n * The route id is used to identify the route in the router\n * and is used to match the route with the URL.\n *\n * For now is the `path`\n */\n id?: string;\n isRoot: boolean;\n /**\n * Used for identify the route by matching the URL\n */\n path?: string;\n fullPath!: string;\n\n /**\n * Utility to identify the route in the file system\n * Used i.e. for finding the criticalCss to load\n *\n * The path does not include the file extension\n */\n filePath?: string;\n\n children?: Array<Route>;\n parentRoute?: Route;\n originalIndex?: number;\n component: RouteComponent;\n\n '$$typeof': symbol;\n\n constructor(options: RouteOptions) {\n this.isRoot =\n options.isRoot ?? typeof options.getParentRoute !== 'function';\n this.options = options;\n this.$$typeof = Symbol.for('react.memo');\n\n this.component = options.component;\n }\n\n init = (originalIndex: number): void => {\n this.originalIndex = originalIndex;\n\n this.parentRoute = this.options.getParentRoute?.();\n\n // Only the true root (no parent) claims the shared ROOT_ROUTE_ID. A nested\n // or route-group `layout` is also pathless, but it has a parent — it must\n // get its own id (from its filePath) so it doesn't overwrite the root in\n // `routesById`.\n const isRoot = !this.parentRoute && !this.options.path && !this.options.id;\n\n if (isRoot) {\n this.path = ROOT_ROUTE_ID;\n }\n\n let path: undefined | string = isRoot ? ROOT_ROUTE_ID : this.options.path;\n\n // If the path is anything other than an index path, trim it up\n if (path && path !== '/') {\n path = trimPathLeft(path);\n }\n\n const customId = this.options.id || path || this.options.filePath;\n\n // Strip the parentId prefix from the first level of children\n let id = isRoot ? ROOT_ROUTE_ID : joinPaths([customId]);\n\n if (path === ROOT_ROUTE_ID) {\n path = '/';\n }\n\n if (id !== ROOT_ROUTE_ID) {\n id = joinPaths(['/', id]);\n }\n\n this.filePath = this.options.filePath;\n this.path = path;\n this.id = id;\n this.fullPath = path || '';\n };\n\n addChildren(routes: Array<Route>): this {\n this.children = routes;\n return this;\n }\n\n update = (options: Partial<RouteOptions>): this => {\n Object.assign(this.options, options);\n this.isRoot = options.isRoot || !options.getParentRoute;\n return this;\n };\n}\n\n// TODO: not use yet. To be updated in ossido-fs-router-vite-plugin package\nexport function createRootRoute(options: RouteOptions): Route {\n return new Route({ ...options, isRoot: true });\n}\n"],"mappings":";;;AA0CA,SAAgB,YAAY,SAA8B;CACxD,OAAO,IAAI,MAAM,OAAO;AAC1B;AAEA,MAAa,gBAAgB;AAE7B,IAAa,QAAb,MAAmB;CAgCjB,YAAY,SAAuB;eAS3B,kBAAgC;GACtC,KAAK,gBAAgB;GAErB,KAAK,cAAc,KAAK,QAAQ,iBAAiB;GAMjD,MAAM,SAAS,CAAC,KAAK,eAAe,CAAC,KAAK,QAAQ,QAAQ,CAAC,KAAK,QAAQ;GAExE,IAAI,QACF,KAAK,OAAO;GAGd,IAAI,OAA2B,SAAS,gBAAgB,KAAK,QAAQ;GAGrE,IAAI,QAAQ,SAAS,KACnB,OAAO,aAAa,IAAI;GAG1B,MAAM,WAAW,KAAK,QAAQ,MAAM,QAAQ,KAAK,QAAQ;GAGzD,IAAI,KAAK,SAAS,gBAAgB,UAAU,CAAC,QAAQ,CAAC;GAEtD,IAAI,qBACF,OAAO;GAGT,IAAI,mBACF,KAAK,UAAU,CAAC,KAAK,EAAE,CAAC;GAG1B,KAAK,WAAW,KAAK,QAAQ;GAC7B,KAAK,OAAO;GACZ,KAAK,KAAK;GACV,KAAK,WAAW,QAAQ;EAC1B;iBAOU,YAAyC;GACjD,OAAO,OAAO,KAAK,SAAS,OAAO;GACnC,KAAK,SAAS,QAAQ,UAAU,CAAC,QAAQ;GACzC,OAAO;EACT;EA1DE,KAAK,SACH,QAAQ,UAAU,OAAO,QAAQ,mBAAmB;EACtD,KAAK,UAAU;EACf,KAAK,WAAW,OAAO,IAAI,YAAY;EAEvC,KAAK,YAAY,QAAQ;CAC3B;CA2CA,YAAY,QAA4B;EACtC,KAAK,WAAW;EAChB,OAAO;CACT;AAOF;AAGA,SAAgB,gBAAgB,SAA8B;CAC5D,OAAO,IAAI,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAK,CAAC;AAC/C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentType as ReactComponentType } from 'react';
|
|
2
|
+
import type { Route } from './route';
|
|
3
|
+
type RouteTree = Route;
|
|
4
|
+
interface CreateRouterOptions {
|
|
5
|
+
routeTree: RouteTree;
|
|
6
|
+
basePath?: string;
|
|
7
|
+
options?: RouterOptions;
|
|
8
|
+
}
|
|
9
|
+
interface RouterOptions {
|
|
10
|
+
component?: ReactComponentType;
|
|
11
|
+
hasHandler?: boolean;
|
|
12
|
+
routeTree?: RouteTree;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRouter(options: CreateRouterOptions): Router;
|
|
15
|
+
export type RouterInstanceType = InstanceType<typeof Router>;
|
|
16
|
+
export declare class Router {
|
|
17
|
+
#private;
|
|
18
|
+
options?: RouterOptions;
|
|
19
|
+
basePath: string;
|
|
20
|
+
routeTree?: RouteTree;
|
|
21
|
+
isServer: boolean;
|
|
22
|
+
routesById: Record<string, Route>;
|
|
23
|
+
routesByPath: Record<string, Route>;
|
|
24
|
+
constructor(options: CreateRouterOptions);
|
|
25
|
+
update: (newOptions: CreateRouterOptions) => void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { trimPath, trimPathRight } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/router.ts
|
|
4
|
+
function createRouter(options) {
|
|
5
|
+
return new Router(options);
|
|
6
|
+
}
|
|
7
|
+
var Router = class {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.basePath = "/";
|
|
10
|
+
this.isServer = typeof document === "undefined";
|
|
11
|
+
this.routesById = {};
|
|
12
|
+
this.routesByPath = {};
|
|
13
|
+
this.update = (newOptions) => {
|
|
14
|
+
this.options = {
|
|
15
|
+
...this.options,
|
|
16
|
+
...newOptions
|
|
17
|
+
};
|
|
18
|
+
this.#updateBasePath(newOptions.basePath);
|
|
19
|
+
if (this.options.routeTree !== this.routeTree) {
|
|
20
|
+
this.routeTree = this.options.routeTree;
|
|
21
|
+
this.#buildRouteTree();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
this.#buildRouteTree = () => {
|
|
25
|
+
const recurseRoutes = (childRoutes) => {
|
|
26
|
+
childRoutes.forEach((route, i) => {
|
|
27
|
+
route.init(i);
|
|
28
|
+
this.routesById[route.id || ""] = route;
|
|
29
|
+
if (!route.isRoot && route.options.path) {
|
|
30
|
+
const trimmedFullPath = trimPathRight(route.fullPath);
|
|
31
|
+
if (!this.routesByPath[trimmedFullPath] || route.fullPath.endsWith("/")) this.routesByPath[trimmedFullPath] = route;
|
|
32
|
+
}
|
|
33
|
+
const children = route.children;
|
|
34
|
+
if (children?.length) recurseRoutes(children);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
recurseRoutes([this.routeTree]);
|
|
38
|
+
};
|
|
39
|
+
this.#updateBasePath = (basePath) => {
|
|
40
|
+
if (basePath === void 0) return;
|
|
41
|
+
this.basePath = basePath === "" || basePath === "/" ? "/" : `/${trimPath(basePath)}`;
|
|
42
|
+
};
|
|
43
|
+
this.update({ ...options });
|
|
44
|
+
if (!this.isServer) window.__OSSIDO__ROUTER__ = this;
|
|
45
|
+
}
|
|
46
|
+
#buildRouteTree;
|
|
47
|
+
#updateBasePath;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { Router, createRouter };
|
|
52
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","names":["#updateBasePath","#buildRouteTree"],"sources":["../../src/router.ts"],"sourcesContent":["import type { ComponentType as ReactComponentType } from 'react';\n\nimport { trimPath, trimPathRight } from './utils';\nimport type { Route } from './route';\n\ntype RouteTree = Route;\n\ninterface CreateRouterOptions {\n routeTree: RouteTree;\n basePath?: string;\n options?: RouterOptions;\n}\n\ninterface RouterOptions {\n component?: ReactComponentType;\n hasHandler?: boolean;\n routeTree?: RouteTree;\n}\n\nexport function createRouter(options: CreateRouterOptions): Router {\n return new Router(options);\n}\n\nexport type RouterInstanceType = InstanceType<typeof Router>;\n\nexport class Router {\n options?: RouterOptions;\n basePath = '/';\n routeTree?: RouteTree;\n\n isServer: boolean = typeof document === 'undefined';\n\n routesById: Record<string, Route> = {};\n\n routesByPath: Record<string, Route> = {};\n\n constructor(options: CreateRouterOptions) {\n this.update({\n ...options,\n });\n\n if (!this.isServer) {\n window.__OSSIDO__ROUTER__ = this;\n }\n }\n\n update = (newOptions: CreateRouterOptions): void => {\n this.options = {\n ...this.options,\n ...newOptions,\n };\n\n this.#updateBasePath(newOptions.basePath);\n\n if (this.options.routeTree !== this.routeTree) {\n this.routeTree = this.options.routeTree;\n this.#buildRouteTree();\n }\n };\n\n #buildRouteTree = (): void => {\n const recurseRoutes = (childRoutes: Array<Route>): void => {\n childRoutes.forEach((route: Route, i: number) => {\n route.init(i);\n\n this.routesById[route.id || ''] = route;\n\n if (!route.isRoot && route.options.path) {\n const trimmedFullPath = trimPathRight(route.fullPath);\n if (\n !this.routesByPath[trimmedFullPath] ||\n route.fullPath.endsWith('/')\n ) {\n this.routesByPath[trimmedFullPath] = route;\n }\n }\n\n const children = route.children;\n if (children?.length) {\n recurseRoutes(children);\n }\n });\n };\n\n recurseRoutes([this.routeTree as Route]);\n };\n\n #updateBasePath = (basePath?: string): void => {\n // No option passed → keep the current base path.\n if (basePath === undefined) return;\n\n this.basePath =\n basePath === '' || basePath === '/' ? '/' : `/${trimPath(basePath)}`;\n };\n}\n"],"mappings":";;;AAmBA,SAAgB,aAAa,SAAsC;CACjE,OAAO,IAAI,OAAO,OAAO;AAC3B;AAIA,IAAa,SAAb,MAAoB;CAWlB,YAAY,SAA8B;kBAT/B;kBAGS,OAAO,aAAa;oBAEJ,CAAC;sBAEC,CAAC;iBAY7B,eAA0C;GAClD,KAAK,UAAU;IACb,GAAG,KAAK;IACR,GAAG;GACL;GAEA,KAAKA,gBAAgB,WAAW,QAAQ;GAExC,IAAI,KAAK,QAAQ,cAAc,KAAK,WAAW;IAC7C,KAAK,YAAY,KAAK,QAAQ;IAC9B,KAAKC,gBAAgB;GACvB;EACF;+BAE8B;GAC5B,MAAM,iBAAiB,gBAAoC;IACzD,YAAY,SAAS,OAAc,MAAc;KAC/C,MAAM,KAAK,CAAC;KAEZ,KAAK,WAAW,MAAM,MAAM,MAAM;KAElC,IAAI,CAAC,MAAM,UAAU,MAAM,QAAQ,MAAM;MACvC,MAAM,kBAAkB,cAAc,MAAM,QAAQ;MACpD,IACE,CAAC,KAAK,aAAa,oBACnB,MAAM,SAAS,SAAS,GAAG,GAE3B,KAAK,aAAa,mBAAmB;KAEzC;KAEA,MAAM,WAAW,MAAM;KACvB,IAAI,UAAU,QACZ,cAAc,QAAQ;IAE1B,CAAC;GACH;GAEA,cAAc,CAAC,KAAK,SAAkB,CAAC;EACzC;0BAEmB,aAA4B;GAE7C,IAAI,aAAa,QAAW;GAE5B,KAAK,WACH,aAAa,MAAM,aAAa,MAAM,MAAM,IAAI,SAAS,QAAQ;EACrE;EAxDE,KAAK,OAAO,EACV,GAAG,QACL,CAAC;EAED,IAAI,CAAC,KAAK,UACR,OAAO,qBAAqB;CAEhC;CAgBA;CA2BA;AAOF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import type { ServerErrorSource } from '@ossido-labs/ossido-ui';
|
|
3
|
+
export type Mode = 'Dev' | 'Prod';
|
|
4
|
+
export interface Segment {
|
|
5
|
+
type: 'pathname' | 'param' | 'wildcard';
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Provided by the rust server and used in the ssr env
|
|
10
|
+
* @see ossido {@link ServerPayloadLocation}
|
|
11
|
+
*/
|
|
12
|
+
export interface ServerInitialLocation {
|
|
13
|
+
href: string;
|
|
14
|
+
pathname: string;
|
|
15
|
+
searchStr: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Props a route component may receive.
|
|
19
|
+
*
|
|
20
|
+
* Page components receive their server data spread directly as props, so their
|
|
21
|
+
* concrete prop shape is defined by the user's handler return type — not by this
|
|
22
|
+
* type. Layout/root components receive `children`.
|
|
23
|
+
*/
|
|
24
|
+
export interface RouteProps {
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A route (page or layout) component.
|
|
29
|
+
*
|
|
30
|
+
* Pages are called with the resolved server data spread as props, so props are
|
|
31
|
+
* intentionally untyped here (`any`); the concrete shape lives in user code.
|
|
32
|
+
*/
|
|
33
|
+
export type RouteComponent = ComponentType<any> & {
|
|
34
|
+
/**
|
|
35
|
+
* Loads the component's code chunk, resolving when it's ready (and caching it
|
|
36
|
+
* so the component then renders synchronously). Present on lazily-loaded route
|
|
37
|
+
* components; absent on eagerly-imported ones (e.g. the root layout).
|
|
38
|
+
*/
|
|
39
|
+
preload?: () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Dev-only. Swap the resolved component behind a lazily-loaded route in place,
|
|
42
|
+
* so an HMR update to the route module renders without re-running the dynamic
|
|
43
|
+
* import. Set by the lazy-load wrapper; used by the generated route tree's hot
|
|
44
|
+
* accept handler. Absent in prod and on eagerly-imported components.
|
|
45
|
+
*/
|
|
46
|
+
update?: (next: RouteComponent) => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Serialized Rust handler error, sent by the server (dev mode only) so a
|
|
50
|
+
* backend panic can surface in the error overlay like a JS error.
|
|
51
|
+
* @see crates/ossido/src/server_error.rs
|
|
52
|
+
*/
|
|
53
|
+
export interface ServerErrorPayload {
|
|
54
|
+
name: string;
|
|
55
|
+
message: string;
|
|
56
|
+
stack?: string;
|
|
57
|
+
source?: ServerErrorSource;
|
|
58
|
+
}
|
|
59
|
+
/** A `loading.tsx` component. Rendered as a `<Suspense>` fallback (no props). */
|
|
60
|
+
export type LoadingComponent = ComponentType;
|
|
61
|
+
/** A `not-found.tsx` component. Rendered when no route matches (no props). */
|
|
62
|
+
export type NotFoundComponent = ComponentType;
|
|
63
|
+
export type { OssidoErrorProps, OssidoErrorWithSource, ServerErrorSource, ErrorComponent, } from '@ossido-labs/ossido-ui';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/utils/from-url-to-parsed-location.ts
|
|
2
|
+
function fromUrlToParsedLocation(href) {
|
|
3
|
+
const location = new URL(href, window.location.origin);
|
|
4
|
+
return {
|
|
5
|
+
href: location.href,
|
|
6
|
+
pathname: location.pathname,
|
|
7
|
+
search: Object.fromEntries(location.searchParams),
|
|
8
|
+
searchStr: location.search,
|
|
9
|
+
hash: location.hash
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { fromUrlToParsedLocation };
|
|
15
|
+
//# sourceMappingURL=from-url-to-parsed-location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-url-to-parsed-location.js","names":[],"sources":["../../../src/utils/from-url-to-parsed-location.ts"],"sourcesContent":["import type { ParsedLocation } from '../components/RouterContext';\n\nexport function fromUrlToParsedLocation(href: string): ParsedLocation {\n const location = new URL(href, window.location.origin);\n return {\n href: location.href,\n pathname: location.pathname,\n search: Object.fromEntries(location.searchParams),\n searchStr: location.search,\n hash: location.hash,\n };\n}\n"],"mappings":";AAEA,SAAgB,wBAAwB,MAA8B;CACpE,MAAM,WAAW,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM;CACrD,OAAO;EACL,MAAM,SAAS;EACf,UAAU,SAAS;EACnB,QAAQ,OAAO,YAAY,SAAS,YAAY;EAChD,WAAW,SAAS;EACpB,MAAM,SAAS;CACjB;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Route } from '../route';
|
|
2
|
+
/**
|
|
3
|
+
* In order to correctly handle pathnames that might finish with a slash
|
|
4
|
+
* we first sanitize them by removing the final slash.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sanitizePathname(pathname: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the route that matches the given pathname, from the router's route
|
|
9
|
+
* table. Pure (no hooks) so it can be used both by `useRoute` and at navigation
|
|
10
|
+
* time (to inspect the target route before committing).
|
|
11
|
+
*
|
|
12
|
+
* This matching is also implemented on the server side to pick the bundle to
|
|
13
|
+
* load at the first rendering — see crates/ossido/src/payload.rs. Any
|
|
14
|
+
* optimization should happen on both.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchRoute(routesById: Record<string, Route>, pathname?: string): Route | undefined;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/utils/match-route.ts
|
|
2
|
+
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/;
|
|
3
|
+
/**
|
|
4
|
+
* The dynamic routes of a route table, pre-filtered and pre-split, computed
|
|
5
|
+
* once per `routesById` object instead of on every match. Keyed weakly so the
|
|
6
|
+
* per-request SSR routers are garbage-collected with their cache entry. Sorted
|
|
7
|
+
* by route id so matching order is deterministic and mirrors the server-side
|
|
8
|
+
* matcher (crates/ossido/src/manifest.rs).
|
|
9
|
+
*
|
|
10
|
+
* The route table is only rebuilt when the router receives a new route tree
|
|
11
|
+
* (never after startup in practice), so keying by object identity is safe.
|
|
12
|
+
*/
|
|
13
|
+
const dynamicRoutesCache = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
function getDynamicRoutes(routesById) {
|
|
15
|
+
let cached = dynamicRoutesCache.get(routesById);
|
|
16
|
+
if (!cached) {
|
|
17
|
+
cached = Object.keys(routesById).filter((route) => DYNAMIC_PATH_REGEX.test(route)).sort().map((route) => ({
|
|
18
|
+
route,
|
|
19
|
+
segments: route.split("/").filter(Boolean)
|
|
20
|
+
}));
|
|
21
|
+
dynamicRoutesCache.set(routesById, cached);
|
|
22
|
+
}
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* In order to correctly handle pathnames that might finish with a slash
|
|
27
|
+
* we first sanitize them by removing the final slash.
|
|
28
|
+
*/
|
|
29
|
+
function sanitizePathname(pathname) {
|
|
30
|
+
if (pathname.endsWith("/") && pathname !== "/") return pathname.substring(0, pathname.length - 1);
|
|
31
|
+
return pathname;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns the route that matches the given pathname, from the router's route
|
|
35
|
+
* table. Pure (no hooks) so it can be used both by `useRoute` and at navigation
|
|
36
|
+
* time (to inspect the target route before committing).
|
|
37
|
+
*
|
|
38
|
+
* This matching is also implemented on the server side to pick the bundle to
|
|
39
|
+
* load at the first rendering — see crates/ossido/src/payload.rs. Any
|
|
40
|
+
* optimization should happen on both.
|
|
41
|
+
*/
|
|
42
|
+
function matchRoute(routesById, pathname) {
|
|
43
|
+
if (!pathname) return;
|
|
44
|
+
pathname = sanitizePathname(pathname);
|
|
45
|
+
if (routesById[pathname]) return routesById[pathname];
|
|
46
|
+
const dynamicRoutes = getDynamicRoutes(routesById);
|
|
47
|
+
if (!dynamicRoutes.length) return;
|
|
48
|
+
const pathSegments = pathname.split("/").filter(Boolean);
|
|
49
|
+
let match = void 0;
|
|
50
|
+
for (const { segments: dynamicRouteSegments } of dynamicRoutes) {
|
|
51
|
+
const routeSegmentsCollector = [];
|
|
52
|
+
for (let i = 0; i < dynamicRouteSegments.length; i++) {
|
|
53
|
+
if (dynamicRouteSegments[i]?.startsWith("[...")) {
|
|
54
|
+
routeSegmentsCollector.push(dynamicRouteSegments[i] ?? "");
|
|
55
|
+
match = `/${routeSegmentsCollector.join("/")}`;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (dynamicRouteSegments[i] === pathSegments[i] || DYNAMIC_PATH_REGEX.test(dynamicRouteSegments[i] || "")) routeSegmentsCollector.push(dynamicRouteSegments[i] ?? "");
|
|
59
|
+
else break;
|
|
60
|
+
}
|
|
61
|
+
if (routeSegmentsCollector.length === pathSegments.length) {
|
|
62
|
+
match = `/${routeSegmentsCollector.join("/")}`;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!match) return;
|
|
67
|
+
return routesById[match];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { matchRoute, sanitizePathname };
|
|
72
|
+
//# sourceMappingURL=match-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match-route.js","names":[],"sources":["../../../src/utils/match-route.ts"],"sourcesContent":["import type { Route } from '../route';\n\nconst DYNAMIC_PATH_REGEX = /\\[(.*?)\\]/;\n\ninterface DynamicRouteEntry {\n route: string;\n segments: Array<string>;\n}\n\n/**\n * The dynamic routes of a route table, pre-filtered and pre-split, computed\n * once per `routesById` object instead of on every match. Keyed weakly so the\n * per-request SSR routers are garbage-collected with their cache entry. Sorted\n * by route id so matching order is deterministic and mirrors the server-side\n * matcher (crates/ossido/src/manifest.rs).\n *\n * The route table is only rebuilt when the router receives a new route tree\n * (never after startup in practice), so keying by object identity is safe.\n */\nconst dynamicRoutesCache = new WeakMap<\n Record<string, Route>,\n Array<DynamicRouteEntry>\n>();\n\nfunction getDynamicRoutes(\n routesById: Record<string, Route>,\n): Array<DynamicRouteEntry> {\n let cached = dynamicRoutesCache.get(routesById);\n if (!cached) {\n cached = Object.keys(routesById)\n .filter((route) => DYNAMIC_PATH_REGEX.test(route))\n .sort()\n .map((route) => ({ route, segments: route.split('/').filter(Boolean) }));\n dynamicRoutesCache.set(routesById, cached);\n }\n return cached;\n}\n\n/**\n * In order to correctly handle pathnames that might finish with a slash\n * we first sanitize them by removing the final slash.\n */\nexport function sanitizePathname(pathname: string): string {\n if (pathname.endsWith('/') && pathname !== '/') {\n return pathname.substring(0, pathname.length - 1);\n }\n\n return pathname;\n}\n\n/**\n * Returns the route that matches the given pathname, from the router's route\n * table. Pure (no hooks) so it can be used both by `useRoute` and at navigation\n * time (to inspect the target route before committing).\n *\n * This matching is also implemented on the server side to pick the bundle to\n * load at the first rendering — see crates/ossido/src/payload.rs. Any\n * optimization should happen on both.\n */\nexport function matchRoute(\n routesById: Record<string, Route>,\n pathname?: string,\n): Route | undefined {\n if (!pathname) return;\n\n pathname = sanitizePathname(pathname);\n\n if (routesById[pathname]) return routesById[pathname];\n\n const dynamicRoutes = getDynamicRoutes(routesById);\n\n if (!dynamicRoutes.length) return;\n\n const pathSegments = pathname.split('/').filter(Boolean);\n\n let match = undefined;\n\n for (const { segments: dynamicRouteSegments } of dynamicRoutes) {\n const routeSegmentsCollector: Array<string> = [];\n\n for (let i = 0; i < dynamicRouteSegments.length; i++) {\n if (dynamicRouteSegments[i]?.startsWith('[...')) {\n routeSegmentsCollector.push(dynamicRouteSegments[i] ?? '');\n match = `/${routeSegmentsCollector.join('/')}`;\n break;\n }\n if (\n dynamicRouteSegments[i] === pathSegments[i] ||\n DYNAMIC_PATH_REGEX.test(dynamicRouteSegments[i] || '')\n ) {\n routeSegmentsCollector.push(dynamicRouteSegments[i] ?? '');\n } else {\n break;\n }\n }\n\n if (routeSegmentsCollector.length === pathSegments.length) {\n match = `/${routeSegmentsCollector.join('/')}`;\n break;\n }\n }\n\n if (!match) return;\n return routesById[match];\n}\n"],"mappings":";AAEA,MAAM,qBAAqB;;;;;;;;;;;AAiB3B,MAAM,qCAAqB,IAAI,QAG7B;AAEF,SAAS,iBACP,YAC0B;CAC1B,IAAI,SAAS,mBAAmB,IAAI,UAAU;CAC9C,IAAI,CAAC,QAAQ;EACX,SAAS,OAAO,KAAK,UAAU,CAAC,CAC7B,QAAQ,UAAU,mBAAmB,KAAK,KAAK,CAAC,CAAC,CACjD,KAAK,CAAC,CACN,KAAK,WAAW;GAAE;GAAO,UAAU,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;EAAE,EAAE;EACzE,mBAAmB,IAAI,YAAY,MAAM;CAC3C;CACA,OAAO;AACT;;;;;AAMA,SAAgB,iBAAiB,UAA0B;CACzD,IAAI,SAAS,SAAS,GAAG,KAAK,aAAa,KACzC,OAAO,SAAS,UAAU,GAAG,SAAS,SAAS,CAAC;CAGlD,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,WACd,YACA,UACmB;CACnB,IAAI,CAAC,UAAU;CAEf,WAAW,iBAAiB,QAAQ;CAEpC,IAAI,WAAW,WAAW,OAAO,WAAW;CAE5C,MAAM,gBAAgB,iBAAiB,UAAU;CAEjD,IAAI,CAAC,cAAc,QAAQ;CAE3B,MAAM,eAAe,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;CAEvD,IAAI,QAAQ;CAEZ,KAAK,MAAM,EAAE,UAAU,0BAA0B,eAAe;EAC9D,MAAM,yBAAwC,CAAC;EAE/C,KAAK,IAAI,IAAI,GAAG,IAAI,qBAAqB,QAAQ,KAAK;GACpD,IAAI,qBAAqB,EAAE,EAAE,WAAW,MAAM,GAAG;IAC/C,uBAAuB,KAAK,qBAAqB,MAAM,EAAE;IACzD,QAAQ,IAAI,uBAAuB,KAAK,GAAG;IAC3C;GACF;GACA,IACE,qBAAqB,OAAO,aAAa,MACzC,mBAAmB,KAAK,qBAAqB,MAAM,EAAE,GAErD,uBAAuB,KAAK,qBAAqB,MAAM,EAAE;QAEzD;EAEJ;EAEA,IAAI,uBAAuB,WAAW,aAAa,QAAQ;GACzD,QAAQ,IAAI,uBAAuB,KAAK,GAAG;GAC3C;EACF;CACF;CAEA,IAAI,CAAC,OAAO;CACZ,OAAO,WAAW;AACpB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RouterInstanceType } from '../router';
|
|
2
|
+
/**
|
|
3
|
+
* Preload the code of the route matching `pathname` and of every layout that
|
|
4
|
+
* wraps it, so a subsequent render commits the components eagerly instead of
|
|
5
|
+
* suspending on their `React.lazy` chunk.
|
|
6
|
+
*
|
|
7
|
+
* Used ahead of the *initial* render on both sides — the server before
|
|
8
|
+
* streaming (a suspending route would push the page content into an
|
|
9
|
+
* out-of-order late chunk, painting an empty shell first) and the client
|
|
10
|
+
* before hydration (so the first client render matches that inline HTML).
|
|
11
|
+
* Client-side navigation has its own preload in `RouterContext`.
|
|
12
|
+
*
|
|
13
|
+
* A failed chunk load resolves anyway: the render then falls back to the lazy
|
|
14
|
+
* component, which surfaces the load error through the route's error boundary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function preloadRouteChain(router: RouterInstanceType, pathname?: string): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { matchRoute } from "./match-route.js";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/preload-route-chain.ts
|
|
4
|
+
/**
|
|
5
|
+
* Preload the code of the route matching `pathname` and of every layout that
|
|
6
|
+
* wraps it, so a subsequent render commits the components eagerly instead of
|
|
7
|
+
* suspending on their `React.lazy` chunk.
|
|
8
|
+
*
|
|
9
|
+
* Used ahead of the *initial* render on both sides — the server before
|
|
10
|
+
* streaming (a suspending route would push the page content into an
|
|
11
|
+
* out-of-order late chunk, painting an empty shell first) and the client
|
|
12
|
+
* before hydration (so the first client render matches that inline HTML).
|
|
13
|
+
* Client-side navigation has its own preload in `RouterContext`.
|
|
14
|
+
*
|
|
15
|
+
* A failed chunk load resolves anyway: the render then falls back to the lazy
|
|
16
|
+
* component, which surfaces the load error through the route's error boundary.
|
|
17
|
+
*/
|
|
18
|
+
async function preloadRouteChain(router, pathname) {
|
|
19
|
+
const matched = matchRoute(router.routesById, pathname);
|
|
20
|
+
if (!matched) return;
|
|
21
|
+
const pending = [];
|
|
22
|
+
for (let node = matched; node; node = node.isRoot ? void 0 : node.options.getParentRoute?.()) {
|
|
23
|
+
const preload = node.component.preload;
|
|
24
|
+
if (preload) pending.push(preload().catch(() => void 0));
|
|
25
|
+
}
|
|
26
|
+
await Promise.all(pending);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { preloadRouteChain };
|
|
31
|
+
//# sourceMappingURL=preload-route-chain.js.map
|