weifuwu 0.34.0 → 0.35.1

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';
@@ -11,7 +11,13 @@ var jsxs = jsx;
11
11
  function jsxDEV(type, props, key) {
12
12
  return jsx(type, props, key);
13
13
  }
14
- var h = jsx;
14
+ function h(type, props, ...children) {
15
+ const p = normalizeProps(props ?? {});
16
+ if (children.length > 0) {
17
+ p.children = children.length === 1 ? children[0] : children;
18
+ }
19
+ return { type, props: p, key: props?.key ?? void 0 };
20
+ }
15
21
  function normalizeProps(props) {
16
22
  if (!props) return {};
17
23
  const result = {};
@@ -59,7 +65,19 @@ function renderComponent(Comp, props, vnode, ctx) {
59
65
  if (!prev$) vnode._$ = {};
60
66
  ctx.ui = ctx.ui ?? {};
61
67
  ctx.ui.ready = !!prev$;
62
- const childVNode = Comp(props, ctx);
68
+ let childVNode;
69
+ try {
70
+ childVNode = Comp(props, ctx);
71
+ } catch (e) {
72
+ const errHandler = ctx.ui?._errorHandler;
73
+ if (errHandler) {
74
+ errHandler(e);
75
+ childVNode = null;
76
+ } else {
77
+ console.error("Component render error:", e);
78
+ childVNode = null;
79
+ }
80
+ }
63
81
  if (childVNode == null) {
64
82
  vnode._child = null;
65
83
  return document.createTextNode("");
@@ -150,11 +168,9 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
150
168
  if (oldV._$) newV._$ = oldV._$;
151
169
  ctx.ui = ctx.ui ?? {};
152
170
  ctx.ui.ready = !!newV._$;
153
- const childOld = oldV._child != null ? oldV._child : comp(oldV.props, ctx);
154
- ctx.ui.ready = !!newV._$;
155
171
  const childNew = comp(newV.props, ctx);
156
172
  newV._child = childNew;
157
- return patchValue(parent, oldNode, childOld, childNew, ctx);
173
+ return patchValue(parent, oldNode, oldV._child, childNew, ctx);
158
174
  }
159
175
  if (newV.type === Fragment) {
160
176
  patchChildren(parent, oldV, newV, ctx);
@@ -330,88 +346,6 @@ function callRef(input, el) {
330
346
  forEach(vnode.props?.children, (child) => callRef(child, el));
331
347
  }
332
348
 
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
349
  // src/client/router.ts
416
350
  function flattenRoutes(routes, basePath = "", chain = []) {
417
351
  const result = [];
@@ -426,6 +360,7 @@ function flattenRoutes(routes, basePath = "", chain = []) {
426
360
  }
427
361
  return result;
428
362
  }
363
+ var layoutDepth = /* @__PURE__ */ new WeakMap();
429
364
  function joinPaths(a, b) {
430
365
  if (!b || b === "/") return a || "/";
431
366
  const left = a.endsWith("/") ? a.slice(0, -1) : a;
@@ -451,6 +386,14 @@ function matchRoute(path, routes) {
451
386
  }
452
387
  function router(opts) {
453
388
  const flatRoutes = flattenRoutes(opts.routes);
389
+ const mode = opts.mode || "history";
390
+ function getPath() {
391
+ if (mode === "hash") {
392
+ const hash = window.location.hash.replace(/^#/, "") || "/";
393
+ return hash;
394
+ }
395
+ return window.location.pathname;
396
+ }
454
397
  function resolve(path) {
455
398
  const match = matchRoute(path, flatRoutes);
456
399
  if (match) {
@@ -471,20 +414,30 @@ function router(opts) {
471
414
  return { path, params: {}, query: {}, chain: [] };
472
415
  }
473
416
  return (ctx) => {
474
- const resolved = resolve(window.location.pathname);
417
+ const resolved = resolve(getPath());
475
418
  ctx.route = resolved;
476
419
  if (!ctx.app) ctx.app = {};
477
420
  ctx.app.navigate = (path) => {
478
- window.history.pushState({}, "", path);
479
- const resolved2 = resolve(path);
480
- ctx.route = resolved2;
421
+ if (mode === "hash") {
422
+ window.location.hash = "#" + path;
423
+ const resolved2 = resolve(path);
424
+ ctx.route = resolved2;
425
+ } else {
426
+ window.history.pushState({}, "", path);
427
+ const resolved2 = resolve(path);
428
+ ctx.route = resolved2;
429
+ }
481
430
  ctx.ui?.render();
482
431
  };
483
- window.addEventListener("popstate", () => {
484
- const resolved2 = resolve(window.location.pathname);
432
+ const onPop = () => {
433
+ const resolved2 = resolve(getPath());
485
434
  ctx.route = resolved2;
486
435
  ctx.ui?.render();
487
- });
436
+ };
437
+ window.addEventListener("popstate", onPop);
438
+ if (mode === "hash") {
439
+ window.addEventListener("hashchange", onPop);
440
+ }
488
441
  return ctx;
489
442
  };
490
443
  }
@@ -492,12 +445,135 @@ function RouteView(_props, ctx) {
492
445
  const route = ctx.route;
493
446
  if (!route?.chain?.length) return null;
494
447
  const ctxAny = ctx;
495
- const depth = ctxAny._rvDepth ?? 0;
448
+ const depth = layoutDepth.get(ctxAny) ?? 0;
496
449
  if (depth >= route.chain.length) return null;
497
450
  const def = route.chain[depth];
498
451
  const Comp = def.layout ?? def.component;
499
452
  if (!Comp) return null;
500
- if (def.layout) ctxAny._rvDepth = depth + 1;
453
+ if (def.layout) {
454
+ layoutDepth.set(ctxAny, depth + 1);
455
+ }
456
+ return { type: Comp, props: {}, key: void 0 };
457
+ }
458
+
459
+ // src/client/app.ts
460
+ var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
461
+ var wrappedCache = /* @__PURE__ */ new WeakMap();
462
+ function wrapDeep(val, dirty) {
463
+ if (val === null || typeof val !== "object") return val;
464
+ if (val instanceof Node) return val;
465
+ if (wrappedCache.has(val)) return wrappedCache.get(val);
466
+ let proxy;
467
+ if (Array.isArray(val)) {
468
+ proxy = new Proxy(val, {
469
+ get(target, key, receiver) {
470
+ const v = Reflect.get(target, key, receiver);
471
+ if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
472
+ return function(...args) {
473
+ const r = v.apply(target, args);
474
+ dirty();
475
+ return r;
476
+ };
477
+ }
478
+ return wrapDeep(v, dirty);
479
+ },
480
+ set(target, key, val2) {
481
+ target[key] = wrapDeep(val2, dirty);
482
+ dirty();
483
+ return true;
484
+ }
485
+ });
486
+ } else {
487
+ proxy = new Proxy(val, {
488
+ get(_target, key, receiver) {
489
+ const v = Reflect.get(_target, key, receiver);
490
+ return wrapDeep(v, dirty);
491
+ },
492
+ set(target, key, val2) {
493
+ target[key] = wrapDeep(val2, dirty);
494
+ dirty();
495
+ return true;
496
+ }
497
+ });
498
+ }
499
+ wrappedCache.set(val, proxy);
500
+ return proxy;
501
+ }
502
+ function createApp() {
503
+ const middlewares = [];
504
+ let ctx = {};
505
+ let container = null;
506
+ let rootComponent = null;
507
+ let oldVNode = null;
508
+ let rendered = false;
509
+ const app = {
510
+ get ctx() {
511
+ return ctx;
512
+ },
513
+ use(mw) {
514
+ middlewares.push(mw);
515
+ return app;
516
+ },
517
+ async mount(rootSelector, RootComponent) {
518
+ rootComponent = RootComponent;
519
+ for (const mw of middlewares) {
520
+ ctx = await mw(ctx);
521
+ }
522
+ const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
523
+ if (!el) throw new Error(`mount target not found: ${rootSelector}`);
524
+ container = el;
525
+ container.innerHTML = "";
526
+ let _dirty = false;
527
+ function scheduleRender() {
528
+ if (_dirty) return;
529
+ _dirty = true;
530
+ queueMicrotask(() => {
531
+ if (!_dirty) return;
532
+ _dirty = false;
533
+ ctx.ui.render();
534
+ });
535
+ }
536
+ const $target = {};
537
+ ctx.ui = {
538
+ render: () => {
539
+ if (!container || !rootComponent || !oldVNode) return;
540
+ _dirty = false;
541
+ layoutDepth.delete(ctx);
542
+ const newVNode = wrapComponent(rootComponent, ctx);
543
+ const oldNode = container.firstChild;
544
+ if (oldNode) {
545
+ patchValue(container, oldNode, oldVNode, newVNode, ctx);
546
+ }
547
+ oldVNode = newVNode;
548
+ },
549
+ /** 极少需要:仅当绕过 Proxy 直接操作底层对象时使用 */
550
+ dirty: () => {
551
+ scheduleRender();
552
+ },
553
+ $: new Proxy($target, {
554
+ set(_target, key, val) {
555
+ _target[key] = wrapDeep(val, scheduleRender);
556
+ scheduleRender();
557
+ return true;
558
+ }
559
+ }),
560
+ ready: false
561
+ };
562
+ oldVNode = wrapComponent(RootComponent, ctx);
563
+ const node = render(oldVNode, ctx);
564
+ if (node instanceof Node) container.appendChild(node);
565
+ _dirty = false;
566
+ rendered = true;
567
+ },
568
+ destroy() {
569
+ if (container) container.innerHTML = "";
570
+ container = null;
571
+ ctx = {};
572
+ }
573
+ };
574
+ return app;
575
+ }
576
+ function wrapComponent(Comp, _ctx) {
501
577
  return { type: Comp, props: {}, key: void 0 };
502
578
  }
503
579
 
@@ -761,8 +837,38 @@ function auth(options) {
761
837
  return extendCtx(ctx, { auth: authClient });
762
838
  };
763
839
  }
840
+
841
+ // src/client/error-boundary.ts
842
+ function ErrorBoundary(props, ctx) {
843
+ const $ = ctx.ui.$;
844
+ if (!("error" in $)) $.error = null;
845
+ if ($.error) {
846
+ const fb = props.fallback;
847
+ if (typeof fb === "function") {
848
+ return fb({ error: $.error });
849
+ }
850
+ return fb ?? null;
851
+ }
852
+ ;
853
+ ctx.ui._errorHandler = (err) => {
854
+ $.error = err;
855
+ ctx.ui.dirty();
856
+ };
857
+ try {
858
+ const result = props.children;
859
+ return result ?? null;
860
+ } catch (e) {
861
+ $.error = e;
862
+ const fb = props.fallback;
863
+ if (typeof fb === "function") {
864
+ return fb({ error: e });
865
+ }
866
+ return fb ?? null;
867
+ }
868
+ }
764
869
  export {
765
870
  ApiError,
871
+ ErrorBoundary,
766
872
  Fragment,
767
873
  RouteView,
768
874
  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;
@@ -32,7 +32,8 @@ declare global {
32
32
  export declare function jsx(type: VNodeType, props: Record<string, any> | null, key?: string | null): VNode;
33
33
  export declare const jsxs: typeof jsx;
34
34
  export declare function jsxDEV(type: VNodeType, props: Record<string, any> | null, key?: string | null): VNode;
35
- export declare const h: typeof jsx;
35
+ /** `h`(hyperscript)支持 variadic children: `h('div', {class:'x'}, child1, child2)` */
36
+ export declare function h(type: VNodeType, props: Record<string, any> | null, ...children: any[]): VNode;
36
37
  export declare function isNative(vnode: VNode): boolean;
37
38
  export declare function isComponent(vnode: VNode): boolean;
38
39
  export declare function isFragment(vnode: VNode): boolean;
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.1",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {