weifuwu 0.33.1 → 0.33.2
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 +160 -3
- package/dist/client/app.d.ts +21 -0
- package/dist/client/index.d.ts +21 -0
- package/dist/client/index.js +320 -0
- package/dist/client/jsx-runtime.d.ts +60 -0
- package/dist/client/jsx-runtime.js +320 -0
- package/dist/client/router.d.ts +51 -0
- package/dist/client/signal.d.ts +19 -0
- package/dist/client/types.d.ts +45 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.js +11 -475
- package/package.json +11 -24
- package/dist/middleware/esbuild-dev.d.ts +0 -55
- package/dist/middleware/esbuild-dev.js +0 -266
- package/dist/middleware/tailwind-dev.d.ts +0 -43
- package/dist/middleware/tailwind-dev.js +0 -199
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
**AI SaaS framework** — `(req, ctx) => Response`
|
|
3
|
+
**AI SaaS full-stack framework** — `(req, ctx) => Response` + `(props, ctx) => JSX`
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
User system, instant messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage. Configure environment variables and go.
|
|
9
|
+
One package. Backend + frontend. User system, instant messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage, and a reactive frontend framework. Configure environment variables and go.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
|
+
### Backend
|
|
16
|
+
|
|
15
17
|
```ts
|
|
16
18
|
import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
|
|
17
19
|
import { openai } from '@ai-sdk/openai'
|
|
@@ -34,6 +36,46 @@ app.post('/api/chat', async (req, ctx) => {
|
|
|
34
36
|
serve(app, { port: 3000 })
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
### Frontend
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { signal, Show, For, createApp, router, RouteView } from 'weifuwu/client'
|
|
43
|
+
import type { WfuiContext } from 'weifuwu/client'
|
|
44
|
+
|
|
45
|
+
function HomePage(_props: {}, ctx: WfuiContext) {
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<h1>Hello weifuwu</h1>
|
|
49
|
+
<p>当前路径: {ctx.route.path}</p>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const app = createApp()
|
|
55
|
+
app.use(router({
|
|
56
|
+
routes: [
|
|
57
|
+
{ path: '/', component: HomePage, title: '首页' },
|
|
58
|
+
{ path: '/chat/:id', component: ChatPage, title: '聊天' },
|
|
59
|
+
],
|
|
60
|
+
}))
|
|
61
|
+
app.mount('#root', AppShell)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
// tsconfig.json
|
|
66
|
+
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// build.mjs
|
|
71
|
+
esbuild.build({
|
|
72
|
+
entryPoints: ['src/main.tsx'],
|
|
73
|
+
jsx: 'automatic',
|
|
74
|
+
jsxImportSource: 'weifuwu/client',
|
|
75
|
+
bundle: true,
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
37
79
|
### Environment Variables
|
|
38
80
|
|
|
39
81
|
| Variable | Default | Used by |
|
|
@@ -49,6 +91,8 @@ serve(app, { port: 3000 })
|
|
|
49
91
|
|
|
50
92
|
## Modules
|
|
51
93
|
|
|
94
|
+
### Backend
|
|
95
|
+
|
|
52
96
|
| Module | Import | Dependency | Purpose |
|
|
53
97
|
|--------|--------|-----------|---------|
|
|
54
98
|
| User | `user()` | `postgres()` | Auth, JWT, roles |
|
|
@@ -84,6 +128,20 @@ serve(app, { port: 3000 })
|
|
|
84
128
|
| `queue()` | Job queue + cron |
|
|
85
129
|
| `createHub()` | WebSocket pub/sub |
|
|
86
130
|
|
|
131
|
+
### Frontend (weifuwu/client)
|
|
132
|
+
|
|
133
|
+
| Import | Purpose |
|
|
134
|
+
|--------|---------|
|
|
135
|
+
| `signal()` | Reactive state |
|
|
136
|
+
| `computed()` | Derived signals |
|
|
137
|
+
| `effect()` | Auto-tracked side effects |
|
|
138
|
+
| `<Show>` | Conditional rendering |
|
|
139
|
+
| `<For>` | List rendering |
|
|
140
|
+
| `<RouteView>` | Route outlet |
|
|
141
|
+
| `createApp()` | App instance with middleware chain |
|
|
142
|
+
| `router()` | Hash/history router with params + query |
|
|
143
|
+
| `domMount()` | Direct DOM mounting |
|
|
144
|
+
|
|
87
145
|
### Utilities
|
|
88
146
|
|
|
89
147
|
| Import | Purpose |
|
|
@@ -92,6 +150,92 @@ serve(app, { port: 3000 })
|
|
|
92
150
|
|
|
93
151
|
---
|
|
94
152
|
|
|
153
|
+
## Frontend (weifuwu/client)
|
|
154
|
+
|
|
155
|
+
**weifuwu/client** is a reactive frontend framework built on Signal + TSX. Zero virtual DOM, zero dependencies, ~600 lines total.
|
|
156
|
+
|
|
157
|
+
### Concepts
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// 1. Signal — reactive data
|
|
161
|
+
const count = signal(0)
|
|
162
|
+
count.value = count.value + 1 // DOM updates automatically
|
|
163
|
+
|
|
164
|
+
// 2. Computed — derived signals
|
|
165
|
+
const doubled = computed(() => count.value * 2)
|
|
166
|
+
|
|
167
|
+
// 3. Effect — auto-tracked side effects
|
|
168
|
+
effect(() => console.log('count:', count.value))
|
|
169
|
+
|
|
170
|
+
// 4. Component — (props, ctx) => JSX
|
|
171
|
+
function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
|
|
172
|
+
return <div>Hello {name}</div>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### createApp + Middleware
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { createApp, router, RouteView } from 'weifuwu/client'
|
|
180
|
+
import type { WfuiContext, RouteDef } from 'weifuwu/client'
|
|
181
|
+
|
|
182
|
+
const app = createApp()
|
|
183
|
+
app.use(router({ routes, mode: 'hash' }))
|
|
184
|
+
app.mount('#root', AppShell)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Router
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
const routes: RouteDef[] = [
|
|
191
|
+
{ path: '/', component: HomePage, title: '首页' },
|
|
192
|
+
{ path: '/chat/:id', component: ChatPage, title: '聊天' },
|
|
193
|
+
{ path: '/user/:name', component: UserPage, title: '用户' },
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
app.use(router({
|
|
197
|
+
routes,
|
|
198
|
+
notFound: NotFound,
|
|
199
|
+
mode: 'hash', // or 'history'
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
// In layout:
|
|
203
|
+
function AppShell(_, ctx) {
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<nav>
|
|
207
|
+
<a onClick={() => ctx.app.navigate('/')}>首页</a>
|
|
208
|
+
<a onClick={() => ctx.app.navigate('/chat/123')}>聊天</a>
|
|
209
|
+
</nav>
|
|
210
|
+
<main>
|
|
211
|
+
<RouteView /> {/* ← renders matched route */}
|
|
212
|
+
</main>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Route params and query:
|
|
218
|
+
ctx.route.path // "/chat/123"
|
|
219
|
+
ctx.route.params // { id: "123" }
|
|
220
|
+
ctx.route.query // { tab: "settings" }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Show / For
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// Conditional rendering (supports Signal)
|
|
227
|
+
<Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
228
|
+
<Dashboard />
|
|
229
|
+
</Show>
|
|
230
|
+
|
|
231
|
+
// List rendering (supports Signal)
|
|
232
|
+
<For each={filteredItems}>
|
|
233
|
+
{(item) => <div>{item.name}</div>}
|
|
234
|
+
</For>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
95
239
|
## user
|
|
96
240
|
|
|
97
241
|
Auth, registration, JWT, password management, roles.
|
|
@@ -344,7 +488,7 @@ app.post('/api/chat/sync', async (req, ctx) => {
|
|
|
344
488
|
### ctx.agent API
|
|
345
489
|
|
|
346
490
|
| Method | Description |
|
|
347
|
-
|
|
491
|
+
|-------------|-------------|
|
|
348
492
|
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
349
493
|
| `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
|
|
350
494
|
|
|
@@ -602,8 +746,21 @@ src/
|
|
|
602
746
|
├── queue/ ← Job queue + cron
|
|
603
747
|
├── graphql.ts ← GraphQL
|
|
604
748
|
├── hub.ts ← WebSocket hub
|
|
749
|
+
├── client/ ← Frontend framework (signal, JSX, router)
|
|
750
|
+
│ ├── index.ts
|
|
751
|
+
│ ├── signal.ts
|
|
752
|
+
│ ├── jsx-runtime.ts
|
|
753
|
+
│ ├── app.ts
|
|
754
|
+
│ ├── router.ts
|
|
755
|
+
│ └── types.ts
|
|
605
756
|
└── test/ ← 281 tests
|
|
606
757
|
|
|
758
|
+
apps/demo/ ← Full-stack demo
|
|
759
|
+
├── src/main.tsx
|
|
760
|
+
├── public/index.html
|
|
761
|
+
├── tsconfig.json
|
|
762
|
+
└── scripts/build.mjs
|
|
763
|
+
|
|
607
764
|
docker-compose.yml ← postgres (pgvector) + redis
|
|
608
765
|
```
|
|
609
766
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/client 应用 — 创建 ctx + 中间件链 + 挂载组件
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* import { createApp, router } from 'weifuwu/client'
|
|
6
|
+
*
|
|
7
|
+
* const app = createApp()
|
|
8
|
+
* app.use(router({ routes: [...] }))
|
|
9
|
+
* app.mount('#root', AppShell)
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import type { WfuiContext, AppMiddleware } from './types.ts';
|
|
13
|
+
import type { Component } from './jsx-runtime.ts';
|
|
14
|
+
/**
|
|
15
|
+
* 创建 weifuwu/client 应用
|
|
16
|
+
*/
|
|
17
|
+
export declare function createApp(): {
|
|
18
|
+
ctx: WfuiContext;
|
|
19
|
+
use: (mw: AppMiddleware) => any;
|
|
20
|
+
mount: (rootSelector: string, RootComponent: Component) => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/client — 响应式前端框架,TSX + Signal
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* import { signal, computed, Show, For, createApp, router, RouteView } from 'weifuwu/client'
|
|
6
|
+
* import type { WfuiContext } from 'weifuwu/client'
|
|
7
|
+
*
|
|
8
|
+
* const app = createApp()
|
|
9
|
+
* app.use(router({ routes: [
|
|
10
|
+
* { path: '/', component: HomePage },
|
|
11
|
+
* ]}))
|
|
12
|
+
* app.mount('#root', AppShell)
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { signal, computed, effect, isSignal } from './signal.ts';
|
|
16
|
+
export type { Signal } from './signal.ts';
|
|
17
|
+
export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount } from './jsx-runtime.ts';
|
|
18
|
+
export type { Component } from './jsx-runtime.ts';
|
|
19
|
+
export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
|
|
20
|
+
export { createApp } from './app.ts';
|
|
21
|
+
export { router, RouteView } from './router.ts';
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// src/client/signal.ts
|
|
2
|
+
var currentEffect = null;
|
|
3
|
+
var Signal = class {
|
|
4
|
+
#value;
|
|
5
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
constructor(value) {
|
|
7
|
+
this.#value = value;
|
|
8
|
+
}
|
|
9
|
+
get value() {
|
|
10
|
+
if (currentEffect) this.#listeners.add(currentEffect);
|
|
11
|
+
return this.#value;
|
|
12
|
+
}
|
|
13
|
+
set value(v) {
|
|
14
|
+
if (v !== this.#value) {
|
|
15
|
+
this.#value = v;
|
|
16
|
+
const fns = [...this.#listeners];
|
|
17
|
+
for (const fn of fns) fn();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function signal(initial) {
|
|
22
|
+
return new Signal(initial);
|
|
23
|
+
}
|
|
24
|
+
function isSignal(value) {
|
|
25
|
+
return value instanceof Signal;
|
|
26
|
+
}
|
|
27
|
+
function effect(fn) {
|
|
28
|
+
const wrapper = () => {
|
|
29
|
+
const prev = currentEffect;
|
|
30
|
+
currentEffect = wrapper;
|
|
31
|
+
try {
|
|
32
|
+
fn();
|
|
33
|
+
} finally {
|
|
34
|
+
currentEffect = prev;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
wrapper();
|
|
38
|
+
return () => {
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function computed(fn) {
|
|
42
|
+
const s = new Signal(void 0);
|
|
43
|
+
effect(() => {
|
|
44
|
+
s.value = fn();
|
|
45
|
+
});
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/client/jsx-runtime.ts
|
|
50
|
+
var currentCtx = null;
|
|
51
|
+
function setCtx(ctx) {
|
|
52
|
+
currentCtx = ctx;
|
|
53
|
+
}
|
|
54
|
+
function setProp(el, key, value) {
|
|
55
|
+
if (key === "class" || key === "className") {
|
|
56
|
+
if (isSignal(value)) {
|
|
57
|
+
effect(() => {
|
|
58
|
+
el.className = String(value.value);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
el.className = String(value ?? "");
|
|
62
|
+
}
|
|
63
|
+
} else if (key === "style" && typeof value === "object" && value !== null) {
|
|
64
|
+
Object.assign(el.style, value);
|
|
65
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
66
|
+
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
67
|
+
} else if (key === "ref" && typeof value === "function") {
|
|
68
|
+
value(el);
|
|
69
|
+
} else if (isSignal(value)) {
|
|
70
|
+
effect(() => {
|
|
71
|
+
const v = value.value;
|
|
72
|
+
if (v == null || v === false) el.removeAttribute(key);
|
|
73
|
+
else if (v === true) el.setAttribute(key, "");
|
|
74
|
+
else el.setAttribute(key, String(v));
|
|
75
|
+
});
|
|
76
|
+
} else if (value != null && value !== false) {
|
|
77
|
+
if (value === true) el.setAttribute(key, "");
|
|
78
|
+
else el.setAttribute(key, String(value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function appendChild(parent, child) {
|
|
82
|
+
if (child == null || child === false || child === true) return;
|
|
83
|
+
if (Array.isArray(child)) {
|
|
84
|
+
child.forEach((c) => appendChild(parent, c));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (child instanceof Node) {
|
|
88
|
+
parent.appendChild(child);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (isSignal(child)) {
|
|
92
|
+
const text = document.createTextNode("");
|
|
93
|
+
effect(() => {
|
|
94
|
+
text.textContent = String(child.value);
|
|
95
|
+
});
|
|
96
|
+
parent.appendChild(text);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
100
|
+
}
|
|
101
|
+
function jsx(type, props, ...children) {
|
|
102
|
+
if (typeof type === "function") {
|
|
103
|
+
const merged = children.length > 0 ? { ...props, children } : props;
|
|
104
|
+
return type(merged, currentCtx) ?? document.createDocumentFragment();
|
|
105
|
+
}
|
|
106
|
+
const el = document.createElement(type);
|
|
107
|
+
if (props) {
|
|
108
|
+
for (const [k, v] of Object.entries(props)) {
|
|
109
|
+
if (k !== "children") setProp(el, k, v);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const childList = children.length > 0 ? children : props?.children != null ? [props.children] : [];
|
|
113
|
+
for (const child of childList) appendChild(el, child);
|
|
114
|
+
return el;
|
|
115
|
+
}
|
|
116
|
+
function jsxs(type, props, ...children) {
|
|
117
|
+
return jsx(type, props, ...children);
|
|
118
|
+
}
|
|
119
|
+
function jsxDEV(type, props, _key, _isStatic, _source, _self) {
|
|
120
|
+
return jsx(type, props, ...props?.children ? [props.children] : []);
|
|
121
|
+
}
|
|
122
|
+
function Fragment(props, ...children) {
|
|
123
|
+
const frag = document.createDocumentFragment();
|
|
124
|
+
for (const child of children) appendChild(frag, child);
|
|
125
|
+
return frag;
|
|
126
|
+
}
|
|
127
|
+
function domMount(root, app) {
|
|
128
|
+
const container = typeof root === "string" ? document.querySelector(root) : root;
|
|
129
|
+
if (!container) throw new Error(`mount target not found: ${root}`);
|
|
130
|
+
container.innerHTML = "";
|
|
131
|
+
container.appendChild(app);
|
|
132
|
+
}
|
|
133
|
+
function toNode(v) {
|
|
134
|
+
if (v instanceof Node) return v;
|
|
135
|
+
if (typeof v === "function") return toNode(v());
|
|
136
|
+
return document.createTextNode(String(v ?? ""));
|
|
137
|
+
}
|
|
138
|
+
function Show({ when, children, fallback }) {
|
|
139
|
+
const el = document.createElement("div");
|
|
140
|
+
function render(show) {
|
|
141
|
+
el.textContent = "";
|
|
142
|
+
if (show && children != null) {
|
|
143
|
+
el.appendChild(toNode(children));
|
|
144
|
+
} else if (!show && fallback != null) {
|
|
145
|
+
el.appendChild(toNode(fallback));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (isSignal(when)) {
|
|
149
|
+
effect(() => render(Boolean(when.value)));
|
|
150
|
+
} else {
|
|
151
|
+
render(Boolean(when));
|
|
152
|
+
}
|
|
153
|
+
return el;
|
|
154
|
+
}
|
|
155
|
+
function For({ each, children }) {
|
|
156
|
+
const el = document.createElement("div");
|
|
157
|
+
function render(list) {
|
|
158
|
+
el.textContent = "";
|
|
159
|
+
for (let i = 0; i < list.length; i++) {
|
|
160
|
+
el.appendChild(children(list[i], i));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isSignal(each)) {
|
|
164
|
+
effect(() => render(each.value));
|
|
165
|
+
} else {
|
|
166
|
+
render(each);
|
|
167
|
+
}
|
|
168
|
+
return el;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/client/app.ts
|
|
172
|
+
function createApp() {
|
|
173
|
+
const middlewares = [];
|
|
174
|
+
const provides = /* @__PURE__ */ new Map();
|
|
175
|
+
let ctx = {
|
|
176
|
+
route: {
|
|
177
|
+
path: window.location.pathname,
|
|
178
|
+
params: {},
|
|
179
|
+
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
180
|
+
hash: window.location.hash,
|
|
181
|
+
component: null
|
|
182
|
+
},
|
|
183
|
+
app: {
|
|
184
|
+
navigate(path) {
|
|
185
|
+
window.history.pushState({}, "", path);
|
|
186
|
+
ctx.route.path = path;
|
|
187
|
+
ctx.route.query = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
188
|
+
ctx.route.hash = window.location.hash;
|
|
189
|
+
window.dispatchEvent(new CustomEvent("wefu:navigate", { detail: { path } }));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
provide(key, value) {
|
|
193
|
+
provides.set(key, value);
|
|
194
|
+
},
|
|
195
|
+
inject(key) {
|
|
196
|
+
return provides.get(key) ?? null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
get ctx() {
|
|
201
|
+
return ctx;
|
|
202
|
+
},
|
|
203
|
+
use(mw) {
|
|
204
|
+
middlewares.push(mw);
|
|
205
|
+
return this;
|
|
206
|
+
},
|
|
207
|
+
mount(rootSelector, RootComponent) {
|
|
208
|
+
for (const mw of middlewares) {
|
|
209
|
+
ctx = mw(ctx);
|
|
210
|
+
}
|
|
211
|
+
setCtx(ctx);
|
|
212
|
+
const app = jsx(RootComponent, {});
|
|
213
|
+
domMount(rootSelector, app);
|
|
214
|
+
setCtx(null);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/client/router.ts
|
|
220
|
+
function router(opts) {
|
|
221
|
+
const mode = opts.mode ?? "hash";
|
|
222
|
+
const matchers = opts.routes.map((route) => {
|
|
223
|
+
const parts = route.path.split("/").filter(Boolean);
|
|
224
|
+
const keys = [];
|
|
225
|
+
const reStr = "^/" + parts.map((p) => {
|
|
226
|
+
if (p.startsWith(":")) {
|
|
227
|
+
keys.push(p.slice(1));
|
|
228
|
+
return "([^/]+)";
|
|
229
|
+
}
|
|
230
|
+
return p;
|
|
231
|
+
}).join("/") + "$";
|
|
232
|
+
return { re: new RegExp(reStr), keys, route };
|
|
233
|
+
});
|
|
234
|
+
function matchPath(path) {
|
|
235
|
+
for (const { re, keys, route } of matchers) {
|
|
236
|
+
const m = path.match(re);
|
|
237
|
+
if (m) {
|
|
238
|
+
const params = {};
|
|
239
|
+
keys.forEach((k, i) => {
|
|
240
|
+
params[k] = decodeURIComponent(m[i + 1]);
|
|
241
|
+
});
|
|
242
|
+
return { component: route.component, params, title: route.title, auth: route.auth };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return (ctx) => {
|
|
248
|
+
function handleRoute() {
|
|
249
|
+
const raw = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
|
|
250
|
+
const [path, qs] = raw.split("?");
|
|
251
|
+
ctx.route.query = Object.fromEntries(new URLSearchParams(qs ?? ""));
|
|
252
|
+
const matched = matchPath(path);
|
|
253
|
+
if (matched) {
|
|
254
|
+
ctx.route.path = path;
|
|
255
|
+
ctx.route.params = matched.params;
|
|
256
|
+
ctx.route.component = matched.component;
|
|
257
|
+
ctx.route.title = matched.title;
|
|
258
|
+
ctx.route.auth = matched.auth;
|
|
259
|
+
if (matched.title) document.title = matched.title;
|
|
260
|
+
if (matched.auth && !ctx.user) {
|
|
261
|
+
setTimeout(() => ctx.app.navigate("/login"), 0);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
ctx.route.component = opts.notFound ?? null;
|
|
266
|
+
}
|
|
267
|
+
window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
|
|
268
|
+
}
|
|
269
|
+
const origNavigate = ctx.app.navigate;
|
|
270
|
+
ctx.app.navigate = (path) => {
|
|
271
|
+
if (mode === "hash") {
|
|
272
|
+
window.location.hash = "#" + path;
|
|
273
|
+
} else {
|
|
274
|
+
window.history.pushState({}, "", path);
|
|
275
|
+
handleRoute();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
if (mode === "hash") {
|
|
279
|
+
window.addEventListener("hashchange", handleRoute);
|
|
280
|
+
} else {
|
|
281
|
+
window.addEventListener("popstate", handleRoute);
|
|
282
|
+
}
|
|
283
|
+
handleRoute();
|
|
284
|
+
return ctx;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function RouteView(_props, ctx) {
|
|
288
|
+
const el = document.createElement("div");
|
|
289
|
+
function render() {
|
|
290
|
+
const Component = ctx.route.component;
|
|
291
|
+
if (!Component) {
|
|
292
|
+
el.textContent = "";
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
el.textContent = "";
|
|
296
|
+
setCtx(ctx);
|
|
297
|
+
const page = jsx(Component, {});
|
|
298
|
+
el.appendChild(page);
|
|
299
|
+
setCtx(null);
|
|
300
|
+
}
|
|
301
|
+
render();
|
|
302
|
+
window.addEventListener("wefu:route", render);
|
|
303
|
+
return el;
|
|
304
|
+
}
|
|
305
|
+
export {
|
|
306
|
+
For,
|
|
307
|
+
Fragment,
|
|
308
|
+
RouteView,
|
|
309
|
+
Show,
|
|
310
|
+
computed,
|
|
311
|
+
createApp,
|
|
312
|
+
domMount,
|
|
313
|
+
effect,
|
|
314
|
+
isSignal,
|
|
315
|
+
jsx,
|
|
316
|
+
jsxDEV,
|
|
317
|
+
jsxs,
|
|
318
|
+
router,
|
|
319
|
+
signal
|
|
320
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/client JSX runtime — TSX 编译目标
|
|
3
|
+
*
|
|
4
|
+
* esbuild: --jsx=automatic --jsxImportSource=weifuwu/client
|
|
5
|
+
* tsconfig: "jsx": "react-jsx", "jsxImportSource": "weifuwu/client"
|
|
6
|
+
*
|
|
7
|
+
* createElement 直接创建真实 DOM 节点。
|
|
8
|
+
* Signal 值自动绑定:.value 变化时只更新对应 DOM 节点。
|
|
9
|
+
* 组件签名:(props, ctx) => JSX
|
|
10
|
+
*/
|
|
11
|
+
import { type Signal } from './signal.ts';
|
|
12
|
+
import type { WfuiContext } from './types.ts';
|
|
13
|
+
declare global {
|
|
14
|
+
namespace JSX {
|
|
15
|
+
interface IntrinsicElements {
|
|
16
|
+
[tag: string]: any;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export declare function setCtx(ctx: WfuiContext | null): void;
|
|
21
|
+
export declare function getCtx(): WfuiContext | null;
|
|
22
|
+
export type Component<P = {}> = (props: P, ctx: WfuiContext) => Node;
|
|
23
|
+
export declare function jsx(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
|
|
24
|
+
export declare function jsxs(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
|
|
25
|
+
export declare function jsxDEV(type: string | Component, props: Record<string, unknown> | null, _key: string | null, _isStatic: boolean, _source: {
|
|
26
|
+
fileName: string;
|
|
27
|
+
lineNumber: number;
|
|
28
|
+
}, _self: unknown): Node;
|
|
29
|
+
export declare function Fragment(props: Record<string, unknown> | null, ...children: unknown[]): Node;
|
|
30
|
+
/**
|
|
31
|
+
* 直接挂载 DOM(低层 API,一般用 createApp().mount())
|
|
32
|
+
*/
|
|
33
|
+
export declare function domMount(root: string | Element, app: Node): void;
|
|
34
|
+
/**
|
|
35
|
+
* 条件渲染 — when 为 Signal 时响应式切换
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
39
|
+
* <ChatPage />
|
|
40
|
+
* </Show>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function Show({ when, children, fallback }: {
|
|
44
|
+
when: boolean | Signal<boolean>;
|
|
45
|
+
children?: Node | (() => Node);
|
|
46
|
+
fallback?: Node | (() => Node);
|
|
47
|
+
}): Node;
|
|
48
|
+
/**
|
|
49
|
+
* 列表渲染 — each 为 Signal 时响应式更新
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <For each={items}>
|
|
53
|
+
* {(item) => <div>{item.name}</div>}
|
|
54
|
+
* </For>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function For<T>({ each, children }: {
|
|
58
|
+
each: T[] | Signal<T[]>;
|
|
59
|
+
children: (item: T, index: number) => Node;
|
|
60
|
+
}): Node;
|