@funstack/router 0.0.3-alpha.1 → 0.0.4
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/dist/index.d.mts +110 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +179 -26
- package/dist/index.mjs.map +1 -1
- package/dist/route-gXPaU-cv.d.mts +235 -0
- package/dist/route-gXPaU-cv.d.mts.map +1 -0
- package/dist/{route-DiO6FM2A.mjs → route-p_gr5yPI.mjs} +3 -3
- package/dist/route-p_gr5yPI.mjs.map +1 -0
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +1 -1
- package/package.json +5 -5
- package/dist/route-BXVQqSlR.d.mts +0 -149
- package/dist/route-BXVQqSlR.d.mts.map +0 -1
- package/dist/route-DiO6FM2A.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as RouteDefinition, i as
|
|
1
|
+
import { a as LoaderArgs, c as RouteComponentProps, d as RouteDefinition, f as TypefulOpaqueRouteDefinition, i as ExtractRouteState, l as RouteComponentPropsOf, m as routeState, n as ExtractRouteId, o as OpaqueRouteDefinition, p as route, r as ExtractRouteParams, s as PathParams, t as ExtractRouteData, u as RouteComponentPropsWithData } from "./route-gXPaU-cv.mjs";
|
|
2
2
|
import { ComponentType, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
@@ -7,20 +7,28 @@ declare const InternalRouteDefinitionSymbol: unique symbol;
|
|
|
7
7
|
* Internal structure for storing per-route state in NavigationHistoryEntry.
|
|
8
8
|
* Each route in the matched stack gets its own state slot indexed by match position.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
10
|
/**
|
|
12
11
|
* Route definition for the router.
|
|
13
12
|
* When a loader is defined, the component receives the loader result as a `data` prop.
|
|
14
13
|
*/
|
|
15
14
|
type InternalRouteDefinition = {
|
|
16
|
-
[InternalRouteDefinitionSymbol]: never;
|
|
17
|
-
/**
|
|
18
|
-
path: string;
|
|
19
|
-
/** Child routes for nested routing */
|
|
15
|
+
[InternalRouteDefinitionSymbol]: never; /** Path pattern to match (e.g., "users/:id"). If omitted, the route is pathless (always matches, consumes nothing). */
|
|
16
|
+
path?: string; /** Child routes for nested routing */
|
|
20
17
|
children?: InternalRouteDefinition[];
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Whether this route requires an exact match.
|
|
20
|
+
* - true: Only matches exact pathname
|
|
21
|
+
* - false: Matches as prefix
|
|
22
|
+
* - undefined: Default (exact for leaf, prefix for parent)
|
|
23
|
+
*/
|
|
24
|
+
exact?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether this route requires a child to match when it has children.
|
|
27
|
+
* - true (default): Parent does not match if no child matches
|
|
28
|
+
* - false: Parent can match alone if it has a component, outlet will be null
|
|
29
|
+
*/
|
|
30
|
+
requireChildren?: boolean; /** Data loader function for this route */
|
|
31
|
+
loader?: (args: LoaderArgs<Record<string, string>>) => unknown; /** Component to render when this route matches */
|
|
24
32
|
component?: ComponentType<{
|
|
25
33
|
data?: unknown;
|
|
26
34
|
params?: Record<string, string>;
|
|
@@ -35,29 +43,29 @@ type InternalRouteDefinition = {
|
|
|
35
43
|
* A matched route with its parameters.
|
|
36
44
|
*/
|
|
37
45
|
type MatchedRoute = {
|
|
38
|
-
/** The original route definition */
|
|
39
|
-
|
|
40
|
-
/** Extracted path parameters */
|
|
41
|
-
params: Record<string, string>;
|
|
42
|
-
/** The matched pathname segment */
|
|
46
|
+
/** The original route definition */route: InternalRouteDefinition; /** Extracted path parameters */
|
|
47
|
+
params: Record<string, string>; /** The matched pathname segment */
|
|
43
48
|
pathname: string;
|
|
44
49
|
};
|
|
45
50
|
/**
|
|
46
51
|
* A matched route with loader data.
|
|
47
52
|
*/
|
|
48
53
|
type MatchedRouteWithData = MatchedRoute & {
|
|
49
|
-
/** Data returned from the loader (undefined if no loader) */
|
|
50
|
-
|
|
54
|
+
/** Data returned from the loader (undefined if no loader) */data: unknown | undefined;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Information passed to onNavigate callback.
|
|
58
|
+
*/
|
|
59
|
+
type OnNavigateInfo = {
|
|
60
|
+
/** Array of matched routes, or null if no routes matched */matches: readonly MatchedRoute[] | null; /** Whether the router will intercept this navigation (before user's preventDefault() call) */
|
|
61
|
+
intercepting: boolean;
|
|
51
62
|
};
|
|
52
63
|
/**
|
|
53
64
|
* Options for navigation.
|
|
54
65
|
*/
|
|
55
66
|
type NavigateOptions = {
|
|
56
|
-
/** Replace current history entry instead of pushing */
|
|
57
|
-
|
|
58
|
-
/** State to associate with the navigation */
|
|
59
|
-
state?: unknown;
|
|
60
|
-
/** Ephemeral info for this navigation only (not persisted in history) */
|
|
67
|
+
/** Replace current history entry instead of pushing */replace?: boolean; /** State to associate with the navigation */
|
|
68
|
+
state?: unknown; /** Ephemeral info for this navigation only (not persisted in history) */
|
|
61
69
|
info?: unknown;
|
|
62
70
|
};
|
|
63
71
|
/**
|
|
@@ -73,9 +81,9 @@ type Location = {
|
|
|
73
81
|
* Call `event.preventDefault()` to prevent the router from handling this navigation.
|
|
74
82
|
*
|
|
75
83
|
* @param event - The NavigateEvent from the Navigation API
|
|
76
|
-
* @param
|
|
84
|
+
* @param info - Information about the navigation including matched routes and whether it will be intercepted
|
|
77
85
|
*/
|
|
78
|
-
type OnNavigateCallback = (event: NavigateEvent,
|
|
86
|
+
type OnNavigateCallback = (event: NavigateEvent, info: OnNavigateInfo) => void;
|
|
79
87
|
/**
|
|
80
88
|
* Fallback mode when Navigation API is unavailable.
|
|
81
89
|
*
|
|
@@ -92,7 +100,7 @@ type RouterProps = {
|
|
|
92
100
|
* Call `event.preventDefault()` to prevent the router from handling this navigation.
|
|
93
101
|
*
|
|
94
102
|
* @param event - The NavigateEvent from the Navigation API
|
|
95
|
-
* @param
|
|
103
|
+
* @param info - Information about the navigation including matched routes and whether it will be intercepted
|
|
96
104
|
*/
|
|
97
105
|
onNavigate?: OnNavigateCallback;
|
|
98
106
|
/**
|
|
@@ -172,21 +180,91 @@ type UseBlockerOptions = {
|
|
|
172
180
|
*/
|
|
173
181
|
declare function useBlocker(options: UseBlockerOptions): void;
|
|
174
182
|
//#endregion
|
|
183
|
+
//#region src/hooks/useRouteParams.d.ts
|
|
184
|
+
/**
|
|
185
|
+
* Returns typed route parameters for the given route definition.
|
|
186
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
187
|
+
* in the current route hierarchy.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const userRoute = route({
|
|
192
|
+
* id: "user",
|
|
193
|
+
* path: "/users/:userId",
|
|
194
|
+
* component: UserPage,
|
|
195
|
+
* });
|
|
196
|
+
*
|
|
197
|
+
* function UserPage() {
|
|
198
|
+
* const params = useRouteParams(userRoute);
|
|
199
|
+
* // params is typed as { userId: string }
|
|
200
|
+
* return <div>User ID: {params.userId}</div>;
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
declare function useRouteParams<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteParams<T>;
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/hooks/useRouteState.d.ts
|
|
207
|
+
/**
|
|
208
|
+
* Returns typed navigation state for the given route definition.
|
|
209
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
210
|
+
* in the current route hierarchy.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```typescript
|
|
214
|
+
* type ScrollState = { scrollPos: number };
|
|
215
|
+
* const scrollRoute = routeState<ScrollState>()({
|
|
216
|
+
* id: "scroll",
|
|
217
|
+
* path: "/scroll",
|
|
218
|
+
* component: ScrollPage,
|
|
219
|
+
* });
|
|
220
|
+
*
|
|
221
|
+
* function ScrollPage() {
|
|
222
|
+
* const state = useRouteState(scrollRoute);
|
|
223
|
+
* // state is typed as ScrollState | undefined
|
|
224
|
+
* return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
|
|
225
|
+
* }
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
declare function useRouteState<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteState<T> | undefined;
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/hooks/useRouteData.d.ts
|
|
231
|
+
/**
|
|
232
|
+
* Returns typed loader data for the given route definition.
|
|
233
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
234
|
+
* in the current route hierarchy.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const userRoute = route({
|
|
239
|
+
* id: "user",
|
|
240
|
+
* path: "/users/:userId",
|
|
241
|
+
* loader: async ({ params }) => {
|
|
242
|
+
* const res = await fetch(`/api/users/${params.userId}`);
|
|
243
|
+
* return res.json() as Promise<{ name: string; age: number }>;
|
|
244
|
+
* },
|
|
245
|
+
* component: UserPage,
|
|
246
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* function UserPage() {
|
|
249
|
+
* const data = useRouteData(userRoute);
|
|
250
|
+
* // data is typed as Promise<{ name: string; age: number }>
|
|
251
|
+
* return <div>User: {data.name}</div>;
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteData<T>;
|
|
256
|
+
//#endregion
|
|
175
257
|
//#region src/core/RouterAdapter.d.ts
|
|
176
258
|
/**
|
|
177
259
|
* Represents the current location state.
|
|
178
260
|
* Abstracts NavigationHistoryEntry for static mode compatibility.
|
|
179
261
|
*/
|
|
180
262
|
type LocationEntry = {
|
|
181
|
-
/** The current URL */
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
key: string;
|
|
185
|
-
/** State associated with this entry */
|
|
186
|
-
state: unknown;
|
|
187
|
-
/** Ephemeral info from current navigation (undefined if not from navigation event) */
|
|
263
|
+
/** The current URL */url: URL; /** Unique key for this entry (used for loader caching) */
|
|
264
|
+
key: string; /** State associated with this entry */
|
|
265
|
+
state: unknown; /** Ephemeral info from current navigation (undefined if not from navigation event) */
|
|
188
266
|
info: unknown;
|
|
189
267
|
};
|
|
190
268
|
//#endregion
|
|
191
|
-
export { type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type UseBlockerOptions, route, routeState, useBlocker, useLocation, useNavigate, useSearchParams };
|
|
269
|
+
export { type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
|
|
192
270
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/core/RouterAdapter.ts"],"mappings":";;;;cAGM,6BAAA;;AAFwD;;;;;;;KAgBlD,uBAAA;EAAA,CACT,6BAAA,UA4Bc;EA1Bf,IAAA,WAwBI;EAtBJ,QAAA,GAAW,uBAAA;EAiCE;;;;;;EA1Bb,KAAA;EAMA;;;;;EAAA,eAAA,YASI;EAHJ,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,+BAKrB;EAHN,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;;KAmBM,YAAA;EAMV,oCAJA,KAAA,EAAO,uBAAA,EAIC;EAFR,MAAA,EAAQ,MAAA,kBAQsB;EAN9B,QAAA;AAAA;;AAcF;;KARY,oBAAA,GAAuB,YAAA;EAUH,6DAR9B,IAAA;AAAA;;;;KAMU,cAAA;EAUe,4DARzB,OAAA,WAAkB,YAAA,WAQO;EANzB,YAAA;AAAA;;;;KAMU,eAAA;EAYQ,uDAVlB,OAAA,YAUkB;EARlB,KAAA,YAUA;EARA,IAAA;AAAA;;AAmBF;;KAbY,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;AAqBF;;;;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;ACvGR;;;KDgHY,YAAA;;;KChHA,WAAA;EACV,MAAA,EAAQ,eAAA;ED1BoC;;;;AAc9C;;;ECoBE,UAAA,GAAa,kBAAA;EDfF;;;;;;ECsBX,QAAA,GAAW,YAAA;AAAA;AAAA,iBAGG,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;ADlD6C;iBEM9C,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,IAAgB,EAAA,UAAY,OAAA,GAAU,eAAA;;;;;;iBCAtC,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ANFkD;EMO5D,WAAA;AAAA;;;ANSF;;;;;;;;;;;;;;;;;;;;;;;;;iBMqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ANrC0B;;;;;AAgB9D;;;;;;;;;;;;iBOSgB,cAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;APhC8B;;;;;AAgB9D;;;;;;;;;;;;;iBQUgB,aAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;ARjC+B;;;;;AAgB9D;;;;;;;;;;;;;;;;iBSagB,YAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;ATpCgC;KUSlD,aAAA;wBAEV,GAAA,EAAK,GAAA,EVTuC;EUW5C,GAAA,UVGiC;EUDjC,KAAA,WVEC;EUAD,IAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { n as routeState, t as route } from "./route-
|
|
3
|
+
import { n as routeState, t as route } from "./route-p_gr5yPI.mjs";
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
|
|
@@ -10,6 +10,18 @@ const RouterContext = createContext(null);
|
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/context/RouteContext.ts
|
|
12
12
|
const RouteContext = createContext(null);
|
|
13
|
+
/**
|
|
14
|
+
* Find a route context by ID in the ancestor chain.
|
|
15
|
+
* Returns the matching context or null if not found.
|
|
16
|
+
*/
|
|
17
|
+
function findRouteContextById(context, id) {
|
|
18
|
+
let current = context;
|
|
19
|
+
while (current !== null) {
|
|
20
|
+
if (current.id === id) return current;
|
|
21
|
+
current = current.parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
13
25
|
|
|
14
26
|
//#endregion
|
|
15
27
|
//#region src/context/BlockerContext.ts
|
|
@@ -54,8 +66,8 @@ function internalRoutes(routes) {
|
|
|
54
66
|
* Returns null if no match is found.
|
|
55
67
|
*/
|
|
56
68
|
function matchRoutes(routes, pathname) {
|
|
57
|
-
for (const route
|
|
58
|
-
const matched = matchRoute(route
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
const matched = matchRoute(route, pathname);
|
|
59
71
|
if (matched) return matched;
|
|
60
72
|
}
|
|
61
73
|
return null;
|
|
@@ -63,12 +75,29 @@ function matchRoutes(routes, pathname) {
|
|
|
63
75
|
/**
|
|
64
76
|
* Match a single route and its children recursively.
|
|
65
77
|
*/
|
|
66
|
-
function matchRoute(route
|
|
67
|
-
const hasChildren = Boolean(route
|
|
68
|
-
|
|
78
|
+
function matchRoute(route, pathname) {
|
|
79
|
+
const hasChildren = Boolean(route.children?.length);
|
|
80
|
+
if (route.path === void 0) {
|
|
81
|
+
const result = {
|
|
82
|
+
route,
|
|
83
|
+
params: {},
|
|
84
|
+
pathname: ""
|
|
85
|
+
};
|
|
86
|
+
if (hasChildren) {
|
|
87
|
+
for (const child of route.children) {
|
|
88
|
+
const childMatch = matchRoute(child, pathname);
|
|
89
|
+
if (childMatch) return [result, ...childMatch];
|
|
90
|
+
}
|
|
91
|
+
if (route.component && route.requireChildren === false) return [result];
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return [result];
|
|
95
|
+
}
|
|
96
|
+
const isExact = route.exact ?? !hasChildren;
|
|
97
|
+
const { matched, params, consumedPathname } = matchPath(route.path, pathname, isExact);
|
|
69
98
|
if (!matched) return null;
|
|
70
99
|
const result = {
|
|
71
|
-
route
|
|
100
|
+
route,
|
|
72
101
|
params,
|
|
73
102
|
pathname: consumedPathname
|
|
74
103
|
};
|
|
@@ -76,7 +105,7 @@ function matchRoute(route$1, pathname) {
|
|
|
76
105
|
let remainingPathname = pathname.slice(consumedPathname.length);
|
|
77
106
|
if (!remainingPathname.startsWith("/")) remainingPathname = "/" + remainingPathname;
|
|
78
107
|
if (remainingPathname === "") remainingPathname = "/";
|
|
79
|
-
for (const child of route
|
|
108
|
+
for (const child of route.children) {
|
|
80
109
|
const childMatch = matchRoute(child, remainingPathname);
|
|
81
110
|
if (childMatch) return [result, ...childMatch.map((m) => ({
|
|
82
111
|
...m,
|
|
@@ -86,7 +115,7 @@ function matchRoute(route$1, pathname) {
|
|
|
86
115
|
}
|
|
87
116
|
}))];
|
|
88
117
|
}
|
|
89
|
-
if (route
|
|
118
|
+
if (route.component && route.requireChildren === false) return [result];
|
|
90
119
|
return null;
|
|
91
120
|
}
|
|
92
121
|
return [result];
|
|
@@ -133,10 +162,10 @@ const loaderCache = /* @__PURE__ */ new Map();
|
|
|
133
162
|
* Get or create a loader result from cache.
|
|
134
163
|
* If the result is not cached, executes the loader and caches the result.
|
|
135
164
|
*/
|
|
136
|
-
function getOrCreateLoaderResult(entryId, matchIndex, route
|
|
137
|
-
if (!route
|
|
165
|
+
function getOrCreateLoaderResult(entryId, matchIndex, route, args) {
|
|
166
|
+
if (!route.loader) return;
|
|
138
167
|
const cacheKey = `${entryId}:${matchIndex}`;
|
|
139
|
-
if (!loaderCache.has(cacheKey)) loaderCache.set(cacheKey, route
|
|
168
|
+
if (!loaderCache.has(cacheKey)) loaderCache.set(cacheKey, route.loader(args));
|
|
140
169
|
return loaderCache.get(cacheKey);
|
|
141
170
|
}
|
|
142
171
|
/**
|
|
@@ -151,8 +180,8 @@ function createLoaderRequest(url) {
|
|
|
151
180
|
*/
|
|
152
181
|
function executeLoaders(matchedRoutes, entryId, request, signal) {
|
|
153
182
|
return matchedRoutes.map((match, index) => {
|
|
154
|
-
const { route
|
|
155
|
-
const data = getOrCreateLoaderResult(entryId, index, route
|
|
183
|
+
const { route, params } = match;
|
|
184
|
+
const data = getOrCreateLoaderResult(entryId, index, route, {
|
|
156
185
|
params,
|
|
157
186
|
request,
|
|
158
187
|
signal
|
|
@@ -255,17 +284,23 @@ var NavigationAPIAdapter = class {
|
|
|
255
284
|
return;
|
|
256
285
|
}
|
|
257
286
|
if (!event.canIntercept) {
|
|
258
|
-
onNavigate?.(event,
|
|
287
|
+
onNavigate?.(event, {
|
|
288
|
+
matches: [],
|
|
289
|
+
intercepting: false
|
|
290
|
+
});
|
|
259
291
|
return;
|
|
260
292
|
}
|
|
261
293
|
const url = new URL(event.destination.url);
|
|
262
294
|
const matched = matchRoutes(routes, url.pathname);
|
|
295
|
+
const willIntercept = matched !== null && !event.hashChange && event.downloadRequest === null;
|
|
263
296
|
if (onNavigate) {
|
|
264
|
-
onNavigate(event,
|
|
297
|
+
onNavigate(event, {
|
|
298
|
+
matches: matched,
|
|
299
|
+
intercepting: willIntercept
|
|
300
|
+
});
|
|
265
301
|
if (event.defaultPrevented) return;
|
|
266
302
|
}
|
|
267
|
-
if (
|
|
268
|
-
if (!matched) return;
|
|
303
|
+
if (!willIntercept) return;
|
|
269
304
|
if (idleController) {
|
|
270
305
|
idleController.abort();
|
|
271
306
|
idleController = null;
|
|
@@ -450,13 +485,14 @@ function Router({ routes: inputRoutes, onNavigate, fallback = "none" }) {
|
|
|
450
485
|
* Recursively render matched routes with proper context.
|
|
451
486
|
*/
|
|
452
487
|
function RouteRenderer({ matchedRoutes, index }) {
|
|
488
|
+
const parentRouteContext = useContext(RouteContext);
|
|
453
489
|
const match = matchedRoutes[index];
|
|
454
490
|
if (!match) return null;
|
|
455
|
-
const { route
|
|
491
|
+
const { route, params, pathname, data } = match;
|
|
456
492
|
const routerContext = useContext(RouterContext);
|
|
457
493
|
if (!routerContext) throw new Error("RouteRenderer must be used within RouterContext");
|
|
458
494
|
const { locationEntry, url, navigateAsync, updateCurrentEntryState } = routerContext;
|
|
459
|
-
const routeState
|
|
495
|
+
const routeState = locationEntry.state?.__routeStates?.[index];
|
|
460
496
|
const setStateSync = useCallback((stateOrUpdater) => {
|
|
461
497
|
const currentStates = locationEntry.state?.__routeStates ?? [];
|
|
462
498
|
const currentRouteState = currentStates[index];
|
|
@@ -498,28 +534,37 @@ function RouteRenderer({ matchedRoutes, index }) {
|
|
|
498
534
|
matchedRoutes,
|
|
499
535
|
index: index + 1
|
|
500
536
|
}) : null;
|
|
537
|
+
const routeId = route.id;
|
|
501
538
|
const routeContextValue = useMemo(() => ({
|
|
539
|
+
id: routeId,
|
|
502
540
|
params,
|
|
503
541
|
matchedPath: pathname,
|
|
504
|
-
|
|
542
|
+
state: routeState,
|
|
543
|
+
data,
|
|
544
|
+
outlet,
|
|
545
|
+
parent: parentRouteContext
|
|
505
546
|
}), [
|
|
547
|
+
routeId,
|
|
506
548
|
params,
|
|
507
549
|
pathname,
|
|
508
|
-
|
|
550
|
+
routeState,
|
|
551
|
+
data,
|
|
552
|
+
outlet,
|
|
553
|
+
parentRouteContext
|
|
509
554
|
]);
|
|
510
555
|
const renderComponent = () => {
|
|
511
|
-
const componentOrElement = route
|
|
556
|
+
const componentOrElement = route.component;
|
|
512
557
|
if (componentOrElement == null) return outlet;
|
|
513
558
|
if (typeof componentOrElement !== "function") return componentOrElement;
|
|
514
559
|
const Component = componentOrElement;
|
|
515
560
|
const stateProps = {
|
|
516
|
-
state: routeState
|
|
561
|
+
state: routeState,
|
|
517
562
|
setState,
|
|
518
563
|
setStateSync,
|
|
519
564
|
resetState
|
|
520
565
|
};
|
|
521
566
|
const { info } = locationEntry;
|
|
522
|
-
if (route
|
|
567
|
+
if (route.loader) return /* @__PURE__ */ jsx(Component, {
|
|
523
568
|
data,
|
|
524
569
|
params,
|
|
525
570
|
...stateProps,
|
|
@@ -644,5 +689,113 @@ function useBlocker(options) {
|
|
|
644
689
|
}
|
|
645
690
|
|
|
646
691
|
//#endregion
|
|
647
|
-
|
|
692
|
+
//#region src/hooks/useRouteContext.ts
|
|
693
|
+
/**
|
|
694
|
+
* Internal hook that returns the RouteContextValue for the given route.
|
|
695
|
+
* If the route has an ID, it searches the ancestor chain for a matching route.
|
|
696
|
+
* If no ID is provided, it returns the current (nearest) route context.
|
|
697
|
+
*
|
|
698
|
+
* @param hookName - Name of the calling hook (for error messages)
|
|
699
|
+
* @param routeId - Optional route ID to search for in the ancestor chain
|
|
700
|
+
* @returns The matching RouteContextValue
|
|
701
|
+
* @throws If called outside a route component or if the route ID is not found
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
function useRouteContext(hookName, routeId) {
|
|
705
|
+
const context = useContext(RouteContext);
|
|
706
|
+
if (!context) throw new Error(`${hookName} must be used within a route component`);
|
|
707
|
+
if (routeId === void 0) return context;
|
|
708
|
+
const matchedContext = findRouteContextById(context, routeId);
|
|
709
|
+
if (!matchedContext) throw new Error(`${hookName}: Route ID "${routeId}" not found in current route hierarchy. Current route is "${context.id ?? "(no id)"}"`);
|
|
710
|
+
return matchedContext;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/hooks/useRouteParams.ts
|
|
715
|
+
/**
|
|
716
|
+
* Returns typed route parameters for the given route definition.
|
|
717
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
718
|
+
* in the current route hierarchy.
|
|
719
|
+
*
|
|
720
|
+
* @example
|
|
721
|
+
* ```typescript
|
|
722
|
+
* const userRoute = route({
|
|
723
|
+
* id: "user",
|
|
724
|
+
* path: "/users/:userId",
|
|
725
|
+
* component: UserPage,
|
|
726
|
+
* });
|
|
727
|
+
*
|
|
728
|
+
* function UserPage() {
|
|
729
|
+
* const params = useRouteParams(userRoute);
|
|
730
|
+
* // params is typed as { userId: string }
|
|
731
|
+
* return <div>User ID: {params.userId}</div>;
|
|
732
|
+
* }
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
function useRouteParams(route) {
|
|
736
|
+
const routeId = route.id;
|
|
737
|
+
return useRouteContext("useRouteParams", routeId).params;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/hooks/useRouteState.ts
|
|
742
|
+
/**
|
|
743
|
+
* Returns typed navigation state for the given route definition.
|
|
744
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
745
|
+
* in the current route hierarchy.
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* ```typescript
|
|
749
|
+
* type ScrollState = { scrollPos: number };
|
|
750
|
+
* const scrollRoute = routeState<ScrollState>()({
|
|
751
|
+
* id: "scroll",
|
|
752
|
+
* path: "/scroll",
|
|
753
|
+
* component: ScrollPage,
|
|
754
|
+
* });
|
|
755
|
+
*
|
|
756
|
+
* function ScrollPage() {
|
|
757
|
+
* const state = useRouteState(scrollRoute);
|
|
758
|
+
* // state is typed as ScrollState | undefined
|
|
759
|
+
* return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
|
|
760
|
+
* }
|
|
761
|
+
* ```
|
|
762
|
+
*/
|
|
763
|
+
function useRouteState(route) {
|
|
764
|
+
const routeId = route.id;
|
|
765
|
+
return useRouteContext("useRouteState", routeId).state;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/hooks/useRouteData.ts
|
|
770
|
+
/**
|
|
771
|
+
* Returns typed loader data for the given route definition.
|
|
772
|
+
* Throws an error if called outside a matching route or if the route ID is not found
|
|
773
|
+
* in the current route hierarchy.
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```typescript
|
|
777
|
+
* const userRoute = route({
|
|
778
|
+
* id: "user",
|
|
779
|
+
* path: "/users/:userId",
|
|
780
|
+
* loader: async ({ params }) => {
|
|
781
|
+
* const res = await fetch(`/api/users/${params.userId}`);
|
|
782
|
+
* return res.json() as Promise<{ name: string; age: number }>;
|
|
783
|
+
* },
|
|
784
|
+
* component: UserPage,
|
|
785
|
+
* });
|
|
786
|
+
*
|
|
787
|
+
* function UserPage() {
|
|
788
|
+
* const data = useRouteData(userRoute);
|
|
789
|
+
* // data is typed as Promise<{ name: string; age: number }>
|
|
790
|
+
* return <div>User: {data.name}</div>;
|
|
791
|
+
* }
|
|
792
|
+
* ```
|
|
793
|
+
*/
|
|
794
|
+
function useRouteData(route) {
|
|
795
|
+
const routeId = route.id;
|
|
796
|
+
return useRouteContext("useRouteData", routeId).data;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
//#endregion
|
|
800
|
+
export { Outlet, Router, route, routeState, useBlocker, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
|
|
648
801
|
//# sourceMappingURL=index.mjs.map
|