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,8 @@
1
1
  // src/client/vnode.ts
2
+ var ASYNC_MARK = "__wfAsyncComponent";
3
+ function isAsyncComponent(type) {
4
+ return typeof type === "function" && type?.[ASYNC_MARK] === true;
5
+ }
2
6
  var Fragment = /* @__PURE__ */ Symbol("Fragment");
3
7
  var Portal = /* @__PURE__ */ Symbol("Portal");
4
8
  function h(type, props, ...children) {
@@ -467,10 +471,11 @@ function renderValue(v, ctx) {
467
471
  }
468
472
  if (vnode.type === Fragment) {
469
473
  const frag = document.createDocumentFragment();
470
- forEach(vnode.props?.children, (child) => {
474
+ const children = vnode.props?.children == null ? [] : Array.isArray(vnode.props.children) ? vnode.props.children : [vnode.props.children];
475
+ for (const child of children) {
471
476
  const node = renderValue(child, ctx);
472
477
  if (node != null) frag.appendChild(node);
473
- });
478
+ }
474
479
  return frag;
475
480
  }
476
481
  if (typeof vnode.type === "function") {
@@ -513,6 +518,54 @@ function renderValue(v, ctx) {
513
518
  if (typeof vnode.props?.ref === "function") vnode.props.ref(el);
514
519
  return el;
515
520
  }
521
+ var asyncFactoryCache = /* @__PURE__ */ new WeakMap();
522
+ function startAsyncFactory(Comp, ctx) {
523
+ const existing = asyncFactoryCache.get(Comp);
524
+ if (existing) return existing;
525
+ const entry = { promise: null };
526
+ entry.promise = Promise.resolve().then(() => Comp(ctx)).then((def) => {
527
+ if (typeof def !== "function") {
528
+ throw new Error(
529
+ `asyncComponent factory <${Comp.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
530
+ );
531
+ }
532
+ entry.resolved = def;
533
+ return def;
534
+ });
535
+ asyncFactoryCache.set(Comp, entry);
536
+ return entry;
537
+ }
538
+ function resolveAsyncFactorySync(Comp) {
539
+ return asyncFactoryCache.get(Comp)?.resolved;
540
+ }
541
+ function scheduleFullReRender(ctx) {
542
+ const ui = ctx.ui;
543
+ if (ui && typeof ui.render === "function") ui.render(["_wf_root"]);
544
+ }
545
+ function mountComponent(Comp, props, vnode, ctx) {
546
+ let def;
547
+ if (isAsyncComponent(Comp)) {
548
+ def = resolveAsyncFactorySync(Comp);
549
+ if (!def) {
550
+ void startAsyncFactory(Comp, ctx).promise.then(
551
+ () => scheduleFullReRender(ctx),
552
+ () => {
553
+ }
554
+ );
555
+ return null;
556
+ }
557
+ } else {
558
+ def = Comp;
559
+ }
560
+ let childVNode = def(props, ctx);
561
+ if (typeof childVNode !== "function") {
562
+ throw new Error(
563
+ `Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
564
+ );
565
+ }
566
+ vnode._render = childVNode;
567
+ return childVNode(props);
568
+ }
516
569
  function renderComponent(Comp, props, vnode, ctx) {
517
570
  ;
518
571
  ctx.ui = ctx.ui ?? {};
@@ -527,14 +580,7 @@ function renderComponent(Comp, props, vnode, ctx) {
527
580
  vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
528
581
  let childVNode;
529
582
  try {
530
- childVNode = Comp(props, childCtx);
531
- if (typeof childVNode !== "function") {
532
- throw new Error(
533
- `Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
534
- );
535
- }
536
- vnode._render = childVNode;
537
- childVNode = childVNode(props);
583
+ childVNode = mountComponent(Comp, props, vnode, childCtx);
538
584
  } catch (e) {
539
585
  const errHandler = ctx.ui?._errorHandler;
540
586
  if (errHandler) {
@@ -591,14 +637,6 @@ function renderPortal(vnode, ctx) {
591
637
  if (node != null) sub.appendChild(node);
592
638
  }
593
639
  }
594
- function forEach(children, fn) {
595
- if (children == null) return;
596
- if (Array.isArray(children)) {
597
- children.forEach(fn);
598
- return;
599
- }
600
- fn(children);
601
- }
602
640
  function flattenChildren(children) {
603
641
  if (children == null) return [];
604
642
  if (!Array.isArray(children)) return [children];
@@ -0,0 +1,23 @@
1
+ // src/dev/index.ts
2
+ import { registerHooks } from "node:module";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { transformSync } from "esbuild";
6
+ registerHooks({
7
+ load(url, context, next) {
8
+ if (url.startsWith("file:") && (url.endsWith(".ts") || url.endsWith(".tsx"))) {
9
+ const file = fileURLToPath(url);
10
+ const code = readFileSync(file, "utf-8");
11
+ const result = transformSync(code, {
12
+ loader: url.endsWith(".tsx") ? "tsx" : "ts",
13
+ jsx: "automatic",
14
+ jsxImportSource: "weifuwu/client",
15
+ format: "esm",
16
+ sourcemap: "inline",
17
+ target: "node22"
18
+ });
19
+ return { format: "module", source: result.code, shortCircuit: true };
20
+ }
21
+ return next(url, context);
22
+ }
23
+ });
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export type { CORSOptions } from './middleware/cors.ts';
12
12
  export { serveStatic } from './middleware/static.ts';
13
13
  export type { ServeStaticOptions } from './middleware/static.ts';
14
14
  export { ui } from './ui/index.ts';
15
+ export { uiSsr } from './ui/ssr-page.ts';
16
+ export type { UiSsrOptions } from './ui/ssr-page.ts';
15
17
  export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
16
18
  export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts';
17
19
  export type { PostgresOptions, PostgresClient, PostgresInjected } from './postgres/types.ts';
package/dist/index.js CHANGED
@@ -1083,23 +1083,219 @@ import { build } from "esbuild";
1083
1083
  import { readFile, stat } from "node:fs/promises";
1084
1084
  import { resolve as resolve2 } from "node:path";
1085
1085
  import { fileURLToPath } from "node:url";
1086
+
1087
+ // src/ui/html-safe.ts
1086
1088
  var HtmlSafe = class {
1089
+ value;
1087
1090
  constructor(value) {
1088
1091
  this.value = value;
1089
1092
  }
1090
- value;
1091
1093
  toString() {
1092
1094
  return this.value;
1093
1095
  }
1094
1096
  };
1097
+
1098
+ // src/client/vnode.ts
1099
+ var ASYNC_MARK = "__wfAsyncComponent";
1100
+ function isAsyncComponent(type) {
1101
+ return typeof type === "function" && type?.[ASYNC_MARK] === true;
1102
+ }
1103
+ var Fragment = /* @__PURE__ */ Symbol("Fragment");
1104
+ var Portal = /* @__PURE__ */ Symbol("Portal");
1105
+
1106
+ // src/client/reactive.ts
1107
+ function createReactiveState(dirty) {
1108
+ const proxyCache = /* @__PURE__ */ new WeakMap();
1109
+ const reactive = (target) => {
1110
+ if (target === null || typeof target !== "object") return target;
1111
+ if (proxyCache.has(target)) return proxyCache.get(target);
1112
+ const proxy = new Proxy(target, {
1113
+ set(target2, key, value) {
1114
+ const old = Reflect.get(target2, key);
1115
+ if (old === value) return true;
1116
+ Reflect.set(target2, key, value);
1117
+ dirty();
1118
+ return true;
1119
+ },
1120
+ get(target2, key) {
1121
+ const value = Reflect.get(target2, key);
1122
+ if (typeof value === "object" && value !== null) return reactive(value);
1123
+ return value;
1124
+ },
1125
+ deleteProperty(target2, key) {
1126
+ if (Reflect.has(target2, key)) {
1127
+ Reflect.deleteProperty(target2, key);
1128
+ dirty();
1129
+ }
1130
+ return true;
1131
+ }
1132
+ });
1133
+ proxyCache.set(target, proxy);
1134
+ return proxy;
1135
+ };
1136
+ return reactive({});
1137
+ }
1138
+
1139
+ // src/ui/ssr.ts
1095
1140
  function escape(s) {
1096
1141
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1097
1142
  }
1143
+ var VOID_TAGS = /* @__PURE__ */ new Set([
1144
+ "area",
1145
+ "base",
1146
+ "br",
1147
+ "col",
1148
+ "embed",
1149
+ "hr",
1150
+ "img",
1151
+ "input",
1152
+ "link",
1153
+ "meta",
1154
+ "param",
1155
+ "source",
1156
+ "track",
1157
+ "wbr"
1158
+ ]);
1159
+ function classToString(v) {
1160
+ if (typeof v === "string") return v;
1161
+ if (Array.isArray(v)) return v.filter(Boolean).join(" ");
1162
+ if (v && typeof v === "object") {
1163
+ return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
1164
+ }
1165
+ return "";
1166
+ }
1167
+ function styleToString(v) {
1168
+ if (typeof v === "string") return v;
1169
+ if (v && typeof v === "object") {
1170
+ return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
1171
+ }
1172
+ return "";
1173
+ }
1174
+ function createSsrContext(serverCtx, dataStore) {
1175
+ const usedIds = /* @__PURE__ */ new Set();
1176
+ const ui2 = {
1177
+ $: () => createReactiveState(() => {
1178
+ }),
1179
+ dirty: () => {
1180
+ },
1181
+ render: () => {
1182
+ },
1183
+ selfId: (name) => {
1184
+ if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
1185
+ usedIds.add(name);
1186
+ },
1187
+ useMedia: () => {
1188
+ },
1189
+ useBreakpoint: () => {
1190
+ },
1191
+ usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
1192
+ } })
1193
+ };
1194
+ const ctx = Object.create(serverCtx ?? {});
1195
+ ctx.ui = ui2;
1196
+ ctx.data = {
1197
+ async get(key, fetcher) {
1198
+ if (dataStore.has(key)) return dataStore.get(key);
1199
+ if (!fetcher) return void 0;
1200
+ const val = await fetcher();
1201
+ dataStore.set(key, val);
1202
+ return val;
1203
+ },
1204
+ set(key, val) {
1205
+ dataStore.set(key, val);
1206
+ },
1207
+ has(key) {
1208
+ return dataStore.has(key);
1209
+ }
1210
+ };
1211
+ return ctx;
1212
+ }
1213
+ async function renderSsr(input, ctx) {
1214
+ if (input == null || typeof input === "boolean") return "";
1215
+ if (typeof input === "string" || typeof input === "number") return escape(String(input));
1216
+ if (Array.isArray(input)) {
1217
+ let s = "";
1218
+ for (const c of input) s += await renderSsr(c, ctx);
1219
+ return s;
1220
+ }
1221
+ const vnode = input;
1222
+ if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
1223
+ if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
1224
+ if (typeof vnode.type === "function") {
1225
+ let def;
1226
+ if (isAsyncComponent(vnode.type)) {
1227
+ def = await vnode.type(ctx);
1228
+ if (typeof def !== "function") {
1229
+ throw new Error(
1230
+ `asyncComponent factory <${vnode.type.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
1231
+ );
1232
+ }
1233
+ } else {
1234
+ def = vnode.type;
1235
+ }
1236
+ const childCtx = Object.create(ctx);
1237
+ const renderFn = def(vnode.props ?? {}, childCtx);
1238
+ if (typeof renderFn !== "function") {
1239
+ throw new Error(
1240
+ `Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
1241
+ );
1242
+ }
1243
+ return renderSsr(renderFn(vnode.props ?? {}), childCtx);
1244
+ }
1245
+ const tag = vnode.type;
1246
+ const props = vnode.props ?? {};
1247
+ const attrs = [];
1248
+ let innerHTML;
1249
+ for (const [key, value] of Object.entries(props)) {
1250
+ if (key === "children" || key === "key") continue;
1251
+ if (key === "ref") continue;
1252
+ if (key.startsWith("on") && typeof value === "function") continue;
1253
+ if (key === "innerHTML") {
1254
+ innerHTML = String(value ?? "");
1255
+ continue;
1256
+ }
1257
+ if (key === "class" || key === "className") {
1258
+ const cls = classToString(value);
1259
+ if (cls) attrs.push(` class="${escape(cls)}"`);
1260
+ continue;
1261
+ }
1262
+ if (key === "style" && value && typeof value === "object") {
1263
+ attrs.push(` style="${escape(styleToString(value))}"`);
1264
+ continue;
1265
+ }
1266
+ if (value === true) {
1267
+ attrs.push(` ${key}`);
1268
+ continue;
1269
+ }
1270
+ if (value === false || value == null) continue;
1271
+ attrs.push(` ${key}="${escape(String(value))}"`);
1272
+ }
1273
+ const attrStr = attrs.join("");
1274
+ if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
1275
+ if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
1276
+ const children = await renderSsr(props.children, ctx);
1277
+ return `<${tag}${attrStr}>${children}</${tag}>`;
1278
+ }
1279
+ async function ssrToString(Comp, props, serverCtx, opts = {}) {
1280
+ const dataStore = opts.data ?? /* @__PURE__ */ new Map();
1281
+ const ctx = createSsrContext(serverCtx, dataStore);
1282
+ const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
1283
+ return new HtmlSafe(html);
1284
+ }
1285
+ function serializeData(data) {
1286
+ const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
1287
+ return `<script>window.__DATA__=${json};</script>`;
1288
+ }
1289
+
1290
+ // src/ui/index.ts
1291
+ function escape2(s) {
1292
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1293
+ }
1098
1294
  function stringify(v) {
1099
1295
  if (v == null || v === false || v === true) return "";
1100
1296
  if (Array.isArray(v)) return v.map(stringify).join("");
1101
1297
  if (v instanceof HtmlSafe) return v.value;
1102
- return escape(String(v));
1298
+ return escape2(String(v));
1103
1299
  }
1104
1300
  function unsafe(s) {
1105
1301
  return new HtmlSafe(s);
@@ -1153,6 +1349,15 @@ function ui() {
1153
1349
  }
1154
1350
  ctx.ui = {
1155
1351
  html: Object.assign(htmlTag, { unsafe }),
1352
+ /** SSR 渲染组件 → HTML 片段(HtmlSafe:内联进 ctx.ui.html 不二次转义) */
1353
+ async ssr(Comp, props, opts) {
1354
+ const safe = await ssrToString(Comp, props ?? {}, ctx, opts);
1355
+ return safe;
1356
+ },
1357
+ /** 序列化 SSR 数据存储 → window.__DATA__ 脚本(HtmlSafe) */
1358
+ ssrData(data) {
1359
+ return new HtmlSafe(serializeData(data));
1360
+ },
1156
1361
  async js(entryPath) {
1157
1362
  const absPath = resolveEntry(entryPath);
1158
1363
  const cached = jsCache.get(absPath);
@@ -1217,6 +1422,98 @@ function ui() {
1217
1422
  };
1218
1423
  }
1219
1424
 
1425
+ // src/client/route-match.ts
1426
+ function joinPaths(a, b) {
1427
+ if (!b || b === "/") return a || "/";
1428
+ const left = a.endsWith("/") ? a.slice(0, -1) : a;
1429
+ const right = b.startsWith("/") ? b : "/" + b;
1430
+ return left + right;
1431
+ }
1432
+ function compilePath(path) {
1433
+ const keys = [];
1434
+ const reStr = path.replace(/:(\w+)/g, (_, key) => {
1435
+ keys.push(key);
1436
+ return "([^/]+)";
1437
+ }).replace(/\*/g, ".*");
1438
+ return { re: new RegExp(`^${reStr}$`), keys };
1439
+ }
1440
+ function flattenRoutes(routes, basePath = "", chain = []) {
1441
+ const result = [];
1442
+ for (const route of routes) {
1443
+ const fullPath = joinPaths(basePath, route.path);
1444
+ const { re, keys } = compilePath(fullPath);
1445
+ const fullChain = [...chain, route];
1446
+ result.push({ re, keys, def: route, chain: fullChain });
1447
+ if (route.children) {
1448
+ result.push(...flattenRoutes(route.children, fullPath, fullChain));
1449
+ }
1450
+ }
1451
+ return result;
1452
+ }
1453
+ function matchRoute(path, routes) {
1454
+ let best = null;
1455
+ for (const fr of routes) {
1456
+ if (path.match(fr.re)) {
1457
+ if (!best || fr.chain.length > best.chain.length) best = fr;
1458
+ }
1459
+ }
1460
+ return best;
1461
+ }
1462
+ function extractParams(path, match) {
1463
+ const params = {};
1464
+ const m = path.match(match.re);
1465
+ if (m) {
1466
+ for (let i = 0; i < match.keys.length; i++) {
1467
+ params[match.keys[i]] = decodeURIComponent(m[i + 1]);
1468
+ }
1469
+ }
1470
+ return params;
1471
+ }
1472
+
1473
+ // src/ui/ssr-page.ts
1474
+ var DEFAULT_TEMPLATE = (p) => `<!DOCTYPE html>
1475
+ <html lang="zh-CN">
1476
+ <head>
1477
+ <meta charset="UTF-8">
1478
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1479
+ <title>${escapeHtml(p.title)}</title>
1480
+ ${(p.styles ?? []).map((s) => `<link rel="stylesheet" href="${escapeHtml(s)}">`).join("\n ")}
1481
+ </head>
1482
+ <body>
1483
+ <div id="root">${p.html}</div>
1484
+ ${p.dataScript}
1485
+ ${p.bundle ? `<script src="${escapeHtml(p.bundle)}"></script>` : ""}
1486
+ </body>
1487
+ </html>`;
1488
+ function escapeHtml(s) {
1489
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1490
+ }
1491
+ function uiSsr(opts) {
1492
+ const flatRoutes = flattenRoutes(opts.routes);
1493
+ const template = opts.template ?? DEFAULT_TEMPLATE;
1494
+ return async (req, ctx, next) => {
1495
+ if (req.method !== "GET") return next(req, ctx);
1496
+ const url = new URL(req.url);
1497
+ const match = matchRoute(url.pathname, flatRoutes);
1498
+ if (!match) return next(req, ctx);
1499
+ const params = extractParams(url.pathname, match);
1500
+ const def = match.chain[match.chain.length - 1] ?? match.def;
1501
+ const pageCtx = Object.assign(Object.create(ctx), {
1502
+ params,
1503
+ route: { path: url.pathname, params, query: Object.fromEntries(url.searchParams) }
1504
+ });
1505
+ if (!def.component) return next(req, ctx);
1506
+ const data = /* @__PURE__ */ new Map();
1507
+ const html = await ssrToString(def.component, {}, pageCtx, { data });
1508
+ const title = opts.title ? opts.title({ params, def }) : def.title || "weifuwu";
1509
+ const dataScript = new HtmlSafe(serializeData(data)).toString();
1510
+ const body = template({ html: html.toString(), dataScript, title, bundle: opts.bundle, styles: opts.styles });
1511
+ return new Response(body, {
1512
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1513
+ });
1514
+ };
1515
+ }
1516
+
1220
1517
  // src/db/postgres/connection.ts
1221
1518
  import net from "node:net";
1222
1519
  import crypto2 from "node:crypto";
@@ -2962,6 +3259,7 @@ export {
2962
3259
  serverError,
2963
3260
  tooManyRequests,
2964
3261
  ui,
3262
+ uiSsr,
2965
3263
  unauthorized,
2966
3264
  unprocessable
2967
3265
  };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * HtmlSafe — 标记"已是安全 HTML"的字符串(SSR 输出/unsafe 用户输入)。
3
+ *
4
+ * ctx.ui.html 的 stringify 对插值默认转义;instanceof HtmlSafe 的值原样内联。
5
+ * 与 ctx.ui.ssr 的产物共用同一类,保证模板内联不二次转义。
6
+ */
7
+ export declare class HtmlSafe {
8
+ value: string;
9
+ constructor(value: string);
10
+ toString(): string;
11
+ }
@@ -26,6 +26,7 @@
26
26
  * ```
27
27
  */
28
28
  import type { Middleware } from '../types.ts';
29
+ import type { Component, AsyncComponent } from '../client/vnode.ts';
29
30
  declare module '../types.ts' {
30
31
  interface Context {
31
32
  ui: {
@@ -35,6 +36,16 @@ declare module '../types.ts' {
35
36
  js: (entryPath: string) => Promise<Response>;
36
37
  /** 读取 CSS → CSS Response(支持包名 weifuwu/layout、weifuwu/components/style.css 或文件路径) */
37
38
  css: (entryPath: string) => Promise<Response>;
39
+ /**
40
+ * SSR 渲染组件 → HTML 片段(HtmlSafe,可直接内联进 ctx.ui.html 模板)
41
+ * 支持 async 工厂组件:await 工厂 → 数据进 HTML。
42
+ * opts.data(Map)收集 ctx.data 预取结果,用 ctx.ui.ssrData(data) 序列化进 __DATA__。
43
+ */
44
+ ssr: (Comp: Component | AsyncComponent, props?: Record<string, any>, opts?: {
45
+ data?: Map<string, unknown>;
46
+ }) => Promise<string>;
47
+ /** 序列化 SSR 数据存储 → <script>window.__DATA__=...</script>(HtmlSafe) */
48
+ ssrData: (data: Map<string, unknown>) => string;
38
49
  };
39
50
  }
40
51
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * uiSsr — 路由级 SSR 中间件:前端路由表驱动的自动服务端渲染
3
+ *
4
+ * 开发者只声明路由(path + component),GET 匹配即自动 SSR:
5
+ * 匹配 → 注入 params → ctx.ui.ssr 渲染组件(async 工厂 await)→ 页面模板 + __DATA__
6
+ * 未匹配 → next()(交给后续 API/静态/404)
7
+ *
8
+ * 与客户端 router() 共用同一份路由定义(route-match.ts),SPA/SSR 一个声明。
9
+ */
10
+ import type { Middleware } from '../types.ts';
11
+ import type { RouteDef } from '../client/types.ts';
12
+ export interface UiSsrOptions {
13
+ /** 前端路由定义(与 router() 共享同一份) */
14
+ routes: RouteDef[];
15
+ /** 客户端 bundle 路径(自动注入 <script src>,如 '/static/app.js') */
16
+ bundle?: string;
17
+ /** CSS 路径数组(自动注入 <link rel="stylesheet">,如 ['/static/style.css']) */
18
+ styles?: string[];
19
+ /** 自定义 title(默认取路由定义 title 或 'weifuwu') */
20
+ title?: (ctx: {
21
+ params: Record<string, string>;
22
+ def: RouteDef;
23
+ }) => string;
24
+ /** 自定义页面模板:返回完整 HTML(默认内置,含 head/root/__DATA__/styles/bundle) */
25
+ template?: (parts: {
26
+ html: string;
27
+ dataScript: string;
28
+ title: string;
29
+ bundle?: string;
30
+ styles?: string[];
31
+ }) => string;
32
+ }
33
+ /**
34
+ * 路由级 SSR 中间件:
35
+ *
36
+ * ```ts
37
+ * const app = new Router()
38
+ * app.use(uiSsr({ routes, bundle: '/static/app.js' }))
39
+ * ```
40
+ */
41
+ export declare function uiSsr(opts: UiSsrOptions): Middleware;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 服务端 SSR 字符串遍历器 — async 工厂组件(形态 C)的 HTML 序列化
3
+ *
4
+ * 支持:
5
+ * - 同步组件(mount → render → VNode → HTML)
6
+ * - async 工厂组件(await 工厂 → 数据进 HTML;服务端不缓存定义,数据 per-request)
7
+ * - Fragment / Portal(内联子节点)
8
+ * - innerHTML prop(原样输出)、class/style 对象序列化
9
+ * - 事件处理器 / ref 剥离、文本自动转义(XSS)
10
+ * - 服务端 ctx shim($ no-op dirty、ctx.data 预取、selfId 隔离)
11
+ *
12
+ * 产物为 HtmlSafe 标记的安全 HTML 片段,可直接内联进 ctx.ui.html 模板。
13
+ */
14
+ import type { Component, AsyncComponent } from '../client/vnode.ts';
15
+ import { HtmlSafe } from './html-safe.ts';
16
+ export { HtmlSafe } from './html-safe.ts';
17
+ export interface SsrOptions {
18
+ /** 服务端数据存储:ctx.data 预取结果写入此处,调用方序列化进 __DATA__ */
19
+ data?: Map<string, unknown>;
20
+ }
21
+ /**
22
+ * SSR 渲染组件 → HTML 片段(HtmlSafe 标记,可直接内联进 ctx.ui.html 模板,不二次转义)。
23
+ *
24
+ * ```ts
25
+ * const data = new Map()
26
+ * const html = await ctx.ui.ssr(BlogPage, { slug }, { data })
27
+ * return ctx.ui.html`
28
+ * <div id="root">${html}</div>
29
+ * ${ctx.ui.ssrData(data)}
30
+ * `
31
+ * ```
32
+ */
33
+ export declare function ssrToString(Comp: Component | AsyncComponent, props: Record<string, any>, serverCtx: any, opts?: SsrOptions): Promise<HtmlSafe>;
34
+ /** 序列化数据存储 → window.__DATA__ 脚本(防 XSS:转义 <) */
35
+ export declare function serializeData(data: Map<string, unknown>): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.55.3",
4
+ "version": "0.56.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {
@@ -22,7 +22,8 @@
22
22
  "types": "./dist/components/index.d.ts",
23
23
  "default": "./dist/components/index.js"
24
24
  },
25
- "./components/style.css": "./dist/components/style.css"
25
+ "./components/style.css": "./dist/components/style.css",
26
+ "./dev": "./dist/dev/index.js"
26
27
  },
27
28
  "files": [
28
29
  "dist/",
@@ -35,7 +36,9 @@
35
36
  "pretest": "docker compose up -d --wait postgres redis 2>/dev/null; sleep 1",
36
37
  "test": "node --env-file=.env --test 'src/test/**/*.test.ts' 'src/components/**/*.test.ts' 'src/db/**/*.test.ts'",
37
38
  "release": "node scripts/release.mjs",
38
- "release:dry": "node scripts/release.mjs --dry-run"
39
+ "release:dry": "node scripts/release.mjs --dry-run",
40
+ "demo": "node --import weifuwu/dev --env-file=.env apps/demo/server.ts",
41
+ "demo:build": "node apps/demo/scripts/build.mjs"
39
42
  },
40
43
  "dependencies": {
41
44
  "esbuild": "^0.28.1",