router-kit 1.3.4 → 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.
- package/README.md +117 -421
- package/dist/components/Link.d.ts +23 -6
- package/dist/components/Link.js +51 -6
- package/dist/components/NavLink.d.ts +44 -7
- package/dist/components/NavLink.js +111 -10
- package/dist/components/Outlet.d.ts +66 -0
- package/dist/components/Outlet.js +69 -0
- package/dist/components/Router.d.ts +57 -11
- package/dist/components/Router.js +60 -12
- package/dist/components/route.d.ts +57 -7
- package/dist/components/route.js +35 -5
- package/dist/context/OutletContext.d.ts +41 -0
- package/dist/context/OutletContext.js +31 -0
- package/dist/context/RouterContext.d.ts +9 -0
- package/dist/context/RouterContext.js +21 -1
- package/dist/context/RouterProvider.d.ts +15 -4
- package/dist/context/RouterProvider.js +321 -84
- package/dist/core/createRouter.d.ts +65 -0
- package/dist/core/createRouter.js +126 -7
- package/dist/hooks/useBlocker.d.ts +65 -0
- package/dist/hooks/useBlocker.js +152 -0
- package/dist/hooks/useDynamicComponents.d.ts +61 -2
- package/dist/hooks/useDynamicComponents.js +89 -17
- package/dist/hooks/useLoaderData.d.ts +98 -0
- package/dist/hooks/useLoaderData.js +107 -0
- package/dist/hooks/useLocation.d.ts +37 -0
- package/dist/hooks/useLocation.js +106 -1
- package/dist/hooks/useMatches.d.ts +99 -0
- package/dist/hooks/useMatches.js +114 -0
- package/dist/hooks/useNavigate.d.ts +59 -0
- package/dist/hooks/useNavigate.js +70 -0
- package/dist/hooks/useParams.d.ts +57 -2
- package/dist/hooks/useParams.js +60 -14
- package/dist/hooks/useQuery.d.ts +53 -3
- package/dist/hooks/useQuery.js +107 -8
- package/dist/hooks/useRouter.d.ts +34 -0
- package/dist/hooks/useRouter.js +35 -1
- package/dist/index.d.ts +16 -6
- package/dist/index.js +21 -5
- package/dist/ssr/StaticRouter.d.ts +65 -0
- package/dist/ssr/StaticRouter.js +292 -0
- package/dist/ssr/hydrateRouter.d.ts +44 -0
- package/dist/ssr/hydrateRouter.js +60 -0
- package/dist/ssr/index.d.ts +92 -0
- package/dist/ssr/index.js +92 -0
- package/dist/ssr/serverUtils.d.ts +107 -0
- package/dist/ssr/serverUtils.js +263 -0
- package/dist/types/index.d.ts +201 -2
- package/package.json +14 -2
package/dist/components/route.js
CHANGED
|
@@ -4,14 +4,44 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```tsx
|
|
7
|
-
*
|
|
8
|
-
* <Route path=
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* // Basic route
|
|
8
|
+
* <Route path="/users/:id" component={<UserProfile />} />
|
|
9
|
+
*
|
|
10
|
+
* // Multiple paths
|
|
11
|
+
* <Route path={["/about", "/about-us"]} component={<About />} />
|
|
12
|
+
*
|
|
13
|
+
* // Nested routes
|
|
14
|
+
* <Route path="/dashboard" component={<Dashboard />}>
|
|
15
|
+
* <Route path="settings" component={<Settings />} />
|
|
16
|
+
* <Route path="profile" component={<Profile />} />
|
|
11
17
|
* </Route>
|
|
18
|
+
*
|
|
19
|
+
* // With loader
|
|
20
|
+
* <Route
|
|
21
|
+
* path="/user/:id"
|
|
22
|
+
* component={<UserPage />}
|
|
23
|
+
* loader={async ({ params }) => fetchUser(params.id)}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* // With guard
|
|
27
|
+
* <Route
|
|
28
|
+
* path="/admin"
|
|
29
|
+
* component={<AdminPanel />}
|
|
30
|
+
* guard={() => isAdmin() || '/login'}
|
|
31
|
+
* />
|
|
32
|
+
*
|
|
33
|
+
* // With metadata
|
|
34
|
+
* <Route
|
|
35
|
+
* path="/about"
|
|
36
|
+
* component={<About />}
|
|
37
|
+
* meta={{ title: 'About Us', description: 'Learn about us' }}
|
|
38
|
+
* />
|
|
39
|
+
*
|
|
40
|
+
* // Catch-all route
|
|
41
|
+
* <Route path="*" component={<NotFound />} />
|
|
12
42
|
* ```
|
|
13
43
|
*/
|
|
14
|
-
export function Route(
|
|
44
|
+
export function Route(_props) {
|
|
15
45
|
// This component doesn't render anything directly
|
|
16
46
|
// It's used as a declarative way to define routes that will be processed by Router
|
|
17
47
|
return null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import type { Route, RouteMatch } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Outlet context for nested route rendering
|
|
5
|
+
*/
|
|
6
|
+
export interface OutletContextType {
|
|
7
|
+
/** Current outlet content to render */
|
|
8
|
+
outlet: ReactNode;
|
|
9
|
+
/** Remaining child routes */
|
|
10
|
+
childRoutes: Route[];
|
|
11
|
+
/** Current route matches */
|
|
12
|
+
matches: RouteMatch[];
|
|
13
|
+
/** Current depth in route tree */
|
|
14
|
+
depth: number;
|
|
15
|
+
/** Custom context data passed to outlet */
|
|
16
|
+
context?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Context for outlet data
|
|
20
|
+
*/
|
|
21
|
+
export declare const OutletDataContext: import("react").Context<OutletContextType | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Hook to access outlet context
|
|
24
|
+
*/
|
|
25
|
+
export declare function useOutletContext<T = unknown>(): T;
|
|
26
|
+
/**
|
|
27
|
+
* Hook to check if there's an outlet available
|
|
28
|
+
*/
|
|
29
|
+
export declare function useOutlet(): ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Provider for outlet context
|
|
32
|
+
*/
|
|
33
|
+
export declare function OutletProvider({ children, outlet, childRoutes, matches, depth, context, }: {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
outlet: ReactNode;
|
|
36
|
+
childRoutes?: Route[];
|
|
37
|
+
matches?: RouteMatch[];
|
|
38
|
+
depth?: number;
|
|
39
|
+
context?: unknown;
|
|
40
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export default OutletDataContext;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Context for outlet data
|
|
5
|
+
*/
|
|
6
|
+
export const OutletDataContext = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access outlet context
|
|
9
|
+
*/
|
|
10
|
+
export function useOutletContext() {
|
|
11
|
+
const context = useContext(OutletDataContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useOutletContext must be used within a route component");
|
|
14
|
+
}
|
|
15
|
+
return context.context;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Hook to check if there's an outlet available
|
|
19
|
+
*/
|
|
20
|
+
export function useOutlet() {
|
|
21
|
+
var _a;
|
|
22
|
+
const context = useContext(OutletDataContext);
|
|
23
|
+
return (_a = context === null || context === void 0 ? void 0 : context.outlet) !== null && _a !== void 0 ? _a : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Provider for outlet context
|
|
27
|
+
*/
|
|
28
|
+
export function OutletProvider({ children, outlet, childRoutes = [], matches = [], depth = 0, context, }) {
|
|
29
|
+
return (_jsx(OutletDataContext.Provider, { value: { outlet, childRoutes, matches, depth, context }, children: children }));
|
|
30
|
+
}
|
|
31
|
+
export default OutletDataContext;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import type { RouterContextType } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Router context - provides routing state and navigation functions
|
|
4
|
+
* throughout the application
|
|
5
|
+
*/
|
|
2
6
|
declare const RouterContext: import("react").Context<RouterContextType | undefined>;
|
|
7
|
+
/**
|
|
8
|
+
* Internal hook to access router context with validation
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function useRouterContext(): RouterContextType;
|
|
3
12
|
export default RouterContext;
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Router context - provides routing state and navigation functions
|
|
4
|
+
* throughout the application
|
|
5
|
+
*/
|
|
2
6
|
const RouterContext = createContext(undefined);
|
|
7
|
+
/**
|
|
8
|
+
* Display name for debugging
|
|
9
|
+
*/
|
|
10
|
+
RouterContext.displayName = "RouterContext";
|
|
11
|
+
/**
|
|
12
|
+
* Internal hook to access router context with validation
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function useRouterContext() {
|
|
16
|
+
const context = useContext(RouterContext);
|
|
17
|
+
if (context === undefined) {
|
|
18
|
+
throw new Error("[router-kit] useRouter must be used within a RouterProvider. " +
|
|
19
|
+
"Wrap your application with <RouterProvider> or use createRouter().");
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
3
23
|
export default RouterContext;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { RouterProviderProps } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* RouterProvider - Professional-grade router provider component
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Static and dynamic route matching with priority
|
|
7
|
+
* - Route params extraction
|
|
8
|
+
* - Nested routes support
|
|
9
|
+
* - Route guards and redirects
|
|
10
|
+
* - Loader data support
|
|
11
|
+
* - Navigation transitions
|
|
12
|
+
* - Scroll restoration
|
|
13
|
+
* - History management
|
|
14
|
+
*/
|
|
15
|
+
declare const RouterProvider: ({ routes, basename, fallbackElement, }: RouterProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
16
|
export default RouterProvider;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition, } from "react";
|
|
3
3
|
import join from "url-join";
|
|
4
4
|
import Page404 from "../pages/404";
|
|
5
5
|
import { createRouterError, RouterErrorCode, RouterErrors, } from "../utils/error/errors";
|
|
6
|
+
import { OutletProvider } from "./OutletContext";
|
|
6
7
|
import RouterContext from "./RouterContext";
|
|
8
|
+
/**
|
|
9
|
+
* Validates a URL string
|
|
10
|
+
*/
|
|
7
11
|
const validateUrl = (url) => {
|
|
8
12
|
try {
|
|
9
13
|
new URL(url, window.location.origin);
|
|
@@ -13,101 +17,218 @@ const validateUrl = (url) => {
|
|
|
13
17
|
return false;
|
|
14
18
|
}
|
|
15
19
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
window.history.replaceState = patchHistory("replaceState");
|
|
34
|
-
const handleLocationChange = () => {
|
|
35
|
-
setPath(window.location.pathname);
|
|
36
|
-
};
|
|
37
|
-
window.addEventListener("popstate", handleLocationChange);
|
|
38
|
-
window.addEventListener("locationchange", handleLocationChange);
|
|
39
|
-
return () => {
|
|
40
|
-
window.history.pushState = originalPush;
|
|
41
|
-
window.history.replaceState = originalReplace;
|
|
42
|
-
window.removeEventListener("popstate", handleLocationChange);
|
|
43
|
-
window.removeEventListener("locationchange", handleLocationChange);
|
|
20
|
+
/**
|
|
21
|
+
* Creates a unique key for location tracking
|
|
22
|
+
*/
|
|
23
|
+
const createKey = () => {
|
|
24
|
+
return Math.random().toString(36).substring(2, 10);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Gets current location from window
|
|
28
|
+
*/
|
|
29
|
+
const getCurrentLocation = () => {
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
return {
|
|
32
|
+
pathname: "/",
|
|
33
|
+
search: "",
|
|
34
|
+
hash: "",
|
|
35
|
+
state: null,
|
|
36
|
+
key: "default",
|
|
44
37
|
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
pathname: window.location.pathname,
|
|
41
|
+
search: window.location.search,
|
|
42
|
+
hash: window.location.hash,
|
|
43
|
+
state: window.history.state,
|
|
44
|
+
key: createKey(),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Extracts params from a path using a pattern
|
|
49
|
+
*/
|
|
50
|
+
const extractParams = (pattern, pathname) => {
|
|
51
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
52
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
53
|
+
if (patternParts.length !== pathParts.length) {
|
|
54
|
+
// Check for catch-all pattern
|
|
55
|
+
const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
|
|
56
|
+
if (!hasCatchAll)
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const params = {};
|
|
60
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
61
|
+
const patternPart = patternParts[i];
|
|
62
|
+
const pathPart = pathParts[i];
|
|
63
|
+
// Catch-all segment (*splat or **)
|
|
64
|
+
if (patternPart.startsWith("*")) {
|
|
65
|
+
const paramName = patternPart.slice(1) || "splat";
|
|
66
|
+
params[paramName] = pathParts.slice(i).join("/");
|
|
67
|
+
return params;
|
|
68
|
+
}
|
|
69
|
+
// Dynamic segment (:param)
|
|
70
|
+
if (patternPart.startsWith(":")) {
|
|
71
|
+
const paramName = patternPart.slice(1);
|
|
72
|
+
// Handle optional params (:param?)
|
|
73
|
+
if (paramName.endsWith("?")) {
|
|
74
|
+
params[paramName.slice(0, -1)] = pathPart !== null && pathPart !== void 0 ? pathPart : "";
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (pathPart === undefined)
|
|
78
|
+
return null;
|
|
79
|
+
params[paramName] = pathPart;
|
|
63
80
|
}
|
|
64
|
-
|
|
65
|
-
return routePath;
|
|
81
|
+
continue;
|
|
66
82
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
// Static segment - must match exactly
|
|
84
|
+
if (patternPart !== pathPart)
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return params;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* RouterProvider - Professional-grade router provider component
|
|
91
|
+
*
|
|
92
|
+
* Features:
|
|
93
|
+
* - Static and dynamic route matching with priority
|
|
94
|
+
* - Route params extraction
|
|
95
|
+
* - Nested routes support
|
|
96
|
+
* - Route guards and redirects
|
|
97
|
+
* - Loader data support
|
|
98
|
+
* - Navigation transitions
|
|
99
|
+
* - Scroll restoration
|
|
100
|
+
* - History management
|
|
101
|
+
*/
|
|
102
|
+
const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
|
|
103
|
+
const [location, setLocation] = useState(getCurrentLocation);
|
|
104
|
+
const [pattern, setPattern] = useState("");
|
|
105
|
+
const [params, setParams] = useState({});
|
|
106
|
+
const [matches, setMatches] = useState([]);
|
|
107
|
+
const [loaderData, setLoaderData] = useState(null);
|
|
108
|
+
const [meta, setMeta] = useState(null);
|
|
109
|
+
const [isPending, startTransition] = useTransition();
|
|
110
|
+
const page404Ref = useRef(null);
|
|
111
|
+
const scrollPositions = useRef(new Map());
|
|
112
|
+
const isNavigatingRef = useRef(false);
|
|
113
|
+
/**
|
|
114
|
+
* Normalize pathname by removing basename
|
|
115
|
+
*/
|
|
116
|
+
const normalizePathname = useCallback((pathname) => {
|
|
117
|
+
if (basename && pathname.startsWith(basename)) {
|
|
118
|
+
return pathname.slice(basename.length) || "/";
|
|
119
|
+
}
|
|
120
|
+
return pathname;
|
|
121
|
+
}, [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 = "/") => {
|
|
70
139
|
const staticRoutes = [];
|
|
71
140
|
const dynamicRoutes = [];
|
|
141
|
+
const catchAllRoutes = [];
|
|
72
142
|
for (const route of routesList) {
|
|
73
143
|
const is404 = route.path === "404" || route.path === "/404";
|
|
74
144
|
if (is404) {
|
|
75
|
-
|
|
145
|
+
page404Ref.current = route.component;
|
|
76
146
|
continue;
|
|
77
147
|
}
|
|
78
148
|
const pathArray = Array.isArray(route.path) ? route.path : [route.path];
|
|
149
|
+
const hasCatchAll = pathArray.some((p) => p.includes("*"));
|
|
79
150
|
const hasDynamicParams = pathArray.some((p) => p.includes(":"));
|
|
80
|
-
if (
|
|
151
|
+
if (hasCatchAll) {
|
|
152
|
+
catchAllRoutes.push(route);
|
|
153
|
+
}
|
|
154
|
+
else if (hasDynamicParams) {
|
|
81
155
|
dynamicRoutes.push(route);
|
|
82
156
|
}
|
|
83
157
|
else {
|
|
84
158
|
staticRoutes.push(route);
|
|
85
159
|
}
|
|
86
160
|
}
|
|
87
|
-
|
|
161
|
+
// Priority: static > dynamic > catch-all
|
|
162
|
+
const orderedRoutes = [
|
|
163
|
+
...staticRoutes,
|
|
164
|
+
...dynamicRoutes,
|
|
165
|
+
...catchAllRoutes,
|
|
166
|
+
];
|
|
167
|
+
for (const route of orderedRoutes) {
|
|
88
168
|
const fullPath = join(parentPath, `/${route.path}`);
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
176
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 }));
|
|
108
228
|
}
|
|
109
229
|
return route.component;
|
|
110
230
|
}
|
|
231
|
+
// Check children routes (for routes without matching parent)
|
|
111
232
|
if (route.children) {
|
|
112
233
|
const childMatch = getComponent(route.children, currentPath, fullPath);
|
|
113
234
|
if (childMatch)
|
|
@@ -115,32 +236,148 @@ const RouterProvider = ({ routes }) => {
|
|
|
115
236
|
}
|
|
116
237
|
}
|
|
117
238
|
return null;
|
|
118
|
-
};
|
|
119
|
-
|
|
239
|
+
}, [location.search, matchPath, params, pattern]);
|
|
240
|
+
/**
|
|
241
|
+
* Navigate to a new location
|
|
242
|
+
*/
|
|
243
|
+
const navigate = useCallback((to, options) => {
|
|
244
|
+
// Handle numeric (delta) navigation
|
|
245
|
+
if (typeof to === "number") {
|
|
246
|
+
window.history.go(to);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Normalize path
|
|
250
|
+
let targetPath = to;
|
|
120
251
|
if (!/^https?:\/\//i.test(to)) {
|
|
121
|
-
|
|
252
|
+
targetPath = to.startsWith("/") ? to : `/${to}`;
|
|
253
|
+
if (basename) {
|
|
254
|
+
targetPath = join(basename, targetPath);
|
|
255
|
+
}
|
|
122
256
|
}
|
|
123
|
-
|
|
257
|
+
// Validate URL
|
|
258
|
+
if (!validateUrl(targetPath)) {
|
|
124
259
|
RouterErrors.invalidRoute(to, "Invalid URL format");
|
|
125
260
|
return;
|
|
126
261
|
}
|
|
127
262
|
try {
|
|
263
|
+
// Save scroll position before navigation
|
|
264
|
+
if (!(options === null || options === void 0 ? void 0 : options.preventScrollReset)) {
|
|
265
|
+
scrollPositions.current.set(location.key, window.scrollY);
|
|
266
|
+
}
|
|
267
|
+
isNavigatingRef.current = true;
|
|
128
268
|
if (options === null || options === void 0 ? void 0 : options.replace) {
|
|
129
|
-
window.history.replaceState(
|
|
269
|
+
window.history.replaceState({ ...options === null || options === void 0 ? void 0 : options.state, key: createKey() }, "", targetPath);
|
|
130
270
|
}
|
|
131
271
|
else {
|
|
132
|
-
window.history.pushState(
|
|
272
|
+
window.history.pushState({ ...options === null || options === void 0 ? void 0 : options.state, key: createKey() }, "", targetPath);
|
|
273
|
+
}
|
|
274
|
+
// Use transition for better UX
|
|
275
|
+
startTransition(() => {
|
|
276
|
+
setLocation(getCurrentLocation());
|
|
277
|
+
setMatches([]); // Reset matches for new route
|
|
278
|
+
});
|
|
279
|
+
// Scroll to top unless prevented
|
|
280
|
+
if (!(options === null || options === void 0 ? void 0 : options.preventScrollReset)) {
|
|
281
|
+
window.scrollTo(0, 0);
|
|
133
282
|
}
|
|
134
|
-
setPath(to);
|
|
135
283
|
}
|
|
136
284
|
catch (error) {
|
|
137
285
|
const navError = createRouterError(RouterErrorCode.NAVIGATION_ABORTED, `Navigation to "${to}" failed: ${error instanceof Error ? error.message : String(error)}`, { to, error });
|
|
138
286
|
console.error(navError.toConsoleMessage());
|
|
139
287
|
throw navError;
|
|
140
288
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
289
|
+
finally {
|
|
290
|
+
isNavigatingRef.current = false;
|
|
291
|
+
}
|
|
292
|
+
}, [basename, location.key]);
|
|
293
|
+
/**
|
|
294
|
+
* Go back in history
|
|
295
|
+
*/
|
|
296
|
+
const back = useCallback(() => {
|
|
297
|
+
window.history.back();
|
|
298
|
+
}, []);
|
|
299
|
+
/**
|
|
300
|
+
* Go forward in history
|
|
301
|
+
*/
|
|
302
|
+
const forward = useCallback(() => {
|
|
303
|
+
window.history.forward();
|
|
304
|
+
}, []);
|
|
305
|
+
/**
|
|
306
|
+
* Setup history listeners
|
|
307
|
+
*/
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const handleLocationChange = () => {
|
|
310
|
+
startTransition(() => {
|
|
311
|
+
setLocation(getCurrentLocation());
|
|
312
|
+
setMatches([]); // Reset matches for new route
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
// Patch history methods to dispatch custom event
|
|
316
|
+
const patchHistory = (method) => {
|
|
317
|
+
const original = window.history[method];
|
|
318
|
+
return function (state, title, url) {
|
|
319
|
+
const result = original.apply(this, [state, title, url]);
|
|
320
|
+
window.dispatchEvent(new CustomEvent("locationchange", {
|
|
321
|
+
detail: { action: method === "pushState" ? "PUSH" : "REPLACE" },
|
|
322
|
+
}));
|
|
323
|
+
return result;
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
const originalPush = window.history.pushState;
|
|
327
|
+
const originalReplace = window.history.replaceState;
|
|
328
|
+
window.history.pushState = patchHistory("pushState");
|
|
329
|
+
window.history.replaceState = patchHistory("replaceState");
|
|
330
|
+
window.addEventListener("popstate", handleLocationChange);
|
|
331
|
+
window.addEventListener("locationchange", handleLocationChange);
|
|
332
|
+
return () => {
|
|
333
|
+
window.history.pushState = originalPush;
|
|
334
|
+
window.history.replaceState = originalReplace;
|
|
335
|
+
window.removeEventListener("popstate", handleLocationChange);
|
|
336
|
+
window.removeEventListener("locationchange", handleLocationChange);
|
|
337
|
+
};
|
|
338
|
+
}, []);
|
|
339
|
+
/**
|
|
340
|
+
* Compute matched component
|
|
341
|
+
*/
|
|
342
|
+
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, {}));
|
|
345
|
+
/**
|
|
346
|
+
* Build context value with memoization
|
|
347
|
+
*/
|
|
348
|
+
const contextValue = useMemo(() => ({
|
|
349
|
+
// New API
|
|
350
|
+
pathname: normalizedPath,
|
|
351
|
+
pattern,
|
|
352
|
+
search: location.search,
|
|
353
|
+
hash: location.hash,
|
|
354
|
+
state: location.state,
|
|
355
|
+
params,
|
|
356
|
+
matches,
|
|
357
|
+
navigate,
|
|
358
|
+
back,
|
|
359
|
+
forward,
|
|
360
|
+
isNavigating: isPending || isNavigatingRef.current,
|
|
361
|
+
loaderData,
|
|
362
|
+
meta,
|
|
363
|
+
// Legacy aliases for backward compatibility
|
|
364
|
+
path: normalizedPath,
|
|
365
|
+
fullPathWithParams: pattern,
|
|
366
|
+
}), [
|
|
367
|
+
normalizedPath,
|
|
368
|
+
pattern,
|
|
369
|
+
location.search,
|
|
370
|
+
location.hash,
|
|
371
|
+
location.state,
|
|
372
|
+
params,
|
|
373
|
+
matches,
|
|
374
|
+
navigate,
|
|
375
|
+
back,
|
|
376
|
+
forward,
|
|
377
|
+
isPending,
|
|
378
|
+
loaderData,
|
|
379
|
+
meta,
|
|
380
|
+
]);
|
|
381
|
+
return (_jsx(RouterContext.Provider, { value: contextValue, children: fallbackElement && isPending ? (_jsx(Suspense, { fallback: fallbackElement, children: component })) : (component) }));
|
|
145
382
|
};
|
|
146
383
|
export default RouterProvider;
|