weifuwu 0.30.2 → 0.31.1

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.
@@ -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,2 @@
1
+ import { type Context } from 'react';
2
+ export declare const ServerDataContext: Context<Record<string, unknown>>;
@@ -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';