weifuwu 0.34.0 → 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.
package/README.md CHANGED
@@ -315,28 +315,34 @@ esbuild.build({
315
315
  })
316
316
  ```
317
317
 
318
- ### 状态管理
318
+ ### 状态管理 — 深度 Proxy
319
+
320
+ `ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象层面的写入自动触发渲染,无需手动调用。
319
321
 
320
322
  ```tsx
321
- const $ = ctx.ui.$ // 获取状态对象
323
+ const $ = ctx.ui.$
324
+
325
+ // 顶层属性赋值
326
+ $.count = 0 // → 自动渲染
327
+ $.user = { name: 'alice' } // → 新值自动深度包装
322
328
 
323
- $.count = 0 // 赋值 → 自动渲染
324
- $.items = [...$.items, newItem] // 不可变更新
325
- $.items.push(newItem); ctx.ui.dirty() // 可变更新需显式 dirty
329
+ // 数组突变
330
+ $.items.push(newItem) // → 自动渲染
331
+ $.items.pop() // 自动渲染
332
+ $.items.splice(i, 1) // → 自动渲染
326
333
 
327
- // 派生值 — 每次 render 重新计算
328
- const doubled = $.count * 2
329
- const completed = $.todos.filter(t => t.done)
334
+ // 对象属性突变(数组内部也支持)
335
+ $.items[0].done = true // 自动渲染
336
+ $.msgs[idx].content += event.text // 自动渲染
337
+
338
+ // 不可变更新(同样支持)
339
+ $.items = [...$.items, newItem]
330
340
  ```
331
341
 
332
342
  | API | 说明 |
333
343
  |------|------|
334
- | `ctx.ui.$` | 状态代理对象,赋值自动触发渲染 |
335
- | `ctx.ui.dirty()` | 显式标记脏状态(嵌套突变后用) |
336
- | `ctx.ui.render()` | 同步立即渲染 |
337
-
338
- **原理**:`ctx.ui.$` 是 Proxy 对象,任何属性赋值(`$.x = val`)自动调用 `queueMicrotask(render)`。\
339
- 多次赋值在同一个微任务内合并为一次渲染。
344
+ | `ctx.ui.$` | 深度 Proxy 对象,所有写入自动触发渲染 |
345
+ | `ctx.ui.dirty()` | 极少需要 — 仅当绕过 Proxy 直接操作底层对象时 |
340
346
 
341
347
  ### JSX 运行时
342
348
 
@@ -7,6 +7,13 @@
7
7
  * ctx.ui.render() 触发组件重渲染
8
8
  * ctx.ui.$ 持久化组件状态(由渲染器管理)
9
9
  * ctx.ui.ready 首次执行标记(由渲染器管理)
10
+ *
11
+ * ctx.ui.$ 是深度 Proxy:
12
+ * $.x = val → 自动渲染(顶层属性赋值)
13
+ * $.items.push(val) → 自动渲染(数组突变方法)
14
+ * $.items[0].x = val → 自动渲染(对象属性突变)
15
+ * $.items.push({x: 1}) → 新对象自动深度包装
16
+ * ctx.ui.dirty() → 极少需要(仅当绕过 Proxy 直接操作底层对象)
10
17
  */
11
18
  import type { WfuiContext, AppMiddleware } from './types.ts';
12
19
  import type { Component } from './vnode.ts';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ErrorBoundary — 错误边界组件
3
+ *
4
+ * 捕获子组件渲染时的错误,展示 fallback 替代崩溃白屏。
5
+ *
6
+ * ```tsx
7
+ * <ErrorBoundary fallback={<p>出错了</p>}>
8
+ * <UserProfile />
9
+ * </ErrorBoundary>
10
+ * ```
11
+ *
12
+ * 基于 ctx.ui._errorHandler 机制:在渲染子组件前注入错误处理器,
13
+ * 子组件 render 抛错时设置 $.error → 重新渲染 fallback。
14
+ */
15
+ import type { WfuiContext } from './types.ts';
16
+ import type { VNode } from './vnode.ts';
17
+ export interface ErrorBoundaryProps {
18
+ fallback?: VNode | ((props: {
19
+ error: unknown;
20
+ }) => VNode) | null;
21
+ children?: any;
22
+ }
23
+ export declare function ErrorBoundary(props: ErrorBoundaryProps, ctx: WfuiContext): VNode | null;
@@ -27,3 +27,5 @@ export { ApiError } from './middleware/api.ts';
27
27
  export type { AuthClient, AuthOptions } from './middleware/auth.ts';
28
28
  export { extendCtx } from './types.ts';
29
29
  export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
30
+ export { ErrorBoundary } from './error-boundary.ts';
31
+ export type { ErrorBoundaryProps } from './error-boundary.ts';
@@ -59,7 +59,19 @@ function renderComponent(Comp, props, vnode, ctx) {
59
59
  if (!prev$) vnode._$ = {};
60
60
  ctx.ui = ctx.ui ?? {};
61
61
  ctx.ui.ready = !!prev$;
62
- const childVNode = Comp(props, ctx);
62
+ let childVNode;
63
+ try {
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;
73
+ }
74
+ }
63
75
  if (childVNode == null) {
64
76
  vnode._child = null;
65
77
  return document.createTextNode("");
@@ -150,11 +162,9 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
150
162
  if (oldV._$) newV._$ = oldV._$;
151
163
  ctx.ui = ctx.ui ?? {};
152
164
  ctx.ui.ready = !!newV._$;
153
- const childOld = oldV._child != null ? oldV._child : comp(oldV.props, ctx);
154
- ctx.ui.ready = !!newV._$;
155
165
  const childNew = comp(newV.props, ctx);
156
166
  newV._child = childNew;
157
- return patchValue(parent, oldNode, childOld, childNew, ctx);
167
+ return patchValue(parent, oldNode, oldV._child, childNew, ctx);
158
168
  }
159
169
  if (newV.type === Fragment) {
160
170
  patchChildren(parent, oldV, newV, ctx);
@@ -330,88 +340,6 @@ function callRef(input, el) {
330
340
  forEach(vnode.props?.children, (child) => callRef(child, el));
331
341
  }
332
342
 
333
- // src/client/app.ts
334
- function createApp() {
335
- const middlewares = [];
336
- let ctx = {};
337
- let container = null;
338
- let rootComponent = null;
339
- let oldVNode = null;
340
- let rendered = false;
341
- const app = {
342
- get ctx() {
343
- return ctx;
344
- },
345
- use(mw) {
346
- middlewares.push(mw);
347
- return app;
348
- },
349
- async mount(rootSelector, RootComponent) {
350
- rootComponent = RootComponent;
351
- for (const mw of middlewares) {
352
- ctx = await mw(ctx);
353
- }
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;
402
- },
403
- destroy() {
404
- if (container) container.innerHTML = "";
405
- container = null;
406
- ctx = {};
407
- }
408
- };
409
- return app;
410
- }
411
- function wrapComponent(Comp, _ctx) {
412
- return { type: Comp, props: {}, key: void 0 };
413
- }
414
-
415
343
  // src/client/router.ts
416
344
  function flattenRoutes(routes, basePath = "", chain = []) {
417
345
  const result = [];
@@ -426,6 +354,7 @@ function flattenRoutes(routes, basePath = "", chain = []) {
426
354
  }
427
355
  return result;
428
356
  }
357
+ var layoutDepth = /* @__PURE__ */ new WeakMap();
429
358
  function joinPaths(a, b) {
430
359
  if (!b || b === "/") return a || "/";
431
360
  const left = a.endsWith("/") ? a.slice(0, -1) : a;
@@ -451,6 +380,14 @@ function matchRoute(path, routes) {
451
380
  }
452
381
  function router(opts) {
453
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
+ }
454
391
  function resolve(path) {
455
392
  const match = matchRoute(path, flatRoutes);
456
393
  if (match) {
@@ -471,20 +408,30 @@ function router(opts) {
471
408
  return { path, params: {}, query: {}, chain: [] };
472
409
  }
473
410
  return (ctx) => {
474
- const resolved = resolve(window.location.pathname);
411
+ const resolved = resolve(getPath());
475
412
  ctx.route = resolved;
476
413
  if (!ctx.app) ctx.app = {};
477
414
  ctx.app.navigate = (path) => {
478
- window.history.pushState({}, "", path);
479
- const resolved2 = resolve(path);
480
- ctx.route = resolved2;
415
+ if (mode === "hash") {
416
+ window.location.hash = "#" + path;
417
+ const resolved2 = resolve(path);
418
+ ctx.route = resolved2;
419
+ } else {
420
+ window.history.pushState({}, "", path);
421
+ const resolved2 = resolve(path);
422
+ ctx.route = resolved2;
423
+ }
481
424
  ctx.ui?.render();
482
425
  };
483
- window.addEventListener("popstate", () => {
484
- const resolved2 = resolve(window.location.pathname);
426
+ const onPop = () => {
427
+ const resolved2 = resolve(getPath());
485
428
  ctx.route = resolved2;
486
429
  ctx.ui?.render();
487
- });
430
+ };
431
+ window.addEventListener("popstate", onPop);
432
+ if (mode === "hash") {
433
+ window.addEventListener("hashchange", onPop);
434
+ }
488
435
  return ctx;
489
436
  };
490
437
  }
@@ -492,12 +439,135 @@ function RouteView(_props, ctx) {
492
439
  const route = ctx.route;
493
440
  if (!route?.chain?.length) return null;
494
441
  const ctxAny = ctx;
495
- const depth = ctxAny._rvDepth ?? 0;
442
+ const depth = layoutDepth.get(ctxAny) ?? 0;
496
443
  if (depth >= route.chain.length) return null;
497
444
  const def = route.chain[depth];
498
445
  const Comp = def.layout ?? def.component;
499
446
  if (!Comp) return null;
500
- if (def.layout) ctxAny._rvDepth = depth + 1;
447
+ if (def.layout) {
448
+ layoutDepth.set(ctxAny, depth + 1);
449
+ }
450
+ return { type: Comp, props: {}, key: void 0 };
451
+ }
452
+
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
+ }
496
+ function createApp() {
497
+ const middlewares = [];
498
+ let ctx = {};
499
+ let container = null;
500
+ let rootComponent = null;
501
+ let oldVNode = null;
502
+ let rendered = false;
503
+ const app = {
504
+ get ctx() {
505
+ return ctx;
506
+ },
507
+ use(mw) {
508
+ middlewares.push(mw);
509
+ return app;
510
+ },
511
+ async mount(rootSelector, RootComponent) {
512
+ rootComponent = RootComponent;
513
+ for (const mw of middlewares) {
514
+ ctx = await mw(ctx);
515
+ }
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();
528
+ });
529
+ }
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);
540
+ }
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;
552
+ }
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 = {};
566
+ }
567
+ };
568
+ return app;
569
+ }
570
+ function wrapComponent(Comp, _ctx) {
501
571
  return { type: Comp, props: {}, key: void 0 };
502
572
  }
503
573
 
@@ -761,8 +831,38 @@ function auth(options) {
761
831
  return extendCtx(ctx, { auth: authClient });
762
832
  };
763
833
  }
834
+
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 });
859
+ }
860
+ return fb ?? null;
861
+ }
862
+ }
764
863
  export {
765
864
  ApiError,
865
+ ErrorBoundary,
766
866
  Fragment,
767
867
  RouteView,
768
868
  api,
@@ -9,5 +9,6 @@ export interface RouterOptions {
9
9
  routes: RouteDef[];
10
10
  notFound?: (props: any, ctx: WfuiContext) => any;
11
11
  }
12
+ export declare const layoutDepth: WeakMap<any, number>;
12
13
  export declare function router(opts: RouterOptions): AppMiddleware;
13
14
  export declare function RouteView(_props: {}, ctx: WfuiContext): any;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.34.0",
4
+ "version": "0.35.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {