weifuwu 0.16.4 → 0.16.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 +3 -3
- package/cli.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.js +9 -214
- package/dist/react.d.ts +6 -0
- package/dist/react.js +216 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -451,7 +451,7 @@ The hydration bundle also injects `self.process = { env: {} }` as a safety net s
|
|
|
451
451
|
#### useWebsocket — auto-reconnecting WebSocket
|
|
452
452
|
|
|
453
453
|
```tsx
|
|
454
|
-
import { useWebsocket } from 'weifuwu'
|
|
454
|
+
import { useWebsocket } from 'weifuwu/react'
|
|
455
455
|
|
|
456
456
|
function Chat() {
|
|
457
457
|
const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
|
|
@@ -472,7 +472,7 @@ function Chat() {
|
|
|
472
472
|
#### useAction — async form submission
|
|
473
473
|
|
|
474
474
|
```tsx
|
|
475
|
-
import { useAction } from 'weifuwu'
|
|
475
|
+
import { useAction } from 'weifuwu/react'
|
|
476
476
|
|
|
477
477
|
function FeedbackForm() {
|
|
478
478
|
const { submit, data, error, pending, reset } = useAction('/api/feedback', { method: 'POST' })
|
|
@@ -490,7 +490,7 @@ Auto-serializes JSON, auto-reads `_csrf` cookie and sends as `X-CSRF-Token`. Ret
|
|
|
490
490
|
### Client-side navigation
|
|
491
491
|
|
|
492
492
|
```tsx
|
|
493
|
-
import { Link, useNavigate } from 'weifuwu'
|
|
493
|
+
import { Link, useNavigate } from 'weifuwu/react'
|
|
494
494
|
|
|
495
495
|
function Nav() {
|
|
496
496
|
const navigate = useNavigate()
|
package/cli.ts
CHANGED
|
@@ -123,7 +123,7 @@ async function cmdInit(name: string) {
|
|
|
123
123
|
|
|
124
124
|
await writeFile(join(targetDir, 'ui', 'pages', 'page.tsx'), [
|
|
125
125
|
"import { useState } from 'react'",
|
|
126
|
-
"import { useWebsocket } from 'weifuwu'",
|
|
126
|
+
"import { useWebsocket } from 'weifuwu/react'",
|
|
127
127
|
'',
|
|
128
128
|
'export default function Home() {',
|
|
129
129
|
' const [input, setInput] = useState("")',
|
package/dist/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ async function cmdInit(name) {
|
|
|
110
110
|
].join("\n"));
|
|
111
111
|
await writeFile(join(targetDir, "ui", "pages", "page.tsx"), [
|
|
112
112
|
"import { useState } from 'react'",
|
|
113
|
-
"import { useWebsocket } from 'weifuwu'",
|
|
113
|
+
"import { useWebsocket } from 'weifuwu/react'",
|
|
114
114
|
"",
|
|
115
115
|
"export default function Home() {",
|
|
116
116
|
' const [input, setInput] = useState("")',
|
package/dist/index.d.ts
CHANGED
|
@@ -60,11 +60,6 @@ export { seo, seoMiddleware, seoTags } from './seo.ts';
|
|
|
60
60
|
export type { SeoOptions, RobotsRule, SitemapUrl, SitemapConfig, SeoHeadersConfig, SeoTagsConfig, } from './seo.ts';
|
|
61
61
|
export { mailer } from './mailer.ts';
|
|
62
62
|
export type { MailerOptions, MailOptions, Mailer } from './mailer.ts';
|
|
63
|
-
export { useWebsocket } from './use-websocket.ts';
|
|
64
|
-
export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts';
|
|
65
|
-
export { useAction } from './use-action.ts';
|
|
66
|
-
export type { UseActionOptions, UseActionReturn } from './use-action.ts';
|
|
67
|
-
export { Link, useNavigate, navigate } from './client-router.ts';
|
|
68
63
|
export { csrf } from './csrf.ts';
|
|
69
64
|
export type { CsrfOptions } from './csrf.ts';
|
|
70
65
|
export { logdb } from './logdb/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -995,6 +995,7 @@ ${src}`;
|
|
|
995
995
|
alias: resolveAliases(),
|
|
996
996
|
banner: { js: "self.process={env:{}};" },
|
|
997
997
|
define: Object.keys(publicEnv).length > 0 ? publicEnv : void 0,
|
|
998
|
+
loader: { ".node": "empty" },
|
|
998
999
|
write: false,
|
|
999
1000
|
minify: true
|
|
1000
1001
|
});
|
|
@@ -1773,7 +1774,7 @@ function upload(options) {
|
|
|
1773
1774
|
// rate-limit.ts
|
|
1774
1775
|
function rateLimit(options) {
|
|
1775
1776
|
const max = options?.max ?? 100;
|
|
1776
|
-
const
|
|
1777
|
+
const window = options?.window ?? 6e4;
|
|
1777
1778
|
const getKey = options?.key ?? ((req) => {
|
|
1778
1779
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
1779
1780
|
if (forwarded) return forwarded.split(",")[0].trim();
|
|
@@ -1790,19 +1791,19 @@ function rateLimit(options) {
|
|
|
1790
1791
|
for (const [key, entry] of hits) {
|
|
1791
1792
|
if (entry.reset < now) hits.delete(key);
|
|
1792
1793
|
}
|
|
1793
|
-
},
|
|
1794
|
+
}, window);
|
|
1794
1795
|
if (interval.unref) interval.unref();
|
|
1795
1796
|
const mw = async (req, ctx, next) => {
|
|
1796
1797
|
const key = getKey(req);
|
|
1797
1798
|
const now = Date.now();
|
|
1798
1799
|
const entry = hits.get(key);
|
|
1799
1800
|
if (!entry || entry.reset < now) {
|
|
1800
|
-
hits.set(key, { count: 1, reset: now +
|
|
1801
|
+
hits.set(key, { count: 1, reset: now + window });
|
|
1801
1802
|
const res2 = await next(req, ctx);
|
|
1802
1803
|
const headers2 = new Headers(res2.headers);
|
|
1803
1804
|
headers2.set("X-RateLimit-Limit", String(max));
|
|
1804
1805
|
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1805
|
-
headers2.set("X-RateLimit-Reset", String(Math.ceil((now +
|
|
1806
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window) / 1e3)));
|
|
1806
1807
|
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1807
1808
|
}
|
|
1808
1809
|
entry.count++;
|
|
@@ -6380,7 +6381,7 @@ async function buildRouter4(deps) {
|
|
|
6380
6381
|
skills: allSkills,
|
|
6381
6382
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6382
6383
|
});
|
|
6383
|
-
const
|
|
6384
|
+
const history = await getHistory(sql2, sessionId);
|
|
6384
6385
|
await addTextMessage(sql2, sessionId, "user", content);
|
|
6385
6386
|
const stream = executeGenerator({
|
|
6386
6387
|
sessionId,
|
|
@@ -6388,7 +6389,7 @@ async function buildRouter4(deps) {
|
|
|
6388
6389
|
model,
|
|
6389
6390
|
tools,
|
|
6390
6391
|
systemPrompt: sysPrompt,
|
|
6391
|
-
messages:
|
|
6392
|
+
messages: history,
|
|
6392
6393
|
sql: sql2
|
|
6393
6394
|
});
|
|
6394
6395
|
return createSSEStream(stream);
|
|
@@ -6468,7 +6469,7 @@ function createWSHandler2(deps) {
|
|
|
6468
6469
|
skills: allSkills,
|
|
6469
6470
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6470
6471
|
});
|
|
6471
|
-
const
|
|
6472
|
+
const history = await getHistory(sql2, session_id);
|
|
6472
6473
|
await addTextMessage(sql2, session_id, "user", content);
|
|
6473
6474
|
const stream = executeGenerator({
|
|
6474
6475
|
sessionId: session_id,
|
|
@@ -6476,7 +6477,7 @@ function createWSHandler2(deps) {
|
|
|
6476
6477
|
model,
|
|
6477
6478
|
tools,
|
|
6478
6479
|
systemPrompt: sysPrompt,
|
|
6479
|
-
messages:
|
|
6480
|
+
messages: history,
|
|
6480
6481
|
sql: sql2,
|
|
6481
6482
|
abortSignal: controller.signal
|
|
6482
6483
|
});
|
|
@@ -6913,207 +6914,6 @@ function mailer(options) {
|
|
|
6913
6914
|
return { send, close };
|
|
6914
6915
|
}
|
|
6915
6916
|
|
|
6916
|
-
// use-websocket.ts
|
|
6917
|
-
import { useEffect, useRef, useCallback, useState } from "react";
|
|
6918
|
-
var RECONNECT_DELAY = 3e3;
|
|
6919
|
-
var MAX_RETRIES = 10;
|
|
6920
|
-
function resolveUrl(url) {
|
|
6921
|
-
return typeof url === "function" ? url() : url;
|
|
6922
|
-
}
|
|
6923
|
-
function useWebsocket(url, options) {
|
|
6924
|
-
const { onMessage, reconnect: reconnectOpt = true, protocols, enabled = true } = options ?? {};
|
|
6925
|
-
const [lastMessage, setLastMessage] = useState(null);
|
|
6926
|
-
const [readyState, setReadyState] = useState(WebSocket.CLOSED);
|
|
6927
|
-
const wsRef = useRef(null);
|
|
6928
|
-
const retryRef = useRef(0);
|
|
6929
|
-
const timerRef = useRef(void 0);
|
|
6930
|
-
const mountedRef = useRef(true);
|
|
6931
|
-
const shouldReconnectRef = useRef(true);
|
|
6932
|
-
const urlRef = useRef(url);
|
|
6933
|
-
const optsRef = useRef({ onMessage, reconnectOpt, protocols });
|
|
6934
|
-
urlRef.current = url;
|
|
6935
|
-
optsRef.current = { onMessage, reconnectOpt, protocols };
|
|
6936
|
-
const cleanup = useCallback(() => {
|
|
6937
|
-
clearTimeout(timerRef.current);
|
|
6938
|
-
wsRef.current?.close();
|
|
6939
|
-
wsRef.current = null;
|
|
6940
|
-
}, []);
|
|
6941
|
-
const connect = useCallback(() => {
|
|
6942
|
-
if (!mountedRef.current || !enabled) return;
|
|
6943
|
-
const resolved = resolveUrl(urlRef.current);
|
|
6944
|
-
if (!resolved) return;
|
|
6945
|
-
wsRef.current?.close();
|
|
6946
|
-
const ws = new WebSocket(resolved, optsRef.current.protocols);
|
|
6947
|
-
wsRef.current = ws;
|
|
6948
|
-
setReadyState(WebSocket.CONNECTING);
|
|
6949
|
-
ws.addEventListener("open", () => {
|
|
6950
|
-
if (!mountedRef.current) return;
|
|
6951
|
-
retryRef.current = 0;
|
|
6952
|
-
setReadyState(WebSocket.OPEN);
|
|
6953
|
-
});
|
|
6954
|
-
ws.addEventListener("message", (e) => {
|
|
6955
|
-
if (!mountedRef.current) return;
|
|
6956
|
-
const data = typeof e.data === "string" ? e.data : String(e.data);
|
|
6957
|
-
setLastMessage(data);
|
|
6958
|
-
optsRef.current.onMessage?.(data);
|
|
6959
|
-
});
|
|
6960
|
-
ws.addEventListener("close", () => {
|
|
6961
|
-
if (!mountedRef.current) return;
|
|
6962
|
-
setReadyState(WebSocket.CLOSED);
|
|
6963
|
-
const ro = optsRef.current.reconnectOpt;
|
|
6964
|
-
if (ro && shouldReconnectRef.current && mountedRef.current) {
|
|
6965
|
-
const maxRetries = typeof ro === "object" ? ro.maxRetries ?? MAX_RETRIES : MAX_RETRIES;
|
|
6966
|
-
const delay = typeof ro === "object" ? ro.delay ?? RECONNECT_DELAY : RECONNECT_DELAY;
|
|
6967
|
-
if (retryRef.current < maxRetries) {
|
|
6968
|
-
retryRef.current++;
|
|
6969
|
-
timerRef.current = setTimeout(() => connect(), delay);
|
|
6970
|
-
}
|
|
6971
|
-
}
|
|
6972
|
-
});
|
|
6973
|
-
}, [enabled]);
|
|
6974
|
-
useEffect(() => {
|
|
6975
|
-
mountedRef.current = true;
|
|
6976
|
-
shouldReconnectRef.current = true;
|
|
6977
|
-
if (enabled) connect();
|
|
6978
|
-
return () => {
|
|
6979
|
-
mountedRef.current = false;
|
|
6980
|
-
cleanup();
|
|
6981
|
-
};
|
|
6982
|
-
}, [enabled, connect, cleanup]);
|
|
6983
|
-
const send = useCallback((data) => {
|
|
6984
|
-
wsRef.current?.send(data);
|
|
6985
|
-
}, []);
|
|
6986
|
-
const close = useCallback(() => {
|
|
6987
|
-
shouldReconnectRef.current = false;
|
|
6988
|
-
cleanup();
|
|
6989
|
-
setReadyState(WebSocket.CLOSED);
|
|
6990
|
-
}, [cleanup]);
|
|
6991
|
-
const reconnectFn = useCallback(() => {
|
|
6992
|
-
retryRef.current = 0;
|
|
6993
|
-
shouldReconnectRef.current = true;
|
|
6994
|
-
cleanup();
|
|
6995
|
-
connect();
|
|
6996
|
-
}, [cleanup, connect]);
|
|
6997
|
-
return { send, close, readyState, lastMessage, reconnect: reconnectFn };
|
|
6998
|
-
}
|
|
6999
|
-
|
|
7000
|
-
// use-action.ts
|
|
7001
|
-
import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
7002
|
-
function getCsrfToken() {
|
|
7003
|
-
if (typeof document === "undefined") return void 0;
|
|
7004
|
-
const match = document.cookie.match(/(?:^|;\s*)_csrf=([^;]+)/);
|
|
7005
|
-
return match ? decodeURIComponent(match[1]) : void 0;
|
|
7006
|
-
}
|
|
7007
|
-
function useAction(url, options) {
|
|
7008
|
-
const { method = "POST", headers, onSuccess, onError } = options ?? {};
|
|
7009
|
-
const [data, setData] = useState2(null);
|
|
7010
|
-
const [error, setError] = useState2(null);
|
|
7011
|
-
const [pending, setPending] = useState2(false);
|
|
7012
|
-
const mountedRef = useRef2(true);
|
|
7013
|
-
const submit = useCallback2(async (body) => {
|
|
7014
|
-
setPending(true);
|
|
7015
|
-
setError(null);
|
|
7016
|
-
try {
|
|
7017
|
-
const csrfToken = getCsrfToken();
|
|
7018
|
-
const hdrs = { ...headers };
|
|
7019
|
-
if (csrfToken) hdrs["x-csrf-token"] = csrfToken;
|
|
7020
|
-
if (body && typeof body === "object" && !(body instanceof FormData)) {
|
|
7021
|
-
hdrs["content-type"] = "application/json";
|
|
7022
|
-
}
|
|
7023
|
-
const res = await fetch(url, {
|
|
7024
|
-
method,
|
|
7025
|
-
headers: hdrs,
|
|
7026
|
-
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
7027
|
-
});
|
|
7028
|
-
if (!res.ok) {
|
|
7029
|
-
const text2 = await res.text();
|
|
7030
|
-
throw new Error(text2 || `HTTP ${res.status}`);
|
|
7031
|
-
}
|
|
7032
|
-
const result = res.status === 204 ? void 0 : await res.json();
|
|
7033
|
-
if (mountedRef.current) {
|
|
7034
|
-
setData(result);
|
|
7035
|
-
onSuccess?.(result);
|
|
7036
|
-
}
|
|
7037
|
-
return result;
|
|
7038
|
-
} catch (err) {
|
|
7039
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
7040
|
-
if (mountedRef.current) {
|
|
7041
|
-
setError(e);
|
|
7042
|
-
onError?.(e);
|
|
7043
|
-
}
|
|
7044
|
-
return void 0;
|
|
7045
|
-
} finally {
|
|
7046
|
-
if (mountedRef.current) setPending(false);
|
|
7047
|
-
}
|
|
7048
|
-
}, [url, method, headers, onSuccess, onError]);
|
|
7049
|
-
const reset = useCallback2(() => {
|
|
7050
|
-
setData(null);
|
|
7051
|
-
setError(null);
|
|
7052
|
-
}, []);
|
|
7053
|
-
return { submit, data, error, pending, reset };
|
|
7054
|
-
}
|
|
7055
|
-
|
|
7056
|
-
// client-router.ts
|
|
7057
|
-
import { createElement as createElement2, useCallback as useCallback3 } from "react";
|
|
7058
|
-
async function navigate(href) {
|
|
7059
|
-
if (typeof document === "undefined") return;
|
|
7060
|
-
const url = new URL(href, location.origin);
|
|
7061
|
-
if (url.origin !== location.origin) {
|
|
7062
|
-
location.href = href;
|
|
7063
|
-
return;
|
|
7064
|
-
}
|
|
7065
|
-
const html = await fetch(url.pathname + url.search, {
|
|
7066
|
-
headers: { accept: "text/html" }
|
|
7067
|
-
}).then((r) => r.text());
|
|
7068
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
7069
|
-
const rootEl = doc.getElementById("__weifuwu_root");
|
|
7070
|
-
if (!rootEl) {
|
|
7071
|
-
location.href = href;
|
|
7072
|
-
return;
|
|
7073
|
-
}
|
|
7074
|
-
const newHtml = rootEl.innerHTML;
|
|
7075
|
-
const propsMatch = html.match(/window\.__WEIFUWU_PROPS=(.+?)<\/script>/);
|
|
7076
|
-
if (!propsMatch) {
|
|
7077
|
-
location.href = href;
|
|
7078
|
-
return;
|
|
7079
|
-
}
|
|
7080
|
-
const bundleMatch = html.match(/src="(\/__wfw\/client\/[^"]+\.js)"/);
|
|
7081
|
-
const bundleUrl = bundleMatch ? bundleMatch[1] : null;
|
|
7082
|
-
const currentRoot = document.getElementById("__weifuwu_root");
|
|
7083
|
-
if (!currentRoot) {
|
|
7084
|
-
location.href = href;
|
|
7085
|
-
return;
|
|
7086
|
-
}
|
|
7087
|
-
;
|
|
7088
|
-
window.__WEIFUWU_ROOT?.unmount();
|
|
7089
|
-
currentRoot.innerHTML = newHtml;
|
|
7090
|
-
window.__WEIFUWU_PROPS = JSON.parse(propsMatch[1]);
|
|
7091
|
-
history.pushState(null, "", url.pathname + url.search);
|
|
7092
|
-
if (bundleUrl) {
|
|
7093
|
-
const cacheBust = bundleUrl.includes("?") ? "&_t=" : "?_t=";
|
|
7094
|
-
try {
|
|
7095
|
-
await import(
|
|
7096
|
-
/* @vite-ignore */
|
|
7097
|
-
`${bundleUrl}${cacheBust}${Date.now()}`
|
|
7098
|
-
);
|
|
7099
|
-
} catch (e) {
|
|
7100
|
-
console.error("[weifuwu/router] hydration failed:", e);
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
}
|
|
7104
|
-
function useNavigate() {
|
|
7105
|
-
return useCallback3((href) => navigate(href), []);
|
|
7106
|
-
}
|
|
7107
|
-
function Link({ href, children, onClick, ...props }) {
|
|
7108
|
-
const handleClick = useCallback3((e) => {
|
|
7109
|
-
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
7110
|
-
e.preventDefault();
|
|
7111
|
-
navigate(href);
|
|
7112
|
-
onClick?.(e);
|
|
7113
|
-
}, [href, onClick]);
|
|
7114
|
-
return createElement2("a", { href, onClick: handleClick, ...props }, children);
|
|
7115
|
-
}
|
|
7116
|
-
|
|
7117
6917
|
// csrf.ts
|
|
7118
6918
|
function csrf(options) {
|
|
7119
6919
|
const cookieName = options?.cookie ?? "_csrf";
|
|
@@ -8305,7 +8105,6 @@ function registerWorker(url) {
|
|
|
8305
8105
|
};
|
|
8306
8106
|
}
|
|
8307
8107
|
export {
|
|
8308
|
-
Link,
|
|
8309
8108
|
Router,
|
|
8310
8109
|
TsxContext,
|
|
8311
8110
|
agent,
|
|
@@ -8339,7 +8138,6 @@ export {
|
|
|
8339
8138
|
logger,
|
|
8340
8139
|
mailer,
|
|
8341
8140
|
messager,
|
|
8342
|
-
navigate,
|
|
8343
8141
|
openai,
|
|
8344
8142
|
opencode,
|
|
8345
8143
|
postgres,
|
|
@@ -8362,10 +8160,7 @@ export {
|
|
|
8362
8160
|
tool2 as tool,
|
|
8363
8161
|
tsx,
|
|
8364
8162
|
upload,
|
|
8365
|
-
useAction,
|
|
8366
|
-
useNavigate,
|
|
8367
8163
|
useTsx,
|
|
8368
|
-
useWebsocket,
|
|
8369
8164
|
user,
|
|
8370
8165
|
validate
|
|
8371
8166
|
};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useWebsocket } from './use-websocket.ts';
|
|
2
|
+
export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts';
|
|
3
|
+
export { useAction } from './use-action.ts';
|
|
4
|
+
export type { UseActionOptions, UseActionReturn } from './use-action.ts';
|
|
5
|
+
export { Link, useNavigate, navigate } from './client-router.ts';
|
|
6
|
+
export { TsxContext, useTsx } from './tsx-context.ts';
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// use-websocket.ts
|
|
2
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
3
|
+
var RECONNECT_DELAY = 3e3;
|
|
4
|
+
var MAX_RETRIES = 10;
|
|
5
|
+
function resolveUrl(url) {
|
|
6
|
+
return typeof url === "function" ? url() : url;
|
|
7
|
+
}
|
|
8
|
+
function useWebsocket(url, options) {
|
|
9
|
+
const { onMessage, reconnect: reconnectOpt = true, protocols, enabled = true } = options ?? {};
|
|
10
|
+
const [lastMessage, setLastMessage] = useState(null);
|
|
11
|
+
const [readyState, setReadyState] = useState(WebSocket.CLOSED);
|
|
12
|
+
const wsRef = useRef(null);
|
|
13
|
+
const retryRef = useRef(0);
|
|
14
|
+
const timerRef = useRef(void 0);
|
|
15
|
+
const mountedRef = useRef(true);
|
|
16
|
+
const shouldReconnectRef = useRef(true);
|
|
17
|
+
const urlRef = useRef(url);
|
|
18
|
+
const optsRef = useRef({ onMessage, reconnectOpt, protocols });
|
|
19
|
+
urlRef.current = url;
|
|
20
|
+
optsRef.current = { onMessage, reconnectOpt, protocols };
|
|
21
|
+
const cleanup = useCallback(() => {
|
|
22
|
+
clearTimeout(timerRef.current);
|
|
23
|
+
wsRef.current?.close();
|
|
24
|
+
wsRef.current = null;
|
|
25
|
+
}, []);
|
|
26
|
+
const connect = useCallback(() => {
|
|
27
|
+
if (!mountedRef.current || !enabled) return;
|
|
28
|
+
const resolved = resolveUrl(urlRef.current);
|
|
29
|
+
if (!resolved) return;
|
|
30
|
+
wsRef.current?.close();
|
|
31
|
+
const ws = new WebSocket(resolved, optsRef.current.protocols);
|
|
32
|
+
wsRef.current = ws;
|
|
33
|
+
setReadyState(WebSocket.CONNECTING);
|
|
34
|
+
ws.addEventListener("open", () => {
|
|
35
|
+
if (!mountedRef.current) return;
|
|
36
|
+
retryRef.current = 0;
|
|
37
|
+
setReadyState(WebSocket.OPEN);
|
|
38
|
+
});
|
|
39
|
+
ws.addEventListener("message", (e) => {
|
|
40
|
+
if (!mountedRef.current) return;
|
|
41
|
+
const data = typeof e.data === "string" ? e.data : String(e.data);
|
|
42
|
+
setLastMessage(data);
|
|
43
|
+
optsRef.current.onMessage?.(data);
|
|
44
|
+
});
|
|
45
|
+
ws.addEventListener("close", () => {
|
|
46
|
+
if (!mountedRef.current) return;
|
|
47
|
+
setReadyState(WebSocket.CLOSED);
|
|
48
|
+
const ro = optsRef.current.reconnectOpt;
|
|
49
|
+
if (ro && shouldReconnectRef.current && mountedRef.current) {
|
|
50
|
+
const maxRetries = typeof ro === "object" ? ro.maxRetries ?? MAX_RETRIES : MAX_RETRIES;
|
|
51
|
+
const delay = typeof ro === "object" ? ro.delay ?? RECONNECT_DELAY : RECONNECT_DELAY;
|
|
52
|
+
if (retryRef.current < maxRetries) {
|
|
53
|
+
retryRef.current++;
|
|
54
|
+
timerRef.current = setTimeout(() => connect(), delay);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}, [enabled]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
mountedRef.current = true;
|
|
61
|
+
shouldReconnectRef.current = true;
|
|
62
|
+
if (enabled) connect();
|
|
63
|
+
return () => {
|
|
64
|
+
mountedRef.current = false;
|
|
65
|
+
cleanup();
|
|
66
|
+
};
|
|
67
|
+
}, [enabled, connect, cleanup]);
|
|
68
|
+
const send = useCallback((data) => {
|
|
69
|
+
wsRef.current?.send(data);
|
|
70
|
+
}, []);
|
|
71
|
+
const close = useCallback(() => {
|
|
72
|
+
shouldReconnectRef.current = false;
|
|
73
|
+
cleanup();
|
|
74
|
+
setReadyState(WebSocket.CLOSED);
|
|
75
|
+
}, [cleanup]);
|
|
76
|
+
const reconnectFn = useCallback(() => {
|
|
77
|
+
retryRef.current = 0;
|
|
78
|
+
shouldReconnectRef.current = true;
|
|
79
|
+
cleanup();
|
|
80
|
+
connect();
|
|
81
|
+
}, [cleanup, connect]);
|
|
82
|
+
return { send, close, readyState, lastMessage, reconnect: reconnectFn };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// use-action.ts
|
|
86
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
87
|
+
function getCsrfToken() {
|
|
88
|
+
if (typeof document === "undefined") return void 0;
|
|
89
|
+
const match = document.cookie.match(/(?:^|;\s*)_csrf=([^;]+)/);
|
|
90
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
91
|
+
}
|
|
92
|
+
function useAction(url, options) {
|
|
93
|
+
const { method = "POST", headers, onSuccess, onError } = options ?? {};
|
|
94
|
+
const [data, setData] = useState2(null);
|
|
95
|
+
const [error, setError] = useState2(null);
|
|
96
|
+
const [pending, setPending] = useState2(false);
|
|
97
|
+
const mountedRef = useRef2(true);
|
|
98
|
+
const submit = useCallback2(async (body) => {
|
|
99
|
+
setPending(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
try {
|
|
102
|
+
const csrfToken = getCsrfToken();
|
|
103
|
+
const hdrs = { ...headers };
|
|
104
|
+
if (csrfToken) hdrs["x-csrf-token"] = csrfToken;
|
|
105
|
+
if (body && typeof body === "object" && !(body instanceof FormData)) {
|
|
106
|
+
hdrs["content-type"] = "application/json";
|
|
107
|
+
}
|
|
108
|
+
const res = await fetch(url, {
|
|
109
|
+
method,
|
|
110
|
+
headers: hdrs,
|
|
111
|
+
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
116
|
+
}
|
|
117
|
+
const result = res.status === 204 ? void 0 : await res.json();
|
|
118
|
+
if (mountedRef.current) {
|
|
119
|
+
setData(result);
|
|
120
|
+
onSuccess?.(result);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
125
|
+
if (mountedRef.current) {
|
|
126
|
+
setError(e);
|
|
127
|
+
onError?.(e);
|
|
128
|
+
}
|
|
129
|
+
return void 0;
|
|
130
|
+
} finally {
|
|
131
|
+
if (mountedRef.current) setPending(false);
|
|
132
|
+
}
|
|
133
|
+
}, [url, method, headers, onSuccess, onError]);
|
|
134
|
+
const reset = useCallback2(() => {
|
|
135
|
+
setData(null);
|
|
136
|
+
setError(null);
|
|
137
|
+
}, []);
|
|
138
|
+
return { submit, data, error, pending, reset };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// client-router.ts
|
|
142
|
+
import { createElement, useCallback as useCallback3 } from "react";
|
|
143
|
+
async function navigate(href) {
|
|
144
|
+
if (typeof document === "undefined") return;
|
|
145
|
+
const url = new URL(href, location.origin);
|
|
146
|
+
if (url.origin !== location.origin) {
|
|
147
|
+
location.href = href;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const html = await fetch(url.pathname + url.search, {
|
|
151
|
+
headers: { accept: "text/html" }
|
|
152
|
+
}).then((r) => r.text());
|
|
153
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
154
|
+
const rootEl = doc.getElementById("__weifuwu_root");
|
|
155
|
+
if (!rootEl) {
|
|
156
|
+
location.href = href;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const newHtml = rootEl.innerHTML;
|
|
160
|
+
const propsMatch = html.match(/window\.__WEIFUWU_PROPS=(.+?)<\/script>/);
|
|
161
|
+
if (!propsMatch) {
|
|
162
|
+
location.href = href;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const bundleMatch = html.match(/src="(\/__wfw\/client\/[^"]+\.js)"/);
|
|
166
|
+
const bundleUrl = bundleMatch ? bundleMatch[1] : null;
|
|
167
|
+
const currentRoot = document.getElementById("__weifuwu_root");
|
|
168
|
+
if (!currentRoot) {
|
|
169
|
+
location.href = href;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
;
|
|
173
|
+
window.__WEIFUWU_ROOT?.unmount();
|
|
174
|
+
currentRoot.innerHTML = newHtml;
|
|
175
|
+
window.__WEIFUWU_PROPS = JSON.parse(propsMatch[1]);
|
|
176
|
+
history.pushState(null, "", url.pathname + url.search);
|
|
177
|
+
if (bundleUrl) {
|
|
178
|
+
const cacheBust = bundleUrl.includes("?") ? "&_t=" : "?_t=";
|
|
179
|
+
try {
|
|
180
|
+
await import(
|
|
181
|
+
/* @vite-ignore */
|
|
182
|
+
`${bundleUrl}${cacheBust}${Date.now()}`
|
|
183
|
+
);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error("[weifuwu/router] hydration failed:", e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function useNavigate() {
|
|
190
|
+
return useCallback3((href) => navigate(href), []);
|
|
191
|
+
}
|
|
192
|
+
function Link({ href, children, onClick, ...props }) {
|
|
193
|
+
const handleClick = useCallback3((e) => {
|
|
194
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
navigate(href);
|
|
197
|
+
onClick?.(e);
|
|
198
|
+
}, [href, onClick]);
|
|
199
|
+
return createElement("a", { href, onClick: handleClick, ...props }, children);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// tsx-context.ts
|
|
203
|
+
import { createContext, useContext } from "react";
|
|
204
|
+
var TsxContext = createContext({ params: {}, query: {} });
|
|
205
|
+
function useTsx() {
|
|
206
|
+
return useContext(TsxContext);
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
Link,
|
|
210
|
+
TsxContext,
|
|
211
|
+
navigate,
|
|
212
|
+
useAction,
|
|
213
|
+
useNavigate,
|
|
214
|
+
useTsx,
|
|
215
|
+
useWebsocket
|
|
216
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/index.js",
|
|
9
|
-
"./
|
|
9
|
+
"./react": "./dist/react.js"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"weifuwu": "dist/cli.js"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --packages=external && esbuild cli.ts --bundle --format=esm --platform=node --outfile=dist/cli.js --packages=external",
|
|
21
|
+
"build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --packages=external && esbuild cli.ts --bundle --format=esm --platform=node --outfile=dist/cli.js --packages=external && esbuild react.ts --bundle --format=esm --outfile=dist/react.js --external:react --external:react-dom",
|
|
22
22
|
"prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
|
|
23
23
|
"test": "node --test 'test/**/*.test.ts'"
|
|
24
24
|
},
|