router-kit 1.3.4 → 2.0.1
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/README.md +117 -421
- package/dist/components/Link.d.ts +23 -6
- package/dist/components/Link.js +51 -6
- package/dist/components/NavLink.d.ts +44 -7
- package/dist/components/NavLink.js +111 -10
- package/dist/components/Outlet.d.ts +66 -0
- package/dist/components/Outlet.js +69 -0
- package/dist/components/Router.d.ts +57 -11
- package/dist/components/Router.js +60 -12
- package/dist/components/route.d.ts +57 -7
- package/dist/components/route.js +35 -5
- package/dist/context/OutletContext.d.ts +41 -0
- package/dist/context/OutletContext.js +31 -0
- package/dist/context/RouterContext.d.ts +9 -0
- package/dist/context/RouterContext.js +21 -1
- package/dist/context/RouterProvider.d.ts +15 -4
- package/dist/context/RouterProvider.js +441 -101
- package/dist/core/createRouter.d.ts +65 -0
- package/dist/core/createRouter.js +129 -7
- package/dist/hooks/useBlocker.d.ts +65 -0
- package/dist/hooks/useBlocker.js +152 -0
- package/dist/hooks/useDynamicComponents.d.ts +61 -2
- package/dist/hooks/useDynamicComponents.js +89 -17
- package/dist/hooks/useLoaderData.d.ts +98 -0
- package/dist/hooks/useLoaderData.js +107 -0
- package/dist/hooks/useLocation.d.ts +37 -0
- package/dist/hooks/useLocation.js +130 -2
- package/dist/hooks/useMatches.d.ts +99 -0
- package/dist/hooks/useMatches.js +114 -0
- package/dist/hooks/useNavigate.d.ts +59 -0
- package/dist/hooks/useNavigate.js +70 -0
- package/dist/hooks/useParams.d.ts +57 -2
- package/dist/hooks/useParams.js +60 -14
- package/dist/hooks/useQuery.d.ts +53 -3
- package/dist/hooks/useQuery.js +107 -8
- package/dist/hooks/useRouter.d.ts +34 -0
- package/dist/hooks/useRouter.js +35 -1
- package/dist/index.d.ts +16 -6
- package/dist/index.js +21 -5
- package/dist/ssr/StaticRouter.d.ts +65 -0
- package/dist/ssr/StaticRouter.js +322 -0
- package/dist/ssr/hydrateRouter.d.ts +44 -0
- package/dist/ssr/hydrateRouter.js +60 -0
- package/dist/ssr/index.d.ts +92 -0
- package/dist/ssr/index.js +92 -0
- package/dist/ssr/serverUtils.d.ts +107 -0
- package/dist/ssr/serverUtils.js +288 -0
- package/dist/types/index.d.ts +201 -2
- package/package.json +14 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Route, RouteMatch, RouteMeta } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Result from matching routes on the server
|
|
4
|
+
*/
|
|
5
|
+
export interface ServerMatchResult {
|
|
6
|
+
/** Matched route path */
|
|
7
|
+
matches: RouteMatch[];
|
|
8
|
+
/** Extracted route params */
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
/** Redirect URL if route redirects */
|
|
11
|
+
redirect?: string;
|
|
12
|
+
/** HTTP status code */
|
|
13
|
+
statusCode: number;
|
|
14
|
+
/** Route metadata */
|
|
15
|
+
meta?: RouteMeta;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result from data loading on the server
|
|
19
|
+
*/
|
|
20
|
+
export interface ServerLoaderResult<T = any> {
|
|
21
|
+
/** Loaded data keyed by route path */
|
|
22
|
+
data: Record<string, T>;
|
|
23
|
+
/** Errors encountered during loading */
|
|
24
|
+
errors: Record<string, Error>;
|
|
25
|
+
/** Time taken to load data (ms) */
|
|
26
|
+
loadTime: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Match routes for a given URL on the server
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const result = matchServerRoutes(routes, '/users/123');
|
|
34
|
+
* if (result.redirect) {
|
|
35
|
+
* return res.redirect(result.redirect);
|
|
36
|
+
* }
|
|
37
|
+
* console.log(result.params); // { id: '123' }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchServerRoutes(routes: Route[], pathname: string, parentPath?: string): ServerMatchResult;
|
|
41
|
+
/**
|
|
42
|
+
* Prefetch loader data for matched routes
|
|
43
|
+
*
|
|
44
|
+
* This function runs all route loaders in parallel and returns
|
|
45
|
+
* the combined data. Use this on the server before rendering.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // server.ts
|
|
50
|
+
* app.get('*', async (req, res) => {
|
|
51
|
+
* const matchResult = matchServerRoutes(routes, req.url);
|
|
52
|
+
*
|
|
53
|
+
* if (matchResult.redirect) {
|
|
54
|
+
* return res.redirect(matchResult.redirect);
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* const loaderResult = await prefetchLoaderData(
|
|
58
|
+
* matchResult.matches,
|
|
59
|
+
* req.url,
|
|
60
|
+
* { headers: req.headers }
|
|
61
|
+
* );
|
|
62
|
+
*
|
|
63
|
+
* const html = renderToString(
|
|
64
|
+
* <StaticRouter
|
|
65
|
+
* routes={routes}
|
|
66
|
+
* location={req.url}
|
|
67
|
+
* loaderData={loaderResult.data}
|
|
68
|
+
* />
|
|
69
|
+
* );
|
|
70
|
+
*
|
|
71
|
+
* // Inject loader data for hydration
|
|
72
|
+
* const finalHtml = html.replace(
|
|
73
|
+
* '</head>',
|
|
74
|
+
* `<script>window.__LOADER_DATA__ = ${JSON.stringify(loaderResult.data)}</script></head>`
|
|
75
|
+
* );
|
|
76
|
+
*
|
|
77
|
+
* res.send(finalHtml);
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function prefetchLoaderData(matches: RouteMatch[], url: string, requestInit?: RequestInit): Promise<ServerLoaderResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Create a Request object from Node.js IncomingMessage
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { createRequestFromNode } from 'router-kit/ssr';
|
|
88
|
+
*
|
|
89
|
+
* app.get('*', (req, res) => {
|
|
90
|
+
* const request = createRequestFromNode(req);
|
|
91
|
+
* // Use request with loaders
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function createRequestFromNode(nodeRequest: {
|
|
96
|
+
url?: string;
|
|
97
|
+
method?: string;
|
|
98
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
99
|
+
}, baseUrl?: string): Request;
|
|
100
|
+
/**
|
|
101
|
+
* Generate script tag for hydrating loader data on the client
|
|
102
|
+
*/
|
|
103
|
+
export declare function getLoaderDataScript(data: Record<string, any>): string;
|
|
104
|
+
/**
|
|
105
|
+
* Get loader data from window on the client side
|
|
106
|
+
*/
|
|
107
|
+
export declare function getHydratedLoaderData(): Record<string, any> | null;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract params from a path using a pattern
|
|
3
|
+
*/
|
|
4
|
+
const extractParams = (pattern, pathname) => {
|
|
5
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
6
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
7
|
+
if (patternParts.length !== pathParts.length) {
|
|
8
|
+
const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
|
|
9
|
+
if (!hasCatchAll)
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const params = {};
|
|
13
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
14
|
+
const patternPart = patternParts[i];
|
|
15
|
+
const pathPart = pathParts[i];
|
|
16
|
+
if (patternPart.startsWith("*")) {
|
|
17
|
+
const paramName = patternPart.slice(1) || "splat";
|
|
18
|
+
params[paramName] = pathParts.slice(i).join("/");
|
|
19
|
+
return params;
|
|
20
|
+
}
|
|
21
|
+
if (patternPart.startsWith(":")) {
|
|
22
|
+
const paramName = patternPart.slice(1);
|
|
23
|
+
if (paramName.endsWith("?")) {
|
|
24
|
+
params[paramName.slice(0, -1)] = pathPart !== null && pathPart !== void 0 ? pathPart : "";
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (pathPart === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
params[paramName] = pathPart;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (patternPart !== pathPart)
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return params;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Join paths safely
|
|
40
|
+
*/
|
|
41
|
+
const joinPaths = (parent, child) => {
|
|
42
|
+
const normalizedParent = parent.endsWith("/") ? parent.slice(0, -1) : parent;
|
|
43
|
+
const normalizedChild = child.startsWith("/") ? child : `/${child}`;
|
|
44
|
+
return `${normalizedParent}${normalizedChild}`;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Normalize path to string (handles array paths)
|
|
48
|
+
*/
|
|
49
|
+
const normalizePath = (path) => {
|
|
50
|
+
if (Array.isArray(path)) {
|
|
51
|
+
return path.map((p) => (p.startsWith("/") ? p.slice(1) : p)).join("|");
|
|
52
|
+
}
|
|
53
|
+
return path;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Get the first path from a path (string or array)
|
|
57
|
+
*/
|
|
58
|
+
const getFirstPath = (path) => {
|
|
59
|
+
if (Array.isArray(path)) {
|
|
60
|
+
return path[0] || "";
|
|
61
|
+
}
|
|
62
|
+
// Handle pipe-separated paths (already normalized)
|
|
63
|
+
if (path.includes("|")) {
|
|
64
|
+
return path.split("|")[0];
|
|
65
|
+
}
|
|
66
|
+
return path;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Match routes for a given URL on the server
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const result = matchServerRoutes(routes, '/users/123');
|
|
74
|
+
* if (result.redirect) {
|
|
75
|
+
* return res.redirect(result.redirect);
|
|
76
|
+
* }
|
|
77
|
+
* console.log(result.params); // { id: '123' }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function matchServerRoutes(routes, pathname, parentPath = "/") {
|
|
81
|
+
const matches = [];
|
|
82
|
+
// Sort routes by priority
|
|
83
|
+
const staticRoutes = [];
|
|
84
|
+
const dynamicRoutes = [];
|
|
85
|
+
const catchAllRoutes = [];
|
|
86
|
+
for (const route of routes) {
|
|
87
|
+
const is404 = route.path === "404" || route.path === "/404";
|
|
88
|
+
if (is404)
|
|
89
|
+
continue;
|
|
90
|
+
const pathArray = Array.isArray(route.path) ? route.path : [route.path];
|
|
91
|
+
const hasCatchAll = pathArray.some((p) => p.includes("*"));
|
|
92
|
+
const hasDynamicParams = pathArray.some((p) => p.includes(":"));
|
|
93
|
+
if (hasCatchAll) {
|
|
94
|
+
catchAllRoutes.push(route);
|
|
95
|
+
}
|
|
96
|
+
else if (hasDynamicParams) {
|
|
97
|
+
dynamicRoutes.push(route);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
staticRoutes.push(route);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const orderedRoutes = [...staticRoutes, ...dynamicRoutes, ...catchAllRoutes];
|
|
104
|
+
for (const route of orderedRoutes) {
|
|
105
|
+
const normalizedRoutePath = normalizePath(route.path);
|
|
106
|
+
const firstPath = getFirstPath(route.path);
|
|
107
|
+
const fullPath = joinPaths(parentPath, firstPath);
|
|
108
|
+
const patterns = normalizedRoutePath.split("|");
|
|
109
|
+
for (const pattern of patterns) {
|
|
110
|
+
const fullPattern = joinPaths(parentPath, pattern);
|
|
111
|
+
const extractedParams = extractParams(fullPattern, pathname);
|
|
112
|
+
if (extractedParams !== null) {
|
|
113
|
+
// Handle redirects
|
|
114
|
+
if (route.redirectTo) {
|
|
115
|
+
return {
|
|
116
|
+
matches: [],
|
|
117
|
+
params: extractedParams,
|
|
118
|
+
redirect: route.redirectTo,
|
|
119
|
+
statusCode: 302,
|
|
120
|
+
meta: route.meta,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const match = {
|
|
124
|
+
route,
|
|
125
|
+
params: extractedParams,
|
|
126
|
+
pathname,
|
|
127
|
+
pathnameBase: parentPath,
|
|
128
|
+
pattern: fullPattern,
|
|
129
|
+
};
|
|
130
|
+
matches.push(match);
|
|
131
|
+
// Check nested routes
|
|
132
|
+
if (route.children && route.children.length > 0) {
|
|
133
|
+
const childResult = matchServerRoutes(route.children, pathname, fullPath);
|
|
134
|
+
if (childResult.redirect) {
|
|
135
|
+
return childResult;
|
|
136
|
+
}
|
|
137
|
+
if (childResult.matches.length > 0) {
|
|
138
|
+
return {
|
|
139
|
+
matches: [...matches, ...childResult.matches],
|
|
140
|
+
params: { ...extractedParams, ...childResult.params },
|
|
141
|
+
statusCode: childResult.statusCode,
|
|
142
|
+
meta: childResult.meta || route.meta,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
matches,
|
|
148
|
+
params: extractedParams,
|
|
149
|
+
statusCode: 200,
|
|
150
|
+
meta: route.meta,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Check children even if parent doesn't match
|
|
155
|
+
if (route.children) {
|
|
156
|
+
const firstPath = getFirstPath(route.path);
|
|
157
|
+
const childFullPath = joinPaths(parentPath, firstPath);
|
|
158
|
+
const childResult = matchServerRoutes(route.children, pathname, childFullPath);
|
|
159
|
+
if (childResult.matches.length > 0 || childResult.redirect) {
|
|
160
|
+
return childResult;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// No match found
|
|
165
|
+
return {
|
|
166
|
+
matches: [],
|
|
167
|
+
params: {},
|
|
168
|
+
statusCode: 404,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Prefetch loader data for matched routes
|
|
173
|
+
*
|
|
174
|
+
* This function runs all route loaders in parallel and returns
|
|
175
|
+
* the combined data. Use this on the server before rendering.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* // server.ts
|
|
180
|
+
* app.get('*', async (req, res) => {
|
|
181
|
+
* const matchResult = matchServerRoutes(routes, req.url);
|
|
182
|
+
*
|
|
183
|
+
* if (matchResult.redirect) {
|
|
184
|
+
* return res.redirect(matchResult.redirect);
|
|
185
|
+
* }
|
|
186
|
+
*
|
|
187
|
+
* const loaderResult = await prefetchLoaderData(
|
|
188
|
+
* matchResult.matches,
|
|
189
|
+
* req.url,
|
|
190
|
+
* { headers: req.headers }
|
|
191
|
+
* );
|
|
192
|
+
*
|
|
193
|
+
* const html = renderToString(
|
|
194
|
+
* <StaticRouter
|
|
195
|
+
* routes={routes}
|
|
196
|
+
* location={req.url}
|
|
197
|
+
* loaderData={loaderResult.data}
|
|
198
|
+
* />
|
|
199
|
+
* );
|
|
200
|
+
*
|
|
201
|
+
* // Inject loader data for hydration
|
|
202
|
+
* const finalHtml = html.replace(
|
|
203
|
+
* '</head>',
|
|
204
|
+
* `<script>window.__LOADER_DATA__ = ${JSON.stringify(loaderResult.data)}</script></head>`
|
|
205
|
+
* );
|
|
206
|
+
*
|
|
207
|
+
* res.send(finalHtml);
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export async function prefetchLoaderData(matches, url, requestInit) {
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
const data = {};
|
|
214
|
+
const errors = {};
|
|
215
|
+
const loaderPromises = matches
|
|
216
|
+
.filter((match) => match.route.loader)
|
|
217
|
+
.map(async (match) => {
|
|
218
|
+
const routePath = match.pattern;
|
|
219
|
+
const abortController = new AbortController();
|
|
220
|
+
try {
|
|
221
|
+
// Create a Request object for the loader
|
|
222
|
+
const request = new Request(url, {
|
|
223
|
+
...requestInit,
|
|
224
|
+
signal: abortController.signal,
|
|
225
|
+
});
|
|
226
|
+
const loaderArgs = {
|
|
227
|
+
params: match.params,
|
|
228
|
+
request,
|
|
229
|
+
signal: abortController.signal,
|
|
230
|
+
};
|
|
231
|
+
const result = await match.route.loader(loaderArgs);
|
|
232
|
+
data[routePath] = result;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
errors[routePath] =
|
|
236
|
+
error instanceof Error ? error : new Error(String(error));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
await Promise.all(loaderPromises);
|
|
240
|
+
return {
|
|
241
|
+
data,
|
|
242
|
+
errors,
|
|
243
|
+
loadTime: Date.now() - startTime,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Create a Request object from Node.js IncomingMessage
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* import { createRequestFromNode } from 'router-kit/ssr';
|
|
252
|
+
*
|
|
253
|
+
* app.get('*', (req, res) => {
|
|
254
|
+
* const request = createRequestFromNode(req);
|
|
255
|
+
* // Use request with loaders
|
|
256
|
+
* });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export function createRequestFromNode(nodeRequest, baseUrl = "http://localhost") {
|
|
260
|
+
const url = new URL(nodeRequest.url || "/", baseUrl);
|
|
261
|
+
const headers = new Headers();
|
|
262
|
+
if (nodeRequest.headers) {
|
|
263
|
+
for (const [key, value] of Object.entries(nodeRequest.headers)) {
|
|
264
|
+
if (value) {
|
|
265
|
+
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return new Request(url.toString(), {
|
|
270
|
+
method: nodeRequest.method || "GET",
|
|
271
|
+
headers,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Generate script tag for hydrating loader data on the client
|
|
276
|
+
*/
|
|
277
|
+
export function getLoaderDataScript(data) {
|
|
278
|
+
const serialized = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
279
|
+
return `<script>window.__ROUTER_KIT_DATA__ = ${serialized}</script>`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get loader data from window on the client side
|
|
283
|
+
*/
|
|
284
|
+
export function getHydratedLoaderData() {
|
|
285
|
+
if (typeof window === "undefined")
|
|
286
|
+
return null;
|
|
287
|
+
return window.__ROUTER_KIT_DATA__ || null;
|
|
288
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,8 +1,70 @@
|
|
|
1
|
-
import { JSX } from "react";
|
|
1
|
+
import { ComponentType, JSX, LazyExoticComponent, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Route configuration interface
|
|
4
|
+
*/
|
|
2
5
|
export interface Route {
|
|
6
|
+
/** Path pattern(s) for the route */
|
|
3
7
|
path: string | string[];
|
|
8
|
+
/** Component to render */
|
|
4
9
|
component: JSX.Element;
|
|
10
|
+
/** Nested child routes */
|
|
5
11
|
children?: Route[];
|
|
12
|
+
/** Index route flag - renders when parent path matches exactly */
|
|
13
|
+
index?: boolean;
|
|
14
|
+
/** Lazy-loaded component */
|
|
15
|
+
lazy?: LazyExoticComponent<ComponentType<any>>;
|
|
16
|
+
/** Route loader function for data fetching */
|
|
17
|
+
loader?: RouteLoader;
|
|
18
|
+
/** Error boundary element for this route */
|
|
19
|
+
errorElement?: JSX.Element;
|
|
20
|
+
/** Redirect to another path */
|
|
21
|
+
redirectTo?: string;
|
|
22
|
+
/** Route guard function */
|
|
23
|
+
guard?: RouteGuard;
|
|
24
|
+
/** Route metadata */
|
|
25
|
+
meta?: RouteMeta;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Route loader function type
|
|
29
|
+
*/
|
|
30
|
+
export type RouteLoader<T = any> = (args: LoaderArgs) => Promise<T> | T;
|
|
31
|
+
/**
|
|
32
|
+
* Loader function arguments
|
|
33
|
+
*/
|
|
34
|
+
export interface LoaderArgs {
|
|
35
|
+
params: Record<string, string>;
|
|
36
|
+
request: Request;
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Route guard function type
|
|
41
|
+
*/
|
|
42
|
+
export type RouteGuard = (args: GuardArgs) => boolean | Promise<boolean> | string;
|
|
43
|
+
/**
|
|
44
|
+
* Guard function arguments
|
|
45
|
+
*/
|
|
46
|
+
export interface GuardArgs {
|
|
47
|
+
pathname: string;
|
|
48
|
+
params: Record<string, string>;
|
|
49
|
+
search: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Route metadata
|
|
53
|
+
*/
|
|
54
|
+
export interface RouteMeta {
|
|
55
|
+
title?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
[key: string]: any;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Match result from route matching
|
|
61
|
+
*/
|
|
62
|
+
export interface RouteMatch {
|
|
63
|
+
route: Route;
|
|
64
|
+
params: Record<string, string>;
|
|
65
|
+
pathname: string;
|
|
66
|
+
pathnameBase: string;
|
|
67
|
+
pattern: string;
|
|
6
68
|
}
|
|
7
69
|
export interface GetComponent {
|
|
8
70
|
(routes: Route[], currentPath: string, parentPath?: string): JSX.Element | null;
|
|
@@ -12,20 +74,96 @@ export interface Routes {
|
|
|
12
74
|
fullPath: string;
|
|
13
75
|
path: string;
|
|
14
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Navigation options
|
|
79
|
+
*/
|
|
15
80
|
export interface NavigateOptions {
|
|
81
|
+
/** Replace current history entry instead of pushing */
|
|
16
82
|
replace?: boolean;
|
|
83
|
+
/** State to pass with navigation */
|
|
17
84
|
state?: any;
|
|
85
|
+
/** Prevent scroll reset after navigation */
|
|
86
|
+
preventScrollReset?: boolean;
|
|
87
|
+
/** Relative navigation base */
|
|
88
|
+
relative?: "route" | "path";
|
|
18
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Navigation function type
|
|
92
|
+
*/
|
|
93
|
+
export type NavigateFunction = {
|
|
94
|
+
(to: string, options?: NavigateOptions): void;
|
|
95
|
+
(delta: number): void;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Router context type
|
|
99
|
+
*/
|
|
19
100
|
export interface RouterContextType {
|
|
101
|
+
/** Current pathname */
|
|
102
|
+
pathname: string;
|
|
103
|
+
/** Full path pattern with params (e.g., /users/:id) */
|
|
104
|
+
pattern: string;
|
|
105
|
+
/** Current search string */
|
|
106
|
+
search: string;
|
|
107
|
+
/** Current hash */
|
|
108
|
+
hash: string;
|
|
109
|
+
/** History state */
|
|
110
|
+
state: any;
|
|
111
|
+
/** Route parameters */
|
|
112
|
+
params: Record<string, string>;
|
|
113
|
+
/** Current route match */
|
|
114
|
+
matches: RouteMatch[];
|
|
115
|
+
/** Navigate function */
|
|
116
|
+
navigate: NavigateFunction;
|
|
117
|
+
/** Go back in history */
|
|
118
|
+
back: () => void;
|
|
119
|
+
/** Go forward in history */
|
|
120
|
+
forward: () => void;
|
|
121
|
+
/** Navigation in progress */
|
|
122
|
+
isNavigating: boolean;
|
|
123
|
+
/** Loader data from current route */
|
|
124
|
+
loaderData: any;
|
|
125
|
+
/** Current route meta */
|
|
126
|
+
meta: RouteMeta | null;
|
|
127
|
+
/** @deprecated Use pathname instead */
|
|
20
128
|
path: string;
|
|
129
|
+
/** @deprecated Use pattern instead */
|
|
21
130
|
fullPathWithParams: string;
|
|
22
|
-
navigate: (to: string, options?: NavigateOptions) => void;
|
|
23
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Location object
|
|
134
|
+
*/
|
|
24
135
|
export interface Location {
|
|
136
|
+
/** Current pathname */
|
|
25
137
|
pathname: string;
|
|
138
|
+
/** Query string including leading ? */
|
|
26
139
|
search: string;
|
|
140
|
+
/** Hash including leading # */
|
|
27
141
|
hash: string;
|
|
142
|
+
/** History state */
|
|
28
143
|
state: any;
|
|
144
|
+
/** Unique key for this location */
|
|
145
|
+
key: string;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* History action types
|
|
149
|
+
*/
|
|
150
|
+
export type HistoryAction = "POP" | "PUSH" | "REPLACE";
|
|
151
|
+
/**
|
|
152
|
+
* Navigation blocker function
|
|
153
|
+
*/
|
|
154
|
+
export type BlockerFunction = (args: {
|
|
155
|
+
currentLocation: Location;
|
|
156
|
+
nextLocation: Location;
|
|
157
|
+
action: HistoryAction;
|
|
158
|
+
}) => boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Blocker state
|
|
161
|
+
*/
|
|
162
|
+
export interface Blocker {
|
|
163
|
+
state: "blocked" | "proceeding" | "unblocked";
|
|
164
|
+
proceed: () => void;
|
|
165
|
+
reset: () => void;
|
|
166
|
+
location?: Location;
|
|
29
167
|
}
|
|
30
168
|
export interface RouterError extends Error {
|
|
31
169
|
code: "NAVIGATION_ABORTED" | "ROUTER_NOT_FOUND" | "INVALID_ROUTE";
|
|
@@ -33,4 +171,65 @@ export interface RouterError extends Error {
|
|
|
33
171
|
export interface DynamicComponents {
|
|
34
172
|
(dynamicComponentsObject: Record<string, JSX.Element>, variationParam: string): JSX.Element;
|
|
35
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Link component props
|
|
176
|
+
*/
|
|
177
|
+
export interface LinkProps {
|
|
178
|
+
/** Target path */
|
|
179
|
+
to: string;
|
|
180
|
+
/** Children to render */
|
|
181
|
+
children: ReactNode;
|
|
182
|
+
/** CSS class name */
|
|
183
|
+
className?: string;
|
|
184
|
+
/** Navigation options */
|
|
185
|
+
replace?: boolean;
|
|
186
|
+
/** State to pass */
|
|
187
|
+
state?: any;
|
|
188
|
+
/** Prevent scroll reset */
|
|
189
|
+
preventScrollReset?: boolean;
|
|
190
|
+
/** Target attribute */
|
|
191
|
+
target?: string;
|
|
192
|
+
/** Rel attribute */
|
|
193
|
+
rel?: string;
|
|
194
|
+
/** Title attribute */
|
|
195
|
+
title?: string;
|
|
196
|
+
/** onClick handler */
|
|
197
|
+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* NavLink component props
|
|
201
|
+
*/
|
|
202
|
+
export interface NavLinkProps extends LinkProps {
|
|
203
|
+
/** Class name when active */
|
|
204
|
+
activeClassName?: string;
|
|
205
|
+
/** Style when active */
|
|
206
|
+
activeStyle?: React.CSSProperties;
|
|
207
|
+
/** Custom active check function */
|
|
208
|
+
isActive?: (match: RouteMatch | null, location: Location) => boolean;
|
|
209
|
+
/** Match end of path (exact matching) */
|
|
210
|
+
end?: boolean;
|
|
211
|
+
/** Case sensitive matching */
|
|
212
|
+
caseSensitive?: boolean;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Router provider props
|
|
216
|
+
*/
|
|
217
|
+
export interface RouterProviderProps {
|
|
218
|
+
routes: Route[];
|
|
219
|
+
/** Base path for all routes */
|
|
220
|
+
basename?: string;
|
|
221
|
+
/** Initial entries for memory history (SSR) */
|
|
222
|
+
initialEntries?: string[];
|
|
223
|
+
/** Fallback element during suspense */
|
|
224
|
+
fallbackElement?: JSX.Element;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Scroll restoration options
|
|
228
|
+
*/
|
|
229
|
+
export interface ScrollRestorationProps {
|
|
230
|
+
/** Custom scroll key generator */
|
|
231
|
+
getKey?: (location: Location, matches: RouteMatch[]) => string;
|
|
232
|
+
/** Storage key for scroll positions */
|
|
233
|
+
storageKey?: string;
|
|
234
|
+
}
|
|
36
235
|
export type { RouterKitError } from "../utils/error/errors";
|
package/package.json
CHANGED
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
"email": "mohammed.bencheikh.dev@gmail.com",
|
|
6
6
|
"url": "https://mohammedbencheikh.com/"
|
|
7
7
|
},
|
|
8
|
-
"version": "
|
|
9
|
-
"description": "A
|
|
8
|
+
"version": "2.0.1",
|
|
9
|
+
"description": "A professional React routing library with guards, loaders, and navigation blocking",
|
|
10
10
|
"main": "dist/index.js",
|
|
11
11
|
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./ssr": {
|
|
19
|
+
"types": "./dist/ssr/index.d.ts",
|
|
20
|
+
"import": "./dist/ssr/index.js",
|
|
21
|
+
"require": "./dist/ssr/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
12
24
|
"files": [
|
|
13
25
|
"dist"
|
|
14
26
|
],
|