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.
- package/dist/context/RouterProvider.js +242 -139
- package/dist/core/createRouter.js +8 -5
- package/dist/hooks/useLocation.js +34 -11
- package/dist/ssr/StaticRouter.js +34 -4
- package/dist/ssr/serverUtils.js +29 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
52
|
-
const
|
|
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
|
|
418
|
+
* Compute matched route result (pure computation)
|
|
341
419
|
*/
|
|
342
420
|
const normalizedPath = normalizePathname(location.pathname);
|
|
343
|
-
const
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
*
|
|
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/ssr/StaticRouter.js
CHANGED
|
@@ -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
|
|
161
|
-
const
|
|
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
|
|
223
|
-
const
|
|
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
|
}
|
package/dist/ssr/serverUtils.js
CHANGED
|
@@ -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
|
|
84
|
-
const
|
|
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
|
|
133
|
-
const
|
|
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.
|
|
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",
|