weifuwu 0.30.1 → 0.31.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 +228 -198
- package/dist/core/router.d.ts +21 -58
- package/dist/core/serve.d.ts +0 -4
- package/dist/core/ws.d.ts +28 -0
- package/dist/index.d.ts +9 -24
- package/dist/index.js +710 -1853
- package/dist/queue/index.d.ts +10 -0
- package/dist/queue/types.d.ts +5 -9
- package/dist/react/client.d.ts +39 -0
- package/dist/react/client.js +300 -0
- package/dist/react/context.d.ts +2 -0
- package/dist/react/error-boundary.d.ts +31 -0
- package/dist/react/hooks.d.ts +6 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +342 -0
- package/dist/react/navigation.d.ts +63 -0
- package/dist/react/navigation.js +154 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/route-utils.d.ts +31 -0
- package/dist/react/types.d.ts +46 -0
- package/package.json +34 -6
- package/dist/core/cookie.d.ts +0 -36
- package/dist/core/env.d.ts +0 -69
- package/dist/core/html.d.ts +0 -34
- package/dist/core/sse.d.ts +0 -47
- package/dist/middleware/csrf.d.ts +0 -31
- package/dist/middleware/flash.d.ts +0 -44
- package/dist/middleware/health.d.ts +0 -24
- package/dist/middleware/i18n.d.ts +0 -39
- package/dist/middleware/request-id.d.ts +0 -40
- package/dist/middleware/theme.d.ts +0 -31
- package/dist/middleware/validate.d.ts +0 -32
- package/dist/test/test-utils.d.ts +0 -193
package/dist/queue/index.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed job queue with cron scheduling.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const q = queue({ url: process.env.REDIS_URL })
|
|
6
|
+
* q.process('email', async (job) => { await send(job.data) })
|
|
7
|
+
* await q.add('email', { to: '...' })
|
|
8
|
+
* q.cron('cleanup', '0 3 * * *', async () => { ... })
|
|
9
|
+
* await q.run()
|
|
10
|
+
*/
|
|
1
11
|
import type { Queue, QueueOptions } from './types.ts';
|
|
2
12
|
export declare function queue(opts?: QueueOptions): Queue;
|
package/dist/queue/types.d.ts
CHANGED
|
@@ -13,16 +13,14 @@ export interface QueueJob<T = unknown> {
|
|
|
13
13
|
schedule?: string;
|
|
14
14
|
}
|
|
15
15
|
export interface QueueOptions {
|
|
16
|
-
/**
|
|
17
|
-
store?: 'memory' | 'pg' | 'redis';
|
|
16
|
+
/** Redis client instance (auto-created if not provided). */
|
|
18
17
|
redis?: Redis;
|
|
18
|
+
/** Redis URL (default: REDIS_URL env or redis://localhost:6379). */
|
|
19
19
|
url?: string;
|
|
20
|
+
/** Key prefix (default: 'queue'). */
|
|
20
21
|
prefix?: string;
|
|
22
|
+
/** Poll interval in ms (default: 200). */
|
|
21
23
|
pollInterval?: number;
|
|
22
|
-
/** PostgreSQL client (required when store: 'pg'). */
|
|
23
|
-
pg?: {
|
|
24
|
-
sql: import('../types.ts').SqlClient;
|
|
25
|
-
};
|
|
26
24
|
}
|
|
27
25
|
export interface QueueInjected {
|
|
28
26
|
queue: Queue;
|
|
@@ -32,7 +30,7 @@ export interface QueueJobWithError<T = unknown> extends QueueJob<T> {
|
|
|
32
30
|
failedAt: number;
|
|
33
31
|
}
|
|
34
32
|
export interface Queue extends Middleware<Context, Context & QueueInjected>, Closeable {
|
|
35
|
-
/** Register a cron job.
|
|
33
|
+
/** Register a cron job. */
|
|
36
34
|
cron(pattern: string, handler: () => void | Promise<void>): {
|
|
37
35
|
stop: () => void;
|
|
38
36
|
};
|
|
@@ -55,7 +53,5 @@ export interface Queue extends Middleware<Context, Context & QueueInjected>, Clo
|
|
|
55
53
|
retryFailed(jobId: string): Promise<boolean>;
|
|
56
54
|
retryAllFailed(type?: string): Promise<number>;
|
|
57
55
|
dashboard(): import('../core/router.ts').Router;
|
|
58
|
-
/** Create the jobs table (PG mode only; safe to call multiple times). */
|
|
59
|
-
migrate?(): Promise<void>;
|
|
60
56
|
close(): Promise<void>;
|
|
61
57
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side entry for React hydration + SPA navigation.
|
|
3
|
+
*
|
|
4
|
+
* Usage (in a client bundle):
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { hydrate, createClientRouter } from 'weifuwu/react/client'
|
|
7
|
+
* import HomePage from './pages/HomePage.js'
|
|
8
|
+
*
|
|
9
|
+
* const router = createClientRouter([
|
|
10
|
+
* { path: '/', component: HomePage },
|
|
11
|
+
* { path: '/users/:id', component: UserPage, loader: ... },
|
|
12
|
+
* ])
|
|
13
|
+
* hydrate(router.App)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { type ComponentType, type ReactElement } from 'react';
|
|
17
|
+
import { type LinkProps } from './navigation.ts';
|
|
18
|
+
export interface HydrateOptions {
|
|
19
|
+
container?: HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
export interface ClientRoute {
|
|
22
|
+
path: string;
|
|
23
|
+
component: ComponentType;
|
|
24
|
+
loader?: (params: Record<string, string>) => Promise<Record<string, unknown>>;
|
|
25
|
+
}
|
|
26
|
+
export interface ClientRouter {
|
|
27
|
+
App: ComponentType;
|
|
28
|
+
Link: (props: LinkProps) => ReactElement;
|
|
29
|
+
useParams: () => Record<string, string>;
|
|
30
|
+
navigate: (url: string) => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export { Link, useParams, useNavigate, useRevalidate, Form, useNavigation } from './navigation.ts';
|
|
33
|
+
export type { LinkProps, FormProps, NavigationState } from './navigation.ts';
|
|
34
|
+
export { ErrorBoundary } from './error-boundary.ts';
|
|
35
|
+
export type { ErrorBoundaryProps } from './error-boundary.ts';
|
|
36
|
+
export { defineRoute } from './route-utils.ts';
|
|
37
|
+
export declare function createClientRouter(routes: ClientRoute[]): ClientRouter;
|
|
38
|
+
export declare function hydrate(App: ComponentType, opts?: HydrateOptions): void;
|
|
39
|
+
export { useServerData } from './hooks.ts';
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// src/react/client.ts
|
|
2
|
+
import {
|
|
3
|
+
createElement as createElement2,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useSyncExternalStore
|
|
7
|
+
} from "react";
|
|
8
|
+
import { hydrateRoot } from "react-dom/client";
|
|
9
|
+
|
|
10
|
+
// src/react/context.ts
|
|
11
|
+
import { createContext } from "react";
|
|
12
|
+
var CTX_KEY = /* @__PURE__ */ Symbol.for("weifuwu.react.ServerDataContext");
|
|
13
|
+
function getServerDataContext() {
|
|
14
|
+
const globalStore = globalThis;
|
|
15
|
+
if (globalStore[CTX_KEY]) return globalStore[CTX_KEY];
|
|
16
|
+
const ctx = createContext({});
|
|
17
|
+
ctx.displayName = "ServerData";
|
|
18
|
+
globalStore[CTX_KEY] = ctx;
|
|
19
|
+
return ctx;
|
|
20
|
+
}
|
|
21
|
+
var ServerDataContext = getServerDataContext();
|
|
22
|
+
|
|
23
|
+
// src/react/navigation.ts
|
|
24
|
+
import {
|
|
25
|
+
createElement,
|
|
26
|
+
createContext as createContext2,
|
|
27
|
+
useContext as useContext2
|
|
28
|
+
} from "react";
|
|
29
|
+
|
|
30
|
+
// src/react/hooks.ts
|
|
31
|
+
import { useContext } from "react";
|
|
32
|
+
function useServerData() {
|
|
33
|
+
return useContext(ServerDataContext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/react/error-boundary.ts
|
|
37
|
+
import { Component } from "react";
|
|
38
|
+
var ErrorBoundary = class extends Component {
|
|
39
|
+
state = { error: null };
|
|
40
|
+
static getDerivedStateFromError(error) {
|
|
41
|
+
return { error };
|
|
42
|
+
}
|
|
43
|
+
componentDidCatch(error, info) {
|
|
44
|
+
this.props.onError?.(error, info);
|
|
45
|
+
}
|
|
46
|
+
render() {
|
|
47
|
+
if (this.state.error) {
|
|
48
|
+
return this.props.fallback;
|
|
49
|
+
}
|
|
50
|
+
return this.props.children;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/react/route-utils.ts
|
|
55
|
+
function defineRoute(config) {
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/react/navigation.ts
|
|
60
|
+
var RouterContext = createContext2(null);
|
|
61
|
+
RouterContext.displayName = "ClientRouter";
|
|
62
|
+
function useParams() {
|
|
63
|
+
return useContext2(RouterContext)?.params ?? {};
|
|
64
|
+
}
|
|
65
|
+
function useNavigate() {
|
|
66
|
+
const ctx = useContext2(RouterContext);
|
|
67
|
+
if (!ctx) throw new Error("useNavigate() must be used within a client router");
|
|
68
|
+
return ctx.navigate;
|
|
69
|
+
}
|
|
70
|
+
function useNavigation() {
|
|
71
|
+
const ctx = useContext2(RouterContext);
|
|
72
|
+
return { state: ctx?.state ?? "idle" };
|
|
73
|
+
}
|
|
74
|
+
function Link({ href, children, ...props }) {
|
|
75
|
+
const router = useContext2(RouterContext);
|
|
76
|
+
const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
77
|
+
if (!router || isExternal || props.target !== void 0) {
|
|
78
|
+
return createElement("a", { href, ...props }, children);
|
|
79
|
+
}
|
|
80
|
+
const handleClick = (e) => {
|
|
81
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
|
|
82
|
+
if (e.button !== 0) return;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
router.navigate(href);
|
|
85
|
+
};
|
|
86
|
+
return createElement("a", { href, onClick: handleClick, ...props }, children);
|
|
87
|
+
}
|
|
88
|
+
function useRevalidate() {
|
|
89
|
+
const ctx = useContext2(RouterContext);
|
|
90
|
+
if (!ctx) throw new Error("useRevalidate() must be used within a client router");
|
|
91
|
+
return ctx.revalidate;
|
|
92
|
+
}
|
|
93
|
+
function Form({ method = "post", action, children, ...props }) {
|
|
94
|
+
const router = useContext2(RouterContext);
|
|
95
|
+
if (!router) {
|
|
96
|
+
return createElement("form", { method, action, ...props }, children);
|
|
97
|
+
}
|
|
98
|
+
const handleSubmit = async (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const form = e.target;
|
|
101
|
+
const formData = new FormData(form);
|
|
102
|
+
const url = action || window.location.pathname;
|
|
103
|
+
const fetchMethod = method === "get" ? "GET" : method.toUpperCase();
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method: fetchMethod,
|
|
107
|
+
body: fetchMethod === "GET" ? void 0 : formData,
|
|
108
|
+
redirect: "manual"
|
|
109
|
+
});
|
|
110
|
+
if (res.status >= 300 && res.status < 400) {
|
|
111
|
+
const loc = res.headers.get("Location");
|
|
112
|
+
if (loc) {
|
|
113
|
+
router.navigate(loc);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await router.revalidate();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("[weifuwu/react] Form submit failed:", err);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return createElement(
|
|
123
|
+
"form",
|
|
124
|
+
{ method, action, ...props, onSubmit: handleSubmit },
|
|
125
|
+
children
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
function matchPath(pathname, pattern) {
|
|
129
|
+
const patParts = pattern.split("/").filter(Boolean);
|
|
130
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
131
|
+
const hasWildcard = patParts[patParts.length - 1] === "*";
|
|
132
|
+
if (!hasWildcard && patParts.length !== pathParts.length) return null;
|
|
133
|
+
if (hasWildcard && pathParts.length < patParts.length - 1) return null;
|
|
134
|
+
const params = {};
|
|
135
|
+
for (let i = 0; i < patParts.length; i++) {
|
|
136
|
+
if (patParts[i] === "*") {
|
|
137
|
+
params["*"] = pathParts.slice(i).join("/");
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (patParts[i].startsWith(":")) {
|
|
141
|
+
params[patParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
|
|
142
|
+
} else if (patParts[i] !== pathParts[i]) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return params;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/react/client.ts
|
|
150
|
+
function readInitialData() {
|
|
151
|
+
if (typeof document === "undefined") return {};
|
|
152
|
+
const el = document.getElementById("__WEIFUWU_DATA__");
|
|
153
|
+
if (el?.textContent) {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(el.textContent);
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
function createClientRouter(routes) {
|
|
162
|
+
function findRoute(pathname) {
|
|
163
|
+
for (const route of routes) {
|
|
164
|
+
const params = matchPath(pathname, route.path);
|
|
165
|
+
if (params) return { route, params };
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
let state = {
|
|
170
|
+
location: typeof window !== "undefined" ? window.location.pathname : "/",
|
|
171
|
+
data: readInitialData(),
|
|
172
|
+
navId: 0,
|
|
173
|
+
state: "idle"
|
|
174
|
+
};
|
|
175
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
176
|
+
function getSnapshot() {
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
function subscribe(fn) {
|
|
180
|
+
listeners.add(fn);
|
|
181
|
+
return () => {
|
|
182
|
+
listeners.delete(fn);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function emit() {
|
|
186
|
+
listeners.forEach((fn) => fn());
|
|
187
|
+
}
|
|
188
|
+
async function navigate(url, { push = true } = {}) {
|
|
189
|
+
const match = findRoute(url);
|
|
190
|
+
if (!match) {
|
|
191
|
+
window.location.href = url;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const id = state.navId + 1;
|
|
195
|
+
state = { ...state, navId: id, state: match.route.loader ? "loading" : "idle" };
|
|
196
|
+
emit();
|
|
197
|
+
if (match.route.loader) {
|
|
198
|
+
try {
|
|
199
|
+
const newData = await match.route.loader(match.params);
|
|
200
|
+
if (id < state.navId) return;
|
|
201
|
+
state = { ...state, data: newData, state: "idle" };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error("[weifuwu/react] loader failed:", err);
|
|
204
|
+
state = { ...state, state: "idle" };
|
|
205
|
+
emit();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (push) history.pushState({ weifuwu: true }, "", url);
|
|
210
|
+
state = { ...state, location: url, state: "idle" };
|
|
211
|
+
emit();
|
|
212
|
+
}
|
|
213
|
+
function RouterApp() {
|
|
214
|
+
const { location, data, state: navState } = useSyncExternalStore(
|
|
215
|
+
subscribe,
|
|
216
|
+
getSnapshot
|
|
217
|
+
);
|
|
218
|
+
const match = findRoute(location);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const handler = () => navigate(window.location.pathname, { push: false });
|
|
221
|
+
window.addEventListener("popstate", handler);
|
|
222
|
+
return () => window.removeEventListener("popstate", handler);
|
|
223
|
+
}, []);
|
|
224
|
+
const ctxNavigate = useCallback(
|
|
225
|
+
(url) => navigate(url, { push: true }),
|
|
226
|
+
[]
|
|
227
|
+
);
|
|
228
|
+
const ctxRevalidate = useCallback(async () => {
|
|
229
|
+
const currentMatch = findRoute(window.location.pathname);
|
|
230
|
+
if (currentMatch?.route.loader) {
|
|
231
|
+
state = { ...state, state: "loading" };
|
|
232
|
+
emit();
|
|
233
|
+
try {
|
|
234
|
+
const newData = await currentMatch.route.loader(currentMatch.params);
|
|
235
|
+
state = { ...state, data: newData, state: "idle" };
|
|
236
|
+
emit();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error("[weifuwu/react] revalidate failed:", err);
|
|
239
|
+
state = { ...state, state: "idle" };
|
|
240
|
+
emit();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}, []);
|
|
244
|
+
const ctxValue = {
|
|
245
|
+
params: match?.params ?? {},
|
|
246
|
+
navigate: ctxNavigate,
|
|
247
|
+
revalidate: ctxRevalidate,
|
|
248
|
+
state: navState
|
|
249
|
+
};
|
|
250
|
+
const content = match ? createElement2(match.route.component) : createElement2("div", null, "Page not found");
|
|
251
|
+
return createElement2(
|
|
252
|
+
RouterContext.Provider,
|
|
253
|
+
{ value: ctxValue },
|
|
254
|
+
createElement2(
|
|
255
|
+
ServerDataContext.Provider,
|
|
256
|
+
{ value: data },
|
|
257
|
+
content
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function RouterLink(props) {
|
|
262
|
+
return createElement2(Link, props);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
App: RouterApp,
|
|
266
|
+
Link: RouterLink,
|
|
267
|
+
useParams,
|
|
268
|
+
navigate: (url) => navigate(url, { push: true })
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function hydrate(App, opts) {
|
|
272
|
+
const container = opts?.container ?? document.getElementById("root");
|
|
273
|
+
if (!container) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"weifuwu/react: hydrate() \u2014 no container element found."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const serverData = readInitialData();
|
|
279
|
+
hydrateRoot(
|
|
280
|
+
container,
|
|
281
|
+
createElement2(
|
|
282
|
+
ServerDataContext.Provider,
|
|
283
|
+
{ value: serverData },
|
|
284
|
+
createElement2(App)
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
ErrorBoundary,
|
|
290
|
+
Form,
|
|
291
|
+
Link,
|
|
292
|
+
createClientRouter,
|
|
293
|
+
defineRoute,
|
|
294
|
+
hydrate,
|
|
295
|
+
useNavigate,
|
|
296
|
+
useNavigation,
|
|
297
|
+
useParams,
|
|
298
|
+
useRevalidate,
|
|
299
|
+
useServerData
|
|
300
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR-safe error boundary.
|
|
3
|
+
*
|
|
4
|
+
* Catches React render errors on both server and client:
|
|
5
|
+
* - Server (SSR): renders fallback HTML instead of crashing
|
|
6
|
+
* - Client (hydration): catches errors, shows fallback
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <ErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
11
|
+
* <UserProfile />
|
|
12
|
+
* </ErrorBoundary>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { Component, type ReactNode, type ErrorInfo } from 'react';
|
|
16
|
+
export interface ErrorBoundaryProps {
|
|
17
|
+
fallback: ReactNode;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
/** Called when an error is caught (both SSR and client). */
|
|
20
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
21
|
+
}
|
|
22
|
+
interface ErrorBoundaryState {
|
|
23
|
+
error: Error | null;
|
|
24
|
+
}
|
|
25
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
26
|
+
state: ErrorBoundaryState;
|
|
27
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
28
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
29
|
+
render(): ReactNode;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access server-loaded data in any component.
|
|
3
|
+
* On the server, reads from the request-scoped ServerDataContext.
|
|
4
|
+
* On the client (after hydration), reads from the same context populated by hydrate().
|
|
5
|
+
*/
|
|
6
|
+
export declare function useServerData<T extends Record<string, unknown> = Record<string, unknown>>(): T;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactOptions, ReactMiddleware } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* React SSR middleware.
|
|
4
|
+
*
|
|
5
|
+
* Injects ctx.render() and ctx.renderStream() for server-side rendering
|
|
6
|
+
* React components to HTML. Supports layout composition via mount nesting.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* app.use(react({
|
|
11
|
+
* layout: ({ children }) => (
|
|
12
|
+
* <html><body><div id="root">{children}</div></body></html>
|
|
13
|
+
* ),
|
|
14
|
+
* }))
|
|
15
|
+
*
|
|
16
|
+
* app.get('/', async (req, ctx) => {
|
|
17
|
+
* return ctx.render(<Home />, { data: { title: 'Home' } })
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function react(opts?: ReactOptions): ReactMiddleware;
|
|
22
|
+
export { useServerData } from './hooks.ts';
|
|
23
|
+
export { ServerDataContext } from './context.ts';
|
|
24
|
+
export { Link, useParams, useNavigate, useRevalidate, Form, useNavigation } from './navigation.ts';
|
|
25
|
+
export type { LinkProps, FormProps, NavigationState } from './navigation.ts';
|
|
26
|
+
export { ErrorBoundary } from './error-boundary.ts';
|
|
27
|
+
export type { ErrorBoundaryProps } from './error-boundary.ts';
|
|
28
|
+
export type { ReactOptions, RenderOptions, ReactInjected } from './types.ts';
|