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.
@@ -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
- return pathArray
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
- .filter(Boolean)
14
- .join("|");
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
- const pathArray = Array.isArray(route.path) ? route.path : [route.path];
30
- for (const p of pathArray) {
31
- // Check for invalid characters
32
- if (/[<>"|\\]/.test(p)) {
33
- console.warn(`[router-kit] Route path "${p}" contains invalid characters.`);
34
- }
35
- // Warn about potential issues with catch-all routes
36
- if (p.includes("*") && !p.endsWith("*") && !p.includes("*/")) {
37
- console.warn(`[router-kit] Catch-all (*) should typically be at the end of a path: "${p}"`);
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
- * Get current location snapshot
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, _b;
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
- return {
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: (_b = (_a = window.history.state) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : createKey(),
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
- window.addEventListener("popstate", callback);
38
- window.addEventListener("locationchange", callback);
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", callback);
41
- window.removeEventListener("locationchange", callback);
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 getServerSnapshot = () => ({
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";
@@ -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 (patternParts.length !== pathParts.length) {
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) ? route.path : [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 fullPath = joinPaths(parentPath, route.path);
161
- const matchResult = matchPath(fullPath, currentPath);
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
- return {
214
- component: route.component,
215
- match: routeMatch,
216
- pattern: matchResult.pattern,
217
- params: matchResult.params,
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 fullPath = joinPaths(parentPath, route.path);
223
- const childMatch = findMatch(route.children, currentPath, fullPath);
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
  }
@@ -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 (patternParts.length !== pathParts.length) {
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) ? route.path : [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 fullPath = joinPaths(parentPath, route.path);
84
- const patterns = route.path.split("|");
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 extractedParams = extractParams(fullPattern, pathname);
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
- return {
123
- matches,
124
- params: extractedParams,
125
- statusCode: 200,
126
- meta: route.meta,
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 fullPath = joinPaths(parentPath, route.path);
133
- const childResult = matchServerRoutes(route.children, pathname, fullPath);
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
  }
@@ -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: string | string[];
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
- * Route guard function type
44
+ * Middleware context passed to middleware functions
41
45
  */
42
- export type RouteGuard = (args: GuardArgs) => boolean | Promise<boolean> | string;
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;