weifuwu 0.16.4 → 0.16.6
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 +17 -219
- 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
|
@@ -123,12 +123,15 @@ function serve(handler, options) {
|
|
|
123
123
|
resolveReady = r;
|
|
124
124
|
});
|
|
125
125
|
if (options?.shutdown !== false) {
|
|
126
|
-
|
|
126
|
+
let shuttingDown = false;
|
|
127
|
+
const shutdown = () => {
|
|
128
|
+
if (shuttingDown) return;
|
|
129
|
+
shuttingDown = true;
|
|
127
130
|
server.close();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
process.exit(0);
|
|
132
|
+
};
|
|
133
|
+
process.on("SIGTERM", shutdown);
|
|
134
|
+
process.on("SIGINT", shutdown);
|
|
132
135
|
}
|
|
133
136
|
if (options?.signal) {
|
|
134
137
|
if (options.signal.aborted) {
|
|
@@ -995,6 +998,7 @@ ${src}`;
|
|
|
995
998
|
alias: resolveAliases(),
|
|
996
999
|
banner: { js: "self.process={env:{}};" },
|
|
997
1000
|
define: Object.keys(publicEnv).length > 0 ? publicEnv : void 0,
|
|
1001
|
+
loader: { ".node": "empty" },
|
|
998
1002
|
write: false,
|
|
999
1003
|
minify: true
|
|
1000
1004
|
});
|
|
@@ -1773,7 +1777,7 @@ function upload(options) {
|
|
|
1773
1777
|
// rate-limit.ts
|
|
1774
1778
|
function rateLimit(options) {
|
|
1775
1779
|
const max = options?.max ?? 100;
|
|
1776
|
-
const
|
|
1780
|
+
const window = options?.window ?? 6e4;
|
|
1777
1781
|
const getKey = options?.key ?? ((req) => {
|
|
1778
1782
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
1779
1783
|
if (forwarded) return forwarded.split(",")[0].trim();
|
|
@@ -1790,19 +1794,19 @@ function rateLimit(options) {
|
|
|
1790
1794
|
for (const [key, entry] of hits) {
|
|
1791
1795
|
if (entry.reset < now) hits.delete(key);
|
|
1792
1796
|
}
|
|
1793
|
-
},
|
|
1797
|
+
}, window);
|
|
1794
1798
|
if (interval.unref) interval.unref();
|
|
1795
1799
|
const mw = async (req, ctx, next) => {
|
|
1796
1800
|
const key = getKey(req);
|
|
1797
1801
|
const now = Date.now();
|
|
1798
1802
|
const entry = hits.get(key);
|
|
1799
1803
|
if (!entry || entry.reset < now) {
|
|
1800
|
-
hits.set(key, { count: 1, reset: now +
|
|
1804
|
+
hits.set(key, { count: 1, reset: now + window });
|
|
1801
1805
|
const res2 = await next(req, ctx);
|
|
1802
1806
|
const headers2 = new Headers(res2.headers);
|
|
1803
1807
|
headers2.set("X-RateLimit-Limit", String(max));
|
|
1804
1808
|
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1805
|
-
headers2.set("X-RateLimit-Reset", String(Math.ceil((now +
|
|
1809
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window) / 1e3)));
|
|
1806
1810
|
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1807
1811
|
}
|
|
1808
1812
|
entry.count++;
|
|
@@ -6380,7 +6384,7 @@ async function buildRouter4(deps) {
|
|
|
6380
6384
|
skills: allSkills,
|
|
6381
6385
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6382
6386
|
});
|
|
6383
|
-
const
|
|
6387
|
+
const history = await getHistory(sql2, sessionId);
|
|
6384
6388
|
await addTextMessage(sql2, sessionId, "user", content);
|
|
6385
6389
|
const stream = executeGenerator({
|
|
6386
6390
|
sessionId,
|
|
@@ -6388,7 +6392,7 @@ async function buildRouter4(deps) {
|
|
|
6388
6392
|
model,
|
|
6389
6393
|
tools,
|
|
6390
6394
|
systemPrompt: sysPrompt,
|
|
6391
|
-
messages:
|
|
6395
|
+
messages: history,
|
|
6392
6396
|
sql: sql2
|
|
6393
6397
|
});
|
|
6394
6398
|
return createSSEStream(stream);
|
|
@@ -6468,7 +6472,7 @@ function createWSHandler2(deps) {
|
|
|
6468
6472
|
skills: allSkills,
|
|
6469
6473
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6470
6474
|
});
|
|
6471
|
-
const
|
|
6475
|
+
const history = await getHistory(sql2, session_id);
|
|
6472
6476
|
await addTextMessage(sql2, session_id, "user", content);
|
|
6473
6477
|
const stream = executeGenerator({
|
|
6474
6478
|
sessionId: session_id,
|
|
@@ -6476,7 +6480,7 @@ function createWSHandler2(deps) {
|
|
|
6476
6480
|
model,
|
|
6477
6481
|
tools,
|
|
6478
6482
|
systemPrompt: sysPrompt,
|
|
6479
|
-
messages:
|
|
6483
|
+
messages: history,
|
|
6480
6484
|
sql: sql2,
|
|
6481
6485
|
abortSignal: controller.signal
|
|
6482
6486
|
});
|
|
@@ -6913,207 +6917,6 @@ function mailer(options) {
|
|
|
6913
6917
|
return { send, close };
|
|
6914
6918
|
}
|
|
6915
6919
|
|
|
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
6920
|
// csrf.ts
|
|
7118
6921
|
function csrf(options) {
|
|
7119
6922
|
const cookieName = options?.cookie ?? "_csrf";
|
|
@@ -8305,7 +8108,6 @@ function registerWorker(url) {
|
|
|
8305
8108
|
};
|
|
8306
8109
|
}
|
|
8307
8110
|
export {
|
|
8308
|
-
Link,
|
|
8309
8111
|
Router,
|
|
8310
8112
|
TsxContext,
|
|
8311
8113
|
agent,
|
|
@@ -8339,7 +8141,6 @@ export {
|
|
|
8339
8141
|
logger,
|
|
8340
8142
|
mailer,
|
|
8341
8143
|
messager,
|
|
8342
|
-
navigate,
|
|
8343
8144
|
openai,
|
|
8344
8145
|
opencode,
|
|
8345
8146
|
postgres,
|
|
@@ -8362,10 +8163,7 @@ export {
|
|
|
8362
8163
|
tool2 as tool,
|
|
8363
8164
|
tsx,
|
|
8364
8165
|
upload,
|
|
8365
|
-
useAction,
|
|
8366
|
-
useNavigate,
|
|
8367
8166
|
useTsx,
|
|
8368
|
-
useWebsocket,
|
|
8369
8167
|
user,
|
|
8370
8168
|
validate
|
|
8371
8169
|
};
|
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.6",
|
|
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
|
},
|