weifuwu 0.33.11 → 0.34.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,604 @@
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);
56
- }
57
- function isSignal(value) {
58
- return value instanceof Signal;
59
- }
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();
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
80
8
  };
81
9
  }
82
- function computed(fn) {
83
- const s = signal(void 0);
84
- effect(() => {
85
- s.value = fn();
86
- });
87
- return s;
88
- }
89
- function batch(fn) {
90
- _batchDepth++;
91
- 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();
99
- }
100
- }
10
+ var jsxs = jsx;
11
+ function jsxDEV(type, props, key) {
12
+ return jsx(type, props, key);
101
13
  }
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;
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];
112
21
  }
22
+ return result;
113
23
  }
114
24
 
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);
122
- }
123
- function onCleanup(fn) {
124
- if (_pendingCleanupQueue) _pendingCleanupQueue.push(fn);
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;
125
56
  }
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;
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
+ const childVNode = Comp(props, ctx);
63
+ if (childVNode == null) {
64
+ vnode._child = null;
65
+ return document.createTextNode("");
66
+ }
67
+ vnode._child = childVNode;
68
+ return renderValue(childVNode, ctx);
69
+ }
70
+ function renderArray(arr, ctx) {
71
+ const frag = document.createDocumentFragment();
72
+ for (const item of arr) frag.appendChild(renderValue(item, ctx));
73
+ return frag;
74
+ }
75
+ function forEach(children, fn) {
76
+ if (children == null) return;
77
+ if (Array.isArray(children)) {
78
+ children.forEach(fn);
79
+ return;
80
+ }
81
+ fn(children);
163
82
  }
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);
83
+ function flattenChildren(children) {
84
+ if (children == null) return [];
85
+ if (!Array.isArray(children)) return [children];
86
+ const result = [];
87
+ for (const child of children) {
88
+ if (Array.isArray(child)) {
89
+ result.push(...child);
90
+ } else {
91
+ result.push(child);
170
92
  }
171
- } else {
172
- entry.mountFns.push(fn);
173
93
  }
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);
94
+ return result;
185
95
  }
186
96
  function setProp(el, key, value) {
187
97
  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
- }
98
+ el.className = String(value ?? "");
195
99
  } else if (key === "style" && typeof value === "object" && value !== null) {
196
100
  Object.assign(el.style, value);
197
101
  } else if (key.startsWith("on") && typeof value === "function") {
198
102
  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
- }));
103
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
104
+ ;
105
+ el.value = String(value ?? "");
106
+ } else if (value === true) {
107
+ el.setAttribute(key, "");
214
108
  } 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));
218
- }
219
- }
220
- function _closestElement(node) {
221
- if (node instanceof Element) return node;
222
- if (node instanceof DocumentFragment) {
223
- return null;
224
- }
225
- let parent = node.parentNode;
226
- while (parent) {
227
- if (parent instanceof Element) return parent;
228
- parent = parent.parentNode;
109
+ el.setAttribute(key, String(value));
229
110
  }
230
- return null;
231
111
  }
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;
237
- }
238
- if (child instanceof Node) {
239
- parent.appendChild(child);
240
- return;
241
- }
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
- }));
112
+ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
113
+ if (oldInput == null) {
114
+ if (newInput == null) return null;
115
+ const node = renderValue(newInput, ctx);
116
+ if (oldNode && oldNode.parentNode) {
117
+ oldNode.parentNode.insertBefore(node, oldNode);
249
118
  } 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;
119
+ parent.appendChild(node);
258
120
  }
259
- parent.appendChild(text);
260
- return;
121
+ return node;
261
122
  }
262
- parent.appendChild(document.createTextNode(String(child)));
263
- }
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
- }
123
+ if (newInput == null) {
124
+ if (oldNode) {
125
+ callRef(oldInput, null);
126
+ oldNode.remove();
127
+ }
128
+ return null;
129
+ }
130
+ const oldType = typeOf(oldInput);
131
+ const newType = typeOf(newInput);
132
+ if (oldType !== newType) {
133
+ callRef(oldInput, null);
134
+ const node = renderValue(newInput, ctx);
135
+ if (oldNode?.parentNode) {
136
+ oldNode.parentNode.replaceChild(node, oldNode);
137
+ }
138
+ return node;
139
+ }
140
+ if (newType === "text") {
141
+ if (oldNode && oldNode.textContent !== String(newInput)) {
142
+ oldNode.textContent = String(newInput);
143
+ }
144
+ return oldNode;
145
+ }
146
+ const newV = newInput;
147
+ const oldV = oldInput;
148
+ if (typeof newV.type === "function") {
149
+ const comp = newV.type;
150
+ if (oldV._$) newV._$ = oldV._$;
151
+ ctx.ui = ctx.ui ?? {};
152
+ ctx.ui.ready = !!newV._$;
153
+ const childOld = oldV._child != null ? oldV._child : comp(oldV.props, ctx);
154
+ ctx.ui.ready = !!newV._$;
155
+ const childNew = comp(newV.props, ctx);
156
+ newV._child = childNew;
157
+ return patchValue(parent, oldNode, childOld, childNew, ctx);
158
+ }
159
+ if (newV.type === Fragment) {
160
+ patchChildren(parent, oldV, newV, ctx);
161
+ return oldNode;
162
+ }
163
+ if (typeof newV.type === "string") {
164
+ if (oldNode && oldNode.nodeType === 1) {
165
+ patchProps(oldNode, oldV.props, newV.props);
166
+ patchChildren(oldNode, oldV, newV, ctx);
167
+ } else if (oldNode) {
168
+ callRef(oldInput, null);
169
+ const node = renderValue(newInput, ctx);
170
+ oldNode.parentNode?.replaceChild(node, oldNode);
171
+ return node;
172
+ }
173
+ return oldNode;
174
+ }
175
+ if (Array.isArray(newInput)) {
176
+ const oldArr = Array.isArray(oldInput) ? oldInput : [];
177
+ patchSimpleChildren(parent, oldArr, newInput, ctx);
178
+ return oldNode;
179
+ }
180
+ return oldNode;
181
+ }
182
+ function typeOf(input) {
183
+ if (input == null || typeof input === "boolean") return "null";
184
+ if (typeof input === "string" || typeof input === "number") return "text";
185
+ if (Array.isArray(input)) return "array";
186
+ const v = input;
187
+ if (typeof v.type === "function") return `fn:${v.type.name || "anon"}`;
188
+ if (v.type === Fragment) return "fragment";
189
+ if (typeof v.type === "string") return "tag:" + v.type;
190
+ return "unknown";
191
+ }
192
+ function patchProps(el, oldProps, newProps) {
193
+ const oldKeys = oldProps ? Object.keys(oldProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
194
+ const newKeys = newProps ? Object.keys(newProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
195
+ for (const key of oldKeys) {
196
+ if (!newKeys.includes(key)) {
197
+ if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
198
+ ;
199
+ el.value = "";
200
+ } else {
201
+ el.removeAttribute(key === "className" ? "class" : key);
296
202
  }
297
- _pendingMountQueue = prevMount;
298
- _pendingCleanupQueue = prevCleanup;
299
203
  }
300
- return result;
301
204
  }
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);
205
+ for (const key of newKeys) {
206
+ const oldVal = oldProps?.[key];
207
+ const newVal = newProps?.[key];
208
+ if (newVal !== oldVal) {
209
+ if (key === "class" || key === "className") {
210
+ el.className = String(newVal ?? "");
211
+ } else if (key === "style" && typeof newVal === "object") {
212
+ Object.assign(el.style, newVal);
213
+ } else if (key.startsWith("on") && typeof newVal === "function") {
214
+ const eventName = key.slice(2).toLowerCase();
215
+ if (typeof oldVal === "function") el.removeEventListener(eventName, oldVal);
216
+ el.addEventListener(eventName, newVal);
217
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
218
+ ;
219
+ el.value = String(newVal ?? "");
220
+ } else if (newVal === true) {
221
+ el.setAttribute(key, "");
222
+ } else if (newVal != null && newVal !== false) {
223
+ el.setAttribute(key, String(newVal));
224
+ } else {
225
+ if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
226
+ ;
227
+ el.value = "";
228
+ } else {
229
+ el.removeAttribute(key);
230
+ }
231
+ }
306
232
  }
307
233
  }
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
- }
312
- function jsxs(type, props, ...children) {
313
- return jsx(type, props, ...children);
314
234
  }
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;
235
+ function getKey(input) {
236
+ if (input == null || typeof input !== "object") return void 0;
237
+ return input.key;
324
238
  }
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);
330
- }
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);
239
+ function patchChildren(parent, oldVNode, newVNode, ctx) {
240
+ const oldChildren = normalize(oldVNode.props?.children);
241
+ const newChildren = normalize(newVNode.props?.children);
242
+ const hasKey = newChildren.some((c) => getKey(c) !== void 0);
243
+ if (hasKey) {
244
+ patchKeyedChildren(parent, oldChildren, newChildren, ctx);
245
+ } else {
246
+ patchSimpleChildren(parent, oldChildren, newChildren, ctx);
338
247
  }
339
248
  }
340
- function createPortal(node, target) {
341
- target.appendChild(node);
342
- return document.createDocumentFragment();
343
- }
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 ?? ""));
348
- }
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));
249
+ function normalize(children) {
250
+ if (children == null) return [];
251
+ if (!Array.isArray(children)) return [children];
252
+ const result = [];
253
+ for (const child of children) {
254
+ if (Array.isArray(child)) {
255
+ result.push(...child);
256
+ } else {
257
+ result.push(child);
358
258
  }
359
259
  }
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;
260
+ return result;
367
261
  }
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));
262
+ function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
263
+ const max = Math.max(oldChildren.length, newChildren.length);
264
+ for (let i = 0; i < max; i++) {
265
+ const oldChild = oldChildren[i];
266
+ const newChild = newChildren[i];
267
+ const existingNode = parent.childNodes[i] || null;
268
+ if (oldChild === void 0 && newChild !== void 0) {
269
+ const node = renderValue(newChild, ctx);
270
+ parent.appendChild(node);
271
+ } else if (newChild === void 0) {
272
+ if (existingNode) {
273
+ callRef(oldChild, null);
274
+ existingNode.remove();
382
275
  }
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);
276
+ } else {
277
+ patchValue(parent, existingNode, oldChild, newChild, ctx);
390
278
  }
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]);
279
+ }
280
+ }
281
+ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
282
+ const oldKeyMap = /* @__PURE__ */ new Map();
283
+ for (let i = 0; i < oldChildren.length; i++) {
284
+ const key = getKey(oldChildren[i]);
285
+ if (key !== void 0) {
286
+ oldKeyMap.set(key, { vnode: oldChildren[i], node: parent.childNodes[i] || null });
396
287
  }
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);
288
+ }
289
+ const newKeys = newChildren.map((c) => getKey(c));
290
+ for (const key of oldKeyMap.keys()) {
291
+ if (!newKeys.includes(key)) {
292
+ const entry = oldKeyMap.get(key);
293
+ callRef(entry.vnode, null);
294
+ entry.node?.remove();
295
+ oldKeyMap.delete(key);
403
296
  }
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;
411
- } 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;
297
+ }
298
+ for (let i = oldChildren.length - 1; i >= 0; i--) {
299
+ const key = getKey(oldChildren[i]);
300
+ if (key === void 0) {
301
+ const node = parent.childNodes[i];
302
+ if (node) {
303
+ callRef(oldChildren[i], null);
304
+ node.remove();
418
305
  }
419
306
  }
420
307
  }
421
- if (isSignal(each)) {
422
- const dispose = effect(() => render(each.value));
423
- _trackEffect(el, dispose);
424
- } else {
425
- render(each);
308
+ let insertBefore = parent.firstChild;
309
+ for (let i = newChildren.length - 1; i >= 0; i--) {
310
+ const key = newKeys[i];
311
+ const newChild = newChildren[i];
312
+ const oldEntry = key !== void 0 ? oldKeyMap.get(key) : void 0;
313
+ if (oldEntry && oldEntry.node) {
314
+ parent.insertBefore(oldEntry.node, insertBefore);
315
+ insertBefore = oldEntry.node;
316
+ patchValue(parent, oldEntry.node, oldEntry.vnode, newChild, ctx);
317
+ } else {
318
+ const node = renderValue(newChild, ctx);
319
+ parent.insertBefore(node, insertBefore);
320
+ insertBefore = node;
321
+ }
426
322
  }
427
- return el;
428
323
  }
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
- };
436
- }
437
- function extendCtx(ctx, fields) {
438
- return Object.assign(Object.create(ctx), fields);
324
+ function callRef(input, el) {
325
+ if (input == null || typeof input !== "object") return;
326
+ const vnode = input;
327
+ if (typeof vnode.props?.ref === "function") {
328
+ vnode.props.ref(el);
329
+ }
330
+ forEach(vnode.props?.children, (child) => callRef(child, el));
439
331
  }
440
332
 
441
333
  // src/client/app.ts
442
334
  function createApp() {
443
335
  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 {
336
+ let ctx = {};
337
+ let container = null;
338
+ let rootComponent = null;
339
+ let oldVNode = null;
340
+ let rendered = false;
341
+ const app = {
474
342
  get ctx() {
475
343
  return ctx;
476
344
  },
477
345
  use(mw) {
478
346
  middlewares.push(mw);
479
- return this;
347
+ return app;
480
348
  },
481
349
  async mount(rootSelector, RootComponent) {
350
+ rootComponent = RootComponent;
482
351
  for (const mw of middlewares) {
483
352
  ctx = await mw(ctx);
484
353
  }
485
- setCtx(ctx);
486
- const app = jsx(RootComponent, {});
487
- domMount(rootSelector, app);
488
- setCtx(null);
354
+ const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
355
+ if (!el) throw new Error(`mount target not found: ${rootSelector}`);
356
+ container = el;
357
+ container.innerHTML = "";
358
+ let _dirty = false;
359
+ const $target = {};
360
+ ctx.ui = {
361
+ render: () => {
362
+ if (!container || !rootComponent || !oldVNode) return;
363
+ _dirty = false;
364
+ ctx._rvDepth = 0;
365
+ const newVNode = wrapComponent(rootComponent, ctx);
366
+ const oldNode = container.firstChild;
367
+ if (oldNode) {
368
+ patchValue(container, oldNode, oldVNode, newVNode, ctx);
369
+ }
370
+ oldVNode = newVNode;
371
+ },
372
+ /** 显式标记脏(用于嵌套突变如 push、arr[i].x +=) */
373
+ dirty: () => {
374
+ if (_dirty) return;
375
+ _dirty = true;
376
+ queueMicrotask(() => {
377
+ if (!_dirty) return;
378
+ _dirty = false;
379
+ ctx.ui.render();
380
+ });
381
+ },
382
+ $: new Proxy($target, {
383
+ set(_target, key, val) {
384
+ _target[key] = val;
385
+ if (!_dirty) {
386
+ _dirty = true;
387
+ queueMicrotask(() => {
388
+ if (!_dirty) return;
389
+ _dirty = false;
390
+ ctx.ui.render();
391
+ });
392
+ }
393
+ return true;
394
+ }
395
+ }),
396
+ ready: false
397
+ };
398
+ oldVNode = wrapComponent(RootComponent, ctx);
399
+ const node = render(oldVNode, ctx);
400
+ if (node instanceof Node) container.appendChild(node);
401
+ rendered = true;
489
402
  },
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);
403
+ destroy() {
404
+ if (container) container.innerHTML = "";
405
+ container = null;
406
+ ctx = {};
501
407
  }
502
408
  };
409
+ return app;
410
+ }
411
+ function wrapComponent(Comp, _ctx) {
412
+ return { type: Comp, props: {}, key: void 0 };
503
413
  }
504
414
 
505
415
  // 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 };
416
+ function flattenRoutes(routes, basePath = "", chain = []) {
417
+ const result = [];
418
+ for (const route of routes) {
419
+ const fullPath = joinPaths(basePath, route.path);
420
+ const { re, keys } = compilePath(fullPath);
421
+ const fullChain = [...chain, route];
422
+ result.push({ re, keys, def: route, chain: fullChain });
423
+ if (route.children) {
424
+ result.push(...flattenRoutes(route.children, fullPath, fullChain));
526
425
  }
527
- return null;
528
426
  }
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;
427
+ return result;
428
+ }
429
+ function joinPaths(a, b) {
430
+ if (!b || b === "/") return a || "/";
431
+ const left = a.endsWith("/") ? a.slice(0, -1) : a;
432
+ const right = b.startsWith("/") ? b : "/" + b;
433
+ return left + right;
434
+ }
435
+ function compilePath(path) {
436
+ const keys = [];
437
+ const reStr = path.replace(/:(\w+)/g, (_, key) => {
438
+ keys.push(key);
439
+ return "([^/]+)";
440
+ }).replace(/\*/g, ".*");
441
+ return { re: new RegExp(`^${reStr}$`), keys };
442
+ }
443
+ function matchRoute(path, routes) {
444
+ let best = null;
445
+ for (const fr of routes) {
446
+ if (path.match(fr.re)) {
447
+ if (!best || fr.chain.length > best.chain.length) best = fr;
541
448
  }
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
449
  }
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);
572
- });
573
- } else {
574
- ctx.route.loading = false;
575
- emit(ctx.route.path);
576
- }
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);
450
+ return best;
451
+ }
452
+ function router(opts) {
453
+ const flatRoutes = flattenRoutes(opts.routes);
454
+ function resolve(path) {
455
+ const match = matchRoute(path, flatRoutes);
456
+ if (match) {
457
+ const params = {};
458
+ const m = path.match(match.re);
459
+ if (m) {
460
+ for (let i = 0; i < match.keys.length; i++) {
461
+ params[match.keys[i]] = decodeURIComponent(m[i + 1]);
590
462
  }
591
- window.history.pushState({}, "", path);
592
- navigateAndLoad(path);
463
+ }
464
+ return {
465
+ path: match.def.path,
466
+ params,
467
+ query: Object.fromEntries(new URLSearchParams(window.location.search)),
468
+ chain: match.chain
593
469
  };
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));
599
- }
600
- }
601
- navigateAndLoad(window.location.pathname + window.location.search);
602
- });
603
470
  }
604
- const initialPath = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
605
- navigateAndLoad(initialPath);
471
+ return { path, params: {}, query: {}, chain: [] };
472
+ }
473
+ return (ctx) => {
474
+ const resolved = resolve(window.location.pathname);
475
+ ctx.route = resolved;
476
+ if (!ctx.app) ctx.app = {};
477
+ ctx.app.navigate = (path) => {
478
+ window.history.pushState({}, "", path);
479
+ const resolved2 = resolve(path);
480
+ ctx.route = resolved2;
481
+ ctx.ui?.render();
482
+ };
483
+ window.addEventListener("popstate", () => {
484
+ const resolved2 = resolve(window.location.pathname);
485
+ ctx.route = resolved2;
486
+ ctx.ui?.render();
487
+ });
606
488
  return ctx;
607
489
  };
608
490
  }
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 "([^/]+)";
631
- }
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
- });
655
- }
656
- }
657
- return result;
658
- }
659
491
  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;
492
+ const route = ctx.route;
493
+ if (!route?.chain?.length) return null;
494
+ const ctxAny = ctx;
495
+ const depth = ctxAny._rvDepth ?? 0;
496
+ if (depth >= route.chain.length) return null;
497
+ const def = route.chain[depth];
498
+ const Comp = def.layout ?? def.component;
499
+ if (!Comp) return null;
500
+ if (def.layout) ctxAny._rvDepth = depth + 1;
501
+ return { type: Comp, props: {}, key: void 0 };
691
502
  }
692
503
 
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
- };
504
+ // src/client/types.ts
505
+ function extendCtx(ctx, fields) {
506
+ return Object.assign(Object.create(ctx), fields);
739
507
  }
740
508
 
741
509
  // 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;
510
+ function ws(options = {}) {
511
+ const wsUrl = options.url ?? "/ws";
512
+ const reconnectInterval = options.reconnectInterval ?? 3e3;
513
+ const maxReconnect = options.maxReconnect ?? 10;
514
+ const pingIntervalMs = options.pingInterval ?? 3e4;
515
+ const pingTimeoutMs = options.pingTimeout ?? 1e4;
746
516
  return (ctx) => {
747
- const isConnected = signal(false);
748
517
  const messageHandlers = /* @__PURE__ */ new Set();
749
518
  let socket = null;
750
- let reconnectCount = 0;
519
+ let reconnectAttempts = 0;
751
520
  let reconnectTimer = null;
752
- function connect() {
753
- if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
754
- const url = wsUrl;
521
+ let pingTimer = null;
522
+ let pingTimeoutTimer = null;
523
+ let destroyed = false;
524
+ const wsClient = {
525
+ isConnected: false,
526
+ onMessage: (fn) => {
527
+ messageHandlers.add(fn);
528
+ return () => {
529
+ messageHandlers.delete(fn);
530
+ };
531
+ },
532
+ send: (msg) => {
533
+ if (socket?.readyState === WebSocket.OPEN) {
534
+ socket.send(JSON.stringify(msg));
535
+ }
536
+ },
537
+ _connect: connect,
538
+ /** 断开连接并清理所有定时器 */
539
+ close: () => {
540
+ destroyed = true;
541
+ clearTimers();
542
+ socket?.close();
543
+ socket = null;
544
+ wsClient.isConnected = false;
545
+ }
546
+ };
547
+ async function connect() {
548
+ if (destroyed) return;
755
549
  try {
756
- socket = new WebSocket(url);
550
+ socket = new WebSocket(wsUrl);
551
+ socket.onopen = () => {
552
+ wsClient.isConnected = true;
553
+ reconnectAttempts = 0;
554
+ startPing();
555
+ };
556
+ socket.onmessage = (e) => {
557
+ try {
558
+ const data = JSON.parse(e.data);
559
+ for (const handler of messageHandlers) handler(data);
560
+ } catch {
561
+ }
562
+ resetPingTimeout();
563
+ };
564
+ socket.onclose = () => {
565
+ wsClient.isConnected = false;
566
+ clearTimers();
567
+ if (!destroyed && reconnectAttempts < maxReconnect) {
568
+ reconnectAttempts++;
569
+ reconnectTimer = setTimeout(connect, reconnectInterval * Math.min(reconnectAttempts, 5));
570
+ }
571
+ };
572
+ socket.onerror = () => {
573
+ socket?.close();
574
+ };
757
575
  } catch {
758
- scheduleReconnect();
759
- return;
576
+ if (!destroyed) {
577
+ reconnectTimer = setTimeout(connect, reconnectInterval);
578
+ }
760
579
  }
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);
580
+ }
581
+ function startPing() {
582
+ if (pingIntervalMs <= 0) return;
583
+ pingTimer = setInterval(() => {
584
+ if (socket?.readyState === WebSocket.OPEN) {
585
+ socket.send(JSON.stringify({ type: "ping" }));
771
586
  }
772
- };
773
- socket.onclose = () => {
774
- isConnected.value = false;
775
- socket = null;
776
- scheduleReconnect();
777
- };
778
- socket.onerror = () => {
779
- socket?.close();
780
- };
587
+ pingTimeoutTimer = setTimeout(() => {
588
+ socket?.close();
589
+ }, pingTimeoutMs);
590
+ }, pingIntervalMs);
781
591
  }
782
- function scheduleReconnect() {
783
- if (reconnectCount >= maxReconnect) return;
784
- reconnectCount++;
785
- reconnectTimer = setTimeout(connect, reconnectInterval * reconnectCount);
592
+ function resetPingTimeout() {
593
+ clearTimeout(pingTimeoutTimer);
786
594
  }
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
- }
595
+ function clearTimers() {
596
+ clearInterval(pingTimer);
597
+ clearTimeout(pingTimeoutTimer);
598
+ clearTimeout(reconnectTimer);
793
599
  }
794
600
  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
- });
601
+ return extendCtx(ctx, { ws: wsClient });
807
602
  };
808
603
  }
809
604
 
@@ -815,13 +610,27 @@ function api(options) {
815
610
  };
816
611
  const onRequest = options?.onRequest;
817
612
  const onResponse = options?.onResponse;
613
+ const timeoutMs = options?.timeout ?? 0;
818
614
  async function request(method, url, body, reqOpts) {
819
615
  const fullURL = opts.baseURL + url;
820
616
  const init = {
821
617
  method,
822
- headers: { ...opts.headers, ...reqOpts?.headers },
823
- signal: reqOpts?.signal
618
+ headers: { ...opts.headers, ...reqOpts?.headers }
824
619
  };
620
+ const hasTimeout = timeoutMs > 0;
621
+ const hasUserSignal = !!reqOpts?.signal;
622
+ let abortController = null;
623
+ let timer = null;
624
+ if (hasTimeout || hasUserSignal) {
625
+ abortController = new AbortController();
626
+ init.signal = abortController.signal;
627
+ if (hasTimeout) {
628
+ timer = setTimeout(() => abortController.abort(new Error("Request timed out")), timeoutMs);
629
+ }
630
+ if (hasUserSignal) {
631
+ reqOpts.signal.addEventListener("abort", () => abortController.abort());
632
+ }
633
+ }
825
634
  if (body !== void 0 && method !== "GET" && method !== "HEAD") {
826
635
  init.body = JSON.stringify(body);
827
636
  }
@@ -829,19 +638,27 @@ function api(options) {
829
638
  if (onRequest) {
830
639
  finalReq = onRequest(finalReq);
831
640
  }
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;
641
+ try {
642
+ const res = await fetch(finalReq.url, finalReq.init);
643
+ if (onResponse) {
644
+ return onResponse(res);
645
+ }
646
+ if (!res.ok) {
647
+ const text = await res.text().catch(() => "");
648
+ throw new ApiError(res.status, text || res.statusText);
649
+ }
650
+ const contentLength = res.headers.get("content-length");
651
+ if (res.status === 204 || contentLength === "0") {
652
+ return void 0;
653
+ }
654
+ const ct = (res.headers.get("content-type") || "").toLowerCase();
655
+ if (ct.includes("application/json") || ct.includes("json")) {
656
+ return res.json();
657
+ }
658
+ return res.text();
659
+ } finally {
660
+ if (timer) clearTimeout(timer);
843
661
  }
844
- return res.json();
845
662
  }
846
663
  return (ctx) => {
847
664
  const client = {
@@ -851,8 +668,7 @@ function api(options) {
851
668
  patch: (url, body, reqOpts) => request("PATCH", url, body, reqOpts),
852
669
  delete: (url, reqOpts) => request("DELETE", url, void 0, reqOpts)
853
670
  };
854
- ctx.api = client;
855
- return ctx;
671
+ return extendCtx(ctx, { api: client });
856
672
  };
857
673
  }
858
674
  var ApiError = class extends Error {
@@ -867,207 +683,96 @@ var ApiError = class extends Error {
867
683
  };
868
684
 
869
685
  // src/client/middleware/auth.ts
686
+ function decodeJWT(token) {
687
+ try {
688
+ const parts = token.split(".");
689
+ if (parts.length !== 3) return null;
690
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
691
+ return JSON.parse(atob(payload));
692
+ } catch {
693
+ return null;
694
+ }
695
+ }
696
+ function isTokenExpired(token) {
697
+ const payload = decodeJWT(token);
698
+ if (!payload?.exp) return true;
699
+ return payload.exp * 1e3 - 3e4 < Date.now();
700
+ }
870
701
  function auth(options) {
871
702
  const storage = options?.storage ?? localStorage;
872
703
  const tokenKey = options?.tokenKey ?? "weifuwu_token";
873
704
  const userKey = options?.userKey ?? "weifuwu_user";
705
+ const refreshTokenKey = options?.refreshTokenKey ?? "weifuwu_refresh";
706
+ const refreshEndpoint = options?.refreshEndpoint ?? "/api/auth/refresh";
874
707
  return (ctx) => {
875
708
  const savedToken = storage.getItem(tokenKey);
876
709
  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
710
  const authClient = {
884
- token,
885
- user,
886
- isLoggedIn,
887
- authorizationHeader,
888
- login(newToken, newUser) {
889
- token.value = newToken;
890
- user.value = newUser;
711
+ token: savedToken,
712
+ user: savedUserStr ? JSON.parse(savedUserStr) : null,
713
+ get isLoggedIn() {
714
+ return this.token !== null;
715
+ },
716
+ login(newToken, newUser, refreshToken) {
717
+ authClient.token = newToken;
718
+ authClient.user = newUser;
891
719
  storage.setItem(tokenKey, newToken);
892
720
  storage.setItem(userKey, JSON.stringify(newUser));
721
+ if (refreshToken) storage.setItem(refreshTokenKey, refreshToken);
893
722
  },
894
723
  logout() {
895
- token.value = null;
896
- user.value = null;
724
+ authClient.token = null;
725
+ authClient.user = null;
897
726
  storage.removeItem(tokenKey);
898
727
  storage.removeItem(userKey);
728
+ storage.removeItem(refreshTokenKey);
899
729
  },
900
730
  setUser(newUser) {
901
- user.value = newUser;
731
+ authClient.user = newUser;
902
732
  storage.setItem(userKey, JSON.stringify(newUser));
903
- }
904
- };
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();
940
- }
941
- };
942
- return [data, state];
943
- }
944
-
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
- }
994
- }
995
- }
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
733
  },
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 };
734
+ async refresh() {
735
+ const rt = storage.getItem(refreshTokenKey);
736
+ if (!rt) return false;
737
+ try {
738
+ const res = await fetch(refreshEndpoint, {
739
+ method: "POST",
740
+ headers: { "Content-Type": "application/json" },
741
+ body: JSON.stringify({ refreshToken: rt })
742
+ });
743
+ if (!res.ok) {
744
+ authClient.logout();
745
+ return false;
746
+ }
747
+ const data = await res.json();
748
+ authClient.token = data.token;
749
+ storage.setItem(tokenKey, data.token);
750
+ if (data.refreshToken) storage.setItem(refreshTokenKey, data.refreshToken);
751
+ return true;
752
+ } catch {
753
+ return false;
1014
754
  }
1015
- },
1016
- get error() {
1017
- return fieldError;
1018
755
  }
1019
756
  };
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
757
+ if (savedToken && isTokenExpired(savedToken)) {
758
+ authClient.refresh().catch(() => {
759
+ });
760
+ }
761
+ return extendCtx(ctx, { auth: authClient });
1040
762
  };
1041
763
  }
1042
764
  export {
1043
765
  ApiError,
1044
- ErrorBoundary,
1045
- For,
1046
766
  Fragment,
1047
767
  RouteView,
1048
- Show,
1049
768
  api,
1050
769
  auth,
1051
- batch,
1052
- computed,
1053
770
  createApp,
1054
- createContext,
1055
- createPortal,
1056
- createResource,
1057
- domMount,
1058
- effect,
1059
771
  extendCtx,
1060
- isSignal,
772
+ h,
1061
773
  jsx,
1062
774
  jsxDEV,
1063
775
  jsxs,
1064
- lazy,
1065
- onCleanup,
1066
- onMount,
1067
776
  router,
1068
- signal,
1069
- untrack,
1070
- useForm,
1071
- wrap,
1072
777
  ws
1073
778
  };