router-kit 1.3.3 → 2.0.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.
Files changed (49) hide show
  1. package/README.md +123 -373
  2. package/dist/components/Link.d.ts +23 -6
  3. package/dist/components/Link.js +51 -6
  4. package/dist/components/NavLink.d.ts +44 -7
  5. package/dist/components/NavLink.js +111 -10
  6. package/dist/components/Outlet.d.ts +66 -0
  7. package/dist/components/Outlet.js +69 -0
  8. package/dist/components/Router.d.ts +69 -0
  9. package/dist/components/Router.js +109 -0
  10. package/dist/components/route.d.ts +77 -0
  11. package/dist/components/route.js +51 -0
  12. package/dist/context/OutletContext.d.ts +41 -0
  13. package/dist/context/OutletContext.js +31 -0
  14. package/dist/context/RouterContext.d.ts +9 -0
  15. package/dist/context/RouterContext.js +21 -1
  16. package/dist/context/RouterProvider.d.ts +15 -4
  17. package/dist/context/RouterProvider.js +321 -84
  18. package/dist/core/createRouter.d.ts +65 -0
  19. package/dist/core/createRouter.js +126 -7
  20. package/dist/hooks/useBlocker.d.ts +65 -0
  21. package/dist/hooks/useBlocker.js +152 -0
  22. package/dist/hooks/useDynamicComponents.d.ts +61 -2
  23. package/dist/hooks/useDynamicComponents.js +89 -17
  24. package/dist/hooks/useLoaderData.d.ts +98 -0
  25. package/dist/hooks/useLoaderData.js +107 -0
  26. package/dist/hooks/useLocation.d.ts +37 -0
  27. package/dist/hooks/useLocation.js +106 -1
  28. package/dist/hooks/useMatches.d.ts +99 -0
  29. package/dist/hooks/useMatches.js +114 -0
  30. package/dist/hooks/useNavigate.d.ts +59 -0
  31. package/dist/hooks/useNavigate.js +70 -0
  32. package/dist/hooks/useParams.d.ts +57 -2
  33. package/dist/hooks/useParams.js +60 -14
  34. package/dist/hooks/useQuery.d.ts +53 -3
  35. package/dist/hooks/useQuery.js +107 -8
  36. package/dist/hooks/useRouter.d.ts +34 -0
  37. package/dist/hooks/useRouter.js +35 -1
  38. package/dist/index.d.ts +19 -5
  39. package/dist/index.js +23 -4
  40. package/dist/ssr/StaticRouter.d.ts +65 -0
  41. package/dist/ssr/StaticRouter.js +292 -0
  42. package/dist/ssr/hydrateRouter.d.ts +44 -0
  43. package/dist/ssr/hydrateRouter.js +60 -0
  44. package/dist/ssr/index.d.ts +92 -0
  45. package/dist/ssr/index.js +92 -0
  46. package/dist/ssr/serverUtils.d.ts +107 -0
  47. package/dist/ssr/serverUtils.js +263 -0
  48. package/dist/types/index.d.ts +201 -2
  49. package/package.json +14 -2
@@ -0,0 +1,263 @@
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
+ * Match routes for a given URL on the server
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const result = matchServerRoutes(routes, '/users/123');
52
+ * if (result.redirect) {
53
+ * return res.redirect(result.redirect);
54
+ * }
55
+ * console.log(result.params); // { id: '123' }
56
+ * ```
57
+ */
58
+ export function matchServerRoutes(routes, pathname, parentPath = "/") {
59
+ const matches = [];
60
+ // Sort routes by priority
61
+ const staticRoutes = [];
62
+ const dynamicRoutes = [];
63
+ const catchAllRoutes = [];
64
+ for (const route of routes) {
65
+ const is404 = route.path === "404" || route.path === "/404";
66
+ if (is404)
67
+ continue;
68
+ const pathArray = Array.isArray(route.path) ? route.path : [route.path];
69
+ const hasCatchAll = pathArray.some((p) => p.includes("*"));
70
+ const hasDynamicParams = pathArray.some((p) => p.includes(":"));
71
+ if (hasCatchAll) {
72
+ catchAllRoutes.push(route);
73
+ }
74
+ else if (hasDynamicParams) {
75
+ dynamicRoutes.push(route);
76
+ }
77
+ else {
78
+ staticRoutes.push(route);
79
+ }
80
+ }
81
+ const orderedRoutes = [...staticRoutes, ...dynamicRoutes, ...catchAllRoutes];
82
+ for (const route of orderedRoutes) {
83
+ const fullPath = joinPaths(parentPath, route.path);
84
+ const patterns = route.path.split("|");
85
+ for (const pattern of patterns) {
86
+ const fullPattern = joinPaths(parentPath, pattern);
87
+ const extractedParams = extractParams(fullPattern, pathname);
88
+ if (extractedParams !== null) {
89
+ // Handle redirects
90
+ if (route.redirectTo) {
91
+ return {
92
+ matches: [],
93
+ params: extractedParams,
94
+ redirect: route.redirectTo,
95
+ statusCode: 302,
96
+ meta: route.meta,
97
+ };
98
+ }
99
+ const match = {
100
+ route,
101
+ params: extractedParams,
102
+ pathname,
103
+ pathnameBase: parentPath,
104
+ pattern: fullPattern,
105
+ };
106
+ matches.push(match);
107
+ // Check nested routes
108
+ if (route.children && route.children.length > 0) {
109
+ const childResult = matchServerRoutes(route.children, pathname, fullPath);
110
+ if (childResult.redirect) {
111
+ return childResult;
112
+ }
113
+ if (childResult.matches.length > 0) {
114
+ return {
115
+ matches: [...matches, ...childResult.matches],
116
+ params: { ...extractedParams, ...childResult.params },
117
+ statusCode: childResult.statusCode,
118
+ meta: childResult.meta || route.meta,
119
+ };
120
+ }
121
+ }
122
+ return {
123
+ matches,
124
+ params: extractedParams,
125
+ statusCode: 200,
126
+ meta: route.meta,
127
+ };
128
+ }
129
+ }
130
+ // Check children even if parent doesn't match
131
+ if (route.children) {
132
+ const fullPath = joinPaths(parentPath, route.path);
133
+ const childResult = matchServerRoutes(route.children, pathname, fullPath);
134
+ if (childResult.matches.length > 0 || childResult.redirect) {
135
+ return childResult;
136
+ }
137
+ }
138
+ }
139
+ // No match found
140
+ return {
141
+ matches: [],
142
+ params: {},
143
+ statusCode: 404,
144
+ };
145
+ }
146
+ /**
147
+ * Prefetch loader data for matched routes
148
+ *
149
+ * This function runs all route loaders in parallel and returns
150
+ * the combined data. Use this on the server before rendering.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * // server.ts
155
+ * app.get('*', async (req, res) => {
156
+ * const matchResult = matchServerRoutes(routes, req.url);
157
+ *
158
+ * if (matchResult.redirect) {
159
+ * return res.redirect(matchResult.redirect);
160
+ * }
161
+ *
162
+ * const loaderResult = await prefetchLoaderData(
163
+ * matchResult.matches,
164
+ * req.url,
165
+ * { headers: req.headers }
166
+ * );
167
+ *
168
+ * const html = renderToString(
169
+ * <StaticRouter
170
+ * routes={routes}
171
+ * location={req.url}
172
+ * loaderData={loaderResult.data}
173
+ * />
174
+ * );
175
+ *
176
+ * // Inject loader data for hydration
177
+ * const finalHtml = html.replace(
178
+ * '</head>',
179
+ * `<script>window.__LOADER_DATA__ = ${JSON.stringify(loaderResult.data)}</script></head>`
180
+ * );
181
+ *
182
+ * res.send(finalHtml);
183
+ * });
184
+ * ```
185
+ */
186
+ export async function prefetchLoaderData(matches, url, requestInit) {
187
+ const startTime = Date.now();
188
+ const data = {};
189
+ const errors = {};
190
+ const loaderPromises = matches
191
+ .filter((match) => match.route.loader)
192
+ .map(async (match) => {
193
+ const routePath = match.pattern;
194
+ const abortController = new AbortController();
195
+ try {
196
+ // Create a Request object for the loader
197
+ const request = new Request(url, {
198
+ ...requestInit,
199
+ signal: abortController.signal,
200
+ });
201
+ const loaderArgs = {
202
+ params: match.params,
203
+ request,
204
+ signal: abortController.signal,
205
+ };
206
+ const result = await match.route.loader(loaderArgs);
207
+ data[routePath] = result;
208
+ }
209
+ catch (error) {
210
+ errors[routePath] =
211
+ error instanceof Error ? error : new Error(String(error));
212
+ }
213
+ });
214
+ await Promise.all(loaderPromises);
215
+ return {
216
+ data,
217
+ errors,
218
+ loadTime: Date.now() - startTime,
219
+ };
220
+ }
221
+ /**
222
+ * Create a Request object from Node.js IncomingMessage
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * import { createRequestFromNode } from 'router-kit/ssr';
227
+ *
228
+ * app.get('*', (req, res) => {
229
+ * const request = createRequestFromNode(req);
230
+ * // Use request with loaders
231
+ * });
232
+ * ```
233
+ */
234
+ export function createRequestFromNode(nodeRequest, baseUrl = "http://localhost") {
235
+ const url = new URL(nodeRequest.url || "/", baseUrl);
236
+ const headers = new Headers();
237
+ if (nodeRequest.headers) {
238
+ for (const [key, value] of Object.entries(nodeRequest.headers)) {
239
+ if (value) {
240
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
241
+ }
242
+ }
243
+ }
244
+ return new Request(url.toString(), {
245
+ method: nodeRequest.method || "GET",
246
+ headers,
247
+ });
248
+ }
249
+ /**
250
+ * Generate script tag for hydrating loader data on the client
251
+ */
252
+ export function getLoaderDataScript(data) {
253
+ const serialized = JSON.stringify(data).replace(/</g, "\\u003c");
254
+ return `<script>window.__ROUTER_KIT_DATA__ = ${serialized}</script>`;
255
+ }
256
+ /**
257
+ * Get loader data from window on the client side
258
+ */
259
+ export function getHydratedLoaderData() {
260
+ if (typeof window === "undefined")
261
+ return null;
262
+ return window.__ROUTER_KIT_DATA__ || null;
263
+ }
@@ -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": "1.3.3",
9
- "description": "A small React routing provider library",
8
+ "version": "2.0.0",
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
  ],