router-kit 2.0.0 → 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.
@@ -17,6 +17,39 @@ const validateUrl = (url) => {
17
17
  return false;
18
18
  }
19
19
  };
20
+ /**
21
+ * Normalize path to string (handles array paths)
22
+ * Preserves '/' as a special case for root path
23
+ */
24
+ const normalizePath = (path) => {
25
+ if (Array.isArray(path)) {
26
+ return path
27
+ .map((p) => {
28
+ // Keep "/" as empty string to represent root
29
+ if (p === "/")
30
+ return "";
31
+ return p.startsWith("/") ? p.slice(1) : p;
32
+ })
33
+ .join("|");
34
+ }
35
+ // Keep "/" as empty string to represent root
36
+ if (path === "/")
37
+ return "";
38
+ return path.startsWith("/") ? path.slice(1) : path;
39
+ };
40
+ /**
41
+ * Get the first path from a path (string or array)
42
+ */
43
+ const getFirstPath = (path) => {
44
+ if (Array.isArray(path)) {
45
+ return path[0] || "";
46
+ }
47
+ // Handle pipe-separated paths (already normalized)
48
+ if (path.includes("|")) {
49
+ return path.split("|")[0];
50
+ }
51
+ return path;
52
+ };
20
53
  /**
21
54
  * Creates a unique key for location tracking
22
55
  */
@@ -48,8 +81,15 @@ const getCurrentLocation = () => {
48
81
  * Extracts params from a path using a pattern
49
82
  */
50
83
  const extractParams = (pattern, pathname) => {
51
- const patternParts = pattern.split("/").filter(Boolean);
52
- const pathParts = pathname.split("/").filter(Boolean);
84
+ // Special case: root path matching
85
+ const normalizedPattern = pattern === "/" ? "" : pattern;
86
+ const normalizedPathname = pathname === "/" ? "" : pathname;
87
+ const patternParts = normalizedPattern.split("/").filter(Boolean);
88
+ const pathParts = normalizedPathname.split("/").filter(Boolean);
89
+ // Both empty means root path match
90
+ if (patternParts.length === 0 && pathParts.length === 0) {
91
+ return {};
92
+ }
53
93
  if (patternParts.length !== pathParts.length) {
54
94
  // Check for catch-all pattern
55
95
  const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
@@ -86,6 +126,168 @@ const extractParams = (pattern, pathname) => {
86
126
  }
87
127
  return params;
88
128
  };
129
+ /**
130
+ * Match a single path pattern against current pathname (pure function)
131
+ */
132
+ const matchPathPattern = (routePattern, currentPath) => {
133
+ const patterns = routePattern.split("|");
134
+ for (const pat of patterns) {
135
+ // Handle root path pattern
136
+ const normalizedPat = pat === "" ? "/" : pat;
137
+ const extractedParams = extractParams(normalizedPat, currentPath);
138
+ if (extractedParams !== null) {
139
+ return { match: true, params: extractedParams, pattern: normalizedPat };
140
+ }
141
+ }
142
+ return null;
143
+ };
144
+ /**
145
+ * Pure function to match routes and return result without side effects
146
+ */
147
+ const matchRoutes = (routesList, currentPath, parentPath = "/", searchString = "", collectedMatches = []) => {
148
+ const staticRoutes = [];
149
+ const dynamicRoutes = [];
150
+ const catchAllRoutes = [];
151
+ let page404Component = null;
152
+ for (const route of routesList) {
153
+ const pathArray = Array.isArray(route.path)
154
+ ? route.path
155
+ : route.path.includes("|")
156
+ ? route.path.split("|")
157
+ : [route.path];
158
+ const is404 = pathArray.some((p) => p === "404" || p === "/404");
159
+ if (is404) {
160
+ page404Component = route.component;
161
+ continue;
162
+ }
163
+ const hasCatchAll = pathArray.some((p) => p.includes("*"));
164
+ const hasDynamicParams = pathArray.some((p) => p.includes(":"));
165
+ if (hasCatchAll) {
166
+ catchAllRoutes.push(route);
167
+ }
168
+ else if (hasDynamicParams) {
169
+ dynamicRoutes.push(route);
170
+ }
171
+ else {
172
+ staticRoutes.push(route);
173
+ }
174
+ }
175
+ // Priority: static > dynamic > catch-all
176
+ const orderedRoutes = [...staticRoutes, ...dynamicRoutes, ...catchAllRoutes];
177
+ for (const route of orderedRoutes) {
178
+ const normalizedRoutePath = normalizePath(route.path);
179
+ const firstPath = getFirstPath(route.path);
180
+ // Handle root path correctly
181
+ const fullPath = firstPath === "/" || firstPath === ""
182
+ ? parentPath
183
+ : join(parentPath, `/${firstPath}`);
184
+ // Build full pattern for matching (handles multiple paths)
185
+ const fullPattern = normalizedRoutePath.includes("|")
186
+ ? normalizedRoutePath
187
+ .split("|")
188
+ .map((p) => {
189
+ // Handle empty string (root path) correctly
190
+ if (p === "")
191
+ return parentPath;
192
+ return join(parentPath, `/${p}`);
193
+ })
194
+ .join("|")
195
+ : fullPath;
196
+ const matchResult = matchPathPattern(fullPattern, currentPath);
197
+ if (matchResult) {
198
+ // Handle redirects
199
+ if (route.redirectTo) {
200
+ return {
201
+ component: null,
202
+ pattern: matchResult.pattern,
203
+ params: matchResult.params,
204
+ matches: collectedMatches,
205
+ meta: null,
206
+ redirect: route.redirectTo,
207
+ page404Component,
208
+ };
209
+ }
210
+ // Handle guards
211
+ if (route.guard) {
212
+ const guardResult = route.guard({
213
+ pathname: currentPath,
214
+ params: matchResult.params,
215
+ search: searchString,
216
+ });
217
+ if (typeof guardResult === "string") {
218
+ return {
219
+ component: null,
220
+ pattern: matchResult.pattern,
221
+ params: matchResult.params,
222
+ matches: collectedMatches,
223
+ meta: null,
224
+ redirect: guardResult,
225
+ page404Component,
226
+ };
227
+ }
228
+ if (guardResult === false) {
229
+ continue; // Skip this route
230
+ }
231
+ }
232
+ // Build match object
233
+ const newMatch = {
234
+ route,
235
+ params: matchResult.params,
236
+ pathname: currentPath,
237
+ pathnameBase: parentPath,
238
+ pattern: matchResult.pattern,
239
+ };
240
+ const newMatches = [...collectedMatches, newMatch];
241
+ // Handle nested routes with Outlet support
242
+ if (route.children && route.children.length > 0) {
243
+ const childResult = matchRoutes(route.children, currentPath, fullPath, searchString, newMatches);
244
+ if (childResult.component || childResult.redirect) {
245
+ // Wrap parent component with OutletProvider to render children via Outlet
246
+ return {
247
+ component: (_jsx(OutletProvider, { outlet: childResult.component, childRoutes: route.children, matches: newMatches, depth: parentPath.split("/").filter(Boolean).length, children: route.component })),
248
+ pattern: matchResult.pattern,
249
+ params: { ...matchResult.params, ...childResult.params },
250
+ matches: childResult.matches,
251
+ meta: route.meta || childResult.meta,
252
+ loader: route.loader
253
+ ? { fn: route.loader, params: matchResult.params }
254
+ : childResult.loader,
255
+ page404Component: page404Component || childResult.page404Component,
256
+ };
257
+ }
258
+ }
259
+ return {
260
+ component: route.component,
261
+ pattern: matchResult.pattern,
262
+ params: matchResult.params,
263
+ matches: newMatches,
264
+ meta: route.meta || null,
265
+ loader: route.loader
266
+ ? { fn: route.loader, params: matchResult.params }
267
+ : undefined,
268
+ page404Component,
269
+ };
270
+ }
271
+ // Check children routes (for routes without matching parent)
272
+ if (route.children) {
273
+ const childResult = matchRoutes(route.children, currentPath, fullPath, searchString, collectedMatches);
274
+ if (childResult.component || childResult.redirect) {
275
+ return {
276
+ ...childResult,
277
+ page404Component: page404Component || childResult.page404Component,
278
+ };
279
+ }
280
+ }
281
+ }
282
+ return {
283
+ component: null,
284
+ pattern: "",
285
+ params: {},
286
+ matches: collectedMatches,
287
+ meta: null,
288
+ page404Component,
289
+ };
290
+ };
89
291
  /**
90
292
  * RouterProvider - Professional-grade router provider component
91
293
  *
@@ -100,14 +302,10 @@ const extractParams = (pattern, pathname) => {
100
302
  * - History management
101
303
  */
102
304
  const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
305
+ var _a;
103
306
  const [location, setLocation] = useState(getCurrentLocation);
104
- const [pattern, setPattern] = useState("");
105
- const [params, setParams] = useState({});
106
- const [matches, setMatches] = useState([]);
107
307
  const [loaderData, setLoaderData] = useState(null);
108
- const [meta, setMeta] = useState(null);
109
308
  const [isPending, startTransition] = useTransition();
110
- const page404Ref = useRef(null);
111
309
  const scrollPositions = useRef(new Map());
112
310
  const isNavigatingRef = useRef(false);
113
311
  /**
@@ -119,124 +317,6 @@ const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
119
317
  }
120
318
  return pathname;
121
319
  }, [basename]);
122
- /**
123
- * Match a single path pattern against current pathname
124
- */
125
- const matchPath = useCallback((routePattern, currentPath) => {
126
- const patterns = routePattern.split("|");
127
- for (const pat of patterns) {
128
- const extractedParams = extractParams(pat, currentPath);
129
- if (extractedParams !== null) {
130
- return { match: true, params: extractedParams, pattern: pat };
131
- }
132
- }
133
- return null;
134
- }, []);
135
- /**
136
- * Get component and match info from routes
137
- */
138
- const getComponent = useCallback((routesList, currentPath, parentPath = "/") => {
139
- const staticRoutes = [];
140
- const dynamicRoutes = [];
141
- const catchAllRoutes = [];
142
- for (const route of routesList) {
143
- const is404 = route.path === "404" || route.path === "/404";
144
- if (is404) {
145
- page404Ref.current = route.component;
146
- continue;
147
- }
148
- const pathArray = Array.isArray(route.path) ? route.path : [route.path];
149
- const hasCatchAll = pathArray.some((p) => p.includes("*"));
150
- const hasDynamicParams = pathArray.some((p) => p.includes(":"));
151
- if (hasCatchAll) {
152
- catchAllRoutes.push(route);
153
- }
154
- else if (hasDynamicParams) {
155
- dynamicRoutes.push(route);
156
- }
157
- else {
158
- staticRoutes.push(route);
159
- }
160
- }
161
- // Priority: static > dynamic > catch-all
162
- const orderedRoutes = [
163
- ...staticRoutes,
164
- ...dynamicRoutes,
165
- ...catchAllRoutes,
166
- ];
167
- for (const route of orderedRoutes) {
168
- const fullPath = join(parentPath, `/${route.path}`);
169
- const matchResult = matchPath(fullPath, currentPath);
170
- if (matchResult) {
171
- // Handle redirects
172
- if (route.redirectTo) {
173
- // Schedule redirect in next tick to avoid state update during render
174
- setTimeout(() => navigate(route.redirectTo), 0);
175
- return null;
176
- }
177
- // Handle guards
178
- if (route.guard) {
179
- const guardResult = route.guard({
180
- pathname: currentPath,
181
- params: matchResult.params,
182
- search: location.search,
183
- });
184
- if (typeof guardResult === "string") {
185
- setTimeout(() => navigate(guardResult), 0);
186
- return null;
187
- }
188
- if (guardResult === false) {
189
- continue; // Skip this route
190
- }
191
- }
192
- // Update matches state
193
- const newMatch = {
194
- route,
195
- params: matchResult.params,
196
- pathname: currentPath,
197
- pathnameBase: parentPath,
198
- pattern: matchResult.pattern,
199
- };
200
- if (pattern !== matchResult.pattern) {
201
- setPattern(matchResult.pattern);
202
- }
203
- if (JSON.stringify(params) !== JSON.stringify(matchResult.params)) {
204
- setParams(matchResult.params);
205
- }
206
- setMatches((prev) => [...prev, newMatch]);
207
- // Handle route meta
208
- if (route.meta) {
209
- setMeta(route.meta);
210
- if (route.meta.title && typeof document !== "undefined") {
211
- document.title = route.meta.title;
212
- }
213
- }
214
- // Handle loader
215
- if (route.loader) {
216
- const abortController = new AbortController();
217
- Promise.resolve(route.loader({
218
- params: matchResult.params,
219
- request: new Request(window.location.href),
220
- signal: abortController.signal,
221
- })).then(setLoaderData);
222
- }
223
- // Handle nested routes with Outlet support
224
- if (route.children && route.children.length > 0) {
225
- const childComponent = getComponent(route.children, currentPath, fullPath);
226
- // Wrap parent component with OutletProvider to render children via Outlet
227
- return (_jsx(OutletProvider, { outlet: childComponent, childRoutes: route.children, matches: matches, depth: parentPath.split("/").filter(Boolean).length, children: route.component }));
228
- }
229
- return route.component;
230
- }
231
- // Check children routes (for routes without matching parent)
232
- if (route.children) {
233
- const childMatch = getComponent(route.children, currentPath, fullPath);
234
- if (childMatch)
235
- return childMatch;
236
- }
237
- }
238
- return null;
239
- }, [location.search, matchPath, params, pattern]);
240
320
  /**
241
321
  * Navigate to a new location
242
322
  */
@@ -274,7 +354,6 @@ const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
274
354
  // Use transition for better UX
275
355
  startTransition(() => {
276
356
  setLocation(getCurrentLocation());
277
- setMatches([]); // Reset matches for new route
278
357
  });
279
358
  // Scroll to top unless prevented
280
359
  if (!(options === null || options === void 0 ? void 0 : options.preventScrollReset)) {
@@ -309,7 +388,6 @@ const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
309
388
  const handleLocationChange = () => {
310
389
  startTransition(() => {
311
390
  setLocation(getCurrentLocation());
312
- setMatches([]); // Reset matches for new route
313
391
  });
314
392
  };
315
393
  // Patch history methods to dispatch custom event
@@ -337,46 +415,71 @@ const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
337
415
  };
338
416
  }, []);
339
417
  /**
340
- * Compute matched component
418
+ * Compute matched route result (pure computation)
341
419
  */
342
420
  const normalizedPath = normalizePathname(location.pathname);
343
- const matchedComponent = useMemo(() => getComponent(routes, normalizedPath), [routes, normalizedPath, getComponent]);
344
- const component = matchedComponent !== null && matchedComponent !== void 0 ? matchedComponent : (page404Ref.current || _jsx(Page404, {}));
421
+ const matchResult = useMemo(() => matchRoutes(routes, normalizedPath, "/", location.search), [routes, normalizedPath, location.search]);
422
+ // Handle redirects
423
+ useEffect(() => {
424
+ if (matchResult.redirect) {
425
+ navigate(matchResult.redirect);
426
+ }
427
+ }, [matchResult.redirect, navigate]);
428
+ // Handle loaders
429
+ useEffect(() => {
430
+ if (matchResult.loader) {
431
+ const abortController = new AbortController();
432
+ Promise.resolve(matchResult.loader.fn({
433
+ params: matchResult.loader.params,
434
+ request: new Request(window.location.href),
435
+ signal: abortController.signal,
436
+ })).then(setLoaderData);
437
+ return () => abortController.abort();
438
+ }
439
+ }, [matchResult.loader]);
440
+ // Handle meta/title updates
441
+ useEffect(() => {
442
+ var _a;
443
+ if (((_a = matchResult.meta) === null || _a === void 0 ? void 0 : _a.title) && typeof document !== "undefined") {
444
+ document.title = matchResult.meta.title;
445
+ }
446
+ }, [matchResult.meta]);
447
+ const component = (_a = matchResult.component) !== null && _a !== void 0 ? _a : (matchResult.page404Component || _jsx(Page404, {}));
345
448
  /**
346
449
  * Build context value with memoization
347
450
  */
348
451
  const contextValue = useMemo(() => ({
349
452
  // New API
350
453
  pathname: normalizedPath,
351
- pattern,
454
+ pattern: matchResult.pattern,
352
455
  search: location.search,
353
456
  hash: location.hash,
354
457
  state: location.state,
355
- params,
356
- matches,
458
+ params: matchResult.params,
459
+ matches: matchResult.matches,
357
460
  navigate,
358
461
  back,
359
462
  forward,
360
463
  isNavigating: isPending || isNavigatingRef.current,
361
464
  loaderData,
362
- meta,
465
+ meta: matchResult.meta,
363
466
  // Legacy aliases for backward compatibility
364
467
  path: normalizedPath,
365
- fullPathWithParams: pattern,
468
+ fullPathWithParams: matchResult.pattern,
366
469
  }), [
367
470
  normalizedPath,
368
- pattern,
471
+ matchResult.pattern,
369
472
  location.search,
370
473
  location.hash,
371
474
  location.state,
372
- params,
373
- matches,
475
+ matchResult.params,
476
+ matchResult.matches,
374
477
  navigate,
375
478
  back,
376
479
  forward,
377
480
  isPending,
378
481
  loaderData,
379
- meta,
482
+ matchResult.meta,
380
483
  ]);
381
484
  return (_jsx(RouterContext.Provider, { value: contextValue, children: fallbackElement && isPending ? (_jsx(Suspense, { fallback: fallbackElement, children: component })) : (component) }));
382
485
  };
@@ -1,17 +1,20 @@
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) => {
5
6
  const pathArray = Array.isArray(path) ? path : [path];
6
- return pathArray
7
- .map((p) => {
7
+ const normalized = pathArray.map((p) => {
8
8
  if (!p)
9
9
  return "";
10
+ // Root path "/" becomes empty string
11
+ if (p === "/")
12
+ return "";
10
13
  // Remove leading slashes but preserve the path structure
11
14
  return p.startsWith("/") ? p.replace(/^\/+/, "") : p;
12
- })
13
- .filter(Boolean)
14
- .join("|");
15
+ });
16
+ // Join with | but don't filter out empty strings (they represent root "/")
17
+ return normalized.join("|");
15
18
  };
16
19
  /**
17
20
  * Validates a route configuration
@@ -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
  *
@@ -67,6 +67,28 @@ const joinPaths = (parent, child) => {
67
67
  const normalizedChild = child.startsWith("/") ? child : `/${child}`;
68
68
  return `${normalizedParent}${normalizedChild}`;
69
69
  };
70
+ /**
71
+ * Normalize path to string (handles array paths)
72
+ */
73
+ const normalizePath = (path) => {
74
+ if (Array.isArray(path)) {
75
+ return path.map((p) => (p.startsWith("/") ? p.slice(1) : p)).join("|");
76
+ }
77
+ return path;
78
+ };
79
+ /**
80
+ * Get the first path from a path (string or array)
81
+ */
82
+ const getFirstPath = (path) => {
83
+ if (Array.isArray(path)) {
84
+ return path[0] || "";
85
+ }
86
+ // Handle pipe-separated paths (already normalized)
87
+ if (path.includes("|")) {
88
+ return path.split("|")[0];
89
+ }
90
+ return path;
91
+ };
70
92
  /**
71
93
  * StaticRouter - Server-side rendering router
72
94
  *
@@ -157,8 +179,15 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
157
179
  ...catchAllRoutes,
158
180
  ];
159
181
  for (const route of orderedRoutes) {
160
- const fullPath = joinPaths(parentPath, route.path);
161
- const matchResult = matchPath(fullPath, currentPath);
182
+ const normalizedRoutePath = normalizePath(route.path);
183
+ const firstPath = getFirstPath(route.path);
184
+ const fullPath = joinPaths(parentPath, firstPath);
185
+ const matchResult = matchPath(normalizedRoutePath.includes("|")
186
+ ? normalizedRoutePath
187
+ .split("|")
188
+ .map((p) => joinPaths(parentPath, p))
189
+ .join("|")
190
+ : fullPath, currentPath);
162
191
  if (matchResult) {
163
192
  // Handle redirects
164
193
  if (route.redirectTo) {
@@ -219,8 +248,9 @@ const StaticRouter = ({ routes, location: locationString, basename = "", loaderD
219
248
  }
220
249
  // Check children routes
221
250
  if (route.children) {
222
- const fullPath = joinPaths(parentPath, route.path);
223
- const childMatch = findMatch(route.children, currentPath, fullPath);
251
+ const firstPath = getFirstPath(route.path);
252
+ const childFullPath = joinPaths(parentPath, firstPath);
253
+ const childMatch = findMatch(route.children, currentPath, childFullPath);
224
254
  if (childMatch)
225
255
  return childMatch;
226
256
  }
@@ -43,6 +43,28 @@ const joinPaths = (parent, child) => {
43
43
  const normalizedChild = child.startsWith("/") ? child : `/${child}`;
44
44
  return `${normalizedParent}${normalizedChild}`;
45
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
+ };
46
68
  /**
47
69
  * Match routes for a given URL on the server
48
70
  *
@@ -80,8 +102,10 @@ export function matchServerRoutes(routes, pathname, parentPath = "/") {
80
102
  }
81
103
  const orderedRoutes = [...staticRoutes, ...dynamicRoutes, ...catchAllRoutes];
82
104
  for (const route of orderedRoutes) {
83
- const fullPath = joinPaths(parentPath, route.path);
84
- const patterns = route.path.split("|");
105
+ const normalizedRoutePath = normalizePath(route.path);
106
+ const firstPath = getFirstPath(route.path);
107
+ const fullPath = joinPaths(parentPath, firstPath);
108
+ const patterns = normalizedRoutePath.split("|");
85
109
  for (const pattern of patterns) {
86
110
  const fullPattern = joinPaths(parentPath, pattern);
87
111
  const extractedParams = extractParams(fullPattern, pathname);
@@ -129,8 +153,9 @@ export function matchServerRoutes(routes, pathname, parentPath = "/") {
129
153
  }
130
154
  // Check children even if parent doesn't match
131
155
  if (route.children) {
132
- const fullPath = joinPaths(parentPath, route.path);
133
- const childResult = matchServerRoutes(route.children, pathname, fullPath);
156
+ const firstPath = getFirstPath(route.path);
157
+ const childFullPath = joinPaths(parentPath, firstPath);
158
+ const childResult = matchServerRoutes(route.children, pathname, childFullPath);
134
159
  if (childResult.matches.length > 0 || childResult.redirect) {
135
160
  return childResult;
136
161
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "email": "mohammed.bencheikh.dev@gmail.com",
6
6
  "url": "https://mohammedbencheikh.com/"
7
7
  },
8
- "version": "2.0.0",
8
+ "version": "2.0.1",
9
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",