router-kit 2.0.0 → 2.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/README.md +4 -4
- package/dist/context/RouterProvider.js +434 -142
- package/dist/core/createRouter.js +21 -14
- package/dist/hooks/useLocation.js +34 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/ssr/StaticRouter.js +69 -15
- package/dist/ssr/serverUtils.js +60 -14
- package/dist/types/index.d.ts +36 -3
- package/dist/utils/middleware.d.ts +81 -0
- package/dist/utils/middleware.js +141 -0
- package/package.json +3 -7
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Normalizes a path by removing leading slashes and handling arrays
|
|
3
|
+
* Preserves "/" as empty string for root path matching
|
|
3
4
|
*/
|
|
4
5
|
const normalizePath = (path) => {
|
|
6
|
+
if (path === undefined)
|
|
7
|
+
return "";
|
|
5
8
|
const pathArray = Array.isArray(path) ? path : [path];
|
|
6
|
-
|
|
7
|
-
.map((p) => {
|
|
9
|
+
const normalized = pathArray.map((p) => {
|
|
8
10
|
if (!p)
|
|
9
11
|
return "";
|
|
12
|
+
// Root path "/" becomes empty string
|
|
13
|
+
if (p === "/")
|
|
14
|
+
return "";
|
|
10
15
|
// Remove leading slashes but preserve the path structure
|
|
11
16
|
return p.startsWith("/") ? p.replace(/^\/+/, "") : p;
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
});
|
|
18
|
+
// Join with | but don't filter out empty strings (they represent root "/")
|
|
19
|
+
return normalized.join("|");
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Validates a route configuration
|
|
@@ -26,15 +31,17 @@ const validateRoute = (route, path) => {
|
|
|
26
31
|
console.warn(`[router-kit] Route "${path}" has both component and lazy defined. Component will take precedence.`);
|
|
27
32
|
}
|
|
28
33
|
// Validate path patterns
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (route.path) {
|
|
35
|
+
const pathArray = Array.isArray(route.path) ? route.path : [route.path];
|
|
36
|
+
for (const p of pathArray) {
|
|
37
|
+
// Check for invalid characters
|
|
38
|
+
if (/[<>"|\\]/.test(p)) {
|
|
39
|
+
console.warn(`[router-kit] Route path "${p}" contains invalid characters.`);
|
|
40
|
+
}
|
|
41
|
+
// Warn about potential issues with catch-all routes
|
|
42
|
+
if (p.includes("*") && !p.endsWith("*") && !p.includes("*/")) {
|
|
43
|
+
console.warn(`[router-kit] Catch-all (*) should typically be at the end of a path: "${p}"`);
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
};
|
|
@@ -6,10 +6,15 @@ const createKey = () => {
|
|
|
6
6
|
return Math.random().toString(36).substring(2, 10);
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Cached location to avoid infinite loops with useSyncExternalStore
|
|
10
|
+
*/
|
|
11
|
+
let cachedLocation = null;
|
|
12
|
+
let cachedLocationString = "";
|
|
13
|
+
/**
|
|
14
|
+
* Get current location snapshot (cached)
|
|
10
15
|
*/
|
|
11
16
|
const getLocationSnapshot = () => {
|
|
12
|
-
var _a
|
|
17
|
+
var _a;
|
|
13
18
|
if (typeof window === "undefined") {
|
|
14
19
|
return {
|
|
15
20
|
pathname: "",
|
|
@@ -19,13 +24,24 @@ const getLocationSnapshot = () => {
|
|
|
19
24
|
key: "default",
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
// Create a string representation to compare
|
|
28
|
+
const currentLocationString = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
29
|
+
const currentStateKey = (_a = window.history.state) === null || _a === void 0 ? void 0 : _a.key;
|
|
30
|
+
const fullLocationString = `${currentLocationString}|${currentStateKey || ""}`;
|
|
31
|
+
// Return cached location if nothing changed
|
|
32
|
+
if (cachedLocation && cachedLocationString === fullLocationString) {
|
|
33
|
+
return cachedLocation;
|
|
34
|
+
}
|
|
35
|
+
// Create new location and cache it
|
|
36
|
+
cachedLocation = {
|
|
23
37
|
pathname: window.location.pathname,
|
|
24
38
|
search: window.location.search,
|
|
25
39
|
hash: window.location.hash,
|
|
26
40
|
state: window.history.state,
|
|
27
|
-
key:
|
|
41
|
+
key: currentStateKey !== null && currentStateKey !== void 0 ? currentStateKey : createKey(),
|
|
28
42
|
};
|
|
43
|
+
cachedLocationString = fullLocationString;
|
|
44
|
+
return cachedLocation;
|
|
29
45
|
};
|
|
30
46
|
/**
|
|
31
47
|
* Subscribe to location changes
|
|
@@ -34,23 +50,30 @@ const subscribeToLocation = (callback) => {
|
|
|
34
50
|
if (typeof window === "undefined") {
|
|
35
51
|
return () => { };
|
|
36
52
|
}
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
const handleLocationChange = () => {
|
|
54
|
+
// Invalidate cache on location change
|
|
55
|
+
cachedLocation = null;
|
|
56
|
+
cachedLocationString = "";
|
|
57
|
+
callback();
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener("popstate", handleLocationChange);
|
|
60
|
+
window.addEventListener("locationchange", handleLocationChange);
|
|
39
61
|
return () => {
|
|
40
|
-
window.removeEventListener("popstate",
|
|
41
|
-
window.removeEventListener("locationchange",
|
|
62
|
+
window.removeEventListener("popstate", handleLocationChange);
|
|
63
|
+
window.removeEventListener("locationchange", handleLocationChange);
|
|
42
64
|
};
|
|
43
65
|
};
|
|
44
66
|
/**
|
|
45
|
-
* Server-side location snapshot
|
|
67
|
+
* Server-side location snapshot (cached)
|
|
46
68
|
*/
|
|
47
|
-
const
|
|
69
|
+
const serverSnapshot = {
|
|
48
70
|
pathname: "",
|
|
49
71
|
search: "",
|
|
50
72
|
hash: "",
|
|
51
73
|
state: null,
|
|
52
74
|
key: "default",
|
|
53
|
-
}
|
|
75
|
+
};
|
|
76
|
+
const getServerSnapshot = () => serverSnapshot;
|
|
54
77
|
/**
|
|
55
78
|
* Hook to access the current location
|
|
56
79
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export { useDynamicComponents } from "./hooks/useDynamicComponents";
|
|
|
18
18
|
export { useIsNavigating, useLoaderData, useRouteMeta, } from "./hooks/useLoaderData";
|
|
19
19
|
export type { OutletProps } from "./components/Outlet";
|
|
20
20
|
export type { RouteProps } from "./components/route";
|
|
21
|
-
export type { Blocker, BlockerFunction, DynamicComponents, GetComponent, GuardArgs, HistoryAction, LinkProps, LoaderArgs, Location, NavigateFunction, NavigateOptions, NavLinkProps, RouteGuard, RouteLoader, RouteMatch, RouteMeta, RouterContextType, RouterError, RouterKitError, RouterProviderProps, Routes, Route as RouteType, ScrollRestorationProps, } from "./types/index";
|
|
21
|
+
export type { Blocker, BlockerFunction, DynamicComponents, GetComponent, GuardArgs, HistoryAction, LinkProps, LoaderArgs, Location, Middleware, MiddlewareContext, MiddlewareResult, NavigateFunction, NavigateOptions, NavLinkProps, RouteGuard, RouteLoader, RouteMatch, RouteMeta, RouterContextType, RouterError, RouterKitError, RouterProviderProps, Routes, Route as RouteType, ScrollRestorationProps, } from "./types/index";
|
|
22
22
|
export { createRouterError, RouterErrorCode, RouterErrors, RouterKitError as RouterKitErrorClass, } from "./utils/error/errors";
|
|
23
|
+
export { executeMiddlewareChain, createAuthMiddleware, createRoleMiddleware, createDataMiddleware, createLoggingMiddleware, } from "./utils/middleware";
|
|
23
24
|
export { createRequestFromNode, getHydratedLoaderData, getLoaderDataScript, hydrateRouter, isBrowser, isServerRendered, matchServerRoutes, prefetchLoaderData, StaticRouter, } from "./ssr";
|
|
24
25
|
export type { HydrateRouterOptions, ServerLoaderResult, ServerMatchResult, StaticRouterContext, StaticRouterProps, } from "./ssr";
|
package/dist/index.js
CHANGED
|
@@ -24,5 +24,7 @@ export { useDynamicComponents } from "./hooks/useDynamicComponents";
|
|
|
24
24
|
export { useIsNavigating, useLoaderData, useRouteMeta, } from "./hooks/useLoaderData";
|
|
25
25
|
// Error utilities
|
|
26
26
|
export { createRouterError, RouterErrorCode, RouterErrors, RouterKitError as RouterKitErrorClass, } from "./utils/error/errors";
|
|
27
|
+
// Middleware utilities
|
|
28
|
+
export { executeMiddlewareChain, createAuthMiddleware, createRoleMiddleware, createDataMiddleware, createLoggingMiddleware, } from "./utils/middleware";
|
|
27
29
|
// SSR - Server-Side Rendering
|
|
28
30
|
export { createRequestFromNode, getHydratedLoaderData, getLoaderDataScript, hydrateRouter, isBrowser, isServerRendered, matchServerRoutes, prefetchLoaderData, StaticRouter, } from "./ssr";
|
package/dist/ssr/StaticRouter.js
CHANGED
|
@@ -25,10 +25,14 @@ const parseUrl = (url) => {
|
|
|
25
25
|
/**
|
|
26
26
|
* Extract params from a path using a pattern
|
|
27
27
|
*/
|
|
28
|
-
const extractParams = (pattern, pathname) => {
|
|
28
|
+
const extractParams = (pattern, pathname, partialMatch = false) => {
|
|
29
29
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
30
30
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
31
|
-
if (
|
|
31
|
+
if (partialMatch) {
|
|
32
|
+
if (patternParts.length > pathParts.length)
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
else if (patternParts.length !== pathParts.length) {
|
|
32
36
|
const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
|
|
33
37
|
if (!hasCatchAll)
|
|
34
38
|
return null;
|
|
@@ -67,6 +71,32 @@ const joinPaths = (parent, child) => {
|
|
|
67
71
|
const normalizedChild = child.startsWith("/") ? child : `/${child}`;
|
|
68
72
|
return `${normalizedParent}${normalizedChild}`;
|
|
69
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Normalize path to string (handles array paths)
|
|
76
|
+
*/
|
|
77
|
+
const normalizePath = (path) => {
|
|
78
|
+
if (path === undefined)
|
|
79
|
+
return "";
|
|
80
|
+
if (Array.isArray(path)) {
|
|
81
|
+
return path.map((p) => (p.startsWith("/") ? p.slice(1) : p)).join("|");
|
|
82
|
+
}
|
|
83
|
+
return path;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Get the first path from a path (string or array)
|
|
87
|
+
*/
|
|
88
|
+
const getFirstPath = (path) => {
|
|
89
|
+
if (path === undefined)
|
|
90
|
+
return "";
|
|
91
|
+
if (Array.isArray(path)) {
|
|
92
|
+
return path[0] || "";
|
|
93
|
+
}
|
|
94
|
+
// Handle pipe-separated paths (already normalized)
|
|
95
|
+
if (path.includes("|")) {
|
|
96
|
+
return path.split("|")[0];
|
|
97
|
+
}
|
|
98
|
+
return path;
|
|
99
|
+
};
|
|
70
100
|
/**
|
|
71
101
|
* StaticRouter - Server-side rendering router
|
|
72
102
|
*
|
|
@@ -115,10 +145,10 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
|
|
|
115
145
|
key: "static",
|
|
116
146
|
};
|
|
117
147
|
// Match path helper
|
|
118
|
-
const matchPath = (routePattern, currentPath) => {
|
|
148
|
+
const matchPath = (routePattern, currentPath, partialMatch = false) => {
|
|
119
149
|
const patterns = routePattern.split("|");
|
|
120
150
|
for (const pat of patterns) {
|
|
121
|
-
const extractedParams = extractParams(pat, currentPath);
|
|
151
|
+
const extractedParams = extractParams(pat, currentPath, partialMatch);
|
|
122
152
|
if (extractedParams !== null) {
|
|
123
153
|
return { match: true, params: extractedParams, pattern: pat };
|
|
124
154
|
}
|
|
@@ -138,7 +168,11 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
|
|
|
138
168
|
page404Component = route.component;
|
|
139
169
|
continue;
|
|
140
170
|
}
|
|
141
|
-
const pathArray = Array.isArray(route.path)
|
|
171
|
+
const pathArray = Array.isArray(route.path)
|
|
172
|
+
? route.path
|
|
173
|
+
: route.path
|
|
174
|
+
? [route.path]
|
|
175
|
+
: [];
|
|
142
176
|
const hasCatchAll = pathArray.some((p) => p.includes("*"));
|
|
143
177
|
const hasDynamicParams = pathArray.some((p) => p.includes(":"));
|
|
144
178
|
if (hasCatchAll) {
|
|
@@ -157,8 +191,16 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
|
|
|
157
191
|
...catchAllRoutes,
|
|
158
192
|
];
|
|
159
193
|
for (const route of orderedRoutes) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
194
|
+
const normalizedRoutePath = normalizePath(route.path);
|
|
195
|
+
const firstPath = getFirstPath(route.path);
|
|
196
|
+
const fullPath = joinPaths(parentPath, firstPath);
|
|
197
|
+
const isParent = route.children && route.children.length > 0;
|
|
198
|
+
const matchResult = matchPath(normalizedRoutePath.includes("|")
|
|
199
|
+
? normalizedRoutePath
|
|
200
|
+
.split("|")
|
|
201
|
+
.map((p) => joinPaths(parentPath, p))
|
|
202
|
+
.join("|")
|
|
203
|
+
: fullPath, currentPath, isParent);
|
|
162
204
|
if (matchResult) {
|
|
163
205
|
// Handle redirects
|
|
164
206
|
if (route.redirectTo) {
|
|
@@ -210,17 +252,29 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
|
|
|
210
252
|
}
|
|
211
253
|
context.action = "OK";
|
|
212
254
|
context.statusCode = 200;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
// If no children matched, check if this is an exact match
|
|
256
|
+
const isExactMatch = matchPath(normalizedRoutePath.includes("|")
|
|
257
|
+
? normalizedRoutePath
|
|
258
|
+
.split("|")
|
|
259
|
+
.map((p) => joinPaths(parentPath, p))
|
|
260
|
+
.join("|")
|
|
261
|
+
: fullPath, currentPath, false // Force exact match check
|
|
262
|
+
);
|
|
263
|
+
if (isExactMatch) {
|
|
264
|
+
return {
|
|
265
|
+
component: route.component,
|
|
266
|
+
match: routeMatch,
|
|
267
|
+
pattern: matchResult.pattern,
|
|
268
|
+
params: matchResult.params,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// If not exact and no children matched, continue loop matching other routes
|
|
219
272
|
}
|
|
220
273
|
// Check children routes
|
|
221
274
|
if (route.children) {
|
|
222
|
-
const
|
|
223
|
-
const
|
|
275
|
+
const firstPath = getFirstPath(route.path);
|
|
276
|
+
const childFullPath = joinPaths(parentPath, firstPath);
|
|
277
|
+
const childMatch = findMatch(route.children, currentPath, childFullPath);
|
|
224
278
|
if (childMatch)
|
|
225
279
|
return childMatch;
|
|
226
280
|
}
|
package/dist/ssr/serverUtils.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extract params from a path using a pattern
|
|
3
3
|
*/
|
|
4
|
-
const extractParams = (pattern, pathname) => {
|
|
4
|
+
const extractParams = (pattern, pathname, partialMatch = false) => {
|
|
5
5
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
6
6
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
7
|
-
if (
|
|
7
|
+
if (partialMatch) {
|
|
8
|
+
if (patternParts.length > pathParts.length)
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
else if (patternParts.length !== pathParts.length) {
|
|
8
12
|
const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
|
|
9
13
|
if (!hasCatchAll)
|
|
10
14
|
return null;
|
|
@@ -43,6 +47,32 @@ const joinPaths = (parent, child) => {
|
|
|
43
47
|
const normalizedChild = child.startsWith("/") ? child : `/${child}`;
|
|
44
48
|
return `${normalizedParent}${normalizedChild}`;
|
|
45
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Normalize path to string (handles array paths)
|
|
52
|
+
*/
|
|
53
|
+
const normalizePath = (path) => {
|
|
54
|
+
if (path === undefined)
|
|
55
|
+
return "";
|
|
56
|
+
if (Array.isArray(path)) {
|
|
57
|
+
return path.map((p) => (p.startsWith("/") ? p.slice(1) : p)).join("|");
|
|
58
|
+
}
|
|
59
|
+
return path;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Get the first path from a path (string or array)
|
|
63
|
+
*/
|
|
64
|
+
const getFirstPath = (path) => {
|
|
65
|
+
if (path === undefined)
|
|
66
|
+
return "";
|
|
67
|
+
if (Array.isArray(path)) {
|
|
68
|
+
return path[0] || "";
|
|
69
|
+
}
|
|
70
|
+
// Handle pipe-separated paths (already normalized)
|
|
71
|
+
if (path.includes("|")) {
|
|
72
|
+
return path.split("|")[0];
|
|
73
|
+
}
|
|
74
|
+
return path;
|
|
75
|
+
};
|
|
46
76
|
/**
|
|
47
77
|
* Match routes for a given URL on the server
|
|
48
78
|
*
|
|
@@ -65,7 +95,11 @@ export function matchServerRoutes(routes, pathname, parentPath = "/") {
|
|
|
65
95
|
const is404 = route.path === "404" || route.path === "/404";
|
|
66
96
|
if (is404)
|
|
67
97
|
continue;
|
|
68
|
-
const pathArray = Array.isArray(route.path)
|
|
98
|
+
const pathArray = Array.isArray(route.path)
|
|
99
|
+
? route.path
|
|
100
|
+
: route.path
|
|
101
|
+
? [route.path]
|
|
102
|
+
: [];
|
|
69
103
|
const hasCatchAll = pathArray.some((p) => p.includes("*"));
|
|
70
104
|
const hasDynamicParams = pathArray.some((p) => p.includes(":"));
|
|
71
105
|
if (hasCatchAll) {
|
|
@@ -80,11 +114,14 @@ export function matchServerRoutes(routes, pathname, parentPath = "/") {
|
|
|
80
114
|
}
|
|
81
115
|
const orderedRoutes = [...staticRoutes, ...dynamicRoutes, ...catchAllRoutes];
|
|
82
116
|
for (const route of orderedRoutes) {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
117
|
+
const normalizedRoutePath = normalizePath(route.path);
|
|
118
|
+
const firstPath = getFirstPath(route.path);
|
|
119
|
+
const fullPath = joinPaths(parentPath, firstPath);
|
|
120
|
+
const patterns = normalizedRoutePath.split("|");
|
|
85
121
|
for (const pattern of patterns) {
|
|
86
122
|
const fullPattern = joinPaths(parentPath, pattern);
|
|
87
|
-
const
|
|
123
|
+
const isParent = route.children && route.children.length > 0;
|
|
124
|
+
const extractedParams = extractParams(fullPattern, pathname, isParent);
|
|
88
125
|
if (extractedParams !== null) {
|
|
89
126
|
// Handle redirects
|
|
90
127
|
if (route.redirectTo) {
|
|
@@ -119,18 +156,27 @@ export function matchServerRoutes(routes, pathname, parentPath = "/") {
|
|
|
119
156
|
};
|
|
120
157
|
}
|
|
121
158
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
159
|
+
// If no children matched, check for exact match
|
|
160
|
+
// Rethink fullPattern logic: patterns contains split parts.
|
|
161
|
+
// We need to re-verify the specific pattern that matched partially.
|
|
162
|
+
const isExactMatch = extractParams(fullPattern, pathname, false);
|
|
163
|
+
if (isExactMatch !== null) {
|
|
164
|
+
return {
|
|
165
|
+
matches,
|
|
166
|
+
params: extractedParams,
|
|
167
|
+
statusCode: 200,
|
|
168
|
+
meta: route.meta,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// If not exact and no children matched, continue loop matching other routes (pop from matches implicitly by not returning)
|
|
172
|
+
matches.pop(); // Remove the partial match from matches array if we are continuing
|
|
128
173
|
}
|
|
129
174
|
}
|
|
130
175
|
// Check children even if parent doesn't match
|
|
131
176
|
if (route.children) {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
177
|
+
const firstPath = getFirstPath(route.path);
|
|
178
|
+
const childFullPath = joinPaths(parentPath, firstPath);
|
|
179
|
+
const childResult = matchServerRoutes(route.children, pathname, childFullPath);
|
|
134
180
|
if (childResult.matches.length > 0 || childResult.redirect) {
|
|
135
181
|
return childResult;
|
|
136
182
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,13 +4,15 @@ import { ComponentType, JSX, LazyExoticComponent, ReactNode } from "react";
|
|
|
4
4
|
*/
|
|
5
5
|
export interface Route {
|
|
6
6
|
/** Path pattern(s) for the route */
|
|
7
|
-
path
|
|
7
|
+
path?: string | string[];
|
|
8
8
|
/** Component to render */
|
|
9
9
|
component: JSX.Element;
|
|
10
10
|
/** Nested child routes */
|
|
11
11
|
children?: Route[];
|
|
12
12
|
/** Index route flag - renders when parent path matches exactly */
|
|
13
13
|
index?: boolean;
|
|
14
|
+
/** Component to render while loading data or performing async tasks */
|
|
15
|
+
loading?: JSX.Element;
|
|
14
16
|
/** Lazy-loaded component */
|
|
15
17
|
lazy?: LazyExoticComponent<ComponentType<any>>;
|
|
16
18
|
/** Route loader function for data fetching */
|
|
@@ -21,6 +23,8 @@ export interface Route {
|
|
|
21
23
|
redirectTo?: string;
|
|
22
24
|
/** Route guard function */
|
|
23
25
|
guard?: RouteGuard;
|
|
26
|
+
/** Middleware chain for route processing (Chain of Responsibility pattern) */
|
|
27
|
+
middleware?: Middleware[];
|
|
24
28
|
/** Route metadata */
|
|
25
29
|
meta?: RouteMeta;
|
|
26
30
|
}
|
|
@@ -37,9 +41,36 @@ export interface LoaderArgs {
|
|
|
37
41
|
signal: AbortSignal;
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
40
|
-
*
|
|
44
|
+
* Middleware context passed to middleware functions
|
|
41
45
|
*/
|
|
42
|
-
export
|
|
46
|
+
export interface MiddlewareContext {
|
|
47
|
+
pathname: string;
|
|
48
|
+
params: Record<string, string>;
|
|
49
|
+
search: string;
|
|
50
|
+
request?: Request;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Middleware result - can redirect, block, or continue
|
|
55
|
+
*/
|
|
56
|
+
export type MiddlewareResult = {
|
|
57
|
+
type: "continue";
|
|
58
|
+
} | {
|
|
59
|
+
type: "redirect";
|
|
60
|
+
to: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: "block";
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Middleware function type - supports both sync and async
|
|
66
|
+
* Returns MiddlewareResult or Promise<MiddlewareResult>
|
|
67
|
+
*/
|
|
68
|
+
export type Middleware = (context: MiddlewareContext, next: () => Promise<MiddlewareResult>) => MiddlewareResult | Promise<MiddlewareResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Route guard function type - supports both sync and async
|
|
71
|
+
* Can return boolean, Promise<boolean>, or redirect string
|
|
72
|
+
*/
|
|
73
|
+
export type RouteGuard = (args: GuardArgs) => boolean | Promise<boolean> | string | Promise<string>;
|
|
43
74
|
/**
|
|
44
75
|
* Guard function arguments
|
|
45
76
|
*/
|
|
@@ -47,6 +78,8 @@ export interface GuardArgs {
|
|
|
47
78
|
pathname: string;
|
|
48
79
|
params: Record<string, string>;
|
|
49
80
|
search: string;
|
|
81
|
+
request?: Request;
|
|
82
|
+
signal?: AbortSignal;
|
|
50
83
|
}
|
|
51
84
|
/**
|
|
52
85
|
* Route metadata
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Middleware, MiddlewareContext, MiddlewareResult } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a middleware chain executor using Chain of Responsibility pattern
|
|
4
|
+
* Each middleware can either:
|
|
5
|
+
* - Continue to the next middleware (return { type: "continue" })
|
|
6
|
+
* - Redirect (return { type: "redirect", to: string })
|
|
7
|
+
* - Block the request (return { type: "block" })
|
|
8
|
+
*
|
|
9
|
+
* @param middlewares - Array of middleware functions
|
|
10
|
+
* @param context - Middleware context with route information
|
|
11
|
+
* @returns Promise resolving to middleware result
|
|
12
|
+
*/
|
|
13
|
+
export declare function executeMiddlewareChain(middlewares: Middleware[], context: MiddlewareContext): Promise<MiddlewareResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Helper to create a middleware that checks authentication
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const authMiddleware: Middleware = createAuthMiddleware({
|
|
19
|
+
* checkAuth: async () => {
|
|
20
|
+
* const token = localStorage.getItem('token');
|
|
21
|
+
* return !!token;
|
|
22
|
+
* },
|
|
23
|
+
* redirectTo: '/login'
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createAuthMiddleware(options: {
|
|
28
|
+
checkAuth: (context: MiddlewareContext) => boolean | Promise<boolean>;
|
|
29
|
+
redirectTo?: string;
|
|
30
|
+
}): Middleware;
|
|
31
|
+
/**
|
|
32
|
+
* Helper to create a middleware that checks permissions/roles
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const adminMiddleware: Middleware = createRoleMiddleware({
|
|
36
|
+
* checkRole: async (context) => {
|
|
37
|
+
* const user = await getCurrentUser();
|
|
38
|
+
* return user?.role === 'admin';
|
|
39
|
+
* },
|
|
40
|
+
* redirectTo: '/unauthorized'
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createRoleMiddleware(options: {
|
|
45
|
+
checkRole: (context: MiddlewareContext) => boolean | Promise<boolean>;
|
|
46
|
+
redirectTo?: string;
|
|
47
|
+
}): Middleware;
|
|
48
|
+
/**
|
|
49
|
+
* Helper to create a middleware that fetches data before route loads
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const dataMiddleware: Middleware = createDataMiddleware({
|
|
53
|
+
* fetchData: async (context) => {
|
|
54
|
+
* const response = await fetch(`/api/data/${context.params.id}`);
|
|
55
|
+
* return response.json();
|
|
56
|
+
* },
|
|
57
|
+
* onData: (data) => {
|
|
58
|
+
* // Store data in context or global state
|
|
59
|
+
* }
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDataMiddleware<T = any>(options: {
|
|
64
|
+
fetchData: (context: MiddlewareContext) => Promise<T> | T;
|
|
65
|
+
onData?: (data: T, context: MiddlewareContext) => void | Promise<void>;
|
|
66
|
+
onError?: (error: Error, context: MiddlewareContext) => void;
|
|
67
|
+
}): Middleware;
|
|
68
|
+
/**
|
|
69
|
+
* Helper to create a middleware that logs route access
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const loggingMiddleware: Middleware = createLoggingMiddleware({
|
|
73
|
+
* log: (context) => {
|
|
74
|
+
* console.log(`Accessing: ${context.pathname}`);
|
|
75
|
+
* }
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createLoggingMiddleware(options: {
|
|
80
|
+
log: (context: MiddlewareContext) => void | Promise<void>;
|
|
81
|
+
}): Middleware;
|