@zap-js/client 0.0.2 → 0.0.4
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 +71 -0
- package/dist/dev-server/route-scanner.js +114 -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 +658 -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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ZapJS Production Router
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Nested layouts
|
|
7
|
+
* - Route-level middleware
|
|
8
|
+
* - Code splitting
|
|
9
|
+
* - Error boundaries
|
|
10
|
+
* - Type-safe navigation
|
|
11
|
+
*/
|
|
12
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useTransition, Suspense, memo, } from 'react';
|
|
13
|
+
import { composeMiddleware } from './middleware.js';
|
|
14
|
+
const RouterContext = createContext(null);
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Route Matching
|
|
17
|
+
// ============================================================================
|
|
18
|
+
function matchRoute(pathname, routes) {
|
|
19
|
+
const normalizedPath = pathname === '' ? '/' : pathname;
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
const match = normalizedPath.match(route.pattern);
|
|
22
|
+
if (match) {
|
|
23
|
+
const params = {};
|
|
24
|
+
route.paramNames.forEach((name, index) => {
|
|
25
|
+
const value = match[index + 1];
|
|
26
|
+
if (value !== undefined && value !== '') {
|
|
27
|
+
params[name] = decodeURIComponent(value);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return { route, params, pathname: normalizedPath };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function parseUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url, window.location.origin);
|
|
38
|
+
return {
|
|
39
|
+
pathname: parsed.pathname,
|
|
40
|
+
search: parsed.search,
|
|
41
|
+
hash: parsed.hash,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const hashIndex = url.indexOf('#');
|
|
46
|
+
const searchIndex = url.indexOf('?');
|
|
47
|
+
let pathname = url;
|
|
48
|
+
let search = '';
|
|
49
|
+
let hash = '';
|
|
50
|
+
if (hashIndex !== -1) {
|
|
51
|
+
hash = url.slice(hashIndex);
|
|
52
|
+
pathname = url.slice(0, hashIndex);
|
|
53
|
+
}
|
|
54
|
+
if (searchIndex !== -1 && (hashIndex === -1 || searchIndex < hashIndex)) {
|
|
55
|
+
search = pathname.slice(searchIndex, hashIndex !== -1 ? hashIndex - searchIndex : undefined);
|
|
56
|
+
pathname = pathname.slice(0, searchIndex);
|
|
57
|
+
}
|
|
58
|
+
return { pathname: pathname || '/', search, hash };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Middleware Runner
|
|
63
|
+
// ============================================================================
|
|
64
|
+
async function runRouteMiddleware(match, pathname, search, hash, state) {
|
|
65
|
+
if (!match.route.middleware) {
|
|
66
|
+
return { allowed: true };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const middlewares = await match.route.middleware();
|
|
70
|
+
const composedMiddleware = composeMiddleware(middlewares);
|
|
71
|
+
const context = {
|
|
72
|
+
match,
|
|
73
|
+
pathname,
|
|
74
|
+
search,
|
|
75
|
+
hash,
|
|
76
|
+
state,
|
|
77
|
+
};
|
|
78
|
+
const result = await composedMiddleware(context);
|
|
79
|
+
switch (result.type) {
|
|
80
|
+
case 'continue':
|
|
81
|
+
return { allowed: true, data: result.data };
|
|
82
|
+
case 'redirect':
|
|
83
|
+
return { allowed: false, redirectTo: result.redirectTo };
|
|
84
|
+
case 'block':
|
|
85
|
+
return { allowed: false, error: result.error };
|
|
86
|
+
default:
|
|
87
|
+
return { allowed: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Middleware execution failed:', error);
|
|
92
|
+
return { allowed: true }; // Allow navigation on middleware error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const LayoutWrapper = memo(({ layouts, layoutPath, children }) => {
|
|
96
|
+
if (!layoutPath) {
|
|
97
|
+
return _jsx(_Fragment, { children: children });
|
|
98
|
+
}
|
|
99
|
+
const layoutChain = [];
|
|
100
|
+
let currentPath = layoutPath;
|
|
101
|
+
while (currentPath) {
|
|
102
|
+
const layout = layouts.find(l => l.path === currentPath);
|
|
103
|
+
if (layout) {
|
|
104
|
+
layoutChain.unshift(layout);
|
|
105
|
+
currentPath = layout.parentLayout;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return layoutChain.reduce((content, layout) => {
|
|
112
|
+
const LayoutComponent = layout.component;
|
|
113
|
+
return (_jsx(Suspense, { fallback: _jsx("div", { children: "Loading layout..." }), children: _jsx(LayoutComponent, { children: content }) }));
|
|
114
|
+
}, children);
|
|
115
|
+
});
|
|
116
|
+
LayoutWrapper.displayName = 'LayoutWrapper';
|
|
117
|
+
export function RouterProvider({ routes, layouts = [], children, notFound: NotFound, fallback = null, onRouteError, }) {
|
|
118
|
+
const [isPending, startTransition] = useTransition();
|
|
119
|
+
const [state, setState] = useState(() => {
|
|
120
|
+
const { pathname, search, hash } = parseUrl(window.location.href);
|
|
121
|
+
return {
|
|
122
|
+
pathname,
|
|
123
|
+
search,
|
|
124
|
+
hash,
|
|
125
|
+
match: matchRoute(pathname, routes),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
const navigate = useCallback(async (path, options = {}) => {
|
|
129
|
+
const { replace = false, scroll = true } = options;
|
|
130
|
+
const { pathname, search, hash } = parseUrl(path);
|
|
131
|
+
// Find route match
|
|
132
|
+
const match = matchRoute(pathname, routes);
|
|
133
|
+
if (match) {
|
|
134
|
+
// Run middleware
|
|
135
|
+
const { allowed, redirectTo, error, data } = await runRouteMiddleware(match, pathname, search, hash, options.state);
|
|
136
|
+
if (!allowed) {
|
|
137
|
+
if (redirectTo) {
|
|
138
|
+
// Redirect instead
|
|
139
|
+
navigate(redirectTo, { replace: true });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (error) {
|
|
143
|
+
onRouteError?.(error);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Update URL
|
|
149
|
+
const url = pathname + search + hash;
|
|
150
|
+
if (replace) {
|
|
151
|
+
window.history.replaceState(options.state ?? null, '', url);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
window.history.pushState(options.state ?? null, '', url);
|
|
155
|
+
}
|
|
156
|
+
// Update state with middleware data
|
|
157
|
+
startTransition(() => {
|
|
158
|
+
setState({
|
|
159
|
+
pathname,
|
|
160
|
+
search,
|
|
161
|
+
hash,
|
|
162
|
+
match,
|
|
163
|
+
middlewareData: data,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
if (scroll) {
|
|
167
|
+
if (hash) {
|
|
168
|
+
const element = document.querySelector(hash);
|
|
169
|
+
element?.scrollIntoView();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
window.scrollTo(0, 0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// No match - update anyway to show 404
|
|
178
|
+
const url = pathname + search + hash;
|
|
179
|
+
if (replace) {
|
|
180
|
+
window.history.replaceState(options.state ?? null, '', url);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
window.history.pushState(options.state ?? null, '', url);
|
|
184
|
+
}
|
|
185
|
+
startTransition(() => {
|
|
186
|
+
setState({
|
|
187
|
+
pathname,
|
|
188
|
+
search,
|
|
189
|
+
hash,
|
|
190
|
+
match: null,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}, [routes, onRouteError]);
|
|
195
|
+
const router = useMemo(() => ({
|
|
196
|
+
push: (path, options) => navigate(path, options),
|
|
197
|
+
replace: (path, options) => navigate(path, { ...options, replace: true }),
|
|
198
|
+
back: () => window.history.back(),
|
|
199
|
+
forward: () => window.history.forward(),
|
|
200
|
+
refresh: () => {
|
|
201
|
+
startTransition(() => {
|
|
202
|
+
setState((prev) => ({ ...prev, match: matchRoute(prev.pathname, routes) }));
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
prefetch: (path) => {
|
|
206
|
+
const { pathname } = parseUrl(path);
|
|
207
|
+
const match = matchRoute(pathname, routes);
|
|
208
|
+
if (match?.route.component) {
|
|
209
|
+
const component = match.route.component;
|
|
210
|
+
if (component._payload && component._init) {
|
|
211
|
+
try {
|
|
212
|
+
component._init(component._payload);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Component will load when rendered
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
}), [navigate, routes]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const handlePopState = async () => {
|
|
223
|
+
const { pathname, search, hash } = parseUrl(window.location.href);
|
|
224
|
+
const match = matchRoute(pathname, routes);
|
|
225
|
+
if (match) {
|
|
226
|
+
// Run middleware for browser navigation too
|
|
227
|
+
const { allowed, redirectTo, error, data } = await runRouteMiddleware(match, pathname, search, hash);
|
|
228
|
+
if (!allowed) {
|
|
229
|
+
if (redirectTo) {
|
|
230
|
+
navigate(redirectTo, { replace: true });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (error) {
|
|
234
|
+
onRouteError?.(error);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
startTransition(() => {
|
|
239
|
+
setState({
|
|
240
|
+
pathname,
|
|
241
|
+
search,
|
|
242
|
+
hash,
|
|
243
|
+
match,
|
|
244
|
+
middlewareData: data,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
startTransition(() => {
|
|
250
|
+
setState({
|
|
251
|
+
pathname,
|
|
252
|
+
search,
|
|
253
|
+
hash,
|
|
254
|
+
match: null,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
window.addEventListener('popstate', handlePopState);
|
|
260
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
261
|
+
}, [routes, navigate, onRouteError]);
|
|
262
|
+
// Update document meta on route change
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (state.match?.route.meta) {
|
|
265
|
+
state.match.route.meta().then((meta) => {
|
|
266
|
+
if (meta.title) {
|
|
267
|
+
document.title = meta.title;
|
|
268
|
+
}
|
|
269
|
+
if (meta.description) {
|
|
270
|
+
const metaDesc = document.querySelector('meta[name="description"]');
|
|
271
|
+
if (metaDesc) {
|
|
272
|
+
metaDesc.setAttribute('content', meta.description);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}).catch(() => {
|
|
276
|
+
// Ignore meta errors
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}, [state.match]);
|
|
280
|
+
const contextValue = useMemo(() => ({
|
|
281
|
+
state,
|
|
282
|
+
router,
|
|
283
|
+
routes,
|
|
284
|
+
layouts,
|
|
285
|
+
isPending,
|
|
286
|
+
}), [state, router, routes, layouts, isPending]);
|
|
287
|
+
return (_jsx(RouterContext.Provider, { value: contextValue, children: _jsx(Suspense, { fallback: fallback, children: children }) }));
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Hooks
|
|
291
|
+
// ============================================================================
|
|
292
|
+
export function useRouter() {
|
|
293
|
+
const context = useContext(RouterContext);
|
|
294
|
+
if (!context) {
|
|
295
|
+
throw new Error('useRouter must be used within a RouterProvider');
|
|
296
|
+
}
|
|
297
|
+
return context.router;
|
|
298
|
+
}
|
|
299
|
+
export function useParams() {
|
|
300
|
+
const context = useContext(RouterContext);
|
|
301
|
+
if (!context) {
|
|
302
|
+
throw new Error('useParams must be used within a RouterProvider');
|
|
303
|
+
}
|
|
304
|
+
return (context.state.match?.params ?? {});
|
|
305
|
+
}
|
|
306
|
+
export function usePathname() {
|
|
307
|
+
const context = useContext(RouterContext);
|
|
308
|
+
if (!context) {
|
|
309
|
+
throw new Error('usePathname must be used within a RouterProvider');
|
|
310
|
+
}
|
|
311
|
+
return context.state.pathname;
|
|
312
|
+
}
|
|
313
|
+
export function useSearchParams() {
|
|
314
|
+
const context = useContext(RouterContext);
|
|
315
|
+
if (!context) {
|
|
316
|
+
throw new Error('useSearchParams must be used within a RouterProvider');
|
|
317
|
+
}
|
|
318
|
+
const searchParams = useMemo(() => new URLSearchParams(context.state.search), [context.state.search]);
|
|
319
|
+
const setSearchParams = useCallback((params) => {
|
|
320
|
+
const newParams = new URLSearchParams(params);
|
|
321
|
+
const newSearch = newParams.toString();
|
|
322
|
+
const path = context.state.pathname + (newSearch ? `?${newSearch}` : '') + context.state.hash;
|
|
323
|
+
context.router.push(path, { scroll: false });
|
|
324
|
+
}, [context.router, context.state.pathname, context.state.hash]);
|
|
325
|
+
return [searchParams, setSearchParams];
|
|
326
|
+
}
|
|
327
|
+
export function useMiddlewareData() {
|
|
328
|
+
const context = useContext(RouterContext);
|
|
329
|
+
if (!context) {
|
|
330
|
+
throw new Error('useMiddlewareData must be used within a RouterProvider');
|
|
331
|
+
}
|
|
332
|
+
return context.state.middlewareData;
|
|
333
|
+
}
|
|
334
|
+
export function useRouteMatch() {
|
|
335
|
+
const context = useContext(RouterContext);
|
|
336
|
+
if (!context) {
|
|
337
|
+
throw new Error('useRouteMatch must be used within a RouterProvider');
|
|
338
|
+
}
|
|
339
|
+
return context.state.match;
|
|
340
|
+
}
|
|
341
|
+
export function useIsPending() {
|
|
342
|
+
const context = useContext(RouterContext);
|
|
343
|
+
if (!context) {
|
|
344
|
+
throw new Error('useIsPending must be used within a RouterProvider');
|
|
345
|
+
}
|
|
346
|
+
return context.isPending;
|
|
347
|
+
}
|
|
348
|
+
export function Link({ to, replace = false, prefetch = true, scroll = true, children, onClick, onMouseEnter, ...props }) {
|
|
349
|
+
const context = useContext(RouterContext);
|
|
350
|
+
const handleClick = useCallback((e) => {
|
|
351
|
+
onClick?.(e);
|
|
352
|
+
if (e.defaultPrevented ||
|
|
353
|
+
e.button !== 0 ||
|
|
354
|
+
e.metaKey ||
|
|
355
|
+
e.ctrlKey ||
|
|
356
|
+
e.shiftKey ||
|
|
357
|
+
e.altKey) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const href = to;
|
|
361
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
context?.router[replace ? 'replace' : 'push'](to, { scroll });
|
|
366
|
+
}, [context?.router, to, replace, scroll, onClick]);
|
|
367
|
+
const handleMouseEnter = useCallback((e) => {
|
|
368
|
+
onMouseEnter?.(e);
|
|
369
|
+
if (prefetch && context) {
|
|
370
|
+
context.router.prefetch(to);
|
|
371
|
+
}
|
|
372
|
+
}, [context, to, prefetch, onMouseEnter]);
|
|
373
|
+
return (_jsx("a", { href: to, onClick: handleClick, onMouseEnter: handleMouseEnter, ...props, children: children }));
|
|
374
|
+
}
|
|
375
|
+
export function Outlet({ notFound: NotFound, fallback = null }) {
|
|
376
|
+
const context = useContext(RouterContext);
|
|
377
|
+
if (!context) {
|
|
378
|
+
throw new Error('Outlet must be used within a RouterProvider');
|
|
379
|
+
}
|
|
380
|
+
const { match, middlewareData } = context.state;
|
|
381
|
+
if (!match) {
|
|
382
|
+
return NotFound ? _jsx(NotFound, {}) : null;
|
|
383
|
+
}
|
|
384
|
+
const { route, params } = match;
|
|
385
|
+
const Component = route.component;
|
|
386
|
+
const ErrorComponent = route.errorComponent;
|
|
387
|
+
const PendingComponent = route.pendingComponent;
|
|
388
|
+
const routeElement = (_jsx(Suspense, { fallback: PendingComponent ? _jsx(PendingComponent, {}) : fallback, children: _jsx(Component, { params: params, ...middlewareData }) }));
|
|
389
|
+
const wrappedElement = (_jsx(LayoutWrapper, { layouts: context.layouts, layoutPath: route.layoutPath, children: ErrorComponent ? (_jsx(RouteErrorBoundary, { fallback: _jsx(ErrorComponent, {}), children: routeElement })) : routeElement }));
|
|
390
|
+
return wrappedElement;
|
|
391
|
+
}
|
|
392
|
+
class RouteErrorBoundary extends React.Component {
|
|
393
|
+
constructor(props) {
|
|
394
|
+
super(props);
|
|
395
|
+
this.state = { hasError: false, error: null };
|
|
396
|
+
}
|
|
397
|
+
static getDerivedStateFromError(error) {
|
|
398
|
+
return { hasError: true, error };
|
|
399
|
+
}
|
|
400
|
+
componentDidCatch(error, errorInfo) {
|
|
401
|
+
console.error('Route error:', error, errorInfo);
|
|
402
|
+
}
|
|
403
|
+
render() {
|
|
404
|
+
if (this.state.hasError) {
|
|
405
|
+
return this.props.fallback;
|
|
406
|
+
}
|
|
407
|
+
return this.props.children;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
export function NavLink({ to, activeClassName, activeStyle, exact = false, pending = false, pendingClassName, pendingStyle, className, style, ...props }) {
|
|
411
|
+
const pathname = usePathname();
|
|
412
|
+
const isPending = useIsPending();
|
|
413
|
+
const isActive = exact
|
|
414
|
+
? pathname === to
|
|
415
|
+
: pathname.startsWith(to) && (to === '/' ? pathname === '/' : true);
|
|
416
|
+
const isPendingRoute = pending && isPending;
|
|
417
|
+
const combinedClassName = [
|
|
418
|
+
className,
|
|
419
|
+
isActive && activeClassName,
|
|
420
|
+
isPendingRoute && pendingClassName,
|
|
421
|
+
].filter(Boolean).join(' ').trim() || undefined;
|
|
422
|
+
const combinedStyle = {
|
|
423
|
+
...style,
|
|
424
|
+
...(isActive ? activeStyle : {}),
|
|
425
|
+
...(isPendingRoute ? pendingStyle : {}),
|
|
426
|
+
};
|
|
427
|
+
return (_jsx(Link, { to: to, className: combinedClassName, style: combinedStyle, ...props }));
|
|
428
|
+
}
|
|
429
|
+
export function Redirect({ to, replace = true }) {
|
|
430
|
+
const router = useRouter();
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
router[replace ? 'replace' : 'push'](to);
|
|
433
|
+
}, [router, to, replace]);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client for calling Rust server functions from TypeScript
|
|
3
|
+
*/
|
|
4
|
+
import { IpcClient } from './ipc-client.js';
|
|
5
|
+
/**
|
|
6
|
+
* Custom error class for RPC errors
|
|
7
|
+
*/
|
|
8
|
+
export declare class RpcError extends Error {
|
|
9
|
+
readonly errorType: string;
|
|
10
|
+
constructor(errorType: string, message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the RPC client with a socket path
|
|
14
|
+
*/
|
|
15
|
+
export declare function initRpcClient(socketPath: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Call a Rust server function via RPC
|
|
18
|
+
*/
|
|
19
|
+
export declare function rpcCall<T = unknown>(functionName: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Wait for a response from a specific request
|
|
22
|
+
*/
|
|
23
|
+
export declare function waitForResponse<T = unknown>(requestId: string, timeoutMs?: number): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Close the RPC client connection
|
|
26
|
+
*/
|
|
27
|
+
export declare function closeRpcClient(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Check if RPC client is initialized
|
|
30
|
+
*/
|
|
31
|
+
export declare function isRpcClientInitialized(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Get current RPC client instance (for advanced usage)
|
|
34
|
+
*/
|
|
35
|
+
export declare function getRpcClient(): IpcClient | null;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client for calling Rust server functions from TypeScript
|
|
3
|
+
*/
|
|
4
|
+
import { IpcClient } from './ipc-client.js';
|
|
5
|
+
import { isRpcResponseMessage, isRpcErrorMessage } from './types.js';
|
|
6
|
+
let ipcClient = null;
|
|
7
|
+
let requestCounter = 0;
|
|
8
|
+
const pendingRequests = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Custom error class for RPC errors
|
|
11
|
+
*/
|
|
12
|
+
export class RpcError extends Error {
|
|
13
|
+
constructor(errorType, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.errorType = errorType;
|
|
16
|
+
this.name = 'RpcError';
|
|
17
|
+
// Restore prototype chain for proper instanceof checks
|
|
18
|
+
Object.setPrototypeOf(this, RpcError.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the RPC client with a socket path
|
|
23
|
+
*/
|
|
24
|
+
export function initRpcClient(socketPath) {
|
|
25
|
+
if (ipcClient) {
|
|
26
|
+
throw new Error('RPC client already initialized');
|
|
27
|
+
}
|
|
28
|
+
ipcClient = new IpcClient(socketPath);
|
|
29
|
+
// Setup response handler
|
|
30
|
+
ipcClient.on('message', (message) => {
|
|
31
|
+
// Validate message structure
|
|
32
|
+
if (!message || typeof message !== 'object') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const msg = message;
|
|
36
|
+
if (isRpcResponseMessage(msg) && msg.request_id) {
|
|
37
|
+
const pending = pendingRequests.get(msg.request_id);
|
|
38
|
+
if (pending) {
|
|
39
|
+
clearTimeout(pending.timeout);
|
|
40
|
+
pending.resolve(msg.result);
|
|
41
|
+
pendingRequests.delete(msg.request_id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (isRpcErrorMessage(msg) && msg.request_id) {
|
|
45
|
+
const pending = pendingRequests.get(msg.request_id);
|
|
46
|
+
if (pending) {
|
|
47
|
+
clearTimeout(pending.timeout);
|
|
48
|
+
const error = new RpcError(msg.error_type || 'UnknownError', msg.error || 'Unknown error');
|
|
49
|
+
pending.reject(error);
|
|
50
|
+
pendingRequests.delete(msg.request_id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
ipcClient.on('error', (error) => {
|
|
55
|
+
// Reject all pending requests on connection error
|
|
56
|
+
for (const [, pending] of pendingRequests) {
|
|
57
|
+
clearTimeout(pending.timeout);
|
|
58
|
+
pending.reject(error);
|
|
59
|
+
}
|
|
60
|
+
pendingRequests.clear();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Call a Rust server function via RPC
|
|
65
|
+
*/
|
|
66
|
+
export async function rpcCall(functionName, params = {}, timeoutMs = 30000) {
|
|
67
|
+
if (!ipcClient) {
|
|
68
|
+
throw new Error('RPC client not initialized. Call initRpcClient() first.');
|
|
69
|
+
}
|
|
70
|
+
const requestId = `req_${Date.now()}_${requestCounter++}`;
|
|
71
|
+
const message = {
|
|
72
|
+
type: 'rpc_call',
|
|
73
|
+
function_name: functionName,
|
|
74
|
+
params,
|
|
75
|
+
request_id: requestId,
|
|
76
|
+
};
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
pendingRequests.delete(requestId);
|
|
80
|
+
reject(new RpcError('TimeoutError', `RPC call to ${functionName} timed out after ${timeoutMs}ms`));
|
|
81
|
+
}, timeoutMs);
|
|
82
|
+
pendingRequests.set(requestId, {
|
|
83
|
+
resolve: resolve,
|
|
84
|
+
reject,
|
|
85
|
+
timeout,
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
ipcClient.send(message);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
pendingRequests.delete(requestId);
|
|
93
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wait for a response from a specific request
|
|
99
|
+
*/
|
|
100
|
+
export async function waitForResponse(requestId, timeoutMs = 30000) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
pendingRequests.delete(requestId);
|
|
104
|
+
reject(new RpcError('TimeoutError', `Request ${requestId} timed out`));
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
const handler = {
|
|
107
|
+
resolve: resolve,
|
|
108
|
+
reject,
|
|
109
|
+
timeout,
|
|
110
|
+
};
|
|
111
|
+
pendingRequests.set(requestId, handler);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Close the RPC client connection
|
|
116
|
+
*/
|
|
117
|
+
export async function closeRpcClient() {
|
|
118
|
+
if (ipcClient) {
|
|
119
|
+
// Reject all pending requests
|
|
120
|
+
for (const [, pending] of pendingRequests) {
|
|
121
|
+
clearTimeout(pending.timeout);
|
|
122
|
+
pending.reject(new Error('RPC client closed'));
|
|
123
|
+
}
|
|
124
|
+
pendingRequests.clear();
|
|
125
|
+
await ipcClient.close();
|
|
126
|
+
ipcClient = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if RPC client is initialized
|
|
131
|
+
*/
|
|
132
|
+
export function isRpcClientInitialized() {
|
|
133
|
+
return ipcClient !== null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get current RPC client instance (for advanced usage)
|
|
137
|
+
*/
|
|
138
|
+
export function getRpcClient() {
|
|
139
|
+
return ipcClient;
|
|
140
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming utilities for ZapJS
|
|
3
|
+
* Helper functions for working with streaming responses
|
|
4
|
+
*/
|
|
5
|
+
import type { StreamChunk } from './types.js';
|
|
6
|
+
import { isAsyncIterable } from './types.js';
|
|
7
|
+
export { isAsyncIterable };
|
|
8
|
+
/**
|
|
9
|
+
* Create a StreamChunk from string data
|
|
10
|
+
* @param data - String data to send
|
|
11
|
+
* @returns StreamChunk with data field
|
|
12
|
+
*/
|
|
13
|
+
export declare function createChunk(data: string): StreamChunk;
|
|
14
|
+
/**
|
|
15
|
+
* Create a StreamChunk from binary data
|
|
16
|
+
* @param bytes - Binary data to send
|
|
17
|
+
* @returns StreamChunk with bytes field
|
|
18
|
+
*/
|
|
19
|
+
export declare function createChunk(bytes: Uint8Array): StreamChunk;
|
|
20
|
+
/**
|
|
21
|
+
* Create a streaming response from an array of strings
|
|
22
|
+
* @param items - Array of strings to stream
|
|
23
|
+
* @param delimiter - Optional delimiter to add after each item (default: newline)
|
|
24
|
+
* @returns Async iterable of stream chunks
|
|
25
|
+
*/
|
|
26
|
+
export declare function createStream(items: string[], delimiter?: string): AsyncIterable<StreamChunk>;
|
|
27
|
+
/**
|
|
28
|
+
* Create a streaming JSON response (NDJSON format)
|
|
29
|
+
* Each object is sent as a separate JSON line
|
|
30
|
+
* @param objects - Array of objects to stream
|
|
31
|
+
* @returns Async iterable of stream chunks
|
|
32
|
+
*/
|
|
33
|
+
export declare function streamJson<T = any>(objects: T[]): AsyncIterable<StreamChunk>;
|
|
34
|
+
/**
|
|
35
|
+
* Stream Server-Sent Events (SSE) format
|
|
36
|
+
* @param events - Array of SSE events
|
|
37
|
+
* @returns Async iterable of stream chunks
|
|
38
|
+
*/
|
|
39
|
+
export declare function streamSSE(events: Array<{
|
|
40
|
+
data: any;
|
|
41
|
+
event?: string;
|
|
42
|
+
id?: string | number;
|
|
43
|
+
retry?: number;
|
|
44
|
+
}>): AsyncIterable<StreamChunk>;
|
|
45
|
+
/**
|
|
46
|
+
* Transform an async iterable with a mapper function
|
|
47
|
+
* @param source - Source async iterable
|
|
48
|
+
* @param mapper - Function to transform each item
|
|
49
|
+
* @returns Async iterable of transformed items
|
|
50
|
+
*/
|
|
51
|
+
export declare function mapStream<T, U>(source: AsyncIterable<T>, mapper: (item: T) => U | Promise<U>): AsyncIterable<U>;
|
|
52
|
+
/**
|
|
53
|
+
* Filter an async iterable with a predicate function
|
|
54
|
+
* @param source - Source async iterable
|
|
55
|
+
* @param predicate - Function to test each item
|
|
56
|
+
* @returns Async iterable of filtered items
|
|
57
|
+
*/
|
|
58
|
+
export declare function filterStream<T>(source: AsyncIterable<T>, predicate: (item: T) => boolean | Promise<boolean>): AsyncIterable<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Batch stream chunks together
|
|
61
|
+
* @param source - Source async iterable
|
|
62
|
+
* @param batchSize - Number of items per batch
|
|
63
|
+
* @returns Async iterable of batched items
|
|
64
|
+
*/
|
|
65
|
+
export declare function batchStream<T>(source: AsyncIterable<T>, batchSize: number): AsyncIterable<T[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Add delay between stream chunks
|
|
68
|
+
* @param source - Source async iterable
|
|
69
|
+
* @param delayMs - Delay in milliseconds between chunks
|
|
70
|
+
* @returns Async iterable with delays
|
|
71
|
+
*/
|
|
72
|
+
export declare function delayStream<T>(source: AsyncIterable<T>, delayMs: number): AsyncIterable<T>;
|
|
73
|
+
/**
|
|
74
|
+
* Convert a ReadableStream to an async iterable
|
|
75
|
+
* @param stream - ReadableStream to convert
|
|
76
|
+
* @returns Async iterable
|
|
77
|
+
*/
|
|
78
|
+
export declare function fromReadableStream<T>(stream: ReadableStream<T>): AsyncIterable<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Create a streaming response that emits at regular intervals
|
|
81
|
+
* @param interval - Interval in milliseconds
|
|
82
|
+
* @param maxCount - Maximum number of emissions (optional)
|
|
83
|
+
* @param generator - Function to generate data for each emission
|
|
84
|
+
* @returns Async iterable of stream chunks
|
|
85
|
+
*/
|
|
86
|
+
export declare function intervalStream<T>(interval: number, generator: (count: number) => T, maxCount?: number): AsyncIterable<StreamChunk>;
|