weifuwu 0.55.3 → 0.56.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,4 +1,13 @@
1
1
  // src/client/vnode.ts
2
+ var ASYNC_MARK = "__wfAsyncComponent";
3
+ function asyncComponent(factory) {
4
+ const fn = factory;
5
+ fn[ASYNC_MARK] = true;
6
+ return fn;
7
+ }
8
+ function isAsyncComponent(type) {
9
+ return typeof type === "function" && type?.[ASYNC_MARK] === true;
10
+ }
2
11
  var Fragment = /* @__PURE__ */ Symbol("Fragment");
3
12
  var Portal = /* @__PURE__ */ Symbol("Portal");
4
13
  function jsx(type, props, key) {
@@ -56,10 +65,11 @@ function renderValue(v, ctx) {
56
65
  }
57
66
  if (vnode.type === Fragment) {
58
67
  const frag = document.createDocumentFragment();
59
- forEach(vnode.props?.children, (child) => {
68
+ const children = vnode.props?.children == null ? [] : Array.isArray(vnode.props.children) ? vnode.props.children : [vnode.props.children];
69
+ for (const child of children) {
60
70
  const node = renderValue(child, ctx);
61
71
  if (node != null) frag.appendChild(node);
62
- });
72
+ }
63
73
  return frag;
64
74
  }
65
75
  if (typeof vnode.type === "function") {
@@ -102,6 +112,60 @@ function renderValue(v, ctx) {
102
112
  if (typeof vnode.props?.ref === "function") vnode.props.ref(el);
103
113
  return el;
104
114
  }
115
+ var asyncFactoryCache = /* @__PURE__ */ new WeakMap();
116
+ function clearAsyncComponentCache() {
117
+ asyncFactoryCache = /* @__PURE__ */ new WeakMap();
118
+ }
119
+ function startAsyncFactory(Comp, ctx) {
120
+ const existing = asyncFactoryCache.get(Comp);
121
+ if (existing) return existing;
122
+ const entry = { promise: null };
123
+ entry.promise = Promise.resolve().then(() => Comp(ctx)).then((def) => {
124
+ if (typeof def !== "function") {
125
+ throw new Error(
126
+ `asyncComponent factory <${Comp.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
127
+ );
128
+ }
129
+ entry.resolved = def;
130
+ return def;
131
+ });
132
+ asyncFactoryCache.set(Comp, entry);
133
+ return entry;
134
+ }
135
+ async function resolveAsyncFactory(Comp, ctx) {
136
+ return startAsyncFactory(Comp, ctx).promise;
137
+ }
138
+ function resolveAsyncFactorySync(Comp) {
139
+ return asyncFactoryCache.get(Comp)?.resolved;
140
+ }
141
+ function scheduleFullReRender(ctx) {
142
+ const ui = ctx.ui;
143
+ if (ui && typeof ui.render === "function") ui.render(["_wf_root"]);
144
+ }
145
+ function mountComponent(Comp, props, vnode, ctx) {
146
+ let def;
147
+ if (isAsyncComponent(Comp)) {
148
+ def = resolveAsyncFactorySync(Comp);
149
+ if (!def) {
150
+ void startAsyncFactory(Comp, ctx).promise.then(
151
+ () => scheduleFullReRender(ctx),
152
+ () => {
153
+ }
154
+ );
155
+ return null;
156
+ }
157
+ } else {
158
+ def = Comp;
159
+ }
160
+ let childVNode = def(props, ctx);
161
+ if (typeof childVNode !== "function") {
162
+ throw new Error(
163
+ `Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
164
+ );
165
+ }
166
+ vnode._render = childVNode;
167
+ return childVNode(props);
168
+ }
105
169
  function renderComponent(Comp, props, vnode, ctx) {
106
170
  ;
107
171
  ctx.ui = ctx.ui ?? {};
@@ -116,14 +180,7 @@ function renderComponent(Comp, props, vnode, ctx) {
116
180
  vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
117
181
  let childVNode;
118
182
  try {
119
- childVNode = Comp(props, childCtx);
120
- if (typeof childVNode !== "function") {
121
- throw new Error(
122
- `Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
123
- );
124
- }
125
- vnode._render = childVNode;
126
- childVNode = childVNode(props);
183
+ childVNode = mountComponent(Comp, props, vnode, childCtx);
127
184
  } catch (e) {
128
185
  const errHandler = ctx.ui?._errorHandler;
129
186
  if (errHandler) {
@@ -193,14 +250,6 @@ function patchPortal(oldV, newV, ctx) {
193
250
  ensureKeys(oldChildren, newChildren);
194
251
  patchKeyedChildren(sub, oldChildren, newChildren, ctx);
195
252
  }
196
- function forEach(children, fn) {
197
- if (children == null) return;
198
- if (Array.isArray(children)) {
199
- children.forEach(fn);
200
- return;
201
- }
202
- fn(children);
203
- }
204
253
  function flattenChildren(children) {
205
254
  if (children == null) return [];
206
255
  if (!Array.isArray(children)) return [children];
@@ -300,11 +349,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
300
349
  if (typeof newV._render === "function") {
301
350
  childNew = newV._render(newV.props);
302
351
  } else {
303
- childNew = comp(newV.props, childCtx);
304
- if (typeof childNew === "function") {
305
- newV._render = childNew;
306
- childNew = childNew(newV.props);
307
- }
352
+ childNew = mountComponent(comp, newV.props, newV, childCtx);
308
353
  }
309
354
  } catch (e) {
310
355
  const errHandler = ctx.ui?._errorHandler;
@@ -395,8 +440,8 @@ function patchProps(el, oldProps, newProps) {
395
440
  if (newVal !== oldVal) el.innerHTML = String(newVal ?? "");
396
441
  } else if (newVal !== oldVal) {
397
442
  if (key === "class" || key === "className") {
398
- if (el instanceof SVGElement) el.setAttribute("class", String(newVal ?? ""));
399
- else el.className = String(newVal ?? "");
443
+ if (el instanceof SVGElement) el.setAttribute("class", classToString(newVal));
444
+ else el.className = classToString(newVal);
400
445
  } else if (key === "style" && typeof newVal === "object") {
401
446
  const st = el.style;
402
447
  for (const sk of Object.keys(newVal)) {
@@ -425,6 +470,14 @@ function patchProps(el, oldProps, newProps) {
425
470
  }
426
471
  }
427
472
  }
473
+ function classToString(v) {
474
+ if (typeof v === "string") return v;
475
+ if (Array.isArray(v)) return v.filter(Boolean).join(" ");
476
+ if (v && typeof v === "object") {
477
+ return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
478
+ }
479
+ return "";
480
+ }
428
481
  function getKey(input) {
429
482
  if (input == null || typeof input !== "object") return void 0;
430
483
  return input.key;
@@ -628,6 +681,192 @@ function callRefCleanup(input) {
628
681
  vnode._remoteEl = void 0;
629
682
  }
630
683
  }
684
+ function cursorAdvance(c) {
685
+ c.node = c.node ? c.node.nextSibling : null;
686
+ }
687
+ function cursorInsert(c, n) {
688
+ if (c.node && c.node.parentNode) c.node.parentNode.insertBefore(n, c.node);
689
+ else c.parent.appendChild(n);
690
+ }
691
+ function cursorReplace(c, n) {
692
+ if (c.node && c.node.parentNode) {
693
+ c.node.parentNode.replaceChild(n, c.node);
694
+ cursorAdvance(c);
695
+ } else {
696
+ c.parent.appendChild(n);
697
+ }
698
+ }
699
+ async function renderValueHydrating(v, ctx, c) {
700
+ if (v == null || typeof v === "boolean") return null;
701
+ if (typeof v === "string" || typeof v === "number") {
702
+ const text = String(v);
703
+ if (c.node && c.node.nodeType === 3) {
704
+ if (c.node.textContent !== text) c.node.textContent = text;
705
+ cursorAdvance(c);
706
+ return c.node;
707
+ }
708
+ const tn = document.createTextNode(text);
709
+ cursorInsert(c, tn);
710
+ return tn;
711
+ }
712
+ if (Array.isArray(v)) {
713
+ let first = null;
714
+ for (const item of v) {
715
+ const n = await renderValueHydrating(item, ctx, c);
716
+ if (n != null && !first) first = n;
717
+ }
718
+ return first;
719
+ }
720
+ const vnode = v;
721
+ if (vnode.type === Portal || vnode.type === Fragment) {
722
+ const children = vnode.props?.children;
723
+ const arr = children == null ? [] : Array.isArray(children) ? children : [children];
724
+ let first = null;
725
+ for (const child of arr) {
726
+ const n = await renderValueHydrating(child, ctx, c);
727
+ if (n != null && !first) first = n;
728
+ }
729
+ return first;
730
+ }
731
+ if (typeof vnode.type === "function") {
732
+ return renderComponentHydrating(vnode, ctx, c);
733
+ }
734
+ const tag = vnode.type;
735
+ const props = vnode.props ?? {};
736
+ let el;
737
+ if (c.node && c.node.nodeType === 1 && c.node.tagName.toLowerCase() === tag.toLowerCase()) {
738
+ el = c.node;
739
+ cursorAdvance(c);
740
+ } else {
741
+ el = SVG_TAGS.has(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
742
+ cursorReplace(c, el);
743
+ }
744
+ vnode.el = el;
745
+ patchProps(el, null, props);
746
+ if ("innerHTML" in props) {
747
+ } else {
748
+ const childCursor = { parent: el, node: el.firstChild };
749
+ const children = flattenChildren(props.children);
750
+ for (const child of children) {
751
+ const n = await renderValueHydrating(child, ctx, childCursor);
752
+ if (n != null && n.parentNode !== el) el.appendChild(n);
753
+ if (child && typeof child === "object" && typeof child.type === "function") {
754
+ const childVNode = child;
755
+ if (!childVNode._parentNode) {
756
+ childVNode._parentNode = el;
757
+ childVNode._refNode = n;
758
+ }
759
+ }
760
+ }
761
+ while (childCursor.node) {
762
+ const n = childCursor.node;
763
+ childCursor.node = n.nextSibling;
764
+ n.parentNode?.removeChild(n);
765
+ }
766
+ }
767
+ if ("value" in props && el instanceof HTMLSelectElement) {
768
+ ;
769
+ el.value = String(props.value ?? "");
770
+ }
771
+ if (typeof props.ref === "function") props.ref(el);
772
+ return el;
773
+ }
774
+ async function renderComponentHydrating(vnode, ctx, c) {
775
+ ;
776
+ ctx.ui = ctx.ui ?? {};
777
+ if (!vnode._id) {
778
+ vnode._id = `_wf_${_idCounter++}`;
779
+ idRegistry.set(vnode._id, vnode);
780
+ }
781
+ const childCtx = Object.create(ctx);
782
+ childCtx.ui = Object.create(ctx.ui);
783
+ childCtx.ui._selfId = vnode._id;
784
+ childCtx.ui._selfVNode = vnode;
785
+ vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
786
+ const Comp = vnode.type;
787
+ let childVNode;
788
+ try {
789
+ let def;
790
+ if (isAsyncComponent(Comp)) {
791
+ def = await resolveAsyncFactory(Comp, childCtx);
792
+ } else {
793
+ def = Comp;
794
+ }
795
+ const renderFn = def(vnode.props ?? {}, childCtx);
796
+ if (typeof renderFn !== "function") {
797
+ throw new Error(
798
+ `Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
799
+ );
800
+ }
801
+ vnode._render = renderFn;
802
+ childVNode = renderFn(vnode.props ?? {});
803
+ } catch (e) {
804
+ const errHandler = ctx.ui?._errorHandler;
805
+ if (errHandler) {
806
+ errHandler(e);
807
+ childVNode = null;
808
+ } else {
809
+ console.error(
810
+ `[weifuwu] Component hydration error in <${Comp.name || "anonymous"}> (id: ${vnode._id ?? "?"})`,
811
+ e
812
+ );
813
+ childVNode = null;
814
+ }
815
+ }
816
+ if (childVNode == null) {
817
+ vnode._child = null;
818
+ return null;
819
+ }
820
+ vnode._child = childVNode;
821
+ const domNode = await renderValueHydrating(childVNode, childCtx, c);
822
+ if (!vnode._refNode) {
823
+ ;
824
+ vnode._refNode = domNode;
825
+ }
826
+ return domNode;
827
+ }
828
+ async function hydrateVNode(container, vnode, ctx) {
829
+ const cursor = { parent: container, node: container.firstChild };
830
+ await renderValueHydrating(vnode, ctx, cursor);
831
+ while (cursor.node) {
832
+ const n = cursor.node;
833
+ cursor.node = n.nextSibling;
834
+ n.parentNode?.removeChild(n);
835
+ }
836
+ }
837
+
838
+ // src/client/reactive.ts
839
+ function createReactiveState(dirty) {
840
+ const proxyCache = /* @__PURE__ */ new WeakMap();
841
+ const reactive = (target) => {
842
+ if (target === null || typeof target !== "object") return target;
843
+ if (proxyCache.has(target)) return proxyCache.get(target);
844
+ const proxy = new Proxy(target, {
845
+ set(target2, key, value) {
846
+ const old = Reflect.get(target2, key);
847
+ if (old === value) return true;
848
+ Reflect.set(target2, key, value);
849
+ dirty();
850
+ return true;
851
+ },
852
+ get(target2, key) {
853
+ const value = Reflect.get(target2, key);
854
+ if (typeof value === "object" && value !== null) return reactive(value);
855
+ return value;
856
+ },
857
+ deleteProperty(target2, key) {
858
+ if (Reflect.has(target2, key)) {
859
+ Reflect.deleteProperty(target2, key);
860
+ dirty();
861
+ }
862
+ return true;
863
+ }
864
+ });
865
+ proxyCache.set(target, proxy);
866
+ return proxy;
867
+ };
868
+ return reactive({});
869
+ }
631
870
 
632
871
  // src/client/app.ts
633
872
  function createApp() {
@@ -724,7 +963,7 @@ function createApp() {
724
963
  middlewares.push(mw);
725
964
  return app;
726
965
  },
727
- async mount(rootSelector, RootComponent) {
966
+ async mount(rootSelector, RootComponent, options) {
728
967
  rootComponent = RootComponent;
729
968
  for (const mw of middlewares) {
730
969
  ctx = await mw(ctx);
@@ -732,7 +971,36 @@ function createApp() {
732
971
  const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
733
972
  if (!el) throw new Error(`mount target not found: ${rootSelector}`);
734
973
  container = el;
735
- container.innerHTML = "";
974
+ const hydrating = !!options?.hydrate;
975
+ if (!hydrating) container.innerHTML = "";
976
+ const dataCache = /* @__PURE__ */ new Map();
977
+ const hydratedData = globalThis.__DATA__;
978
+ if (hydratedData && typeof hydratedData === "object") {
979
+ for (const [k, v] of Object.entries(hydratedData)) {
980
+ dataCache.set(k, { value: v });
981
+ }
982
+ }
983
+ ;
984
+ ctx.data = {
985
+ async get(key, fetcher) {
986
+ const entry = dataCache.get(key);
987
+ if (entry && "value" in entry) return entry.value;
988
+ if (entry?.promise) return entry.promise;
989
+ if (!fetcher) return void 0;
990
+ const promise = Promise.resolve().then(() => fetcher()).then((val) => {
991
+ dataCache.set(key, { value: val });
992
+ return val;
993
+ });
994
+ dataCache.set(key, { promise });
995
+ return promise;
996
+ },
997
+ set(key, value) {
998
+ dataCache.set(key, { value });
999
+ },
1000
+ has(key) {
1001
+ return dataCache.has(key);
1002
+ }
1003
+ };
736
1004
  ctx.ui = {
737
1005
  _selfId: "_wf_root",
738
1006
  // ── ctx 版本号(供三态 skip 判定) ──
@@ -852,16 +1120,16 @@ function createApp() {
852
1120
  * pos 是稳定对象,render 闭包直接读取 top/left;
853
1121
  * 滚动/resize 时自动重算并定向刷新;打开弹层瞬间调用 pos.refresh()。
854
1122
  */
855
- usePopupPosition: function(options) {
1123
+ usePopupPosition: function(options2) {
856
1124
  const selfId = getSelfId(this);
857
1125
  const pos = { top: 0, left: 0, refresh: () => {
858
1126
  } };
859
1127
  if (!selfId) return pos;
860
1128
  const tracker = {
861
1129
  pos,
862
- getEl: options.el,
863
- isOpen: options.isOpen,
864
- compute: options.compute
1130
+ getEl: options2.el,
1131
+ isOpen: options2.isOpen,
1132
+ compute: options2.compute
865
1133
  };
866
1134
  _popupTrackers.set(selfId, tracker);
867
1135
  if (!_popupListenersReady) {
@@ -896,10 +1164,14 @@ function createApp() {
896
1164
  oldVNode = wrapComponent(RootComponent, ctx);
897
1165
  oldVNode._id = "_wf_root";
898
1166
  oldVNode._parentNode = container;
899
- oldVNode._refNode = null;
1167
+ oldVNode._refNode = hydrating ? container.firstChild : null;
900
1168
  idRegistry.set("_wf_root", oldVNode);
901
- const node = render(oldVNode, ctx);
902
- if (node instanceof Node) container.appendChild(node);
1169
+ if (hydrating) {
1170
+ await hydrateVNode(container, oldVNode, ctx);
1171
+ } else {
1172
+ const node = render(oldVNode, ctx);
1173
+ if (node instanceof Node) container.appendChild(node);
1174
+ }
903
1175
  oldVNode._refNode = container.firstChild;
904
1176
  _rendering = false;
905
1177
  flushDirtyBatch();
@@ -921,52 +1193,8 @@ function createApp() {
921
1193
  function wrapComponent(Comp, _ctx) {
922
1194
  return { type: Comp, props: {}, key: void 0 };
923
1195
  }
924
- function createReactiveState(dirty) {
925
- const proxyCache = /* @__PURE__ */ new WeakMap();
926
- const reactive = (target) => {
927
- if (target === null || typeof target !== "object") return target;
928
- if (proxyCache.has(target)) return proxyCache.get(target);
929
- const proxy = new Proxy(target, {
930
- set(target2, key, value) {
931
- const old = Reflect.get(target2, key);
932
- if (old === value) return true;
933
- Reflect.set(target2, key, value);
934
- dirty();
935
- return true;
936
- },
937
- get(target2, key) {
938
- const value = Reflect.get(target2, key);
939
- if (typeof value === "object" && value !== null) return reactive(value);
940
- return value;
941
- },
942
- deleteProperty(target2, key) {
943
- if (Reflect.has(target2, key)) {
944
- Reflect.deleteProperty(target2, key);
945
- dirty();
946
- }
947
- return true;
948
- }
949
- });
950
- proxyCache.set(target, proxy);
951
- return proxy;
952
- };
953
- return reactive({});
954
- }
955
1196
 
956
- // src/client/router.ts
957
- function flattenRoutes(routes, basePath = "", chain = []) {
958
- const result = [];
959
- for (const route of routes) {
960
- const fullPath = joinPaths(basePath, route.path);
961
- const { re, keys } = compilePath(fullPath);
962
- const fullChain = [...chain, route];
963
- result.push({ re, keys, def: route, chain: fullChain });
964
- if (route.children) {
965
- result.push(...flattenRoutes(route.children, fullPath, fullChain));
966
- }
967
- }
968
- return result;
969
- }
1197
+ // src/client/route-match.ts
970
1198
  function joinPaths(a, b) {
971
1199
  if (!b || b === "/") return a || "/";
972
1200
  const left = a.endsWith("/") ? a.slice(0, -1) : a;
@@ -981,6 +1209,19 @@ function compilePath(path) {
981
1209
  }).replace(/\*/g, ".*");
982
1210
  return { re: new RegExp(`^${reStr}$`), keys };
983
1211
  }
1212
+ function flattenRoutes(routes, basePath = "", chain = []) {
1213
+ const result = [];
1214
+ for (const route of routes) {
1215
+ const fullPath = joinPaths(basePath, route.path);
1216
+ const { re, keys } = compilePath(fullPath);
1217
+ const fullChain = [...chain, route];
1218
+ result.push({ re, keys, def: route, chain: fullChain });
1219
+ if (route.children) {
1220
+ result.push(...flattenRoutes(route.children, fullPath, fullChain));
1221
+ }
1222
+ }
1223
+ return result;
1224
+ }
984
1225
  function matchRoute(path, routes) {
985
1226
  let best = null;
986
1227
  for (const fr of routes) {
@@ -990,6 +1231,18 @@ function matchRoute(path, routes) {
990
1231
  }
991
1232
  return best;
992
1233
  }
1234
+ function extractParams(path, match) {
1235
+ const params = {};
1236
+ const m = path.match(match.re);
1237
+ if (m) {
1238
+ for (let i = 0; i < match.keys.length; i++) {
1239
+ params[match.keys[i]] = decodeURIComponent(m[i + 1]);
1240
+ }
1241
+ }
1242
+ return params;
1243
+ }
1244
+
1245
+ // src/client/router.ts
993
1246
  function router(opts) {
994
1247
  const flatRoutes = flattenRoutes(opts.routes);
995
1248
  const mode = opts.mode || "history";
@@ -1003,17 +1256,10 @@ function router(opts) {
1003
1256
  function resolve(path) {
1004
1257
  const match = matchRoute(path, flatRoutes);
1005
1258
  if (match) {
1006
- const params = {};
1007
- const m = path.match(match.re);
1008
- if (m) {
1009
- for (let i = 0; i < match.keys.length; i++) {
1010
- params[match.keys[i]] = decodeURIComponent(m[i + 1]);
1011
- }
1012
- }
1013
1259
  const last = match.chain[match.chain.length - 1];
1014
1260
  return {
1015
1261
  path: match.def.path,
1016
- params,
1262
+ params: extractParams(path, match),
1017
1263
  query: Object.fromEntries(new URLSearchParams(window.location.search)),
1018
1264
  chain: match.chain,
1019
1265
  title: last?.title ?? ""
@@ -1033,6 +1279,7 @@ function router(opts) {
1033
1279
  if (resolved.title) document.title = resolved.title;
1034
1280
  if (!ctx.app) ctx.app = {};
1035
1281
  ctx.app.navigate = (path) => {
1282
+ clearAsyncComponentCache();
1036
1283
  if (mode === "hash") {
1037
1284
  window.location.hash = "#" + path;
1038
1285
  const resolved2 = resolve(path);
@@ -1047,6 +1294,7 @@ function router(opts) {
1047
1294
  ctx.ui?.render();
1048
1295
  };
1049
1296
  const onPop = () => {
1297
+ clearAsyncComponentCache();
1050
1298
  const resolved2 = resolve(getPath());
1051
1299
  ctx.route = resolved2;
1052
1300
  if (ctx.route?.title) document.title = ctx.route.title;
@@ -1595,6 +1843,7 @@ export {
1595
1843
  Portal,
1596
1844
  RouteView,
1597
1845
  api,
1846
+ asyncComponent,
1598
1847
  auth,
1599
1848
  computeFixedPos,
1600
1849
  computeFixedPosRect,
@@ -1604,6 +1853,7 @@ export {
1604
1853
  extendCtx,
1605
1854
  h,
1606
1855
  i18n,
1856
+ isAsyncComponent,
1607
1857
  jsx,
1608
1858
  jsxDEV,
1609
1859
  jsxs,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 创建响应式状态容器:深度 Proxy,任意层级属性赋值自动触发 dirty。
3
+ *
4
+ * 纯 JS(无 DOM),客户端(ctx.ui.$())与服务端(SSR ctx shim)共用。
5
+ */
6
+ export declare function createReactiveState(dirty: () => void): Record<string, any>;
@@ -14,6 +14,12 @@ import type { VNode } from './vnode.ts';
14
14
  import type { WfuiContext } from './types.ts';
15
15
  export declare const idRegistry: Map<string, VNode>;
16
16
  export declare function render(input: any, ctx: WfuiContext): Node | null;
17
+ /**
18
+ * 清空 async 工厂缓存。
19
+ * 页面上下文切换时调用(路由导航/登录登出)——工厂内 ctx.data.get 的 key 依赖 ctx(如 route.params),
20
+ * 上下文变化后旧缓存定义的数据已失效,需要让工厂以新 ctx 重新执行。
21
+ */
22
+ export declare function clearAsyncComponentCache(): void;
17
23
  /** 首次渲染 Portal:创建远程容器、渲染子节点(不返回占位节点) */
18
24
  export declare function renderPortal(vnode: VNode, ctx: WfuiContext): void;
19
25
  /** 更新 Portal:复用远程容器,patch 子节点(不操作父 DOM) */
@@ -22,3 +28,8 @@ export declare function patchValue(parent: Node, oldNode: Node | null, oldInput:
22
28
  /** 通知 ref 清理 + Portal 子容器清理 */
23
29
  export declare function callRefCleanup(input: any): void;
24
30
  export declare function mountVNode(container: Element, vnode: VNode, ctx: WfuiContext): void;
31
+ /**
32
+ * Hydration 挂载入口:收养 container 内现有服务端 HTML。
33
+ * 渲染完收尾:删除服务端有、客户端没有的残留 DOM。
34
+ */
35
+ export declare function hydrateVNode(container: Element, vnode: VNode, ctx: WfuiContext): Promise<void>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 路由匹配纯函数 — 客户端 router 与服务端 uiSsr 共用(无 DOM 依赖)
3
+ *
4
+ * 支持路径参数 :id、通配符 *、嵌套路由(children → chain)。
5
+ * 与后端 Router(Trie)独立——本模块服务于"前端路由表驱动的页面"。
6
+ */
7
+ import type { RouteDef } from './types.ts';
8
+ export interface FlattenedRoute {
9
+ re: RegExp;
10
+ keys: string[];
11
+ def: RouteDef;
12
+ chain: RouteDef[];
13
+ }
14
+ export declare function joinPaths(a: string, b: string): string;
15
+ export declare function compilePath(path: string): {
16
+ re: RegExp;
17
+ keys: string[];
18
+ };
19
+ export declare function flattenRoutes(routes: RouteDef[], basePath?: string, chain?: RouteDef[]): FlattenedRoute[];
20
+ export declare function matchRoute(path: string, routes: FlattenedRoute[]): FlattenedRoute | null;
21
+ /** 从匹配结果提取路径参数 */
22
+ export declare function extractParams(path: string, match: FlattenedRoute): Record<string, string>;
@@ -70,6 +70,23 @@ export interface WfuiContext {
70
70
  delete: <T = any>(url: string, opts?: any) => Promise<T>;
71
71
  [key: string]: any;
72
72
  };
73
+ /** 数据管道(由 createApp 注入):ctx.data.get(key, fetcher) */
74
+ data?: {
75
+ /**
76
+ * 获取数据:
77
+ * - SSR:服务端真 fetch,结果序列化进 __DATA__
78
+ * - hydration:从 __DATA__ 缓存同步命中(工厂 await 微任务即 resolve)
79
+ * - SPA:未命中则触发 fetcher,同 key 并发请求合并
80
+ *
81
+ * key 约定即 URL(`/api/posts/1`),天然唯一。
82
+ */
83
+ get: <T = any>(key: string, fetcher?: () => Promise<T>) => Promise<T>;
84
+ /** 向缓存写入值(如 hydration 种子) */
85
+ set: (key: string, value: unknown) => void;
86
+ /** 是否存在缓存(未触发 fetch) */
87
+ has: (key: string) => boolean;
88
+ [key: string]: any;
89
+ };
73
90
  /** 认证状态 */
74
91
  auth?: {
75
92
  token: string | null;
@@ -7,7 +7,7 @@
7
7
  * --jsxImportSource=weifuwu/client
8
8
  */
9
9
  import type { WfuiContext } from './types.ts';
10
- export type VNodeType = string | Component | typeof Fragment | typeof Portal;
10
+ export type VNodeType = string | Component | AsyncComponent | typeof Fragment | typeof Portal;
11
11
  export interface VNode {
12
12
  type: VNodeType;
13
13
  props: Record<string, any>;
@@ -37,6 +37,35 @@ export interface VNode {
37
37
  * P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
38
38
  */
39
39
  export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => ((props: P) => VNode | null) | null;
40
+ /**
41
+ * 异步组件工厂(形态 C):async (ctx) => (initProps, ctx) => (props) => VNode
42
+ *
43
+ * 工厂层(async,只执行一次并缓存):
44
+ * - 数据声明:const data = await ctx.data.get(key, fetcher)
45
+ * - 代码分割:const { default: def } = await import('./view.tsx')
46
+ * - 异步初始化:模块加载、共享资源
47
+ *
48
+ * 返回标准的 Component——mount/render 保持同步,异步只发生在工厂边界。
49
+ * 服务端遍历器与客户端渲染器都 await 工厂;更新路径(_render 缓存)不碰工厂。
50
+ *
51
+ * 必须用 asyncComponent() 包装以标记(渲染器据此区分调用约定):
52
+ * Component → Comp(initProps, ctx)
53
+ * AsyncComponent → await Comp(ctx) → Component
54
+ */
55
+ export type AsyncComponent<C extends object = {}, P = {}> = (ctx: WfuiContext & C) => Promise<Component<P, C>>;
56
+ /**
57
+ * 包装异步组件工厂。
58
+ *
59
+ * ```tsx
60
+ * const UserProfile = asyncComponent(async (ctx) => {
61
+ * const user = await ctx.data.get(`/api/user/${ctx.params.id}`)
62
+ * return (initProps, ctx) => (props) => h('div', {}, user.name)
63
+ * })
64
+ * ```
65
+ */
66
+ export declare function asyncComponent<C extends object = {}, P = {}>(factory: AsyncComponent<C, P>): AsyncComponent<C, P>;
67
+ /** 判定一个组件类型是否为 async 工厂(asyncComponent 包装过) */
68
+ export declare function isAsyncComponent(type: any): type is AsyncComponent;
40
69
  export declare const Fragment: unique symbol;
41
70
  /** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
42
71
  export declare const Portal: unique symbol;