@zap-js/client 0.0.2 → 0.0.5
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 +310 -24
- package/bin/zap +0 -0
- package/bin/zap-codegen +0 -0
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.js +282 -0
- package/dist/cli/commands/codegen.d.ts +8 -0
- package/dist/cli/commands/codegen.js +95 -0
- package/dist/cli/commands/dev.d.ts +20 -0
- package/dist/cli/commands/dev.js +78 -0
- package/dist/cli/commands/new.d.ts +9 -0
- package/dist/cli/commands/new.js +307 -0
- package/dist/cli/commands/routes-old.d.ts +9 -0
- package/dist/cli/commands/routes-old.js +106 -0
- package/dist/cli/commands/routes.d.ts +11 -0
- package/dist/cli/commands/routes.js +280 -0
- package/dist/cli/commands/serve.d.ts +17 -0
- package/dist/cli/commands/serve.js +386 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +76 -0
- package/dist/cli/utils/index.d.ts +2 -0
- package/dist/cli/utils/index.js +2 -0
- package/dist/cli/utils/logger.d.ts +84 -0
- package/dist/cli/utils/logger.js +181 -0
- package/dist/cli/utils/port-finder.d.ts +8 -0
- package/dist/cli/utils/port-finder.js +48 -0
- package/dist/dev-server/codegen-runner.d.ts +41 -0
- package/dist/dev-server/codegen-runner.js +172 -0
- package/dist/dev-server/hot-reload.d.ts +72 -0
- package/dist/dev-server/hot-reload.js +280 -0
- package/dist/dev-server/index.d.ts +8 -0
- package/dist/dev-server/index.js +8 -0
- package/dist/dev-server/route-scanner.d.ts +84 -0
- package/dist/dev-server/route-scanner.js +113 -0
- package/dist/dev-server/rust-builder.d.ts +66 -0
- package/dist/dev-server/rust-builder.js +286 -0
- package/dist/dev-server/server.d.ts +147 -0
- package/dist/dev-server/server.js +660 -0
- package/dist/dev-server/vite-proxy.d.ts +56 -0
- package/dist/dev-server/vite-proxy.js +212 -0
- package/dist/dev-server/watcher.d.ts +48 -0
- package/dist/dev-server/watcher.js +127 -0
- package/dist/router/codegen-enhanced.d.ts +5 -0
- package/dist/router/codegen-enhanced.js +275 -0
- package/dist/router/codegen.d.ts +17 -0
- package/dist/router/codegen.js +654 -0
- package/dist/router/index.d.ts +16 -0
- package/dist/router/index.js +19 -0
- package/dist/router/scanner.d.ts +86 -0
- package/dist/router/scanner.js +689 -0
- package/dist/router/ssg.d.ts +115 -0
- package/dist/router/ssg.js +202 -0
- package/dist/router/types.d.ts +124 -0
- package/dist/router/types.js +9 -0
- package/dist/router/watch.d.ts +38 -0
- package/dist/router/watch.js +135 -0
- package/dist/runtime/csrf.d.ts +146 -0
- package/dist/runtime/csrf.js +166 -0
- package/dist/runtime/error-boundary.d.ts +129 -0
- package/dist/runtime/error-boundary.js +287 -0
- package/dist/runtime/hooks.d.ts +83 -0
- package/dist/runtime/hooks.js +96 -0
- package/dist/runtime/index.d.ts +229 -0
- package/dist/runtime/index.js +449 -0
- package/dist/runtime/ipc-client.d.ts +144 -0
- package/dist/runtime/ipc-client.js +621 -0
- package/dist/runtime/logger.d.ts +71 -0
- package/dist/runtime/logger.js +164 -0
- package/dist/runtime/middleware.d.ts +66 -0
- package/dist/runtime/middleware.js +114 -0
- package/dist/runtime/process-manager.d.ts +51 -0
- package/dist/runtime/process-manager.js +207 -0
- package/dist/runtime/router-simple.d.ts +98 -0
- package/dist/runtime/router-simple.js +330 -0
- package/dist/runtime/router.d.ts +103 -0
- package/dist/runtime/router.js +435 -0
- package/dist/runtime/rpc-client.d.ts +35 -0
- package/dist/runtime/rpc-client.js +140 -0
- package/dist/runtime/streaming-utils.d.ts +86 -0
- package/dist/runtime/streaming-utils.js +150 -0
- package/dist/runtime/types.d.ts +465 -0
- package/dist/runtime/types.js +60 -0
- package/dist/runtime/websockets-utils.d.ts +50 -0
- package/dist/runtime/websockets-utils.js +92 -0
- package/package.json +30 -20
- package/index.js +0 -29
- package/internal/cli/package.json +0 -46
- package/internal/cli/tsconfig.tsbuildinfo +0 -1
- package/internal/dev-server/node_modules/ora/index.d.ts +0 -332
- package/internal/dev-server/node_modules/ora/index.js +0 -416
- package/internal/dev-server/node_modules/ora/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.d.ts +0 -36
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.js +0 -65
- package/internal/dev-server/node_modules/ora/node_modules/string-width/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/README.md +0 -107
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.d.ts +0 -3
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.js +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.mjs +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/package.json +0 -46
- package/internal/dev-server/node_modules/ora/node_modules/string-width/package.json +0 -60
- package/internal/dev-server/node_modules/ora/node_modules/string-width/readme.md +0 -62
- package/internal/dev-server/node_modules/ora/package.json +0 -66
- package/internal/dev-server/node_modules/ora/readme.md +0 -325
- package/internal/dev-server/package.json +0 -41
- package/internal/router/package.json +0 -28
- package/internal/runtime/package.json +0 -41
- package/internal/runtime/src/error-boundary.tsx +0 -476
- package/internal/runtime/src/router-simple.tsx +0 -640
- package/internal/runtime/src/router.tsx +0 -771
- package/internal/runtime/tsconfig.tsbuildinfo +0 -1
- package/src/errors.js +0 -33
- package/src/logger.js +0 -10
- package/src/middleware.js +0 -32
- package/src/router.js +0 -41
- package/src/types.js +0 -39
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ZapJS Production Router
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Nested layouts
|
|
6
|
-
* - Route-level middleware
|
|
7
|
-
* - Code splitting
|
|
8
|
-
* - Error boundaries
|
|
9
|
-
* - Type-safe navigation
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import React, {
|
|
13
|
-
createContext,
|
|
14
|
-
useContext,
|
|
15
|
-
useState,
|
|
16
|
-
useEffect,
|
|
17
|
-
useCallback,
|
|
18
|
-
useMemo,
|
|
19
|
-
useTransition,
|
|
20
|
-
Suspense,
|
|
21
|
-
memo,
|
|
22
|
-
type ReactNode,
|
|
23
|
-
type ComponentType,
|
|
24
|
-
type MouseEvent,
|
|
25
|
-
} from 'react';
|
|
26
|
-
import { composeMiddleware, type RouteMiddleware, type MiddlewareFunction, type MiddlewareContext } from './middleware.js';
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// Types
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
|
-
export interface LayoutDefinition {
|
|
33
|
-
path: string;
|
|
34
|
-
component: React.LazyExoticComponent<ComponentType<any>>;
|
|
35
|
-
parentLayout?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RouteDefinition {
|
|
39
|
-
path: string;
|
|
40
|
-
pattern: RegExp;
|
|
41
|
-
paramNames: string[];
|
|
42
|
-
component: React.LazyExoticComponent<ComponentType<any>>;
|
|
43
|
-
layoutPath?: string;
|
|
44
|
-
errorComponent?: React.LazyExoticComponent<ComponentType<any>>;
|
|
45
|
-
pendingComponent?: React.LazyExoticComponent<ComponentType<any>>;
|
|
46
|
-
meta?: () => Promise<RouteMeta>;
|
|
47
|
-
middleware?: () => Promise<RouteMiddleware[]>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface RouteMeta {
|
|
51
|
-
title?: string;
|
|
52
|
-
description?: string;
|
|
53
|
-
keywords?: string[];
|
|
54
|
-
[key: string]: any;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface RouteMatch {
|
|
58
|
-
route: RouteDefinition;
|
|
59
|
-
params: Record<string, string>;
|
|
60
|
-
pathname: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface RouterState {
|
|
64
|
-
pathname: string;
|
|
65
|
-
search: string;
|
|
66
|
-
hash: string;
|
|
67
|
-
match: RouteMatch | null;
|
|
68
|
-
middlewareData?: Record<string, any>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface NavigateOptions {
|
|
72
|
-
replace?: boolean;
|
|
73
|
-
scroll?: boolean;
|
|
74
|
-
state?: unknown;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface Router {
|
|
78
|
-
push(path: string, options?: NavigateOptions): void;
|
|
79
|
-
replace(path: string, options?: NavigateOptions): void;
|
|
80
|
-
back(): void;
|
|
81
|
-
forward(): void;
|
|
82
|
-
refresh(): void;
|
|
83
|
-
prefetch(path: string): void;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ============================================================================
|
|
87
|
-
// Context
|
|
88
|
-
// ============================================================================
|
|
89
|
-
|
|
90
|
-
interface RouterContextValue {
|
|
91
|
-
state: RouterState;
|
|
92
|
-
router: Router;
|
|
93
|
-
routes: RouteDefinition[];
|
|
94
|
-
layouts: LayoutDefinition[];
|
|
95
|
-
isPending: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const RouterContext = createContext<RouterContextValue | null>(null);
|
|
99
|
-
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// Route Matching
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
|
-
function matchRoute(pathname: string, routes: RouteDefinition[]): RouteMatch | null {
|
|
105
|
-
const normalizedPath = pathname === '' ? '/' : pathname;
|
|
106
|
-
|
|
107
|
-
for (const route of routes) {
|
|
108
|
-
const match = normalizedPath.match(route.pattern);
|
|
109
|
-
if (match) {
|
|
110
|
-
const params: Record<string, string> = {};
|
|
111
|
-
route.paramNames.forEach((name, index) => {
|
|
112
|
-
const value = match[index + 1];
|
|
113
|
-
if (value !== undefined && value !== '') {
|
|
114
|
-
params[name] = decodeURIComponent(value);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
return { route, params, pathname: normalizedPath };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseUrl(url: string): { pathname: string; search: string; hash: string } {
|
|
126
|
-
try {
|
|
127
|
-
const parsed = new URL(url, window.location.origin);
|
|
128
|
-
return {
|
|
129
|
-
pathname: parsed.pathname,
|
|
130
|
-
search: parsed.search,
|
|
131
|
-
hash: parsed.hash,
|
|
132
|
-
};
|
|
133
|
-
} catch {
|
|
134
|
-
const hashIndex = url.indexOf('#');
|
|
135
|
-
const searchIndex = url.indexOf('?');
|
|
136
|
-
|
|
137
|
-
let pathname = url;
|
|
138
|
-
let search = '';
|
|
139
|
-
let hash = '';
|
|
140
|
-
|
|
141
|
-
if (hashIndex !== -1) {
|
|
142
|
-
hash = url.slice(hashIndex);
|
|
143
|
-
pathname = url.slice(0, hashIndex);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (searchIndex !== -1 && (hashIndex === -1 || searchIndex < hashIndex)) {
|
|
147
|
-
search = pathname.slice(searchIndex, hashIndex !== -1 ? hashIndex - searchIndex : undefined);
|
|
148
|
-
pathname = pathname.slice(0, searchIndex);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { pathname: pathname || '/', search, hash };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ============================================================================
|
|
156
|
-
// Middleware Runner
|
|
157
|
-
// ============================================================================
|
|
158
|
-
|
|
159
|
-
async function runRouteMiddleware(
|
|
160
|
-
match: RouteMatch,
|
|
161
|
-
pathname: string,
|
|
162
|
-
search: string,
|
|
163
|
-
hash: string,
|
|
164
|
-
state?: unknown
|
|
165
|
-
): Promise<{ allowed: boolean; redirectTo?: string; error?: Error; data?: Record<string, any> }> {
|
|
166
|
-
if (!match.route.middleware) {
|
|
167
|
-
return { allowed: true };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const middlewares = await match.route.middleware();
|
|
172
|
-
const composedMiddleware = composeMiddleware(middlewares);
|
|
173
|
-
|
|
174
|
-
const context: MiddlewareContext = {
|
|
175
|
-
match,
|
|
176
|
-
pathname,
|
|
177
|
-
search,
|
|
178
|
-
hash,
|
|
179
|
-
state,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const result = await composedMiddleware(context);
|
|
183
|
-
|
|
184
|
-
switch (result.type) {
|
|
185
|
-
case 'continue':
|
|
186
|
-
return { allowed: true, data: result.data };
|
|
187
|
-
case 'redirect':
|
|
188
|
-
return { allowed: false, redirectTo: result.redirectTo };
|
|
189
|
-
case 'block':
|
|
190
|
-
return { allowed: false, error: result.error };
|
|
191
|
-
default:
|
|
192
|
-
return { allowed: true };
|
|
193
|
-
}
|
|
194
|
-
} catch (error) {
|
|
195
|
-
console.error('Middleware execution failed:', error);
|
|
196
|
-
return { allowed: true }; // Allow navigation on middleware error
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ============================================================================
|
|
201
|
-
// Layout Wrapper
|
|
202
|
-
// ============================================================================
|
|
203
|
-
|
|
204
|
-
interface LayoutWrapperProps {
|
|
205
|
-
layouts: LayoutDefinition[];
|
|
206
|
-
layoutPath?: string;
|
|
207
|
-
children: ReactNode;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const LayoutWrapper = memo(({ layouts, layoutPath, children }: LayoutWrapperProps) => {
|
|
211
|
-
if (!layoutPath) {
|
|
212
|
-
return <>{children}</>;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const layoutChain: LayoutDefinition[] = [];
|
|
216
|
-
let currentPath: string | undefined = layoutPath;
|
|
217
|
-
|
|
218
|
-
while (currentPath) {
|
|
219
|
-
const layout = layouts.find(l => l.path === currentPath);
|
|
220
|
-
if (layout) {
|
|
221
|
-
layoutChain.unshift(layout);
|
|
222
|
-
currentPath = layout.parentLayout;
|
|
223
|
-
} else {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return layoutChain.reduce<ReactNode>((content, layout) => {
|
|
229
|
-
const LayoutComponent = layout.component;
|
|
230
|
-
return (
|
|
231
|
-
<Suspense fallback={<div>Loading layout...</div>}>
|
|
232
|
-
<LayoutComponent>{content}</LayoutComponent>
|
|
233
|
-
</Suspense>
|
|
234
|
-
);
|
|
235
|
-
}, children);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
LayoutWrapper.displayName = 'LayoutWrapper';
|
|
239
|
-
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// RouterProvider
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
|
-
interface RouterProviderProps {
|
|
245
|
-
routes: RouteDefinition[];
|
|
246
|
-
layouts?: LayoutDefinition[];
|
|
247
|
-
children: ReactNode;
|
|
248
|
-
notFound?: ComponentType;
|
|
249
|
-
fallback?: ReactNode;
|
|
250
|
-
onRouteError?: (error: Error) => void;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function RouterProvider({
|
|
254
|
-
routes,
|
|
255
|
-
layouts = [],
|
|
256
|
-
children,
|
|
257
|
-
notFound: NotFound,
|
|
258
|
-
fallback = null,
|
|
259
|
-
onRouteError,
|
|
260
|
-
}: RouterProviderProps): JSX.Element {
|
|
261
|
-
const [isPending, startTransition] = useTransition();
|
|
262
|
-
|
|
263
|
-
const [state, setState] = useState<RouterState>(() => {
|
|
264
|
-
const { pathname, search, hash } = parseUrl(window.location.href);
|
|
265
|
-
return {
|
|
266
|
-
pathname,
|
|
267
|
-
search,
|
|
268
|
-
hash,
|
|
269
|
-
match: matchRoute(pathname, routes),
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const navigate = useCallback(
|
|
274
|
-
async (path: string, options: NavigateOptions = {}) => {
|
|
275
|
-
const { replace = false, scroll = true } = options;
|
|
276
|
-
const { pathname, search, hash } = parseUrl(path);
|
|
277
|
-
|
|
278
|
-
// Find route match
|
|
279
|
-
const match = matchRoute(pathname, routes);
|
|
280
|
-
|
|
281
|
-
if (match) {
|
|
282
|
-
// Run middleware
|
|
283
|
-
const { allowed, redirectTo, error, data } = await runRouteMiddleware(
|
|
284
|
-
match,
|
|
285
|
-
pathname,
|
|
286
|
-
search,
|
|
287
|
-
hash,
|
|
288
|
-
options.state
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
if (!allowed) {
|
|
292
|
-
if (redirectTo) {
|
|
293
|
-
// Redirect instead
|
|
294
|
-
navigate(redirectTo, { replace: true });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (error) {
|
|
298
|
-
onRouteError?.(error);
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Update URL
|
|
305
|
-
const url = pathname + search + hash;
|
|
306
|
-
if (replace) {
|
|
307
|
-
window.history.replaceState(options.state ?? null, '', url);
|
|
308
|
-
} else {
|
|
309
|
-
window.history.pushState(options.state ?? null, '', url);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Update state with middleware data
|
|
313
|
-
startTransition(() => {
|
|
314
|
-
setState({
|
|
315
|
-
pathname,
|
|
316
|
-
search,
|
|
317
|
-
hash,
|
|
318
|
-
match,
|
|
319
|
-
middlewareData: data,
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (scroll) {
|
|
324
|
-
if (hash) {
|
|
325
|
-
const element = document.querySelector(hash);
|
|
326
|
-
element?.scrollIntoView();
|
|
327
|
-
} else {
|
|
328
|
-
window.scrollTo(0, 0);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
// No match - update anyway to show 404
|
|
333
|
-
const url = pathname + search + hash;
|
|
334
|
-
if (replace) {
|
|
335
|
-
window.history.replaceState(options.state ?? null, '', url);
|
|
336
|
-
} else {
|
|
337
|
-
window.history.pushState(options.state ?? null, '', url);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
startTransition(() => {
|
|
341
|
-
setState({
|
|
342
|
-
pathname,
|
|
343
|
-
search,
|
|
344
|
-
hash,
|
|
345
|
-
match: null,
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
[routes, onRouteError]
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
const router = useMemo<Router>(
|
|
354
|
-
() => ({
|
|
355
|
-
push: (path, options) => navigate(path, options),
|
|
356
|
-
replace: (path, options) => navigate(path, { ...options, replace: true }),
|
|
357
|
-
back: () => window.history.back(),
|
|
358
|
-
forward: () => window.history.forward(),
|
|
359
|
-
refresh: () => {
|
|
360
|
-
startTransition(() => {
|
|
361
|
-
setState((prev) => ({ ...prev, match: matchRoute(prev.pathname, routes) }));
|
|
362
|
-
});
|
|
363
|
-
},
|
|
364
|
-
prefetch: (path) => {
|
|
365
|
-
const { pathname } = parseUrl(path);
|
|
366
|
-
const match = matchRoute(pathname, routes);
|
|
367
|
-
if (match?.route.component) {
|
|
368
|
-
const component = match.route.component as any;
|
|
369
|
-
if (component._payload && component._init) {
|
|
370
|
-
try {
|
|
371
|
-
component._init(component._payload);
|
|
372
|
-
} catch {
|
|
373
|
-
// Component will load when rendered
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
},
|
|
378
|
-
}),
|
|
379
|
-
[navigate, routes]
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
useEffect(() => {
|
|
383
|
-
const handlePopState = async () => {
|
|
384
|
-
const { pathname, search, hash } = parseUrl(window.location.href);
|
|
385
|
-
const match = matchRoute(pathname, routes);
|
|
386
|
-
|
|
387
|
-
if (match) {
|
|
388
|
-
// Run middleware for browser navigation too
|
|
389
|
-
const { allowed, redirectTo, error, data } = await runRouteMiddleware(
|
|
390
|
-
match,
|
|
391
|
-
pathname,
|
|
392
|
-
search,
|
|
393
|
-
hash
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
if (!allowed) {
|
|
397
|
-
if (redirectTo) {
|
|
398
|
-
navigate(redirectTo, { replace: true });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (error) {
|
|
402
|
-
onRouteError?.(error);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
startTransition(() => {
|
|
408
|
-
setState({
|
|
409
|
-
pathname,
|
|
410
|
-
search,
|
|
411
|
-
hash,
|
|
412
|
-
match,
|
|
413
|
-
middlewareData: data,
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
} else {
|
|
417
|
-
startTransition(() => {
|
|
418
|
-
setState({
|
|
419
|
-
pathname,
|
|
420
|
-
search,
|
|
421
|
-
hash,
|
|
422
|
-
match: null,
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
window.addEventListener('popstate', handlePopState);
|
|
429
|
-
return () => window.removeEventListener('popstate', handlePopState);
|
|
430
|
-
}, [routes, navigate, onRouteError]);
|
|
431
|
-
|
|
432
|
-
// Update document meta on route change
|
|
433
|
-
useEffect(() => {
|
|
434
|
-
if (state.match?.route.meta) {
|
|
435
|
-
state.match.route.meta().then((meta) => {
|
|
436
|
-
if (meta.title) {
|
|
437
|
-
document.title = meta.title;
|
|
438
|
-
}
|
|
439
|
-
if (meta.description) {
|
|
440
|
-
const metaDesc = document.querySelector('meta[name="description"]');
|
|
441
|
-
if (metaDesc) {
|
|
442
|
-
metaDesc.setAttribute('content', meta.description);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}).catch(() => {
|
|
446
|
-
// Ignore meta errors
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}, [state.match]);
|
|
450
|
-
|
|
451
|
-
const contextValue = useMemo<RouterContextValue>(
|
|
452
|
-
() => ({
|
|
453
|
-
state,
|
|
454
|
-
router,
|
|
455
|
-
routes,
|
|
456
|
-
layouts,
|
|
457
|
-
isPending,
|
|
458
|
-
}),
|
|
459
|
-
[state, router, routes, layouts, isPending]
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
return (
|
|
463
|
-
<RouterContext.Provider value={contextValue}>
|
|
464
|
-
<Suspense fallback={fallback}>
|
|
465
|
-
{children}
|
|
466
|
-
</Suspense>
|
|
467
|
-
</RouterContext.Provider>
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// ============================================================================
|
|
472
|
-
// Hooks
|
|
473
|
-
// ============================================================================
|
|
474
|
-
|
|
475
|
-
export function useRouter(): Router {
|
|
476
|
-
const context = useContext(RouterContext);
|
|
477
|
-
if (!context) {
|
|
478
|
-
throw new Error('useRouter must be used within a RouterProvider');
|
|
479
|
-
}
|
|
480
|
-
return context.router;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
484
|
-
const context = useContext(RouterContext);
|
|
485
|
-
if (!context) {
|
|
486
|
-
throw new Error('useParams must be used within a RouterProvider');
|
|
487
|
-
}
|
|
488
|
-
return (context.state.match?.params ?? {}) as T;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function usePathname(): string {
|
|
492
|
-
const context = useContext(RouterContext);
|
|
493
|
-
if (!context) {
|
|
494
|
-
throw new Error('usePathname must be used within a RouterProvider');
|
|
495
|
-
}
|
|
496
|
-
return context.state.pathname;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export function useSearchParams(): [URLSearchParams, (params: Record<string, string>) => void] {
|
|
500
|
-
const context = useContext(RouterContext);
|
|
501
|
-
if (!context) {
|
|
502
|
-
throw new Error('useSearchParams must be used within a RouterProvider');
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const searchParams = useMemo(
|
|
506
|
-
() => new URLSearchParams(context.state.search),
|
|
507
|
-
[context.state.search]
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
const setSearchParams = useCallback(
|
|
511
|
-
(params: Record<string, string>) => {
|
|
512
|
-
const newParams = new URLSearchParams(params);
|
|
513
|
-
const newSearch = newParams.toString();
|
|
514
|
-
const path = context.state.pathname + (newSearch ? `?${newSearch}` : '') + context.state.hash;
|
|
515
|
-
context.router.push(path, { scroll: false });
|
|
516
|
-
},
|
|
517
|
-
[context.router, context.state.pathname, context.state.hash]
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
return [searchParams, setSearchParams];
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export function useMiddlewareData<T = Record<string, any>>(): T | undefined {
|
|
524
|
-
const context = useContext(RouterContext);
|
|
525
|
-
if (!context) {
|
|
526
|
-
throw new Error('useMiddlewareData must be used within a RouterProvider');
|
|
527
|
-
}
|
|
528
|
-
return context.state.middlewareData as T;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
export function useRouteMatch(): RouteMatch | null {
|
|
532
|
-
const context = useContext(RouterContext);
|
|
533
|
-
if (!context) {
|
|
534
|
-
throw new Error('useRouteMatch must be used within a RouterProvider');
|
|
535
|
-
}
|
|
536
|
-
return context.state.match;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
export function useIsPending(): boolean {
|
|
540
|
-
const context = useContext(RouterContext);
|
|
541
|
-
if (!context) {
|
|
542
|
-
throw new Error('useIsPending must be used within a RouterProvider');
|
|
543
|
-
}
|
|
544
|
-
return context.isPending;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ============================================================================
|
|
548
|
-
// Components
|
|
549
|
-
// ============================================================================
|
|
550
|
-
|
|
551
|
-
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
552
|
-
to: string;
|
|
553
|
-
replace?: boolean;
|
|
554
|
-
prefetch?: boolean;
|
|
555
|
-
scroll?: boolean;
|
|
556
|
-
children: ReactNode;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
export function Link({
|
|
560
|
-
to,
|
|
561
|
-
replace = false,
|
|
562
|
-
prefetch = true,
|
|
563
|
-
scroll = true,
|
|
564
|
-
children,
|
|
565
|
-
onClick,
|
|
566
|
-
onMouseEnter,
|
|
567
|
-
...props
|
|
568
|
-
}: LinkProps): JSX.Element {
|
|
569
|
-
const context = useContext(RouterContext);
|
|
570
|
-
|
|
571
|
-
const handleClick = useCallback(
|
|
572
|
-
(e: MouseEvent<HTMLAnchorElement>) => {
|
|
573
|
-
onClick?.(e);
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
e.defaultPrevented ||
|
|
577
|
-
e.button !== 0 ||
|
|
578
|
-
e.metaKey ||
|
|
579
|
-
e.ctrlKey ||
|
|
580
|
-
e.shiftKey ||
|
|
581
|
-
e.altKey
|
|
582
|
-
) {
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const href = to;
|
|
587
|
-
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
e.preventDefault();
|
|
592
|
-
context?.router[replace ? 'replace' : 'push'](to, { scroll });
|
|
593
|
-
},
|
|
594
|
-
[context?.router, to, replace, scroll, onClick]
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
const handleMouseEnter = useCallback(
|
|
598
|
-
(e: MouseEvent<HTMLAnchorElement>) => {
|
|
599
|
-
onMouseEnter?.(e);
|
|
600
|
-
if (prefetch && context) {
|
|
601
|
-
context.router.prefetch(to);
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
[context, to, prefetch, onMouseEnter]
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
return (
|
|
608
|
-
<a
|
|
609
|
-
href={to}
|
|
610
|
-
onClick={handleClick}
|
|
611
|
-
onMouseEnter={handleMouseEnter}
|
|
612
|
-
{...props}
|
|
613
|
-
>
|
|
614
|
-
{children}
|
|
615
|
-
</a>
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
interface OutletProps {
|
|
620
|
-
notFound?: ComponentType;
|
|
621
|
-
fallback?: ReactNode;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
export function Outlet({ notFound: NotFound, fallback = null }: OutletProps): JSX.Element | null {
|
|
625
|
-
const context = useContext(RouterContext);
|
|
626
|
-
if (!context) {
|
|
627
|
-
throw new Error('Outlet must be used within a RouterProvider');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const { match, middlewareData } = context.state;
|
|
631
|
-
|
|
632
|
-
if (!match) {
|
|
633
|
-
return NotFound ? <NotFound /> : null;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const { route, params } = match;
|
|
637
|
-
const Component = route.component;
|
|
638
|
-
const ErrorComponent = route.errorComponent;
|
|
639
|
-
const PendingComponent = route.pendingComponent;
|
|
640
|
-
|
|
641
|
-
const routeElement = (
|
|
642
|
-
<Suspense fallback={PendingComponent ? <PendingComponent /> : fallback}>
|
|
643
|
-
<Component params={params} {...middlewareData} />
|
|
644
|
-
</Suspense>
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
const wrappedElement = (
|
|
648
|
-
<LayoutWrapper
|
|
649
|
-
layouts={context.layouts}
|
|
650
|
-
layoutPath={route.layoutPath}
|
|
651
|
-
>
|
|
652
|
-
{ErrorComponent ? (
|
|
653
|
-
<RouteErrorBoundary fallback={<ErrorComponent />}>
|
|
654
|
-
{routeElement}
|
|
655
|
-
</RouteErrorBoundary>
|
|
656
|
-
) : routeElement}
|
|
657
|
-
</LayoutWrapper>
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
return wrappedElement;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
interface RouteErrorBoundaryProps {
|
|
664
|
-
children: ReactNode;
|
|
665
|
-
fallback: ReactNode;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
interface RouteErrorBoundaryState {
|
|
669
|
-
hasError: boolean;
|
|
670
|
-
error: Error | null;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
class RouteErrorBoundary extends React.Component<RouteErrorBoundaryProps, RouteErrorBoundaryState> {
|
|
674
|
-
constructor(props: RouteErrorBoundaryProps) {
|
|
675
|
-
super(props);
|
|
676
|
-
this.state = { hasError: false, error: null };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
static getDerivedStateFromError(error: Error): RouteErrorBoundaryState {
|
|
680
|
-
return { hasError: true, error };
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
684
|
-
console.error('Route error:', error, errorInfo);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
render() {
|
|
688
|
-
if (this.state.hasError) {
|
|
689
|
-
return this.props.fallback;
|
|
690
|
-
}
|
|
691
|
-
return this.props.children;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
interface NavLinkProps extends LinkProps {
|
|
696
|
-
activeClassName?: string;
|
|
697
|
-
activeStyle?: React.CSSProperties;
|
|
698
|
-
exact?: boolean;
|
|
699
|
-
pending?: boolean;
|
|
700
|
-
pendingClassName?: string;
|
|
701
|
-
pendingStyle?: React.CSSProperties;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
export function NavLink({
|
|
705
|
-
to,
|
|
706
|
-
activeClassName,
|
|
707
|
-
activeStyle,
|
|
708
|
-
exact = false,
|
|
709
|
-
pending = false,
|
|
710
|
-
pendingClassName,
|
|
711
|
-
pendingStyle,
|
|
712
|
-
className,
|
|
713
|
-
style,
|
|
714
|
-
...props
|
|
715
|
-
}: NavLinkProps): JSX.Element {
|
|
716
|
-
const pathname = usePathname();
|
|
717
|
-
const isPending = useIsPending();
|
|
718
|
-
|
|
719
|
-
const isActive = exact
|
|
720
|
-
? pathname === to
|
|
721
|
-
: pathname.startsWith(to) && (to === '/' ? pathname === '/' : true);
|
|
722
|
-
|
|
723
|
-
const isPendingRoute = pending && isPending;
|
|
724
|
-
|
|
725
|
-
const combinedClassName = [
|
|
726
|
-
className,
|
|
727
|
-
isActive && activeClassName,
|
|
728
|
-
isPendingRoute && pendingClassName,
|
|
729
|
-
].filter(Boolean).join(' ').trim() || undefined;
|
|
730
|
-
|
|
731
|
-
const combinedStyle = {
|
|
732
|
-
...style,
|
|
733
|
-
...(isActive ? activeStyle : {}),
|
|
734
|
-
...(isPendingRoute ? pendingStyle : {}),
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
return (
|
|
738
|
-
<Link
|
|
739
|
-
to={to}
|
|
740
|
-
className={combinedClassName}
|
|
741
|
-
style={combinedStyle}
|
|
742
|
-
{...props}
|
|
743
|
-
/>
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
interface RedirectProps {
|
|
748
|
-
to: string;
|
|
749
|
-
replace?: boolean;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
export function Redirect({ to, replace = true }: RedirectProps): null {
|
|
753
|
-
const router = useRouter();
|
|
754
|
-
|
|
755
|
-
useEffect(() => {
|
|
756
|
-
router[replace ? 'replace' : 'push'](to);
|
|
757
|
-
}, [router, to, replace]);
|
|
758
|
-
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Export types
|
|
763
|
-
export type {
|
|
764
|
-
RouteDefinition,
|
|
765
|
-
LayoutDefinition,
|
|
766
|
-
RouterState,
|
|
767
|
-
RouteMatch,
|
|
768
|
-
NavigateOptions,
|
|
769
|
-
Router,
|
|
770
|
-
RouteMeta,
|
|
771
|
-
};
|