weifuwu 0.33.4 → 0.33.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 +186 -21
- package/dist/client/app.d.ts +12 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +244 -33
- package/dist/client/jsx-runtime.d.ts +362 -1
- package/dist/client/jsx-runtime.js +244 -33
- package/dist/client/lib/form.d.ts +54 -0
- package/dist/client/router.d.ts +1 -34
- package/dist/client/types.d.ts +4 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +96 -48
- package/dist/ui/index.d.ts +31 -22
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -66,7 +66,8 @@ app.mount('#root', AppShell)
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
```js
|
|
69
|
-
// build.mjs
|
|
69
|
+
// build.mjs — 传统构建方式(可选)
|
|
70
|
+
// 推荐:使用 ctx.ui.js() 服务端动态编译,无需独立构建脚本
|
|
70
71
|
esbuild.build({
|
|
71
72
|
entryPoints: ['src/main.tsx'],
|
|
72
73
|
jsx: 'automatic',
|
|
@@ -75,6 +76,12 @@ esbuild.build({
|
|
|
75
76
|
})
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
或用服务端动态编译——一行代码,无需构建步骤、无需 watch 模式:
|
|
80
|
+
```ts
|
|
81
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
82
|
+
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
### Environment Variables
|
|
79
86
|
|
|
80
87
|
| Variable | Default | Used by |
|
|
@@ -126,7 +133,7 @@ esbuild.build({
|
|
|
126
133
|
| `redis()` | Redis client (`ctx.redis`) |
|
|
127
134
|
| `queue()` | Job queue + cron |
|
|
128
135
|
| `createHub()` | WebSocket pub/sub |
|
|
129
|
-
| `ui()` | SPA
|
|
136
|
+
| `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
|
|
130
137
|
|
|
131
138
|
### Frontend (weifuwu/client)
|
|
132
139
|
|
|
@@ -146,6 +153,10 @@ esbuild.build({
|
|
|
146
153
|
| `LoginForm` | Login/register form component |
|
|
147
154
|
| `Chat` | Real-time messaging component |
|
|
148
155
|
| `domMount()` | Direct DOM mounting |
|
|
156
|
+
| `wrap()` | Third-party library integration |
|
|
157
|
+
| `useForm()` | Form state management (validation, submit, reset) |
|
|
158
|
+
| `createPortal()` | Render outside parent DOM hierarchy |
|
|
159
|
+
| `<ErrorBoundary>` | Catch render errors, show fallback |
|
|
149
160
|
|
|
150
161
|
### Utilities
|
|
151
162
|
|
|
@@ -224,6 +235,29 @@ ctx.route.path // "/chat/123"
|
|
|
224
235
|
ctx.route.params // { id: "123" }
|
|
225
236
|
ctx.route.query // { tab: "settings" }
|
|
226
237
|
|
|
238
|
+
#### Route Loader — 数据预取
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
const routes: RouteDef[] = [
|
|
242
|
+
{
|
|
243
|
+
path: '/post/:id',
|
|
244
|
+
component: PostPage,
|
|
245
|
+
loader: async (ctx) => ({
|
|
246
|
+
post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
|
|
247
|
+
}),
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
// In component:
|
|
252
|
+
function PostPage(_, ctx) {
|
|
253
|
+
const post = ctx.route.data.post
|
|
254
|
+
if (!post) return <p class="text-gray-400">加载中...</p>
|
|
255
|
+
return <h1 class="text-2xl font-bold">{post.title}</h1>
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
组件先渲染(显示 loading),loader 完成后自动重渲染。
|
|
260
|
+
|
|
227
261
|
### Middleware: api / auth / ws
|
|
228
262
|
|
|
229
263
|
```tsx
|
|
@@ -255,19 +289,147 @@ function ChatPage(_, ctx) {
|
|
|
255
289
|
}
|
|
256
290
|
```
|
|
257
291
|
|
|
258
|
-
###
|
|
292
|
+
### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
|
|
259
293
|
|
|
260
294
|
```ts
|
|
261
|
-
import { ui
|
|
295
|
+
import { ui } from 'weifuwu'
|
|
296
|
+
|
|
297
|
+
app.use(ui())
|
|
298
|
+
|
|
299
|
+
// SSR page — ctx.ui.html`` returns complete HTML Response
|
|
300
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
301
|
+
<!DOCTYPE html>
|
|
302
|
+
<html>
|
|
303
|
+
<head>
|
|
304
|
+
<title>${post.title}</title>
|
|
305
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
306
|
+
</head>
|
|
307
|
+
<body>
|
|
308
|
+
<div id="root">
|
|
309
|
+
<h1>${post.title}</h1>
|
|
310
|
+
<div>${ctx.ui.html.unsafe(post.body)}</div>
|
|
311
|
+
<div data-hydrate="like"></div>
|
|
312
|
+
</div>
|
|
313
|
+
<script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
|
|
314
|
+
<script src="/static/app.js"></script>
|
|
315
|
+
</body>
|
|
316
|
+
</html>
|
|
317
|
+
`)
|
|
318
|
+
|
|
319
|
+
// Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
|
|
320
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
321
|
+
|
|
322
|
+
// Dynamic CSS serving — ctx.ui.css() reads and serves CSS
|
|
323
|
+
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
324
|
+
|
|
325
|
+
// Client hydrates interactive sections, skips SSR content
|
|
326
|
+
const app = createApp()
|
|
327
|
+
app.use(api())
|
|
328
|
+
|
|
329
|
+
const root = document.getElementById('root')
|
|
330
|
+
if (root && root.children.length > 0) {
|
|
331
|
+
// SSR page — hydrate only interactive areas
|
|
332
|
+
app.hydrate('[data-hydrate="like"]', LikeButton)
|
|
333
|
+
} else {
|
|
334
|
+
// SPA page — full mount
|
|
335
|
+
app.mount('#root', AppShell)
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### wrap() — Third-party Library Integration
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { wrap, effect } from 'weifuwu/client'
|
|
343
|
+
import * as echarts from 'echarts'
|
|
344
|
+
|
|
345
|
+
// wrap(tagName, setup) creates a component:
|
|
346
|
+
// - Creates a <div> container
|
|
347
|
+
// - Calls setup(el, props, ctx) when element enters the document
|
|
348
|
+
// - Runs cleanup when element is removed
|
|
349
|
+
const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
|
|
350
|
+
const chart = echarts.init(el)
|
|
351
|
+
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
352
|
+
effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
|
|
353
|
+
return () => chart.dispose()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Use in JSX like any component
|
|
357
|
+
<Dashboard>
|
|
358
|
+
<PieChart data={salesData} />
|
|
359
|
+
</Dashboard>
|
|
360
|
+
```
|
|
262
361
|
|
|
263
|
-
|
|
264
|
-
app.get('/static/*', serveStatic('./dist/client'))
|
|
362
|
+
### useForm() — 表单状态管理
|
|
265
363
|
|
|
266
|
-
|
|
267
|
-
|
|
364
|
+
```tsx
|
|
365
|
+
import { useForm } from 'weifuwu/client'
|
|
366
|
+
|
|
367
|
+
const form = useForm({
|
|
368
|
+
initial: { email: '', password: '' },
|
|
369
|
+
validate: {
|
|
370
|
+
email: (v) => !v.includes('@') && '请输入有效邮箱',
|
|
371
|
+
password: (v) => v.length < 6 && '至少 6 位',
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// 绑定到 input:{...form.field('name')} 自动设置 value + onInput
|
|
376
|
+
<input {...form.field('email')} placeholder="邮箱" />
|
|
377
|
+
{form.errors.email && <span class="text-red-500">{form.errors.email}</span>}
|
|
378
|
+
|
|
379
|
+
// 提交时自动验证所有字段
|
|
380
|
+
<button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
|
|
381
|
+
登录
|
|
382
|
+
</button>
|
|
383
|
+
|
|
384
|
+
// 重置
|
|
385
|
+
<button onClick={form.reset}>重置</button>
|
|
268
386
|
|
|
269
|
-
//
|
|
270
|
-
|
|
387
|
+
// 编程设置
|
|
388
|
+
form.setValue('email', 'a@b.com')
|
|
389
|
+
form.setValues({ email: 'a@b.com', password: '123' })
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### createPortal() — 渲染到父容器外
|
|
393
|
+
|
|
394
|
+
适用于 Modal、Dropdown、Tooltip 等需突破 `overflow: hidden` 或 z-index 层级的情况。
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
import { createPortal, Show } from 'weifuwu/client'
|
|
398
|
+
|
|
399
|
+
function Modal({ show, title, children }) {
|
|
400
|
+
return <Show when={show}>
|
|
401
|
+
{createPortal(
|
|
402
|
+
<div class="fixed inset-0 bg-black/50 flex items-center justify-center">
|
|
403
|
+
<div class="bg-white rounded-xl p-6 min-w-[400px]">
|
|
404
|
+
<h2 class="text-lg font-bold mb-4">{title}</h2>
|
|
405
|
+
{children}
|
|
406
|
+
</div>
|
|
407
|
+
</div>,
|
|
408
|
+
document.body
|
|
409
|
+
)}
|
|
410
|
+
</Show>
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### ErrorBoundary — 错误边界
|
|
415
|
+
|
|
416
|
+
子组件渲染异常时捕获,显示 fallback 而非白屏。children 必须是 thunk(延迟执行)。
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
import { ErrorBoundary } from 'weifuwu/client'
|
|
420
|
+
|
|
421
|
+
function AppShell(_, ctx) {
|
|
422
|
+
return (
|
|
423
|
+
<div>
|
|
424
|
+
<nav>...</nav>
|
|
425
|
+
<main>
|
|
426
|
+
<ErrorBoundary fallback={(e) => <p>出错了: {e.message}</p>}>
|
|
427
|
+
{() => <RouteView />}
|
|
428
|
+
</ErrorBoundary>
|
|
429
|
+
</main>
|
|
430
|
+
</div>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
271
433
|
```
|
|
272
434
|
|
|
273
435
|
### Show / For
|
|
@@ -796,14 +958,16 @@ src/
|
|
|
796
958
|
├── queue/ ← Job queue + cron
|
|
797
959
|
├── graphql.ts ← GraphQL
|
|
798
960
|
├── hub.ts ← WebSocket hub
|
|
799
|
-
├── ui/ ←
|
|
800
|
-
├── client/ ← Frontend framework
|
|
801
|
-
│ ├── index.ts ← Entry
|
|
961
|
+
├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
962
|
+
├── client/ ← Frontend framework
|
|
963
|
+
│ ├── index.ts ← Entry (exports signal, useForm, wrap, createApp, ...)
|
|
802
964
|
│ ├── signal.ts ← Signal / effect / computed
|
|
803
|
-
│ ├── jsx-runtime.ts ← JSX → DOM / Show / For
|
|
804
|
-
│ ├── app.ts ← createApp / middleware chain
|
|
805
|
-
│ ├── router.ts ← Route matching / RouteView
|
|
965
|
+
│ ├── jsx-runtime.ts ← JSX → DOM / Show / For / wrap / ErrorBoundary / createPortal
|
|
966
|
+
│ ├── app.ts ← createApp / hydrate / middleware chain
|
|
967
|
+
│ ├── router.ts ← Route matching / RouteView / loader
|
|
806
968
|
│ ├── types.ts ← WfuiContext / RouteDef
|
|
969
|
+
│ ├── lib/
|
|
970
|
+
│ │ └── form.ts ← useForm
|
|
807
971
|
│ ├── middleware/
|
|
808
972
|
│ │ ├── api.ts ← HTTP client
|
|
809
973
|
│ │ ├── auth.ts ← Login / logout / token
|
|
@@ -811,14 +975,15 @@ src/
|
|
|
811
975
|
│ └── components/
|
|
812
976
|
│ ├── LoginForm.ts ← Login / register form
|
|
813
977
|
│ └── Chat.ts ← Real-time messaging
|
|
814
|
-
└── test/ ←
|
|
978
|
+
└── test/ ← Tests
|
|
815
979
|
|
|
816
980
|
apps/demo/ ← Full-stack demo
|
|
817
|
-
├── src/main.tsx ← SPA demo pages
|
|
981
|
+
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
818
982
|
├── server.ts ← weifuwu server
|
|
819
|
-
├── public/
|
|
820
|
-
├──
|
|
821
|
-
└──
|
|
983
|
+
├── public/
|
|
984
|
+
│ ├── index.html ← HTML skeleton with placeholders
|
|
985
|
+
│ └── style.css ← Demo styles
|
|
986
|
+
└── tsconfig.json
|
|
822
987
|
|
|
823
988
|
docker-compose.yml ← postgres (pgvector) + redis
|
|
824
989
|
```
|
package/dist/client/app.d.ts
CHANGED
|
@@ -21,4 +21,16 @@ export declare function createApp(): {
|
|
|
21
21
|
ctx: WfuiContext;
|
|
22
22
|
use: (mw: AppMiddleware) => any;
|
|
23
23
|
mount: (rootSelector: string, RootComponent: Component) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Hydrate 一个已由 SSR 渲染的区域。
|
|
26
|
+
* 不清除目标容器内的内容,只附加组件输出。
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const app = createApp()
|
|
30
|
+
* app.use(api())
|
|
31
|
+
* app.use(auth())
|
|
32
|
+
* app.hydrate('#comments', CommentSection, { postId: '123' })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
hydrate: (selector: string, Component: Component, props?: Record<string, unknown>) => void;
|
|
24
36
|
};
|
package/dist/client/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export { signal, computed, effect, isSignal } from './signal.ts';
|
|
17
17
|
export type { Signal } from './signal.ts';
|
|
18
|
-
export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount } from './jsx-runtime.ts';
|
|
18
|
+
export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount, wrap, createPortal, ErrorBoundary } from './jsx-runtime.ts';
|
|
19
19
|
export type { Component } from './jsx-runtime.ts';
|
|
20
20
|
export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
|
|
21
21
|
export { createApp } from './app.ts';
|
|
@@ -24,5 +24,6 @@ export { api, ApiClient, ApiError } from './middleware/api.ts';
|
|
|
24
24
|
export { auth } from './middleware/auth.ts';
|
|
25
25
|
export type { UserRecord } from './middleware/auth.ts';
|
|
26
26
|
export { ws } from './middleware/ws.ts';
|
|
27
|
+
export { useForm } from './lib/form.ts';
|
|
27
28
|
export { LoginForm } from './components/LoginForm.ts';
|
|
28
29
|
export { Chat } from './components/Chat.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -51,6 +51,60 @@ var currentCtx = null;
|
|
|
51
51
|
function setCtx(ctx) {
|
|
52
52
|
currentCtx = ctx;
|
|
53
53
|
}
|
|
54
|
+
var _entries = /* @__PURE__ */ new Map();
|
|
55
|
+
function _ensure(el) {
|
|
56
|
+
let entry = _entries.get(el);
|
|
57
|
+
if (entry) return entry;
|
|
58
|
+
entry = {
|
|
59
|
+
mounted: document.contains(el),
|
|
60
|
+
observer: null,
|
|
61
|
+
mountFns: [],
|
|
62
|
+
disposeFns: []
|
|
63
|
+
};
|
|
64
|
+
_entries.set(el, entry);
|
|
65
|
+
const obs = new MutationObserver(() => {
|
|
66
|
+
const now = document.contains(el);
|
|
67
|
+
if (now && !entry.mounted) {
|
|
68
|
+
entry.mounted = true;
|
|
69
|
+
const fns = entry.mountFns.slice();
|
|
70
|
+
entry.mountFns = [];
|
|
71
|
+
for (const fn of fns) {
|
|
72
|
+
const dispose = fn();
|
|
73
|
+
if (typeof dispose === "function") {
|
|
74
|
+
entry.disposeFns.push(dispose);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else if (!now && entry.mounted) {
|
|
78
|
+
entry.mounted = false;
|
|
79
|
+
for (const fn of entry.disposeFns) fn();
|
|
80
|
+
entry.disposeFns = [];
|
|
81
|
+
entry.mountFns = [];
|
|
82
|
+
obs.disconnect();
|
|
83
|
+
_entries.delete(el);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
87
|
+
entry.observer = obs;
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
function onMount(el, fn) {
|
|
91
|
+
const entry = _ensure(el);
|
|
92
|
+
if (entry.mounted) {
|
|
93
|
+
const dispose = fn();
|
|
94
|
+
if (typeof dispose === "function") {
|
|
95
|
+
entry.disposeFns.push(dispose);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
entry.mountFns.push(fn);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function wrap(tagName, setup) {
|
|
102
|
+
return (props, ctx) => {
|
|
103
|
+
const el = document.createElement(tagName);
|
|
104
|
+
onMount(el, () => setup(el, props, ctx));
|
|
105
|
+
return el;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
54
108
|
function setProp(el, key, value) {
|
|
55
109
|
if (key === "class" || key === "className") {
|
|
56
110
|
if (isSignal(value)) {
|
|
@@ -130,6 +184,17 @@ function domMount(root, app) {
|
|
|
130
184
|
container.innerHTML = "";
|
|
131
185
|
container.appendChild(app);
|
|
132
186
|
}
|
|
187
|
+
function ErrorBoundary({ fallback, children }, _ctx) {
|
|
188
|
+
try {
|
|
189
|
+
return children();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return fallback(e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function createPortal(node, target) {
|
|
195
|
+
target.appendChild(node);
|
|
196
|
+
return document.createDocumentFragment();
|
|
197
|
+
}
|
|
133
198
|
function toNode(v) {
|
|
134
199
|
if (v instanceof Node) return v;
|
|
135
200
|
if (typeof v === "function") return toNode(v());
|
|
@@ -178,7 +243,8 @@ function createApp() {
|
|
|
178
243
|
params: {},
|
|
179
244
|
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
180
245
|
hash: window.location.hash,
|
|
181
|
-
component: null
|
|
246
|
+
component: null,
|
|
247
|
+
data: {}
|
|
182
248
|
},
|
|
183
249
|
app: {
|
|
184
250
|
navigate(path) {
|
|
@@ -223,6 +289,18 @@ function createApp() {
|
|
|
223
289
|
const app = jsx(RootComponent, {});
|
|
224
290
|
domMount(rootSelector, app);
|
|
225
291
|
setCtx(null);
|
|
292
|
+
},
|
|
293
|
+
hydrate(selector, Component, props) {
|
|
294
|
+
const root = document.querySelector(selector);
|
|
295
|
+
if (!root) {
|
|
296
|
+
console.warn(`hydrate target not found: ${selector}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
|
|
300
|
+
setCtx(ctx);
|
|
301
|
+
const vnode = jsx(Component, mergedProps);
|
|
302
|
+
root.appendChild(vnode);
|
|
303
|
+
setCtx(null);
|
|
226
304
|
}
|
|
227
305
|
};
|
|
228
306
|
}
|
|
@@ -242,56 +320,79 @@ function router(opts) {
|
|
|
242
320
|
}).join("/") + "$";
|
|
243
321
|
return { re: new RegExp(reStr), keys, route };
|
|
244
322
|
});
|
|
245
|
-
function
|
|
246
|
-
for (const
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
323
|
+
function matchRoute(path) {
|
|
324
|
+
for (const m of matchers) {
|
|
325
|
+
const result = path.match(m.re);
|
|
326
|
+
if (!result) continue;
|
|
327
|
+
const params = {};
|
|
328
|
+
m.keys.forEach((k, i) => {
|
|
329
|
+
params[k] = decodeURIComponent(result[i + 1]);
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
matched: { component: m.route.component, params, title: m.route.title, auth: m.route.auth },
|
|
333
|
+
routeDef: m.route
|
|
334
|
+
};
|
|
255
335
|
}
|
|
256
336
|
return null;
|
|
257
337
|
}
|
|
258
338
|
return (ctx) => {
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
339
|
+
function emit(path) {
|
|
340
|
+
window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
|
|
341
|
+
}
|
|
342
|
+
function resolve(path) {
|
|
343
|
+
const raw = mode === "hash" ? path || "/" : path;
|
|
344
|
+
const [cleanPath, qs] = raw.split("?");
|
|
262
345
|
ctx.route.query = Object.fromEntries(new URLSearchParams(qs ?? ""));
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
ctx.route.params = matched.params;
|
|
267
|
-
ctx.route.component = matched.component;
|
|
268
|
-
ctx.route.title = matched.title;
|
|
269
|
-
ctx.route.auth = matched.auth;
|
|
270
|
-
if (matched.title) document.title = matched.title;
|
|
271
|
-
if (matched.auth && !ctx.user) {
|
|
272
|
-
setTimeout(() => ctx.app.navigate("/login"), 0);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
346
|
+
ctx.route.path = cleanPath;
|
|
347
|
+
const r = matchRoute(cleanPath);
|
|
348
|
+
if (!r) {
|
|
276
349
|
ctx.route.component = opts.notFound ?? null;
|
|
350
|
+
ctx.route.data = {};
|
|
351
|
+
return { component: null };
|
|
352
|
+
}
|
|
353
|
+
ctx.route.params = r.matched.params;
|
|
354
|
+
ctx.route.component = r.matched.component;
|
|
355
|
+
ctx.route.title = r.matched.title;
|
|
356
|
+
ctx.route.auth = r.matched.auth;
|
|
357
|
+
if (r.matched.title) document.title = r.matched.title;
|
|
358
|
+
return { component: r.matched.component, routeDef: r.routeDef };
|
|
359
|
+
}
|
|
360
|
+
function navigateAndLoad(path) {
|
|
361
|
+
const { routeDef } = resolve(path);
|
|
362
|
+
if (ctx.route.auth && !ctx.user) {
|
|
363
|
+
setTimeout(() => ctx.app.navigate("/login"), 0);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
emit(ctx.route.path);
|
|
367
|
+
if (routeDef?.loader) {
|
|
368
|
+
routeDef.loader(ctx).then((data) => {
|
|
369
|
+
ctx.route.data = data;
|
|
370
|
+
emit(ctx.route.path);
|
|
371
|
+
}).catch(() => {
|
|
372
|
+
ctx.route.data = {};
|
|
373
|
+
emit(ctx.route.path);
|
|
374
|
+
});
|
|
277
375
|
}
|
|
278
|
-
window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
|
|
279
376
|
}
|
|
280
|
-
const origNavigate = ctx.app.navigate;
|
|
281
377
|
ctx.app.navigate = (path) => {
|
|
282
378
|
if (mode === "hash") {
|
|
283
379
|
window.location.hash = "#" + path;
|
|
284
380
|
} else {
|
|
285
381
|
window.history.pushState({}, "", path);
|
|
286
|
-
|
|
382
|
+
navigateAndLoad(path);
|
|
287
383
|
}
|
|
288
384
|
};
|
|
289
385
|
if (mode === "hash") {
|
|
290
|
-
window.addEventListener("hashchange",
|
|
386
|
+
window.addEventListener("hashchange", () => {
|
|
387
|
+
navigateAndLoad(window.location.hash.slice(1) || "/");
|
|
388
|
+
});
|
|
291
389
|
} else {
|
|
292
|
-
window.addEventListener("popstate",
|
|
390
|
+
window.addEventListener("popstate", () => {
|
|
391
|
+
navigateAndLoad(window.location.pathname + window.location.search);
|
|
392
|
+
});
|
|
293
393
|
}
|
|
294
|
-
|
|
394
|
+
const initialPath = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
|
|
395
|
+
navigateAndLoad(initialPath);
|
|
295
396
|
return ctx;
|
|
296
397
|
};
|
|
297
398
|
}
|
|
@@ -517,6 +618,112 @@ function ws(opts = {}) {
|
|
|
517
618
|
};
|
|
518
619
|
}
|
|
519
620
|
|
|
621
|
+
// src/client/lib/form.ts
|
|
622
|
+
function useForm(opts) {
|
|
623
|
+
const fields = /* @__PURE__ */ new Map();
|
|
624
|
+
const errors = {};
|
|
625
|
+
const touched = {};
|
|
626
|
+
for (const key of Object.keys(opts.initial)) {
|
|
627
|
+
fields.set(key, signal(opts.initial[key]));
|
|
628
|
+
errors[key] = null;
|
|
629
|
+
touched[key] = false;
|
|
630
|
+
}
|
|
631
|
+
const valid = signal(true);
|
|
632
|
+
const values = signal({ ...opts.initial });
|
|
633
|
+
function snapshot() {
|
|
634
|
+
const data = {};
|
|
635
|
+
for (const [key, sig] of fields) {
|
|
636
|
+
data[key] = sig.value;
|
|
637
|
+
}
|
|
638
|
+
values.value = data;
|
|
639
|
+
}
|
|
640
|
+
function validateField(name) {
|
|
641
|
+
if (!opts.validate) return null;
|
|
642
|
+
const rule = opts.validate[name];
|
|
643
|
+
if (!rule) return null;
|
|
644
|
+
const sig = fields.get(name);
|
|
645
|
+
const result = rule(sig.value, values.value);
|
|
646
|
+
const error = result && typeof result === "string" ? result : null;
|
|
647
|
+
errors[name] = error;
|
|
648
|
+
return error;
|
|
649
|
+
}
|
|
650
|
+
function validateAll() {
|
|
651
|
+
if (!opts.validate) return true;
|
|
652
|
+
let allValid = true;
|
|
653
|
+
for (const key of Object.keys(opts.initial)) {
|
|
654
|
+
const err = validateField(key);
|
|
655
|
+
if (err) allValid = false;
|
|
656
|
+
}
|
|
657
|
+
valid.value = allValid;
|
|
658
|
+
return allValid;
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
field(name) {
|
|
662
|
+
let sig = fields.get(name);
|
|
663
|
+
if (!sig) {
|
|
664
|
+
sig = signal(opts.initial[name]);
|
|
665
|
+
fields.set(name, sig);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
value: sig,
|
|
669
|
+
onInput(e) {
|
|
670
|
+
const target = e.target;
|
|
671
|
+
touched[name] = true;
|
|
672
|
+
if (target.type === "checkbox") {
|
|
673
|
+
sig.value = target.checked;
|
|
674
|
+
} else {
|
|
675
|
+
sig.value = target.value;
|
|
676
|
+
}
|
|
677
|
+
snapshot();
|
|
678
|
+
validateField(name);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
get errors() {
|
|
683
|
+
return errors;
|
|
684
|
+
},
|
|
685
|
+
get touched() {
|
|
686
|
+
return touched;
|
|
687
|
+
},
|
|
688
|
+
valid,
|
|
689
|
+
get values() {
|
|
690
|
+
return values;
|
|
691
|
+
},
|
|
692
|
+
async submit(handler) {
|
|
693
|
+
if (!validateAll()) return;
|
|
694
|
+
const data = {};
|
|
695
|
+
for (const [key, sig] of fields) {
|
|
696
|
+
data[key] = sig.value;
|
|
697
|
+
}
|
|
698
|
+
await handler(data);
|
|
699
|
+
},
|
|
700
|
+
reset() {
|
|
701
|
+
for (const key of Object.keys(opts.initial)) {
|
|
702
|
+
const sig = fields.get(key);
|
|
703
|
+
if (sig) sig.value = opts.initial[key];
|
|
704
|
+
errors[key] = null;
|
|
705
|
+
touched[key] = false;
|
|
706
|
+
}
|
|
707
|
+
valid.value = true;
|
|
708
|
+
snapshot();
|
|
709
|
+
},
|
|
710
|
+
setValue(name, value) {
|
|
711
|
+
const sig = fields.get(name);
|
|
712
|
+
if (sig) sig.value = value;
|
|
713
|
+
snapshot();
|
|
714
|
+
validateField(name);
|
|
715
|
+
},
|
|
716
|
+
setValues(partial) {
|
|
717
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
718
|
+
const sig = fields.get(key);
|
|
719
|
+
if (sig) sig.value = value;
|
|
720
|
+
}
|
|
721
|
+
snapshot();
|
|
722
|
+
validateAll();
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
520
727
|
// src/client/components/LoginForm.ts
|
|
521
728
|
var h = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
|
|
522
729
|
function LoginForm(_props, ctx) {
|
|
@@ -673,6 +880,7 @@ export {
|
|
|
673
880
|
ApiClient,
|
|
674
881
|
ApiError,
|
|
675
882
|
Chat,
|
|
883
|
+
ErrorBoundary,
|
|
676
884
|
For,
|
|
677
885
|
Fragment,
|
|
678
886
|
LoginForm,
|
|
@@ -682,6 +890,7 @@ export {
|
|
|
682
890
|
auth,
|
|
683
891
|
computed,
|
|
684
892
|
createApp,
|
|
893
|
+
createPortal,
|
|
685
894
|
domMount,
|
|
686
895
|
effect,
|
|
687
896
|
isSignal,
|
|
@@ -690,5 +899,7 @@ export {
|
|
|
690
899
|
jsxs,
|
|
691
900
|
router,
|
|
692
901
|
signal,
|
|
902
|
+
useForm,
|
|
903
|
+
wrap,
|
|
693
904
|
ws
|
|
694
905
|
};
|