weifuwu 0.33.11 → 0.35.0

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.
@@ -1,809 +1,674 @@
1
- // src/client/signal.ts
2
- var currentEffect = null;
3
- var currentDeps = null;
4
- var _batchDepth = 0;
5
- var _pendingBatch = /* @__PURE__ */ new Set();
6
- var Signal = class {
7
- #value;
8
- #listeners = /* @__PURE__ */ new Set();
9
- constructor(value) {
10
- this.#value = value;
11
- }
12
- get value() {
13
- if (currentEffect) {
14
- this.#listeners.add(currentEffect);
15
- currentDeps?.add(this);
16
- }
17
- return this.#value;
18
- }
19
- set value(v) {
20
- if (v !== this.#value) {
21
- this.#value = v;
22
- this.#notify();
23
- }
24
- }
25
- /** @internal 移除监听器(由 effect dispose 调用) */
26
- _removeListener(fn) {
27
- this.#listeners.delete(fn);
28
- }
29
- /**
30
- * 可变更新 — 原地修改信号值并触发通知。
31
- *
32
- * 适用于数组/对象等引用类型:无需创建新引用即可触发更新。
33
- *
34
- * ```ts
35
- * const items = signal([1, 2, 3])
36
- * items.mutate(arr => arr.push(4)) // 数组原地修改
37
- * // items.value === [1, 2, 3, 4]
38
- * ```
39
- */
40
- mutate(fn) {
41
- fn(this.#value);
42
- this.#notify();
43
- }
44
- /** 通知所有监听器 — 批量模式时积攒,否则立即执行 */
45
- #notify() {
46
- if (_batchDepth > 0) {
47
- for (const fn of this.#listeners) _pendingBatch.add(fn);
48
- } else {
49
- const fns = [...this.#listeners];
50
- for (const fn of fns) fn();
51
- }
52
- }
53
- };
54
- function signal(initial) {
55
- return new Signal(initial);
1
+ // src/client/vnode.ts
2
+ var Fragment = /* @__PURE__ */ Symbol("Fragment");
3
+ function jsx(type, props, key) {
4
+ return {
5
+ type,
6
+ props: normalizeProps(props),
7
+ key: key ?? void 0
8
+ };
56
9
  }
57
- function isSignal(value) {
58
- return value instanceof Signal;
10
+ var jsxs = jsx;
11
+ function jsxDEV(type, props, key) {
12
+ return jsx(type, props, key);
59
13
  }
60
- function effect(fn) {
61
- const deps = /* @__PURE__ */ new Set();
62
- const run = () => {
63
- for (const dep of deps) dep._removeListener(run);
64
- deps.clear();
65
- const prevEffect = currentEffect;
66
- const prevDeps = currentDeps;
67
- currentEffect = run;
68
- currentDeps = deps;
69
- try {
70
- fn();
71
- } finally {
72
- currentEffect = prevEffect;
73
- currentDeps = prevDeps;
74
- }
75
- };
76
- run();
77
- return () => {
78
- for (const dep of deps) dep._removeListener(run);
79
- deps.clear();
80
- };
14
+ var h = jsx;
15
+ function normalizeProps(props) {
16
+ if (!props) return {};
17
+ const result = {};
18
+ for (const key of Object.keys(props)) {
19
+ if (key === "key") continue;
20
+ result[key] = props[key];
21
+ }
22
+ return result;
81
23
  }
82
- function computed(fn) {
83
- const s = signal(void 0);
84
- effect(() => {
85
- s.value = fn();
86
- });
87
- return s;
24
+
25
+ // src/client/render.ts
26
+ function render(input, ctx) {
27
+ return renderValue(input, ctx);
28
+ }
29
+ function renderValue(v, ctx) {
30
+ if (v == null || typeof v === "boolean") return document.createTextNode("");
31
+ if (typeof v === "string" || typeof v === "number") return document.createTextNode(String(v));
32
+ if (Array.isArray(v)) return renderArray(v, ctx);
33
+ const vnode = v;
34
+ if (vnode.type === Fragment) {
35
+ const frag = document.createDocumentFragment();
36
+ forEach(vnode.props?.children, (child) => frag.appendChild(renderValue(child, ctx)));
37
+ return frag;
38
+ }
39
+ if (typeof vnode.type === "function") {
40
+ return renderComponent(vnode.type, vnode.props, vnode, ctx);
41
+ }
42
+ const el = document.createElement(vnode.type);
43
+ vnode.el = el;
44
+ for (const [key, value] of Object.entries(vnode.props ?? {})) {
45
+ if (key === "children" || key === "key" || key === "ref") continue;
46
+ setProp(el, key, value);
47
+ }
48
+ const flatChildren = flattenChildren(vnode.props?.children);
49
+ for (const child of flatChildren) {
50
+ el.appendChild(renderValue(child, ctx));
51
+ }
52
+ if (vnode.props?.ref) {
53
+ queueMicrotask(() => vnode.props.ref(el));
54
+ }
55
+ return el;
88
56
  }
89
- function batch(fn) {
90
- _batchDepth++;
57
+ function renderComponent(Comp, props, vnode, ctx) {
58
+ const prev$ = vnode._$;
59
+ if (!prev$) vnode._$ = {};
60
+ ctx.ui = ctx.ui ?? {};
61
+ ctx.ui.ready = !!prev$;
62
+ let childVNode;
91
63
  try {
92
- fn();
93
- } finally {
94
- _batchDepth--;
95
- if (_batchDepth === 0 && _pendingBatch.size > 0) {
96
- const fns = [..._pendingBatch];
97
- _pendingBatch.clear();
98
- for (const fn2 of fns) fn2();
64
+ childVNode = Comp(props, ctx);
65
+ } catch (e) {
66
+ const errHandler = ctx.ui?._errorHandler;
67
+ if (errHandler) {
68
+ errHandler(e);
69
+ childVNode = null;
70
+ } else {
71
+ console.error("Component render error:", e);
72
+ childVNode = null;
99
73
  }
100
74
  }
101
- }
102
- function untrack(fn) {
103
- const prevEffect = currentEffect;
104
- const prevDeps = currentDeps;
105
- currentEffect = null;
106
- currentDeps = null;
107
- try {
108
- return fn();
109
- } finally {
110
- currentEffect = prevEffect;
111
- currentDeps = prevDeps;
75
+ if (childVNode == null) {
76
+ vnode._child = null;
77
+ return document.createTextNode("");
112
78
  }
79
+ vnode._child = childVNode;
80
+ return renderValue(childVNode, ctx);
113
81
  }
114
-
115
- // src/client/jsx-runtime.ts
116
- var currentCtx = null;
117
- function setCtx(ctx) {
118
- currentCtx = ctx;
119
- }
120
- function onMount(fn) {
121
- if (_pendingMountQueue) _pendingMountQueue.push(fn);
82
+ function renderArray(arr, ctx) {
83
+ const frag = document.createDocumentFragment();
84
+ for (const item of arr) frag.appendChild(renderValue(item, ctx));
85
+ return frag;
122
86
  }
123
- function onCleanup(fn) {
124
- if (_pendingCleanupQueue) _pendingCleanupQueue.push(fn);
125
- }
126
- var _pendingMountQueue = null;
127
- var _pendingCleanupQueue = null;
128
- var _entries = /* @__PURE__ */ new Map();
129
- function _ensure(el) {
130
- let entry = _entries.get(el);
131
- if (entry) return entry;
132
- entry = {
133
- mounted: document.contains(el),
134
- observer: null,
135
- mountFns: [],
136
- disposeFns: []
137
- };
138
- _entries.set(el, entry);
139
- const obs = new MutationObserver(() => {
140
- const now = document.contains(el);
141
- if (now && !entry.mounted) {
142
- entry.mounted = true;
143
- const fns = entry.mountFns.slice();
144
- entry.mountFns = [];
145
- for (const fn of fns) {
146
- const dispose = fn();
147
- if (typeof dispose === "function") {
148
- entry.disposeFns.push(dispose);
149
- }
150
- }
151
- } else if (!now && entry.mounted) {
152
- entry.mounted = false;
153
- for (const fn of entry.disposeFns) fn();
154
- entry.disposeFns = [];
155
- entry.mountFns = [];
156
- obs.disconnect();
157
- _entries.delete(el);
158
- }
159
- });
160
- obs.observe(document.body, { childList: true, subtree: true });
161
- entry.observer = obs;
162
- return entry;
87
+ function forEach(children, fn) {
88
+ if (children == null) return;
89
+ if (Array.isArray(children)) {
90
+ children.forEach(fn);
91
+ return;
92
+ }
93
+ fn(children);
163
94
  }
164
- function _onMountElement(el, fn) {
165
- const entry = _ensure(el);
166
- if (entry.mounted) {
167
- const dispose = fn();
168
- if (typeof dispose === "function") {
169
- entry.disposeFns.push(dispose);
95
+ function flattenChildren(children) {
96
+ if (children == null) return [];
97
+ if (!Array.isArray(children)) return [children];
98
+ const result = [];
99
+ for (const child of children) {
100
+ if (Array.isArray(child)) {
101
+ result.push(...child);
102
+ } else {
103
+ result.push(child);
170
104
  }
171
- } else {
172
- entry.mountFns.push(fn);
173
105
  }
174
- }
175
- function wrap(tagName, setup) {
176
- return (props, ctx) => {
177
- const el = document.createElement(tagName);
178
- _onMountElement(el, () => setup(el, props, ctx));
179
- return el;
180
- };
181
- }
182
- function _trackEffect(el, dispose) {
183
- const entry = _ensure(el);
184
- entry.disposeFns.push(dispose);
106
+ return result;
185
107
  }
186
108
  function setProp(el, key, value) {
187
109
  if (key === "class" || key === "className") {
188
- if (isSignal(value)) {
189
- _trackEffect(el, effect(() => {
190
- el.className = String(value.value);
191
- }));
192
- } else {
193
- el.className = String(value ?? "");
194
- }
110
+ el.className = String(value ?? "");
195
111
  } else if (key === "style" && typeof value === "object" && value !== null) {
196
112
  Object.assign(el.style, value);
197
113
  } else if (key.startsWith("on") && typeof value === "function") {
198
114
  el.addEventListener(key.slice(2).toLowerCase(), value);
199
- } else if (key === "ref" && typeof value === "function") {
200
- value(el);
201
- } else if (isSignal(value)) {
202
- _trackEffect(el, effect(() => {
203
- const v = value.value;
204
- if (key === "value" || key === "checked") {
205
- el[key] = v;
206
- } else if (v == null || v === false) {
207
- el.removeAttribute(key);
208
- } else if (v === true) {
209
- el.setAttribute(key, "");
210
- } else {
211
- el.setAttribute(key, String(v));
212
- }
213
- }));
115
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
116
+ ;
117
+ el.value = String(value ?? "");
118
+ } else if (value === true) {
119
+ el.setAttribute(key, "");
214
120
  } else if (value != null && value !== false) {
215
- if (value === true) el.setAttribute(key, "");
216
- else if (key === "value" || key === "checked") el[key] = value;
217
- else el.setAttribute(key, String(value));
121
+ el.setAttribute(key, String(value));
218
122
  }
219
123
  }
220
- function _closestElement(node) {
221
- if (node instanceof Element) return node;
222
- if (node instanceof DocumentFragment) {
124
+ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
125
+ if (oldInput == null) {
126
+ if (newInput == null) return null;
127
+ const node = renderValue(newInput, ctx);
128
+ if (oldNode && oldNode.parentNode) {
129
+ oldNode.parentNode.insertBefore(node, oldNode);
130
+ } else {
131
+ parent.appendChild(node);
132
+ }
133
+ return node;
134
+ }
135
+ if (newInput == null) {
136
+ if (oldNode) {
137
+ callRef(oldInput, null);
138
+ oldNode.remove();
139
+ }
223
140
  return null;
224
141
  }
225
- let parent = node.parentNode;
226
- while (parent) {
227
- if (parent instanceof Element) return parent;
228
- parent = parent.parentNode;
142
+ const oldType = typeOf(oldInput);
143
+ const newType = typeOf(newInput);
144
+ if (oldType !== newType) {
145
+ callRef(oldInput, null);
146
+ const node = renderValue(newInput, ctx);
147
+ if (oldNode?.parentNode) {
148
+ oldNode.parentNode.replaceChild(node, oldNode);
149
+ }
150
+ return node;
151
+ }
152
+ if (newType === "text") {
153
+ if (oldNode && oldNode.textContent !== String(newInput)) {
154
+ oldNode.textContent = String(newInput);
155
+ }
156
+ return oldNode;
157
+ }
158
+ const newV = newInput;
159
+ const oldV = oldInput;
160
+ if (typeof newV.type === "function") {
161
+ const comp = newV.type;
162
+ if (oldV._$) newV._$ = oldV._$;
163
+ ctx.ui = ctx.ui ?? {};
164
+ ctx.ui.ready = !!newV._$;
165
+ const childNew = comp(newV.props, ctx);
166
+ newV._child = childNew;
167
+ return patchValue(parent, oldNode, oldV._child, childNew, ctx);
168
+ }
169
+ if (newV.type === Fragment) {
170
+ patchChildren(parent, oldV, newV, ctx);
171
+ return oldNode;
172
+ }
173
+ if (typeof newV.type === "string") {
174
+ if (oldNode && oldNode.nodeType === 1) {
175
+ patchProps(oldNode, oldV.props, newV.props);
176
+ patchChildren(oldNode, oldV, newV, ctx);
177
+ } else if (oldNode) {
178
+ callRef(oldInput, null);
179
+ const node = renderValue(newInput, ctx);
180
+ oldNode.parentNode?.replaceChild(node, oldNode);
181
+ return node;
182
+ }
183
+ return oldNode;
184
+ }
185
+ if (Array.isArray(newInput)) {
186
+ const oldArr = Array.isArray(oldInput) ? oldInput : [];
187
+ patchSimpleChildren(parent, oldArr, newInput, ctx);
188
+ return oldNode;
189
+ }
190
+ return oldNode;
191
+ }
192
+ function typeOf(input) {
193
+ if (input == null || typeof input === "boolean") return "null";
194
+ if (typeof input === "string" || typeof input === "number") return "text";
195
+ if (Array.isArray(input)) return "array";
196
+ const v = input;
197
+ if (typeof v.type === "function") return `fn:${v.type.name || "anon"}`;
198
+ if (v.type === Fragment) return "fragment";
199
+ if (typeof v.type === "string") return "tag:" + v.type;
200
+ return "unknown";
201
+ }
202
+ function patchProps(el, oldProps, newProps) {
203
+ const oldKeys = oldProps ? Object.keys(oldProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
204
+ const newKeys = newProps ? Object.keys(newProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
205
+ for (const key of oldKeys) {
206
+ if (!newKeys.includes(key)) {
207
+ if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
208
+ ;
209
+ el.value = "";
210
+ } else {
211
+ el.removeAttribute(key === "className" ? "class" : key);
212
+ }
213
+ }
214
+ }
215
+ for (const key of newKeys) {
216
+ const oldVal = oldProps?.[key];
217
+ const newVal = newProps?.[key];
218
+ if (newVal !== oldVal) {
219
+ if (key === "class" || key === "className") {
220
+ el.className = String(newVal ?? "");
221
+ } else if (key === "style" && typeof newVal === "object") {
222
+ Object.assign(el.style, newVal);
223
+ } else if (key.startsWith("on") && typeof newVal === "function") {
224
+ const eventName = key.slice(2).toLowerCase();
225
+ if (typeof oldVal === "function") el.removeEventListener(eventName, oldVal);
226
+ el.addEventListener(eventName, newVal);
227
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
228
+ ;
229
+ el.value = String(newVal ?? "");
230
+ } else if (newVal === true) {
231
+ el.setAttribute(key, "");
232
+ } else if (newVal != null && newVal !== false) {
233
+ el.setAttribute(key, String(newVal));
234
+ } else {
235
+ if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
236
+ ;
237
+ el.value = "";
238
+ } else {
239
+ el.removeAttribute(key);
240
+ }
241
+ }
242
+ }
229
243
  }
230
- return null;
231
244
  }
232
- function appendChild(parent, child) {
233
- if (child == null || child === false || child === true) return;
234
- if (Array.isArray(child)) {
235
- child.forEach((c) => appendChild(parent, c));
236
- return;
245
+ function getKey(input) {
246
+ if (input == null || typeof input !== "object") return void 0;
247
+ return input.key;
248
+ }
249
+ function patchChildren(parent, oldVNode, newVNode, ctx) {
250
+ const oldChildren = normalize(oldVNode.props?.children);
251
+ const newChildren = normalize(newVNode.props?.children);
252
+ const hasKey = newChildren.some((c) => getKey(c) !== void 0);
253
+ if (hasKey) {
254
+ patchKeyedChildren(parent, oldChildren, newChildren, ctx);
255
+ } else {
256
+ patchSimpleChildren(parent, oldChildren, newChildren, ctx);
237
257
  }
238
- if (child instanceof Node) {
239
- parent.appendChild(child);
240
- return;
258
+ }
259
+ function normalize(children) {
260
+ if (children == null) return [];
261
+ if (!Array.isArray(children)) return [children];
262
+ const result = [];
263
+ for (const child of children) {
264
+ if (Array.isArray(child)) {
265
+ result.push(...child);
266
+ } else {
267
+ result.push(child);
268
+ }
241
269
  }
242
- if (isSignal(child)) {
243
- const text = document.createTextNode("");
244
- const parentEl = parent instanceof Element ? parent : _closestElement(parent);
245
- if (parentEl) {
246
- _trackEffect(parentEl, effect(() => {
247
- text.textContent = String(child.value);
248
- }));
270
+ return result;
271
+ }
272
+ function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
273
+ const max = Math.max(oldChildren.length, newChildren.length);
274
+ for (let i = 0; i < max; i++) {
275
+ const oldChild = oldChildren[i];
276
+ const newChild = newChildren[i];
277
+ const existingNode = parent.childNodes[i] || null;
278
+ if (oldChild === void 0 && newChild !== void 0) {
279
+ const node = renderValue(newChild, ctx);
280
+ parent.appendChild(node);
281
+ } else if (newChild === void 0) {
282
+ if (existingNode) {
283
+ callRef(oldChild, null);
284
+ existingNode.remove();
285
+ }
249
286
  } else {
250
- const anchor = document.createElement("div");
251
- anchor.style.display = "contents";
252
- _trackEffect(anchor, effect(() => {
253
- text.textContent = String(child.value);
254
- }));
255
- anchor.appendChild(text);
256
- parent.appendChild(anchor);
257
- return;
287
+ patchValue(parent, existingNode, oldChild, newChild, ctx);
258
288
  }
259
- parent.appendChild(text);
260
- return;
261
289
  }
262
- parent.appendChild(document.createTextNode(String(child)));
263
290
  }
264
- function jsx(type, props, ...children) {
265
- if (typeof type === "function") {
266
- const merged = children.length > 0 ? { ...props, children } : props;
267
- const prevMount = _pendingMountQueue;
268
- const prevCleanup = _pendingCleanupQueue;
269
- _pendingMountQueue = [];
270
- _pendingCleanupQueue = [];
271
- let result = document.createDocumentFragment();
272
- try {
273
- result = type(merged, currentCtx) ?? document.createDocumentFragment();
274
- } finally {
275
- if (_pendingMountQueue.length > 0 || _pendingCleanupQueue.length > 0) {
276
- let targetEl = null;
277
- if (result instanceof Element) {
278
- targetEl = result;
279
- } else if (result instanceof DocumentFragment && result.firstElementChild) {
280
- targetEl = result.firstElementChild;
281
- }
282
- if (targetEl) {
283
- const entry = _ensure(targetEl);
284
- for (const fn of _pendingMountQueue) {
285
- if (entry.mounted) {
286
- const dispose = fn();
287
- if (typeof dispose === "function") entry.disposeFns.push(dispose);
288
- } else {
289
- entry.mountFns.push(fn);
290
- }
291
- }
292
- for (const fn of _pendingCleanupQueue) {
293
- entry.disposeFns.push(fn);
294
- }
295
- }
291
+ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
292
+ const oldKeyMap = /* @__PURE__ */ new Map();
293
+ for (let i = 0; i < oldChildren.length; i++) {
294
+ const key = getKey(oldChildren[i]);
295
+ if (key !== void 0) {
296
+ oldKeyMap.set(key, { vnode: oldChildren[i], node: parent.childNodes[i] || null });
297
+ }
298
+ }
299
+ const newKeys = newChildren.map((c) => getKey(c));
300
+ for (const key of oldKeyMap.keys()) {
301
+ if (!newKeys.includes(key)) {
302
+ const entry = oldKeyMap.get(key);
303
+ callRef(entry.vnode, null);
304
+ entry.node?.remove();
305
+ oldKeyMap.delete(key);
306
+ }
307
+ }
308
+ for (let i = oldChildren.length - 1; i >= 0; i--) {
309
+ const key = getKey(oldChildren[i]);
310
+ if (key === void 0) {
311
+ const node = parent.childNodes[i];
312
+ if (node) {
313
+ callRef(oldChildren[i], null);
314
+ node.remove();
296
315
  }
297
- _pendingMountQueue = prevMount;
298
- _pendingCleanupQueue = prevCleanup;
299
316
  }
300
- return result;
301
317
  }
302
- const el = document.createElement(type);
303
- if (props) {
304
- for (const [k, v] of Object.entries(props)) {
305
- if (k !== "children") setProp(el, k, v);
318
+ let insertBefore = parent.firstChild;
319
+ for (let i = newChildren.length - 1; i >= 0; i--) {
320
+ const key = newKeys[i];
321
+ const newChild = newChildren[i];
322
+ const oldEntry = key !== void 0 ? oldKeyMap.get(key) : void 0;
323
+ if (oldEntry && oldEntry.node) {
324
+ parent.insertBefore(oldEntry.node, insertBefore);
325
+ insertBefore = oldEntry.node;
326
+ patchValue(parent, oldEntry.node, oldEntry.vnode, newChild, ctx);
327
+ } else {
328
+ const node = renderValue(newChild, ctx);
329
+ parent.insertBefore(node, insertBefore);
330
+ insertBefore = node;
306
331
  }
307
332
  }
308
- const childList = children.length > 0 ? children : props?.children != null ? [props.children] : [];
309
- for (const child of childList) appendChild(el, child);
310
- return el;
311
333
  }
312
- function jsxs(type, props, ...children) {
313
- return jsx(type, props, ...children);
314
- }
315
- function jsxDEV(type, props, _key, _isStatic, _source, _self) {
316
- return jsx(type, props, ...props?.children ? [props.children] : []);
317
- }
318
- function Fragment(props, ..._rest) {
319
- const childList = props?.children != null ? Array.isArray(props.children) ? props.children : [props.children] : [];
320
- const el = document.createElement("div");
321
- el.style.display = "contents";
322
- for (const child of childList) appendChild(el, child);
323
- return el;
324
- }
325
- function domMount(root, app) {
326
- const container = typeof root === "string" ? document.querySelector(root) : root;
327
- if (!container) throw new Error(`mount target not found: ${root}`);
328
- container.innerHTML = "";
329
- container.appendChild(app);
334
+ function callRef(input, el) {
335
+ if (input == null || typeof input !== "object") return;
336
+ const vnode = input;
337
+ if (typeof vnode.props?.ref === "function") {
338
+ vnode.props.ref(el);
339
+ }
340
+ forEach(vnode.props?.children, (child) => callRef(child, el));
330
341
  }
331
- function ErrorBoundary({ fallback, children, onError }, _ctx) {
332
- try {
333
- return children();
334
- } catch (e) {
335
- const err = e;
336
- if (onError) onError(err);
337
- return fallback(err);
342
+
343
+ // src/client/router.ts
344
+ function flattenRoutes(routes, basePath = "", chain = []) {
345
+ const result = [];
346
+ for (const route of routes) {
347
+ const fullPath = joinPaths(basePath, route.path);
348
+ const { re, keys } = compilePath(fullPath);
349
+ const fullChain = [...chain, route];
350
+ result.push({ re, keys, def: route, chain: fullChain });
351
+ if (route.children) {
352
+ result.push(...flattenRoutes(route.children, fullPath, fullChain));
353
+ }
338
354
  }
355
+ return result;
339
356
  }
340
- function createPortal(node, target) {
341
- target.appendChild(node);
342
- return document.createDocumentFragment();
357
+ var layoutDepth = /* @__PURE__ */ new WeakMap();
358
+ function joinPaths(a, b) {
359
+ if (!b || b === "/") return a || "/";
360
+ const left = a.endsWith("/") ? a.slice(0, -1) : a;
361
+ const right = b.startsWith("/") ? b : "/" + b;
362
+ return left + right;
343
363
  }
344
- function toNode(v) {
345
- if (v instanceof Node) return v;
346
- if (typeof v === "function") return toNode(v());
347
- return document.createTextNode(String(v ?? ""));
364
+ function compilePath(path) {
365
+ const keys = [];
366
+ const reStr = path.replace(/:(\w+)/g, (_, key) => {
367
+ keys.push(key);
368
+ return "([^/]+)";
369
+ }).replace(/\*/g, ".*");
370
+ return { re: new RegExp(`^${reStr}$`), keys };
348
371
  }
349
- function Show({ when, children, fallback }) {
350
- const el = document.createElement("div");
351
- el.style.display = "contents";
352
- function render(show) {
353
- while (el.lastChild) el.removeChild(el.lastChild);
354
- if (show && children != null) {
355
- el.appendChild(toNode(children));
356
- } else if (!show && fallback != null) {
357
- el.appendChild(toNode(fallback));
372
+ function matchRoute(path, routes) {
373
+ let best = null;
374
+ for (const fr of routes) {
375
+ if (path.match(fr.re)) {
376
+ if (!best || fr.chain.length > best.chain.length) best = fr;
358
377
  }
359
378
  }
360
- if (isSignal(when)) {
361
- const dispose = effect(() => render(Boolean(when.value)));
362
- _trackEffect(el, dispose);
363
- } else {
364
- render(Boolean(when));
365
- }
366
- return el;
379
+ return best;
367
380
  }
368
- function For({ each, children, keyBy }) {
369
- const el = document.createElement("div");
370
- el.style.display = "contents";
371
- function getKey(item, index) {
372
- if (typeof keyBy === "function") return keyBy(item);
373
- if (typeof keyBy === "string") return String(item[keyBy] ?? index);
374
- return String(index);
375
- }
376
- function render(list) {
377
- if (list == null) list = [];
378
- if (!keyBy) {
379
- while (el.lastChild) el.removeChild(el.lastChild);
380
- for (let i = 0; i < list.length; i++) {
381
- el.appendChild(children(list[i], i));
381
+ function router(opts) {
382
+ const flatRoutes = flattenRoutes(opts.routes);
383
+ const mode = opts.mode || "history";
384
+ function getPath() {
385
+ if (mode === "hash") {
386
+ const hash = window.location.hash.replace(/^#/, "") || "/";
387
+ return hash;
388
+ }
389
+ return window.location.pathname;
390
+ }
391
+ function resolve(path) {
392
+ const match = matchRoute(path, flatRoutes);
393
+ if (match) {
394
+ const params = {};
395
+ const m = path.match(match.re);
396
+ if (m) {
397
+ for (let i = 0; i < match.keys.length; i++) {
398
+ params[match.keys[i]] = decodeURIComponent(m[i + 1]);
399
+ }
382
400
  }
383
- return;
384
- }
385
- const oldNodes = Array.from(el.children).filter((n) => n instanceof Element);
386
- const oldKeyMap = /* @__PURE__ */ new Map();
387
- for (const node of oldNodes) {
388
- const k = node.getAttribute("data-key");
389
- if (k !== null) oldKeyMap.set(k, node);
390
- }
391
- const newKeys = [];
392
- const newItems = [];
393
- for (let i = 0; i < list.length; i++) {
394
- newKeys.push(getKey(list[i], i));
395
- newItems.push(list[i]);
396
- }
397
- const removedKeys = new Set(oldKeyMap.keys());
398
- for (const k of newKeys) removedKeys.delete(k);
399
- for (const k of removedKeys) {
400
- const node = oldKeyMap.get(k);
401
- node.remove();
402
- oldKeyMap.delete(k);
401
+ return {
402
+ path: match.def.path,
403
+ params,
404
+ query: Object.fromEntries(new URLSearchParams(window.location.search)),
405
+ chain: match.chain
406
+ };
403
407
  }
404
- let insertBefore = el.firstChild;
405
- for (let i = list.length - 1; i >= 0; i--) {
406
- const key = newKeys[i];
407
- const existing = oldKeyMap.get(key);
408
- if (existing) {
409
- el.insertBefore(existing, insertBefore);
410
- insertBefore = existing;
408
+ return { path, params: {}, query: {}, chain: [] };
409
+ }
410
+ return (ctx) => {
411
+ const resolved = resolve(getPath());
412
+ ctx.route = resolved;
413
+ if (!ctx.app) ctx.app = {};
414
+ ctx.app.navigate = (path) => {
415
+ if (mode === "hash") {
416
+ window.location.hash = "#" + path;
417
+ const resolved2 = resolve(path);
418
+ ctx.route = resolved2;
411
419
  } else {
412
- const node = children(newItems[i], i);
413
- if (node instanceof Element) {
414
- node.setAttribute("data-key", key);
415
- }
416
- el.insertBefore(node, insertBefore);
417
- insertBefore = node;
420
+ window.history.pushState({}, "", path);
421
+ const resolved2 = resolve(path);
422
+ ctx.route = resolved2;
418
423
  }
424
+ ctx.ui?.render();
425
+ };
426
+ const onPop = () => {
427
+ const resolved2 = resolve(getPath());
428
+ ctx.route = resolved2;
429
+ ctx.ui?.render();
430
+ };
431
+ window.addEventListener("popstate", onPop);
432
+ if (mode === "hash") {
433
+ window.addEventListener("hashchange", onPop);
419
434
  }
420
- }
421
- if (isSignal(each)) {
422
- const dispose = effect(() => render(each.value));
423
- _trackEffect(el, dispose);
424
- } else {
425
- render(each);
426
- }
427
- return el;
428
- }
429
-
430
- // src/client/types.ts
431
- function createContext(key) {
432
- return {
433
- provide: (ctx, value) => ctx.provide(key, value),
434
- inject: (ctx) => ctx.inject(key)
435
+ return ctx;
435
436
  };
436
437
  }
437
- function extendCtx(ctx, fields) {
438
- return Object.assign(Object.create(ctx), fields);
438
+ function RouteView(_props, ctx) {
439
+ const route = ctx.route;
440
+ if (!route?.chain?.length) return null;
441
+ const ctxAny = ctx;
442
+ const depth = layoutDepth.get(ctxAny) ?? 0;
443
+ if (depth >= route.chain.length) return null;
444
+ const def = route.chain[depth];
445
+ const Comp = def.layout ?? def.component;
446
+ if (!Comp) return null;
447
+ if (def.layout) {
448
+ layoutDepth.set(ctxAny, depth + 1);
449
+ }
450
+ return { type: Comp, props: {}, key: void 0 };
439
451
  }
440
452
 
441
453
  // src/client/app.ts
454
+ var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
455
+ var wrappedCache = /* @__PURE__ */ new WeakMap();
456
+ function wrapDeep(val, dirty) {
457
+ if (val === null || typeof val !== "object") return val;
458
+ if (val instanceof Node) return val;
459
+ if (wrappedCache.has(val)) return wrappedCache.get(val);
460
+ let proxy;
461
+ if (Array.isArray(val)) {
462
+ proxy = new Proxy(val, {
463
+ get(target, key, receiver) {
464
+ const v = Reflect.get(target, key, receiver);
465
+ if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
466
+ return function(...args) {
467
+ const r = v.apply(target, args);
468
+ dirty();
469
+ return r;
470
+ };
471
+ }
472
+ return wrapDeep(v, dirty);
473
+ },
474
+ set(target, key, val2) {
475
+ target[key] = wrapDeep(val2, dirty);
476
+ dirty();
477
+ return true;
478
+ }
479
+ });
480
+ } else {
481
+ proxy = new Proxy(val, {
482
+ get(_target, key, receiver) {
483
+ const v = Reflect.get(_target, key, receiver);
484
+ return wrapDeep(v, dirty);
485
+ },
486
+ set(target, key, val2) {
487
+ target[key] = wrapDeep(val2, dirty);
488
+ dirty();
489
+ return true;
490
+ }
491
+ });
492
+ }
493
+ wrappedCache.set(val, proxy);
494
+ return proxy;
495
+ }
442
496
  function createApp() {
443
497
  const middlewares = [];
444
- const provides = /* @__PURE__ */ new Map();
445
- let ctx = {
446
- route: {
447
- path: window.location.pathname,
448
- params: {},
449
- query: Object.fromEntries(new URLSearchParams(window.location.search)),
450
- hash: window.location.hash,
451
- component: null,
452
- data: {},
453
- loading: false
454
- },
455
- app: {
456
- navigate(path) {
457
- window.history.pushState({}, "", path);
458
- ctx.route.path = path;
459
- ctx.route.query = Object.fromEntries(new URLSearchParams(window.location.search));
460
- ctx.route.hash = window.location.hash;
461
- const Ctor = window.CustomEvent || CustomEvent;
462
- window.dispatchEvent(new Ctor("wefu:navigate", { detail: { path } }));
463
- }
464
- },
465
- provide(key, value) {
466
- provides.set(key, value);
467
- },
468
- inject(key) {
469
- return provides.get(key) ?? null;
470
- },
471
- ws: null
472
- };
473
- return {
498
+ let ctx = {};
499
+ let container = null;
500
+ let rootComponent = null;
501
+ let oldVNode = null;
502
+ let rendered = false;
503
+ const app = {
474
504
  get ctx() {
475
505
  return ctx;
476
506
  },
477
507
  use(mw) {
478
508
  middlewares.push(mw);
479
- return this;
509
+ return app;
480
510
  },
481
511
  async mount(rootSelector, RootComponent) {
512
+ rootComponent = RootComponent;
482
513
  for (const mw of middlewares) {
483
514
  ctx = await mw(ctx);
484
515
  }
485
- setCtx(ctx);
486
- const app = jsx(RootComponent, {});
487
- domMount(rootSelector, app);
488
- setCtx(null);
489
- },
490
- hydrate(selector, Component, props) {
491
- const root = document.querySelector(selector);
492
- if (!root) {
493
- console.warn(`hydrate target not found: ${selector}`);
494
- return;
495
- }
496
- const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
497
- setCtx(ctx);
498
- const vnode = jsx(Component, mergedProps);
499
- root.appendChild(vnode);
500
- setCtx(null);
501
- }
502
- };
503
- }
504
-
505
- // src/client/router.ts
506
- var CHAIN_KEY = /* @__PURE__ */ Symbol("routeChain");
507
- var DEPTH_KEY = /* @__PURE__ */ Symbol("routeDepth");
508
- function router(opts) {
509
- const mode = opts.mode ?? "hash";
510
- const flatRoutes = flattenRoutes(opts.routes);
511
- function matchRoute(path) {
512
- for (const fr of flatRoutes) {
513
- const result = path.match(fr.re);
514
- if (!result) continue;
515
- const isLeaf = (idx) => idx === fr.chain.length - 1;
516
- const matchedChain = fr.chain.map((item, idx) => {
517
- const params = {};
518
- if (isLeaf(idx)) {
519
- fr.keys.forEach((k, i) => {
520
- params[k] = decodeURIComponent(result[i + 1]);
521
- });
522
- }
523
- return { ...item, params };
524
- });
525
- return { chain: matchedChain, leafRoute: fr.leafRoute };
526
- }
527
- return null;
528
- }
529
- function resolve(ctx2, path) {
530
- const raw = mode === "hash" ? path || "/" : path;
531
- const [cleanPath, qs] = raw.split("?");
532
- ctx2.route.query = Object.fromEntries(new URLSearchParams(qs ?? ""));
533
- ctx2.route.path = cleanPath;
534
- const matched = matchRoute(cleanPath);
535
- if (!matched) {
536
- ctx2.route.component = opts.notFound ?? null;
537
- ctx2.route.data = {};
538
- ctx2.route.transition = opts.transition;
539
- ctx2.route[CHAIN_KEY] = [];
540
- return;
541
- }
542
- const leaf = matched.chain[matched.chain.length - 1];
543
- ctx2.route.params = { ...leaf.params };
544
- ctx2.route.component = leaf.component ?? leaf.routeDef.component ?? null;
545
- ctx2.route.title = matched.leafRoute.title;
546
- ctx2.route.auth = matched.leafRoute.auth;
547
- ctx2.route.transition = matched.leafRoute.transition ?? opts.transition;
548
- if (matched.leafRoute.title) document.title = matched.leafRoute.title;
549
- ctx2.route[CHAIN_KEY] = matched.chain;
550
- ctx2.route[DEPTH_KEY] = 0;
551
- }
552
- return (ctx) => {
553
- function emit(path) {
554
- const Ctor = window.CustomEvent || CustomEvent;
555
- window.dispatchEvent(new Ctor("wefu:route", { detail: { path } }));
556
- }
557
- function navigateAndLoad(path) {
558
- resolve(ctx, path);
559
- const leafChain = ctx.route[CHAIN_KEY];
560
- const leafLoader = leafChain?.[leafChain.length - 1]?.routeDef?.loader;
561
- if (leafLoader) {
562
- ctx.route.loading = true;
563
- emit(ctx.route.path);
564
- leafLoader(ctx).then((data) => {
565
- ctx.route.data = data;
566
- ctx.route.loading = false;
567
- emit(ctx.route.path);
568
- }).catch(() => {
569
- ctx.route.data = {};
570
- ctx.route.loading = false;
571
- emit(ctx.route.path);
516
+ const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
517
+ if (!el) throw new Error(`mount target not found: ${rootSelector}`);
518
+ container = el;
519
+ container.innerHTML = "";
520
+ let _dirty = false;
521
+ function scheduleRender() {
522
+ if (_dirty) return;
523
+ _dirty = true;
524
+ queueMicrotask(() => {
525
+ if (!_dirty) return;
526
+ _dirty = false;
527
+ ctx.ui.render();
572
528
  });
573
- } else {
574
- ctx.route.loading = false;
575
- emit(ctx.route.path);
576
529
  }
577
- }
578
- if (mode === "hash") {
579
- ctx.app.navigate = (path) => {
580
- window.location.hash = "#" + path;
581
- };
582
- window.addEventListener("hashchange", () => {
583
- navigateAndLoad(window.location.hash.slice(1) || "/");
584
- });
585
- } else {
586
- const scrollPositions = /* @__PURE__ */ new Map();
587
- ctx.app.navigate = (path) => {
588
- if (opts.scrollRestoration !== false) {
589
- scrollPositions.set(window.location.pathname, window.scrollY);
590
- }
591
- window.history.pushState({}, "", path);
592
- navigateAndLoad(path);
593
- };
594
- window.addEventListener("popstate", () => {
595
- if (opts.scrollRestoration !== false) {
596
- const savedY = scrollPositions.get(window.location.pathname);
597
- if (savedY !== void 0) {
598
- requestAnimationFrame(() => window.scrollTo(0, savedY));
530
+ const $target = {};
531
+ ctx.ui = {
532
+ render: () => {
533
+ if (!container || !rootComponent || !oldVNode) return;
534
+ _dirty = false;
535
+ layoutDepth.delete(ctx);
536
+ const newVNode = wrapComponent(rootComponent, ctx);
537
+ const oldNode = container.firstChild;
538
+ if (oldNode) {
539
+ patchValue(container, oldNode, oldVNode, newVNode, ctx);
599
540
  }
600
- }
601
- navigateAndLoad(window.location.pathname + window.location.search);
602
- });
603
- }
604
- const initialPath = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
605
- navigateAndLoad(initialPath);
606
- return ctx;
607
- };
608
- }
609
- function flattenRoutes(routes, parentPath = "") {
610
- const result = [];
611
- for (const route of routes) {
612
- const fullPath = parentPath + route.path;
613
- const parts = fullPath.split("/").filter(Boolean);
614
- const keys = [];
615
- const reStr = "^/" + parts.map((p) => {
616
- if (p.startsWith(":")) {
617
- keys.push(p.slice(1));
618
- return "([^/]+)";
619
- }
620
- return p;
621
- }).join("/") + "$";
622
- if (route.children && route.children.length > 0) {
623
- for (const child of route.children) {
624
- const childPath = fullPath + child.path;
625
- const childParts = childPath.split("/").filter(Boolean);
626
- const childKeys = [];
627
- const childReStr = "^/" + childParts.map((p) => {
628
- if (p.startsWith(":")) {
629
- childKeys.push(p.slice(1));
630
- return "([^/]+)";
541
+ oldVNode = newVNode;
542
+ },
543
+ /** 极少需要:仅当绕过 Proxy 直接操作底层对象时使用 */
544
+ dirty: () => {
545
+ scheduleRender();
546
+ },
547
+ $: new Proxy($target, {
548
+ set(_target, key, val) {
549
+ _target[key] = wrapDeep(val, scheduleRender);
550
+ scheduleRender();
551
+ return true;
631
552
  }
632
- return p;
633
- }).join("/") + "$";
634
- const chain = [
635
- { layout: route.layout, params: {}, routeDef: route, depth: 0 },
636
- { component: child.component, params: {}, routeDef: child, depth: 1 }
637
- ];
638
- result.push({
639
- re: new RegExp(childReStr),
640
- keys: childKeys,
641
- chain,
642
- leafRoute: child
643
- });
644
- }
645
- } else {
646
- const chain = [
647
- { component: route.component, params: {}, routeDef: route, depth: 0 }
648
- ];
649
- result.push({
650
- re: new RegExp(reStr),
651
- keys,
652
- chain,
653
- leafRoute: route
654
- });
553
+ }),
554
+ ready: false
555
+ };
556
+ oldVNode = wrapComponent(RootComponent, ctx);
557
+ const node = render(oldVNode, ctx);
558
+ if (node instanceof Node) container.appendChild(node);
559
+ _dirty = false;
560
+ rendered = true;
561
+ },
562
+ destroy() {
563
+ if (container) container.innerHTML = "";
564
+ container = null;
565
+ ctx = {};
655
566
  }
656
- }
657
- return result;
567
+ };
568
+ return app;
658
569
  }
659
- function RouteView(_props, ctx) {
660
- const el = document.createElement("div");
661
- let myDepth = null;
662
- let cachedComp = null;
663
- function render() {
664
- const chain = ctx.route[CHAIN_KEY];
665
- if (myDepth === null) {
666
- const d = ctx.route[DEPTH_KEY] ?? 0;
667
- myDepth = d;
668
- ctx.route[DEPTH_KEY] = myDepth + 1;
669
- }
670
- const depth = myDepth;
671
- if (!chain || depth >= chain.length) {
672
- if (el.children.length > 0) el.textContent = "";
673
- cachedComp = null;
674
- return;
675
- }
676
- const item = chain[depth];
677
- const Comp = item.layout ?? item.component;
678
- if (!Comp) return;
679
- if (cachedComp === Comp) return;
680
- cachedComp = Comp;
681
- el.textContent = "";
682
- setCtx(ctx);
683
- const page = jsx(Comp, {});
684
- el.appendChild(page);
685
- setCtx(null);
686
- }
687
- render();
688
- window.addEventListener("wefu:route", render);
689
- onCleanup(() => window.removeEventListener("wefu:route", render));
690
- return el;
570
+ function wrapComponent(Comp, _ctx) {
571
+ return { type: Comp, props: {}, key: void 0 };
691
572
  }
692
573
 
693
- // src/client/lazy.ts
694
- function lazy(loader, options) {
695
- let loaded = null;
696
- let error = null;
697
- let status = "pending";
698
- let loadingPromise = null;
699
- function startLoad() {
700
- if (status !== "pending") return;
701
- status = "loading";
702
- loadingPromise = loader().then((mod) => {
703
- loaded = "default" in mod ? mod.default : mod;
704
- status = "loaded";
705
- }).catch((e) => {
706
- error = e;
707
- status = "error";
708
- });
709
- }
710
- return (props, ctx) => {
711
- if (loaded) {
712
- return loaded(props, ctx);
713
- }
714
- if (error) {
715
- const ErrorComp = options?.errorFallback;
716
- if (ErrorComp) return ErrorComp(props, ctx);
717
- const el2 = document.createElement("div");
718
- el2.textContent = `\u7EC4\u4EF6\u52A0\u8F7D\u5931\u8D25: ${error.message}`;
719
- el2.style.cssText = "padding: 1rem; color: #ef4444;";
720
- return el2;
721
- }
722
- if (status === "pending") {
723
- startLoad();
724
- }
725
- if (status === "loading" && loadingPromise) {
726
- loadingPromise.then(() => {
727
- setTimeout(() => {
728
- window.dispatchEvent(new CustomEvent("wefu:lazy-loaded"));
729
- }, 0);
730
- });
731
- }
732
- const Fallback = options?.fallback;
733
- if (Fallback) return Fallback(props, ctx);
734
- const el = document.createElement("div");
735
- el.textContent = "\u52A0\u8F7D\u4E2D...";
736
- el.style.cssText = "padding: 1rem; color: #6b7280;";
737
- return el;
738
- };
574
+ // src/client/types.ts
575
+ function extendCtx(ctx, fields) {
576
+ return Object.assign(Object.create(ctx), fields);
739
577
  }
740
578
 
741
579
  // src/client/middleware/ws.ts
742
- function ws(opts = {}) {
743
- const wsUrl = opts.url ?? "/ws";
744
- const reconnectInterval = opts.reconnectInterval ?? 3e3;
745
- const maxReconnect = opts.maxReconnect ?? 10;
580
+ function ws(options = {}) {
581
+ const wsUrl = options.url ?? "/ws";
582
+ const reconnectInterval = options.reconnectInterval ?? 3e3;
583
+ const maxReconnect = options.maxReconnect ?? 10;
584
+ const pingIntervalMs = options.pingInterval ?? 3e4;
585
+ const pingTimeoutMs = options.pingTimeout ?? 1e4;
746
586
  return (ctx) => {
747
- const isConnected = signal(false);
748
587
  const messageHandlers = /* @__PURE__ */ new Set();
749
588
  let socket = null;
750
- let reconnectCount = 0;
589
+ let reconnectAttempts = 0;
751
590
  let reconnectTimer = null;
752
- function connect() {
753
- if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
754
- const url = wsUrl;
591
+ let pingTimer = null;
592
+ let pingTimeoutTimer = null;
593
+ let destroyed = false;
594
+ const wsClient = {
595
+ isConnected: false,
596
+ onMessage: (fn) => {
597
+ messageHandlers.add(fn);
598
+ return () => {
599
+ messageHandlers.delete(fn);
600
+ };
601
+ },
602
+ send: (msg) => {
603
+ if (socket?.readyState === WebSocket.OPEN) {
604
+ socket.send(JSON.stringify(msg));
605
+ }
606
+ },
607
+ _connect: connect,
608
+ /** 断开连接并清理所有定时器 */
609
+ close: () => {
610
+ destroyed = true;
611
+ clearTimers();
612
+ socket?.close();
613
+ socket = null;
614
+ wsClient.isConnected = false;
615
+ }
616
+ };
617
+ async function connect() {
618
+ if (destroyed) return;
755
619
  try {
756
- socket = new WebSocket(url);
620
+ socket = new WebSocket(wsUrl);
621
+ socket.onopen = () => {
622
+ wsClient.isConnected = true;
623
+ reconnectAttempts = 0;
624
+ startPing();
625
+ };
626
+ socket.onmessage = (e) => {
627
+ try {
628
+ const data = JSON.parse(e.data);
629
+ for (const handler of messageHandlers) handler(data);
630
+ } catch {
631
+ }
632
+ resetPingTimeout();
633
+ };
634
+ socket.onclose = () => {
635
+ wsClient.isConnected = false;
636
+ clearTimers();
637
+ if (!destroyed && reconnectAttempts < maxReconnect) {
638
+ reconnectAttempts++;
639
+ reconnectTimer = setTimeout(connect, reconnectInterval * Math.min(reconnectAttempts, 5));
640
+ }
641
+ };
642
+ socket.onerror = () => {
643
+ socket?.close();
644
+ };
757
645
  } catch {
758
- scheduleReconnect();
759
- return;
646
+ if (!destroyed) {
647
+ reconnectTimer = setTimeout(connect, reconnectInterval);
648
+ }
760
649
  }
761
- socket.onopen = () => {
762
- isConnected.value = true;
763
- reconnectCount = 0;
764
- };
765
- socket.onmessage = (event) => {
766
- try {
767
- const data = JSON.parse(event.data);
768
- for (const h of messageHandlers) h(data);
769
- } catch {
770
- for (const h of messageHandlers) h(event.data);
650
+ }
651
+ function startPing() {
652
+ if (pingIntervalMs <= 0) return;
653
+ pingTimer = setInterval(() => {
654
+ if (socket?.readyState === WebSocket.OPEN) {
655
+ socket.send(JSON.stringify({ type: "ping" }));
771
656
  }
772
- };
773
- socket.onclose = () => {
774
- isConnected.value = false;
775
- socket = null;
776
- scheduleReconnect();
777
- };
778
- socket.onerror = () => {
779
- socket?.close();
780
- };
657
+ pingTimeoutTimer = setTimeout(() => {
658
+ socket?.close();
659
+ }, pingTimeoutMs);
660
+ }, pingIntervalMs);
781
661
  }
782
- function scheduleReconnect() {
783
- if (reconnectCount >= maxReconnect) return;
784
- reconnectCount++;
785
- reconnectTimer = setTimeout(connect, reconnectInterval * reconnectCount);
662
+ function resetPingTimeout() {
663
+ clearTimeout(pingTimeoutTimer);
786
664
  }
787
- function send(data) {
788
- if (socket?.readyState === WebSocket.OPEN) {
789
- socket.send(typeof data === "string" ? data : JSON.stringify(data));
790
- } else {
791
- console.warn("ws.send: WebSocket not open, readyState:", socket?.readyState);
792
- }
665
+ function clearTimers() {
666
+ clearInterval(pingTimer);
667
+ clearTimeout(pingTimeoutTimer);
668
+ clearTimeout(reconnectTimer);
793
669
  }
794
670
  connect();
795
- return extendCtx(ctx, {
796
- ws: {
797
- send,
798
- onMessage: (handler) => {
799
- messageHandlers.add(handler);
800
- return () => messageHandlers.delete(handler);
801
- },
802
- get isConnected() {
803
- return isConnected;
804
- }
805
- }
806
- });
671
+ return extendCtx(ctx, { ws: wsClient });
807
672
  };
808
673
  }
809
674
 
@@ -815,13 +680,27 @@ function api(options) {
815
680
  };
816
681
  const onRequest = options?.onRequest;
817
682
  const onResponse = options?.onResponse;
683
+ const timeoutMs = options?.timeout ?? 0;
818
684
  async function request(method, url, body, reqOpts) {
819
685
  const fullURL = opts.baseURL + url;
820
686
  const init = {
821
687
  method,
822
- headers: { ...opts.headers, ...reqOpts?.headers },
823
- signal: reqOpts?.signal
688
+ headers: { ...opts.headers, ...reqOpts?.headers }
824
689
  };
690
+ const hasTimeout = timeoutMs > 0;
691
+ const hasUserSignal = !!reqOpts?.signal;
692
+ let abortController = null;
693
+ let timer = null;
694
+ if (hasTimeout || hasUserSignal) {
695
+ abortController = new AbortController();
696
+ init.signal = abortController.signal;
697
+ if (hasTimeout) {
698
+ timer = setTimeout(() => abortController.abort(new Error("Request timed out")), timeoutMs);
699
+ }
700
+ if (hasUserSignal) {
701
+ reqOpts.signal.addEventListener("abort", () => abortController.abort());
702
+ }
703
+ }
825
704
  if (body !== void 0 && method !== "GET" && method !== "HEAD") {
826
705
  init.body = JSON.stringify(body);
827
706
  }
@@ -829,19 +708,27 @@ function api(options) {
829
708
  if (onRequest) {
830
709
  finalReq = onRequest(finalReq);
831
710
  }
832
- const res = await fetch(finalReq.url, finalReq.init);
833
- if (onResponse) {
834
- return onResponse(res);
835
- }
836
- if (!res.ok) {
837
- const text = await res.text().catch(() => "");
838
- throw new ApiError(res.status, text || res.statusText);
839
- }
840
- const contentLength = res.headers.get("content-length");
841
- if (res.status === 204 || contentLength === "0") {
842
- return void 0;
711
+ try {
712
+ const res = await fetch(finalReq.url, finalReq.init);
713
+ if (onResponse) {
714
+ return onResponse(res);
715
+ }
716
+ if (!res.ok) {
717
+ const text = await res.text().catch(() => "");
718
+ throw new ApiError(res.status, text || res.statusText);
719
+ }
720
+ const contentLength = res.headers.get("content-length");
721
+ if (res.status === 204 || contentLength === "0") {
722
+ return void 0;
723
+ }
724
+ const ct = (res.headers.get("content-type") || "").toLowerCase();
725
+ if (ct.includes("application/json") || ct.includes("json")) {
726
+ return res.json();
727
+ }
728
+ return res.text();
729
+ } finally {
730
+ if (timer) clearTimeout(timer);
843
731
  }
844
- return res.json();
845
732
  }
846
733
  return (ctx) => {
847
734
  const client = {
@@ -851,8 +738,7 @@ function api(options) {
851
738
  patch: (url, body, reqOpts) => request("PATCH", url, body, reqOpts),
852
739
  delete: (url, reqOpts) => request("DELETE", url, void 0, reqOpts)
853
740
  };
854
- ctx.api = client;
855
- return ctx;
741
+ return extendCtx(ctx, { api: client });
856
742
  };
857
743
  }
858
744
  var ApiError = class extends Error {
@@ -867,207 +753,126 @@ var ApiError = class extends Error {
867
753
  };
868
754
 
869
755
  // src/client/middleware/auth.ts
756
+ function decodeJWT(token) {
757
+ try {
758
+ const parts = token.split(".");
759
+ if (parts.length !== 3) return null;
760
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
761
+ return JSON.parse(atob(payload));
762
+ } catch {
763
+ return null;
764
+ }
765
+ }
766
+ function isTokenExpired(token) {
767
+ const payload = decodeJWT(token);
768
+ if (!payload?.exp) return true;
769
+ return payload.exp * 1e3 - 3e4 < Date.now();
770
+ }
870
771
  function auth(options) {
871
772
  const storage = options?.storage ?? localStorage;
872
773
  const tokenKey = options?.tokenKey ?? "weifuwu_token";
873
774
  const userKey = options?.userKey ?? "weifuwu_user";
775
+ const refreshTokenKey = options?.refreshTokenKey ?? "weifuwu_refresh";
776
+ const refreshEndpoint = options?.refreshEndpoint ?? "/api/auth/refresh";
874
777
  return (ctx) => {
875
778
  const savedToken = storage.getItem(tokenKey);
876
779
  const savedUserStr = storage.getItem(userKey);
877
- const token = signal(savedToken);
878
- const user = signal(
879
- savedUserStr ? JSON.parse(savedUserStr) : null
880
- );
881
- const isLoggedIn = computed(() => token.value !== null);
882
- const authorizationHeader = computed(() => token.value ? `Bearer ${token.value}` : null);
883
780
  const authClient = {
884
- token,
885
- user,
886
- isLoggedIn,
887
- authorizationHeader,
888
- login(newToken, newUser) {
889
- token.value = newToken;
890
- user.value = newUser;
781
+ token: savedToken,
782
+ user: savedUserStr ? JSON.parse(savedUserStr) : null,
783
+ get isLoggedIn() {
784
+ return this.token !== null;
785
+ },
786
+ login(newToken, newUser, refreshToken) {
787
+ authClient.token = newToken;
788
+ authClient.user = newUser;
891
789
  storage.setItem(tokenKey, newToken);
892
790
  storage.setItem(userKey, JSON.stringify(newUser));
791
+ if (refreshToken) storage.setItem(refreshTokenKey, refreshToken);
893
792
  },
894
793
  logout() {
895
- token.value = null;
896
- user.value = null;
794
+ authClient.token = null;
795
+ authClient.user = null;
897
796
  storage.removeItem(tokenKey);
898
797
  storage.removeItem(userKey);
798
+ storage.removeItem(refreshTokenKey);
899
799
  },
900
800
  setUser(newUser) {
901
- user.value = newUser;
801
+ authClient.user = newUser;
902
802
  storage.setItem(userKey, JSON.stringify(newUser));
803
+ },
804
+ async refresh() {
805
+ const rt = storage.getItem(refreshTokenKey);
806
+ if (!rt) return false;
807
+ try {
808
+ const res = await fetch(refreshEndpoint, {
809
+ method: "POST",
810
+ headers: { "Content-Type": "application/json" },
811
+ body: JSON.stringify({ refreshToken: rt })
812
+ });
813
+ if (!res.ok) {
814
+ authClient.logout();
815
+ return false;
816
+ }
817
+ const data = await res.json();
818
+ authClient.token = data.token;
819
+ storage.setItem(tokenKey, data.token);
820
+ if (data.refreshToken) storage.setItem(refreshTokenKey, data.refreshToken);
821
+ return true;
822
+ } catch {
823
+ return false;
824
+ }
903
825
  }
904
826
  };
905
- ctx.auth = authClient;
906
- return ctx;
907
- };
908
- }
909
-
910
- // src/client/resource.ts
911
- function createResource(fetcher, options) {
912
- const data = signal(options?.initialValue);
913
- const loading = signal(true);
914
- const error = signal(void 0);
915
- let fetchId = 0;
916
- async function load() {
917
- const id = ++fetchId;
918
- loading.value = true;
919
- error.value = void 0;
920
- try {
921
- const result = await fetcher();
922
- if (id === fetchId) {
923
- data.value = result;
924
- loading.value = false;
925
- }
926
- } catch (e) {
927
- if (id === fetchId) {
928
- error.value = e instanceof Error ? e : new Error(String(e));
929
- loading.value = false;
930
- }
931
- }
932
- }
933
- load();
934
- const state = {
935
- data,
936
- loading,
937
- error,
938
- refetch: () => {
939
- load();
827
+ if (savedToken && isTokenExpired(savedToken)) {
828
+ authClient.refresh().catch(() => {
829
+ });
940
830
  }
831
+ return extendCtx(ctx, { auth: authClient });
941
832
  };
942
- return [data, state];
943
833
  }
944
834
 
945
- // src/client/form.ts
946
- function useForm(options) {
947
- const values = signal({ ...options.initial });
948
- const errors = signal({});
949
- const submitting = signal(false);
950
- const touched = signal({});
951
- function getValidators(name) {
952
- const rules = options.validate?.[name];
953
- if (!rules) return [];
954
- return Array.isArray(rules) ? rules : [rules];
955
- }
956
- function validateField(name, value) {
957
- const validators = getValidators(name);
958
- for (const v of validators) {
959
- const err = v(value);
960
- if (err !== null) return err;
961
- }
962
- return null;
963
- }
964
- function validateAllFields() {
965
- const newErrors = {};
966
- let hasError = false;
967
- for (const key of Object.keys(values.value)) {
968
- const err = validateField(key, values.value[key]);
969
- newErrors[key] = err;
970
- if (err !== null) hasError = true;
971
- }
972
- errors.value = newErrors;
973
- return !hasError;
974
- }
975
- function handleSubmit(e) {
976
- e.preventDefault();
977
- if (submitting.value) return;
978
- const allTouched = {};
979
- for (const key of Object.keys(values.value)) {
980
- allTouched[key] = true;
981
- }
982
- touched.value = allTouched;
983
- if (!validateAllFields()) return;
984
- if (options.onSubmit) {
985
- submitting.value = true;
986
- const result = options.onSubmit({ ...values.value });
987
- if (result instanceof Promise) {
988
- result.finally(() => {
989
- submitting.value = false;
990
- });
991
- } else {
992
- submitting.value = false;
993
- }
835
+ // src/client/error-boundary.ts
836
+ function ErrorBoundary(props, ctx) {
837
+ const $ = ctx.ui.$;
838
+ if (!("error" in $)) $.error = null;
839
+ if ($.error) {
840
+ const fb = props.fallback;
841
+ if (typeof fb === "function") {
842
+ return fb({ error: $.error });
843
+ }
844
+ return fb ?? null;
845
+ }
846
+ ;
847
+ ctx.ui._errorHandler = (err) => {
848
+ $.error = err;
849
+ ctx.ui.dirty();
850
+ };
851
+ try {
852
+ const result = props.children;
853
+ return result ?? null;
854
+ } catch (e) {
855
+ $.error = e;
856
+ const fb = props.fallback;
857
+ if (typeof fb === "function") {
858
+ return fb({ error: e });
994
859
  }
860
+ return fb ?? null;
995
861
  }
996
- function field(name) {
997
- const fieldError = {
998
- get value() {
999
- return errors.value[name];
1000
- }
1001
- };
1002
- return {
1003
- get value() {
1004
- return String(values.value[name] ?? "");
1005
- },
1006
- onInput(e) {
1007
- const target = e.target;
1008
- const newVal = target.type === "checkbox" ? target.checked : target.value;
1009
- const newValues = { ...values.value, [name]: newVal };
1010
- values.value = newValues;
1011
- if (touched.value[name]) {
1012
- const err = validateField(name, newVal);
1013
- errors.value = { ...errors.value, [name]: err };
1014
- }
1015
- },
1016
- get error() {
1017
- return fieldError;
1018
- }
1019
- };
1020
- }
1021
- function setValue(name, value) {
1022
- values.value = { ...values.value, [name]: value };
1023
- }
1024
- function reset() {
1025
- values.value = { ...options.initial };
1026
- errors.value = {};
1027
- touched.value = {};
1028
- submitting.value = false;
1029
- }
1030
- return {
1031
- values,
1032
- errors,
1033
- submitting,
1034
- touched,
1035
- handleSubmit,
1036
- field,
1037
- setValue,
1038
- reset,
1039
- validateAll: validateAllFields
1040
- };
1041
862
  }
1042
863
  export {
1043
864
  ApiError,
1044
865
  ErrorBoundary,
1045
- For,
1046
866
  Fragment,
1047
867
  RouteView,
1048
- Show,
1049
868
  api,
1050
869
  auth,
1051
- batch,
1052
- computed,
1053
870
  createApp,
1054
- createContext,
1055
- createPortal,
1056
- createResource,
1057
- domMount,
1058
- effect,
1059
871
  extendCtx,
1060
- isSignal,
872
+ h,
1061
873
  jsx,
1062
874
  jsxDEV,
1063
875
  jsxs,
1064
- lazy,
1065
- onCleanup,
1066
- onMount,
1067
876
  router,
1068
- signal,
1069
- untrack,
1070
- useForm,
1071
- wrap,
1072
877
  ws
1073
878
  };