weifuwu 0.33.3 → 0.33.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 +143 -23
- package/dist/client/app.d.ts +12 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +67 -0
- package/dist/client/jsx-runtime.d.ts +27 -0
- package/dist/client/jsx-runtime.js +67 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +87 -48
- package/dist/ui/index.d.ts +31 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,25 +39,24 @@ serve(app, { port: 3000 })
|
|
|
39
39
|
### Frontend
|
|
40
40
|
|
|
41
41
|
```tsx
|
|
42
|
-
import { signal,
|
|
42
|
+
import { signal, createApp, api, auth, ws, router, RouteView, LoginForm } from 'weifuwu/client'
|
|
43
43
|
import type { WfuiContext } from 'weifuwu/client'
|
|
44
44
|
|
|
45
|
-
function
|
|
45
|
+
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
46
|
+
if (!ctx.isAuthenticated) return <LoginForm />
|
|
46
47
|
return (
|
|
47
48
|
<div>
|
|
48
|
-
<
|
|
49
|
-
<
|
|
49
|
+
<nav><a onClick={() => ctx.app.navigate('/chat')}>聊天</a></nav>
|
|
50
|
+
<main><RouteView /></main>
|
|
50
51
|
</div>
|
|
51
52
|
)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const app = createApp()
|
|
55
|
-
app.use(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
],
|
|
60
|
-
}))
|
|
56
|
+
app.use(api()) // ← ctx.api.get/post
|
|
57
|
+
app.use(auth()) // ← ctx.user / ctx.login / ctx.logout
|
|
58
|
+
app.use(ws()) // ← ctx.ws.send / onMessage
|
|
59
|
+
app.use(router({ routes })) // ← ctx.route / 路由
|
|
61
60
|
app.mount('#root', AppShell)
|
|
62
61
|
```
|
|
63
62
|
|
|
@@ -67,7 +66,7 @@ app.mount('#root', AppShell)
|
|
|
67
66
|
```
|
|
68
67
|
|
|
69
68
|
```js
|
|
70
|
-
// build.mjs
|
|
69
|
+
// build.mjs — 可选,也可用 ctx.ui.js() 服务端动态编译
|
|
71
70
|
esbuild.build({
|
|
72
71
|
entryPoints: ['src/main.tsx'],
|
|
73
72
|
jsx: 'automatic',
|
|
@@ -76,6 +75,11 @@ esbuild.build({
|
|
|
76
75
|
})
|
|
77
76
|
```
|
|
78
77
|
|
|
78
|
+
或用服务端动态编译(无需独立构建脚本):
|
|
79
|
+
```ts
|
|
80
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
81
|
+
```
|
|
82
|
+
|
|
79
83
|
### Environment Variables
|
|
80
84
|
|
|
81
85
|
| Variable | Default | Used by |
|
|
@@ -127,6 +131,7 @@ esbuild.build({
|
|
|
127
131
|
| `redis()` | Redis client (`ctx.redis`) |
|
|
128
132
|
| `queue()` | Job queue + cron |
|
|
129
133
|
| `createHub()` | WebSocket pub/sub |
|
|
134
|
+
| `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
|
|
130
135
|
|
|
131
136
|
### Frontend (weifuwu/client)
|
|
132
137
|
|
|
@@ -140,6 +145,11 @@ esbuild.build({
|
|
|
140
145
|
| `<RouteView>` | Route outlet |
|
|
141
146
|
| `createApp()` | App instance with middleware chain |
|
|
142
147
|
| `router()` | Hash/history router with params + query |
|
|
148
|
+
| `api()` | HTTP client (`ctx.api.get/post`) |
|
|
149
|
+
| `auth()` | Auth state (`ctx.user/login/logout`) |
|
|
150
|
+
| `ws()` | WebSocket (`ctx.ws.send/onMessage`) |
|
|
151
|
+
| `LoginForm` | Login/register form component |
|
|
152
|
+
| `Chat` | Real-time messaging component |
|
|
143
153
|
| `domMount()` | Direct DOM mounting |
|
|
144
154
|
|
|
145
155
|
### Utilities
|
|
@@ -218,6 +228,106 @@ function AppShell(_, ctx) {
|
|
|
218
228
|
ctx.route.path // "/chat/123"
|
|
219
229
|
ctx.route.params // { id: "123" }
|
|
220
230
|
ctx.route.query // { tab: "settings" }
|
|
231
|
+
|
|
232
|
+
### Middleware: api / auth / ws
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { api, auth, ws } from 'weifuwu/client'
|
|
236
|
+
|
|
237
|
+
app.use(api()) // ctx.api.get/post/put/patch/delete
|
|
238
|
+
app.use(auth()) // ctx.user / ctx.login / ctx.logout / ctx.register
|
|
239
|
+
app.use(ws()) // ctx.ws.send / onMessage / join / leave
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`api()` creates a fetch client with automatic token injection.
|
|
243
|
+
`auth()` persists sessions to localStorage, validates tokens on startup.
|
|
244
|
+
`ws()` manages WebSocket connections with auto-reconnect.
|
|
245
|
+
|
|
246
|
+
### Pre-built Components
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import { LoginForm, Chat } from 'weifuwu/client'
|
|
250
|
+
|
|
251
|
+
// Login / Register form
|
|
252
|
+
function LoginPage(_, ctx) {
|
|
253
|
+
if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
254
|
+
return <LoginForm />
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Real-time chat
|
|
258
|
+
function ChatPage(_, ctx) {
|
|
259
|
+
return <Chat conversationId="123" />
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { ui } from 'weifuwu'
|
|
267
|
+
|
|
268
|
+
app.use(ui())
|
|
269
|
+
|
|
270
|
+
// SSR page — ctx.ui.html`` returns complete HTML Response
|
|
271
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
272
|
+
<!DOCTYPE html>
|
|
273
|
+
<html>
|
|
274
|
+
<head>
|
|
275
|
+
<title>${post.title}</title>
|
|
276
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
277
|
+
</head>
|
|
278
|
+
<body>
|
|
279
|
+
<div id="root">
|
|
280
|
+
<h1>${post.title}</h1>
|
|
281
|
+
<div>${ctx.ui.html.unsafe(post.body)}</div>
|
|
282
|
+
<div data-hydrate="like"></div>
|
|
283
|
+
</div>
|
|
284
|
+
<script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
|
|
285
|
+
<script src="/static/app.js"></script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>
|
|
288
|
+
`)
|
|
289
|
+
|
|
290
|
+
// Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
|
|
291
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
292
|
+
|
|
293
|
+
// Dynamic CSS serving — ctx.ui.css() reads and serves CSS
|
|
294
|
+
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
295
|
+
|
|
296
|
+
// Client hydrates interactive sections, skips SSR content
|
|
297
|
+
const app = createApp()
|
|
298
|
+
app.use(api())
|
|
299
|
+
|
|
300
|
+
const root = document.getElementById('root')
|
|
301
|
+
if (root && root.children.length > 0) {
|
|
302
|
+
// SSR page — hydrate only interactive areas
|
|
303
|
+
app.hydrate('[data-hydrate="like"]', LikeButton)
|
|
304
|
+
} else {
|
|
305
|
+
// SPA page — full mount
|
|
306
|
+
app.mount('#root', AppShell)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### wrap() — Third-party Library Integration
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
import { wrap, effect } from 'weifuwu/client'
|
|
314
|
+
import * as echarts from 'echarts'
|
|
315
|
+
|
|
316
|
+
// wrap(tagName, setup) creates a component:
|
|
317
|
+
// - Creates a <div> container
|
|
318
|
+
// - Calls setup(el, props, ctx) when element enters the document
|
|
319
|
+
// - Runs cleanup when element is removed
|
|
320
|
+
const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
|
|
321
|
+
const chart = echarts.init(el)
|
|
322
|
+
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
323
|
+
effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
|
|
324
|
+
return () => chart.dispose()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Use in JSX like any component
|
|
328
|
+
<Dashboard>
|
|
329
|
+
<PieChart data={salesData} />
|
|
330
|
+
</Dashboard>
|
|
221
331
|
```
|
|
222
332
|
|
|
223
333
|
### Show / For
|
|
@@ -746,20 +856,30 @@ src/
|
|
|
746
856
|
├── queue/ ← Job queue + cron
|
|
747
857
|
├── graphql.ts ← GraphQL
|
|
748
858
|
├── hub.ts ← WebSocket hub
|
|
749
|
-
├──
|
|
750
|
-
|
|
751
|
-
│ ├──
|
|
752
|
-
│ ├──
|
|
753
|
-
│ ├──
|
|
754
|
-
│ ├──
|
|
755
|
-
│
|
|
756
|
-
|
|
859
|
+
├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
860
|
+
├── client/ ← Frontend framework
|
|
861
|
+
│ ├── index.ts ← Entry (exports signal, wrap, createApp, ...)
|
|
862
|
+
│ ├── signal.ts ← Signal / effect / computed
|
|
863
|
+
│ ├── jsx-runtime.ts ← JSX → DOM / Show / For / wrap / onMount
|
|
864
|
+
│ ├── app.ts ← createApp / hydrate / middleware chain
|
|
865
|
+
│ ├── router.ts ← Route matching / RouteView
|
|
866
|
+
│ ├── types.ts ← WfuiContext / RouteDef
|
|
867
|
+
│ ├── middleware/
|
|
868
|
+
│ │ ├── api.ts ← HTTP client
|
|
869
|
+
│ │ ├── auth.ts ← Login / logout / token
|
|
870
|
+
│ │ └── ws.ts ← WebSocket
|
|
871
|
+
│ └── components/
|
|
872
|
+
│ ├── LoginForm.ts ← Login / register form
|
|
873
|
+
│ └── Chat.ts ← Real-time messaging
|
|
874
|
+
└── test/ ← Tests
|
|
757
875
|
|
|
758
876
|
apps/demo/ ← Full-stack demo
|
|
759
|
-
├── src/main.tsx
|
|
760
|
-
├──
|
|
761
|
-
├──
|
|
762
|
-
|
|
877
|
+
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
878
|
+
├── server.ts ← weifuwu server
|
|
879
|
+
├── public/
|
|
880
|
+
│ ├── index.html ← HTML skeleton with placeholders
|
|
881
|
+
│ └── style.css ← Demo styles
|
|
882
|
+
└── tsconfig.json
|
|
763
883
|
|
|
764
884
|
docker-compose.yml ← postgres (pgvector) + redis
|
|
765
885
|
```
|
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 } 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';
|
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)) {
|
|
@@ -223,6 +277,18 @@ function createApp() {
|
|
|
223
277
|
const app = jsx(RootComponent, {});
|
|
224
278
|
domMount(rootSelector, app);
|
|
225
279
|
setCtx(null);
|
|
280
|
+
},
|
|
281
|
+
hydrate(selector, Component, props) {
|
|
282
|
+
const root = document.querySelector(selector);
|
|
283
|
+
if (!root) {
|
|
284
|
+
console.warn(`hydrate target not found: ${selector}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
|
|
288
|
+
setCtx(ctx);
|
|
289
|
+
const vnode = jsx(Component, mergedProps);
|
|
290
|
+
root.appendChild(vnode);
|
|
291
|
+
setCtx(null);
|
|
226
292
|
}
|
|
227
293
|
};
|
|
228
294
|
}
|
|
@@ -690,5 +756,6 @@ export {
|
|
|
690
756
|
jsxs,
|
|
691
757
|
router,
|
|
692
758
|
signal,
|
|
759
|
+
wrap,
|
|
693
760
|
ws
|
|
694
761
|
};
|
|
@@ -20,6 +20,33 @@ declare global {
|
|
|
20
20
|
export declare function setCtx(ctx: WfuiContext | null): void;
|
|
21
21
|
export declare function getCtx(): WfuiContext | null;
|
|
22
22
|
export type Component<P = {}> = (props: P, ctx: WfuiContext) => Node;
|
|
23
|
+
/**
|
|
24
|
+
* 包装第三方库为组件 — 元素挂载后执行 setup,返回函数在元素移除时自动清理。
|
|
25
|
+
*
|
|
26
|
+
* 自动创建容器元素 + 自动管理生命周期。
|
|
27
|
+
* 返回标准的 (props, ctx) => Node 组件。
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const PieChart = wrap('div', (el, props: { data: any }, ctx) => {
|
|
31
|
+
* el.style.cssText = 'width:100%;height:300px'
|
|
32
|
+
* const chart = echarts.init(el)
|
|
33
|
+
* chart.setOption(props.data)
|
|
34
|
+
* effect(() => chart.setOption(props.data))
|
|
35
|
+
* return () => chart.dispose()
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* const CanvasChart = wrap('canvas', (el, props, ctx) => {
|
|
39
|
+
* const chart = new Chart(el, { type: 'line', data: props.data })
|
|
40
|
+
* return () => chart.destroy()
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // 在 JSX 中使用
|
|
44
|
+
* function Dashboard() {
|
|
45
|
+
* return <div><PieChart data={salesData} /><CanvasChart data={trendData} /></div>
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function wrap<P = {}>(tagName: string, setup: (el: HTMLElement, props: P, ctx: WfuiContext) => (() => void) | void): Component<P>;
|
|
23
50
|
export declare function jsx(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
|
|
24
51
|
export declare function jsxs(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
|
|
25
52
|
export declare function jsxDEV(type: string | Component, props: Record<string, unknown> | null, _key: string | null, _isStatic: boolean, _source: {
|
|
@@ -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)) {
|
|
@@ -223,6 +277,18 @@ function createApp() {
|
|
|
223
277
|
const app = jsx(RootComponent, {});
|
|
224
278
|
domMount(rootSelector, app);
|
|
225
279
|
setCtx(null);
|
|
280
|
+
},
|
|
281
|
+
hydrate(selector, Component, props) {
|
|
282
|
+
const root = document.querySelector(selector);
|
|
283
|
+
if (!root) {
|
|
284
|
+
console.warn(`hydrate target not found: ${selector}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
|
|
288
|
+
setCtx(ctx);
|
|
289
|
+
const vnode = jsx(Component, mergedProps);
|
|
290
|
+
root.appendChild(vnode);
|
|
291
|
+
setCtx(null);
|
|
226
292
|
}
|
|
227
293
|
};
|
|
228
294
|
}
|
|
@@ -690,5 +756,6 @@ export {
|
|
|
690
756
|
jsxs,
|
|
691
757
|
router,
|
|
692
758
|
signal,
|
|
759
|
+
wrap,
|
|
693
760
|
ws
|
|
694
761
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { compress } from './middleware/compress.ts';
|
|
|
23
23
|
export type { CompressOptions } from './middleware/compress.ts';
|
|
24
24
|
export { helmet } from './middleware/helmet.ts';
|
|
25
25
|
export type { HelmetOptions } from './middleware/helmet.ts';
|
|
26
|
+
export { ui } from './ui/index.ts';
|
|
26
27
|
export { graphql } from './graphql.ts';
|
|
27
28
|
export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
|
|
28
29
|
export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts';
|
|
@@ -47,6 +48,4 @@ export type { CMSAPI, CMSOptions, Content, ContentStatus, ContentType, Tag, TagW
|
|
|
47
48
|
export { kb, KB } from './kb/index.ts';
|
|
48
49
|
export type { KBAPI, KBOptions, Document, Chunk, SearchResult, ImportOptions, SearchOptions, } from './kb/types.ts';
|
|
49
50
|
export type { BaseAPI, BaseDef, BaseOptions, TableSchema, FieldSchema, FieldType, ColumnMap, CreateBaseInput, UpdateBaseInput, QueryOptions, } from './base/types.ts';
|
|
50
|
-
export { ui } from './ui/index.ts';
|
|
51
|
-
export type { UiOptions, UiRenderOptions } from './ui/index.ts';
|
|
52
51
|
export { requireRole } from './user/types.ts';
|
package/dist/index.js
CHANGED
|
@@ -225,14 +225,14 @@ function serve(router, options) {
|
|
|
225
225
|
if (!server.listening) return;
|
|
226
226
|
server.close();
|
|
227
227
|
server.closeIdleConnections();
|
|
228
|
-
return new Promise((
|
|
228
|
+
return new Promise((resolve4) => {
|
|
229
229
|
const timer = setTimeout(() => {
|
|
230
230
|
server.closeAllConnections();
|
|
231
|
-
|
|
231
|
+
resolve4();
|
|
232
232
|
}, timeoutMs);
|
|
233
233
|
server.on("close", () => {
|
|
234
234
|
clearTimeout(timer);
|
|
235
|
-
|
|
235
|
+
resolve4();
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
238
|
}
|
|
@@ -1384,6 +1384,90 @@ var DEFAULTS = {
|
|
|
1384
1384
|
permissionsPolicy: "camera=(),display-capture=(),fullscreen=(),geolocation=(),microphone=()"
|
|
1385
1385
|
};
|
|
1386
1386
|
|
|
1387
|
+
// src/ui/index.ts
|
|
1388
|
+
import { build } from "esbuild";
|
|
1389
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1390
|
+
import { resolve as resolve3 } from "node:path";
|
|
1391
|
+
var HtmlSafe = class {
|
|
1392
|
+
constructor(value) {
|
|
1393
|
+
this.value = value;
|
|
1394
|
+
}
|
|
1395
|
+
value;
|
|
1396
|
+
toString() {
|
|
1397
|
+
return this.value;
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
function escape(s) {
|
|
1401
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1402
|
+
}
|
|
1403
|
+
function stringify(v) {
|
|
1404
|
+
if (v == null || v === false || v === true) return "";
|
|
1405
|
+
if (Array.isArray(v)) return v.map(stringify).join("");
|
|
1406
|
+
if (v instanceof HtmlSafe) return v.value;
|
|
1407
|
+
return escape(String(v));
|
|
1408
|
+
}
|
|
1409
|
+
function unsafe(s) {
|
|
1410
|
+
return new HtmlSafe(s);
|
|
1411
|
+
}
|
|
1412
|
+
var jsCache = /* @__PURE__ */ new Map();
|
|
1413
|
+
var cssCache = /* @__PURE__ */ new Map();
|
|
1414
|
+
function ui() {
|
|
1415
|
+
return async (_req, ctx, next) => {
|
|
1416
|
+
function htmlTag(strings, ...values) {
|
|
1417
|
+
let body = "";
|
|
1418
|
+
for (let i = 0; i < strings.length; i++) {
|
|
1419
|
+
body += strings[i];
|
|
1420
|
+
if (i < values.length) body += stringify(values[i]);
|
|
1421
|
+
}
|
|
1422
|
+
return new Response(body, {
|
|
1423
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
ctx.ui = {
|
|
1427
|
+
html: Object.assign(htmlTag, { unsafe }),
|
|
1428
|
+
async js(entryPath) {
|
|
1429
|
+
const absPath = resolve3(entryPath);
|
|
1430
|
+
const cached = jsCache.get(absPath);
|
|
1431
|
+
if (cached) {
|
|
1432
|
+
return new Response(cached.code, {
|
|
1433
|
+
headers: { "Content-Type": "application/javascript" }
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
const result = await build({
|
|
1437
|
+
entryPoints: [absPath],
|
|
1438
|
+
bundle: true,
|
|
1439
|
+
format: "esm",
|
|
1440
|
+
platform: "browser",
|
|
1441
|
+
jsx: "automatic",
|
|
1442
|
+
jsxImportSource: "weifuwu/client",
|
|
1443
|
+
write: false
|
|
1444
|
+
});
|
|
1445
|
+
const code = result.outputFiles[0].text;
|
|
1446
|
+
jsCache.set(absPath, { code });
|
|
1447
|
+
return new Response(code, {
|
|
1448
|
+
headers: { "Content-Type": "application/javascript" }
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
async css(entryPath) {
|
|
1452
|
+
const absPath = resolve3(entryPath);
|
|
1453
|
+
const stat = await import("node:fs").then((fs) => fs.promises.stat(absPath));
|
|
1454
|
+
const cached = cssCache.get(absPath);
|
|
1455
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
1456
|
+
return new Response(cached.code, {
|
|
1457
|
+
headers: { "Content-Type": "text/css; charset=utf-8" }
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const code = await readFile2(absPath, "utf-8");
|
|
1461
|
+
cssCache.set(absPath, { code, mtime: stat.mtimeMs });
|
|
1462
|
+
return new Response(code, {
|
|
1463
|
+
headers: { "Content-Type": "text/css; charset=utf-8" }
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
return next(_req, ctx);
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1387
1471
|
// src/graphql.ts
|
|
1388
1472
|
import {
|
|
1389
1473
|
buildSchema,
|
|
@@ -4316,51 +4400,6 @@ function kb(opts) {
|
|
|
4316
4400
|
mw.__meta = { injects: ["kb"], depends: ["sql"] };
|
|
4317
4401
|
return mw;
|
|
4318
4402
|
}
|
|
4319
|
-
|
|
4320
|
-
// src/ui/index.ts
|
|
4321
|
-
function defaultTemplate(title, script, propsJson) {
|
|
4322
|
-
return `<!DOCTYPE html>
|
|
4323
|
-
<html lang="zh-CN">
|
|
4324
|
-
<head>
|
|
4325
|
-
<meta charset="UTF-8">
|
|
4326
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4327
|
-
<title>${escapeHtml(title)}</title>
|
|
4328
|
-
</head>
|
|
4329
|
-
<body>
|
|
4330
|
-
<div id="root"></div>
|
|
4331
|
-
${propsJson ? `<script>window.__WFUI_PROPS__=${propsJson}</script>` : ""}
|
|
4332
|
-
<script src="${escapeHtml(script)}"></script>
|
|
4333
|
-
</body>
|
|
4334
|
-
</html>`;
|
|
4335
|
-
}
|
|
4336
|
-
function escapeHtml(s) {
|
|
4337
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4338
|
-
}
|
|
4339
|
-
function ui(opts = {}) {
|
|
4340
|
-
const defaultTitle = opts.title ?? "weifuwu";
|
|
4341
|
-
const defaultScript = opts.script ?? "/static/app.js";
|
|
4342
|
-
const template = opts.template;
|
|
4343
|
-
return async (req, ctx, next) => {
|
|
4344
|
-
ctx.ui = {
|
|
4345
|
-
html(renderOpts = {}) {
|
|
4346
|
-
const title = renderOpts.title ?? defaultTitle;
|
|
4347
|
-
const script = renderOpts.script ?? defaultScript;
|
|
4348
|
-
let propsJson = "";
|
|
4349
|
-
if (renderOpts.props) {
|
|
4350
|
-
try {
|
|
4351
|
-
propsJson = JSON.stringify(renderOpts.props);
|
|
4352
|
-
} catch {
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
const body = template ? template.replace(/\{\{title\}\}/g, escapeHtml(title)).replace(/\{\{script\}\}/g, escapeHtml(script)).replace(/\{\{props\}\}/g, propsJson) : defaultTemplate(title, script, propsJson);
|
|
4356
|
-
return new Response(body, {
|
|
4357
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
4358
|
-
});
|
|
4359
|
-
}
|
|
4360
|
-
};
|
|
4361
|
-
return next(req, ctx);
|
|
4362
|
-
};
|
|
4363
|
-
}
|
|
4364
4403
|
export {
|
|
4365
4404
|
Base,
|
|
4366
4405
|
CMS,
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ui 中间件 — 注入 ctx.ui.html
|
|
2
|
+
* ui 中间件 — 注入 ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* ctx.ui.html 是 tagged template,返回完整 HTML Response。
|
|
5
|
+
* ctx.ui.js 编译 TSX 入口,返回 JS bundle Response。
|
|
6
|
+
* ctx.ui.css 读取/编译 CSS 入口,返回 CSS Response。
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
|
-
* import { ui
|
|
9
|
+
* import { ui } from 'weifuwu'
|
|
8
10
|
*
|
|
9
|
-
* app.use(ui(
|
|
10
|
-
*
|
|
11
|
-
* app.get('
|
|
11
|
+
* app.use(ui())
|
|
12
|
+
*
|
|
13
|
+
* app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
14
|
+
* <!DOCTYPE html>
|
|
15
|
+
* <html>
|
|
16
|
+
* <head><title>${post.title}</title></head>
|
|
17
|
+
* <body>
|
|
18
|
+
* <div id="root"><article>...</article></div>
|
|
19
|
+
* <script src="/static/app.js"></script>
|
|
20
|
+
* </body>
|
|
21
|
+
* </html>
|
|
22
|
+
* `)
|
|
23
|
+
*
|
|
24
|
+
* app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
25
|
+
* app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
|
|
12
26
|
* ```
|
|
13
27
|
*/
|
|
14
28
|
import type { Middleware } from '../types.ts';
|
|
15
29
|
declare module '../types.ts' {
|
|
16
30
|
interface Context {
|
|
17
31
|
ui: {
|
|
18
|
-
/**
|
|
19
|
-
html:
|
|
32
|
+
/** Tagged template → HTML Response */
|
|
33
|
+
html: UiHtmlTag;
|
|
34
|
+
/** 编译 TSX → JS bundle Response */
|
|
35
|
+
js: (entryPath: string) => Promise<Response>;
|
|
36
|
+
/** 读取/编译 CSS → CSS Response */
|
|
37
|
+
css: (entryPath: string) => Promise<Response>;
|
|
20
38
|
};
|
|
21
39
|
}
|
|
22
40
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/** Client bundle JS 路径,默认 '/static/app.js' */
|
|
27
|
-
script?: string;
|
|
28
|
-
/** 自定义 HTML 模板,覆盖默认 */
|
|
29
|
-
template?: string;
|
|
30
|
-
}
|
|
31
|
-
export interface UiRenderOptions {
|
|
32
|
-
title?: string;
|
|
33
|
-
script?: string;
|
|
34
|
-
/** 内嵌到页面的初始数据(通过 window.__WFUI_PROPS__ 访问) */
|
|
35
|
-
props?: Record<string, unknown>;
|
|
41
|
+
interface UiHtmlTag {
|
|
42
|
+
(strings: TemplateStringsArray, ...values: unknown[]): Response;
|
|
43
|
+
unsafe: (s: string) => string;
|
|
36
44
|
}
|
|
37
|
-
export declare function ui(
|
|
45
|
+
export declare function ui(): Middleware;
|
|
46
|
+
export {};
|