weifuwu 0.30.2 → 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 CHANGED
@@ -169,6 +169,121 @@ const gql = graphql((req, ctx) => ({
169
169
  app.mount('/graphql', gql)
170
170
  ```
171
171
 
172
+ ### React SSR
173
+
174
+ > Requires `react >= 19`, `react-dom >= 19` (optional peerDependencies).
175
+
176
+ ```bash
177
+ npm install react react-dom
178
+ ```
179
+
180
+ ```ts
181
+ import { react, Link, useServerData, ErrorBoundary } from 'weifuwu'
182
+ import { createElement as h } from 'react'
183
+
184
+ // ── Server ──────────────────────────────────────────────
185
+
186
+ app.use(react({
187
+ layout: ({ children }) =>
188
+ h('html', null,
189
+ h('head', null, h('title', null, 'My App')),
190
+ h('body', null, h('div', { id: 'root' }, children)),
191
+ ),
192
+ }))
193
+
194
+ // Render React components to HTML
195
+ app.get('/', (_req, ctx) =>
196
+ ctx.render(h('h1', null, 'Hello SSR'), {
197
+ head: { title: 'Home' },
198
+ data: { greeting: 'Hello' },
199
+ })
200
+ )
201
+
202
+ // Streaming SSR
203
+ app.get('/dashboard', (_req, ctx) =>
204
+ ctx.renderStream(h(Dashboard))
205
+ )
206
+
207
+ // Layout nesting via mount
208
+ const admin = new Router()
209
+ admin.use(react({ layout: AdminLayout }))
210
+ admin.get('/dashboard', (_req, ctx) => ctx.render(h(AdminDashboard)))
211
+ app.mount('/admin', admin)
212
+
213
+ // ── Shared components ──────────────────────────────────
214
+
215
+ // Use the same component on server and client
216
+ function Page() {
217
+ const { greeting } = useServerData<{ greeting: string }>()
218
+ return h('h1', null, greeting)
219
+ }
220
+
221
+ // Link works on both sides: <a> on server, SPA <a> on client
222
+ function Nav() {
223
+ return h('nav', null,
224
+ h(Link, { href: '/' }, 'Home'),
225
+ h(Link, { href: '/users' }, 'Users'),
226
+ )
227
+ }
228
+
229
+ // ErrorBoundary catches client-side render errors
230
+ function SafeUserProfile() {
231
+ return h(ErrorBoundary, { fallback: h('div', null, 'Error') },
232
+ h(UserProfile),
233
+ )
234
+ }
235
+
236
+ // ── Client (SPA navigation) ────────────────────────────
237
+
238
+ // client.ts — bundled separately with esbuild
239
+ import { hydrate, createClientRouter, defineRoute } from 'weifuwu/react/client'
240
+
241
+ const userRoute = defineRoute({
242
+ path: '/users/:id',
243
+ component: UserPage,
244
+ loader: (params) => fetch(`/users/${params.id}?_data`).then(r => r.json()),
245
+ })
246
+
247
+ const router = createClientRouter([
248
+ { path: '/', component: HomePage },
249
+ userRoute,
250
+ ])
251
+
252
+ hydrate(router.App)
253
+ ```
254
+
255
+ **Key concepts:**
256
+
257
+ | Feature | Description |
258
+ |---|---|
259
+ | `ctx.render(el, opts)` | Render React to HTML. Auto-detects `?_data` → returns JSON. |
260
+ | `ctx.renderStream(el)` | Streaming SSR via `renderToReadableStream`. |
261
+ | `useServerData<T>()` | Access data on both server (via `ctx.render({ data })`) and client (via `loader`). |
262
+ | `Link` | Shared component — `<a>` on server, SPA `<a>` on client. |
263
+ | `Form` | Shared — `<form>` on server, `fetch` + revalidate on client. |
264
+ | `ErrorBoundary` | Catches render errors on client (SSR uses `onError`). |
265
+ | `useNavigation()` | `{ state: 'idle' \| 'loading' }` for progress indicators. |
266
+ | `useParams()` / `useNavigate()` / `useRevalidate()` | Router hooks. |
267
+ | `defineRoute()` | Type-safe route config — captures loader return type. |
268
+ | `createClientRouter()` | Client-side SPA router with loader-based data fetching. |
269
+ | `hydrate(App)` | Hydrate server-rendered HTML on the client. |
270
+ | `head: { title, meta }` | Inject dynamic `<title>` and `<meta>` tags. |
271
+
272
+ **Import paths:**
273
+
274
+ ```ts
275
+ // Server-side
276
+ import { react, Link, Form, ErrorBoundary, useServerData } from 'weifuwu'
277
+
278
+ // Client-side (for browser bundles)
279
+ import { hydrate, createClientRouter, defineRoute, Link } from 'weifuwu/react/client'
280
+
281
+ // Shared primitives (safe for both, pure React — no react-dom import)
282
+ import { Link, Form, useServerData, useParams, useNavigation } from 'weifuwu/react/navigation'
283
+ ```
284
+
285
+ See [examples/react-ssr/](examples/react-ssr/) for a complete demo.
286
+
172
287
  ### Types
173
288
 
174
289
  | Export | Description |
@@ -290,10 +405,13 @@ weifuwu/
290
405
  │ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
291
406
  │ ├── postgres/
292
407
  │ ├── redis/
408
+ │ ├── react/ ← react SSR (render, navigation, client)
293
409
  │ ├── queue/ ← cron, index, types
294
410
  │ ├── graphql.ts
295
411
  │ ├── hub.ts
296
- │ └── test/ ← 122 tests (18 files)
412
+ │ └── test/ ← 131 tests (18 files)
413
+ ├── examples/
414
+ │ └── react-ssr/ ← full SPA demo
297
415
  └── dist/
298
416
  ```
299
417
 
@@ -302,5 +420,5 @@ weifuwu/
302
420
  ```bash
303
421
  npm run build # esbuild → dist/index.js
304
422
  npm run typecheck # tsc --noEmit
305
- npm test # 122 tests (requires docker compose)
423
+ npm test # 131 tests (requires docker compose)
306
424
  ```
package/dist/index.d.ts CHANGED
@@ -30,3 +30,10 @@ export { createHub } from './hub.ts';
30
30
  export type { Hub, HubOptions } from './hub.ts';
31
31
  export { queue } from './queue/index.ts';
32
32
  export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types.ts';
33
+ export { react } from './react/index.ts';
34
+ export type { ReactOptions, RenderOptions, ReactInjected } from './react/types.ts';
35
+ export { useServerData, ServerDataContext } from './react/index.ts';
36
+ export { Link, useParams, useNavigate, useRevalidate, Form, useNavigation } from './react/index.ts';
37
+ export type { LinkProps, FormProps, NavigationState } from './react/index.ts';
38
+ export { ErrorBoundary } from './react/index.ts';
39
+ export type { ErrorBoundaryProps } from './react/index.ts';
package/dist/index.js CHANGED
@@ -1111,7 +1111,7 @@ function defaultKey(_req, _ctx) {
1111
1111
  }
1112
1112
  function rateLimit(options) {
1113
1113
  const max = options?.max ?? 100;
1114
- const window = options?.window ?? 6e4;
1114
+ const window2 = options?.window ?? 6e4;
1115
1115
  const getKey = options?.key ?? defaultKey;
1116
1116
  const message = options?.message ?? "Too Many Requests";
1117
1117
  const storeType = options?.store ?? "memory";
@@ -1138,7 +1138,7 @@ function rateLimit(options) {
1138
1138
  }
1139
1139
  }
1140
1140
  },
1141
- Math.min(window, 3e4)
1141
+ Math.min(window2, 3e4)
1142
1142
  ) : null;
1143
1143
  if (interval?.unref) interval.unref();
1144
1144
  async function checkAndIncrement(key) {
@@ -1147,16 +1147,16 @@ function rateLimit(options) {
1147
1147
  const redisKey = `${keyPrefix}${key}`;
1148
1148
  const count = await redis2.incr(redisKey);
1149
1149
  if (count === 1) {
1150
- await redis2.pexpire(redisKey, window);
1150
+ await redis2.pexpire(redisKey, window2);
1151
1151
  }
1152
1152
  const pttl = await redis2.pttl(redisKey);
1153
- const reset = pttl > 0 ? now + pttl : now + window;
1153
+ const reset = pttl > 0 ? now + pttl : now + window2;
1154
1154
  return { count, reset };
1155
1155
  }
1156
1156
  const entry = hits.get(key);
1157
1157
  if (!entry || entry.reset < now) {
1158
- hits.set(key, { count: 1, reset: now + window });
1159
- return { count: 1, reset: now + window };
1158
+ hits.set(key, { count: 1, reset: now + window2 });
1159
+ return { count: 1, reset: now + window2 };
1160
1160
  }
1161
1161
  entry.count++;
1162
1162
  return { count: entry.count, reset: entry.reset };
@@ -1877,11 +1877,346 @@ function queue(opts) {
1877
1877
  };
1878
1878
  return q;
1879
1879
  }
1880
+
1881
+ // src/react/index.ts
1882
+ import { Fragment as Fragment2 } from "react";
1883
+
1884
+ // src/react/render.ts
1885
+ import {
1886
+ createElement,
1887
+ Fragment
1888
+ } from "react";
1889
+ import { renderToString, renderToReadableStream } from "react-dom/server";
1890
+
1891
+ // src/react/context.ts
1892
+ import { createContext } from "react";
1893
+ var CTX_KEY = /* @__PURE__ */ Symbol.for("weifuwu.react.ServerDataContext");
1894
+ function getServerDataContext() {
1895
+ const globalStore = globalThis;
1896
+ if (globalStore[CTX_KEY]) return globalStore[CTX_KEY];
1897
+ const ctx = createContext({});
1898
+ ctx.displayName = "ServerData";
1899
+ globalStore[CTX_KEY] = ctx;
1900
+ return ctx;
1901
+ }
1902
+ var ServerDataContext = getServerDataContext();
1903
+
1904
+ // src/react/render.ts
1905
+ var encoder = new TextEncoder();
1906
+ var decoder = new TextDecoder();
1907
+ function escapeAttr(s) {
1908
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
1909
+ }
1910
+ function buildHeadTags(head) {
1911
+ if (!head) return "";
1912
+ const tags = [];
1913
+ if (head.title) tags.push(`<title>${escapeAttr(head.title)}</title>`);
1914
+ if (head.meta) {
1915
+ for (const [name, content] of Object.entries(head.meta)) {
1916
+ tags.push(`<meta name="${escapeAttr(name)}" content="${escapeAttr(content)}">`);
1917
+ }
1918
+ }
1919
+ if (head.links) {
1920
+ for (const link of head.links) {
1921
+ const attrs = Object.entries(link).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
1922
+ tags.push(`<link ${attrs}>`);
1923
+ }
1924
+ }
1925
+ if (head.scripts) {
1926
+ for (const script of head.scripts) {
1927
+ const entries = Object.entries(script).filter(([, v]) => v !== void 0);
1928
+ const attrs = entries.map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
1929
+ tags.push(`<script ${attrs}></script>`);
1930
+ }
1931
+ }
1932
+ return tags.join("");
1933
+ }
1934
+ function injectHeadIntoHtml(html, headTags) {
1935
+ if (!headTags) return html;
1936
+ let result = html;
1937
+ if (headTags.includes("<title>")) {
1938
+ const titleMatch = headTags.match(/<title>[^<]*<\/title>/);
1939
+ if (titleMatch) {
1940
+ result = result.replace(/<title>[^<]*<\/title>/, titleMatch[0]);
1941
+ headTags = headTags.replace(titleMatch[0], "");
1942
+ }
1943
+ }
1944
+ if (headTags) {
1945
+ result = result.replace("</head>", headTags + "</head>");
1946
+ }
1947
+ return result;
1948
+ }
1949
+ function injectHeadIntoStream(sourceStream, headTags) {
1950
+ if (!headTags) return sourceStream;
1951
+ let titleTag = "";
1952
+ let rest = headTags;
1953
+ const titleMatch = headTags.match(/<title>[^<]*<\/title>/);
1954
+ if (titleMatch) {
1955
+ titleTag = titleMatch[0];
1956
+ rest = headTags.replace(titleMatch[0], "");
1957
+ }
1958
+ let titleInjected = !titleTag;
1959
+ let headInjected = !rest;
1960
+ return new ReadableStream({
1961
+ async start(controller) {
1962
+ const reader = sourceStream.getReader();
1963
+ let buffer = "";
1964
+ try {
1965
+ while (true) {
1966
+ const { done, value } = await reader.read();
1967
+ if (done) break;
1968
+ buffer += decoder.decode(value, { stream: true });
1969
+ if (!titleInjected) {
1970
+ const idx = buffer.indexOf("<title>");
1971
+ const endIdx = buffer.indexOf("</title>", idx);
1972
+ if (idx !== -1 && endIdx !== -1) {
1973
+ controller.enqueue(
1974
+ encoder.encode(buffer.slice(0, idx) + titleTag + buffer.slice(endIdx + 8))
1975
+ );
1976
+ buffer = "";
1977
+ titleInjected = true;
1978
+ continue;
1979
+ }
1980
+ }
1981
+ if (!headInjected) {
1982
+ const idx = buffer.indexOf("</head>");
1983
+ if (idx !== -1) {
1984
+ controller.enqueue(
1985
+ encoder.encode(buffer.slice(0, idx) + rest + buffer.slice(idx))
1986
+ );
1987
+ buffer = "";
1988
+ headInjected = true;
1989
+ continue;
1990
+ }
1991
+ }
1992
+ if (titleInjected && headInjected && buffer) {
1993
+ controller.enqueue(encoder.encode(buffer));
1994
+ buffer = "";
1995
+ }
1996
+ }
1997
+ if (buffer) controller.enqueue(encoder.encode(buffer));
1998
+ controller.close();
1999
+ } catch (err) {
2000
+ controller.error(err);
2001
+ }
2002
+ }
2003
+ });
2004
+ }
2005
+ function wrapElement(element, layouts, data) {
2006
+ let wrapped = element;
2007
+ for (let i = layouts.length - 1; i >= 0; i--) {
2008
+ wrapped = createElement(layouts[i], null, wrapped);
2009
+ }
2010
+ const json = JSON.stringify(data).replace(/</g, "\\u003c");
2011
+ const dataScript = createElement("script", {
2012
+ id: "__WEIFUWU_DATA__",
2013
+ type: "application/json",
2014
+ dangerouslySetInnerHTML: { __html: json }
2015
+ });
2016
+ return createElement(
2017
+ ServerDataContext.Provider,
2018
+ { value: data },
2019
+ createElement(Fragment, null, wrapped, dataScript)
2020
+ );
2021
+ }
2022
+ function render(element, layouts, options = {}) {
2023
+ const data = options.data ?? {};
2024
+ const wrapped = wrapElement(element, layouts, data);
2025
+ let html = renderToString(wrapped);
2026
+ const headTags = buildHeadTags(options.head);
2027
+ html = injectHeadIntoHtml(html, headTags);
2028
+ return new Response("<!DOCTYPE html>\n" + html, {
2029
+ status: options.status ?? 200,
2030
+ headers: {
2031
+ "content-type": "text/html; charset=utf-8",
2032
+ ...options.headers
2033
+ }
2034
+ });
2035
+ }
2036
+ async function renderStream(element, layouts, options = {}) {
2037
+ const data = options.data ?? {};
2038
+ const wrapped = wrapElement(element, layouts, data);
2039
+ const headTags = buildHeadTags(options.head);
2040
+ let stream = await renderToReadableStream(wrapped);
2041
+ stream = injectHeadIntoStream(stream, headTags);
2042
+ const doctype = encoder.encode("<!DOCTYPE html>\n");
2043
+ const combined = new ReadableStream({
2044
+ async start(controller) {
2045
+ controller.enqueue(doctype);
2046
+ try {
2047
+ const reader = stream.getReader();
2048
+ while (true) {
2049
+ const { done, value } = await reader.read();
2050
+ if (done) {
2051
+ controller.close();
2052
+ break;
2053
+ }
2054
+ controller.enqueue(value);
2055
+ }
2056
+ } catch (err) {
2057
+ controller.error(err);
2058
+ }
2059
+ }
2060
+ });
2061
+ return new Response(combined, {
2062
+ status: options.status ?? 200,
2063
+ headers: {
2064
+ "content-type": "text/html; charset=utf-8",
2065
+ "transfer-encoding": "chunked",
2066
+ ...options.headers
2067
+ }
2068
+ });
2069
+ }
2070
+
2071
+ // src/react/hooks.ts
2072
+ import { useContext } from "react";
2073
+ function useServerData() {
2074
+ return useContext(ServerDataContext);
2075
+ }
2076
+
2077
+ // src/react/navigation.ts
2078
+ import {
2079
+ createElement as createElement2,
2080
+ createContext as createContext2,
2081
+ useContext as useContext2
2082
+ } from "react";
2083
+
2084
+ // src/react/error-boundary.ts
2085
+ import { Component } from "react";
2086
+ var ErrorBoundary = class extends Component {
2087
+ state = { error: null };
2088
+ static getDerivedStateFromError(error) {
2089
+ return { error };
2090
+ }
2091
+ componentDidCatch(error, info) {
2092
+ this.props.onError?.(error, info);
2093
+ }
2094
+ render() {
2095
+ if (this.state.error) {
2096
+ return this.props.fallback;
2097
+ }
2098
+ return this.props.children;
2099
+ }
2100
+ };
2101
+
2102
+ // src/react/navigation.ts
2103
+ var RouterContext = createContext2(null);
2104
+ RouterContext.displayName = "ClientRouter";
2105
+ function useParams() {
2106
+ return useContext2(RouterContext)?.params ?? {};
2107
+ }
2108
+ function useNavigate() {
2109
+ const ctx = useContext2(RouterContext);
2110
+ if (!ctx) throw new Error("useNavigate() must be used within a client router");
2111
+ return ctx.navigate;
2112
+ }
2113
+ function useNavigation() {
2114
+ const ctx = useContext2(RouterContext);
2115
+ return { state: ctx?.state ?? "idle" };
2116
+ }
2117
+ function Link({ href, children, ...props }) {
2118
+ const router = useContext2(RouterContext);
2119
+ const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
2120
+ if (!router || isExternal || props.target !== void 0) {
2121
+ return createElement2("a", { href, ...props }, children);
2122
+ }
2123
+ const handleClick = (e) => {
2124
+ if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
2125
+ if (e.button !== 0) return;
2126
+ e.preventDefault();
2127
+ router.navigate(href);
2128
+ };
2129
+ return createElement2("a", { href, onClick: handleClick, ...props }, children);
2130
+ }
2131
+ function useRevalidate() {
2132
+ const ctx = useContext2(RouterContext);
2133
+ if (!ctx) throw new Error("useRevalidate() must be used within a client router");
2134
+ return ctx.revalidate;
2135
+ }
2136
+ function Form({ method = "post", action, children, ...props }) {
2137
+ const router = useContext2(RouterContext);
2138
+ if (!router) {
2139
+ return createElement2("form", { method, action, ...props }, children);
2140
+ }
2141
+ const handleSubmit = async (e) => {
2142
+ e.preventDefault();
2143
+ const form = e.target;
2144
+ const formData = new FormData(form);
2145
+ const url = action || window.location.pathname;
2146
+ const fetchMethod = method === "get" ? "GET" : method.toUpperCase();
2147
+ try {
2148
+ const res = await fetch(url, {
2149
+ method: fetchMethod,
2150
+ body: fetchMethod === "GET" ? void 0 : formData,
2151
+ redirect: "manual"
2152
+ });
2153
+ if (res.status >= 300 && res.status < 400) {
2154
+ const loc = res.headers.get("Location");
2155
+ if (loc) {
2156
+ router.navigate(loc);
2157
+ return;
2158
+ }
2159
+ }
2160
+ await router.revalidate();
2161
+ } catch (err) {
2162
+ console.error("[weifuwu/react] Form submit failed:", err);
2163
+ }
2164
+ };
2165
+ return createElement2(
2166
+ "form",
2167
+ { method, action, ...props, onSubmit: handleSubmit },
2168
+ children
2169
+ );
2170
+ }
2171
+
2172
+ // src/react/index.ts
2173
+ var LAYOUTS_KEY = /* @__PURE__ */ Symbol.for("weifuwu:react:layouts");
2174
+ var SETUP_KEY = /* @__PURE__ */ Symbol.for("weifuwu:react:setup");
2175
+ function bag(ctx) {
2176
+ return ctx;
2177
+ }
2178
+ function isDataRequest(req) {
2179
+ try {
2180
+ return new URL(req.url).searchParams.has("_data");
2181
+ } catch {
2182
+ return false;
2183
+ }
2184
+ }
2185
+ function react(opts = {}) {
2186
+ const layout = opts.layout ?? Fragment2;
2187
+ const mw = (req, ctx, next) => {
2188
+ const b = bag(ctx);
2189
+ const layouts = b[LAYOUTS_KEY] ?? [];
2190
+ b[LAYOUTS_KEY] = layouts;
2191
+ layouts.push(layout);
2192
+ if (!b[SETUP_KEY]) {
2193
+ b[SETUP_KEY] = true;
2194
+ ctx.render = (element, renderOpts) => {
2195
+ if (renderOpts?.data && isDataRequest(req)) {
2196
+ return Promise.resolve(Response.json(renderOpts.data));
2197
+ }
2198
+ return Promise.resolve(render(element, layouts, renderOpts));
2199
+ };
2200
+ ctx.renderStream = (element, renderOpts) => {
2201
+ if (renderOpts?.data && isDataRequest(req)) {
2202
+ return Promise.resolve(Response.json(renderOpts.data));
2203
+ }
2204
+ return renderStream(element, layouts, renderOpts);
2205
+ };
2206
+ }
2207
+ return next(req, ctx);
2208
+ };
2209
+ return mw;
2210
+ }
1880
2211
  export {
1881
2212
  DEFAULT_MAX_BODY,
2213
+ ErrorBoundary,
2214
+ Form,
1882
2215
  HttpError,
2216
+ Link,
1883
2217
  MIGRATIONS_TABLE,
1884
2218
  Router,
2219
+ ServerDataContext,
1885
2220
  compress,
1886
2221
  cors,
1887
2222
  createHub,
@@ -1893,11 +2228,17 @@ export {
1893
2228
  postgres,
1894
2229
  queue,
1895
2230
  rateLimit,
2231
+ react,
1896
2232
  redis,
1897
2233
  runWithTrace,
1898
2234
  serve,
1899
2235
  serveStatic,
1900
2236
  trace,
1901
2237
  traceElapsed,
1902
- upload
2238
+ upload,
2239
+ useNavigate,
2240
+ useNavigation,
2241
+ useParams,
2242
+ useRevalidate,
2243
+ useServerData
1903
2244
  };
@@ -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';