weifuwu 0.33.4 → 0.33.5

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
@@ -66,7 +66,7 @@ app.mount('#root', AppShell)
66
66
  ```
67
67
 
68
68
  ```js
69
- // build.mjs
69
+ // build.mjs — 可选,也可用 ctx.ui.js() 服务端动态编译
70
70
  esbuild.build({
71
71
  entryPoints: ['src/main.tsx'],
72
72
  jsx: 'automatic',
@@ -75,6 +75,11 @@ esbuild.build({
75
75
  })
76
76
  ```
77
77
 
78
+ 或用服务端动态编译(无需独立构建脚本):
79
+ ```ts
80
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
81
+ ```
82
+
78
83
  ### Environment Variables
79
84
 
80
85
  | Variable | Default | Used by |
@@ -126,7 +131,7 @@ esbuild.build({
126
131
  | `redis()` | Redis client (`ctx.redis`) |
127
132
  | `queue()` | Job queue + cron |
128
133
  | `createHub()` | WebSocket pub/sub |
129
- | `ui()` | SPA HTML shell (`ctx.ui.html()`) |
134
+ | `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
130
135
 
131
136
  ### Frontend (weifuwu/client)
132
137
 
@@ -255,19 +260,74 @@ function ChatPage(_, ctx) {
255
260
  }
256
261
  ```
257
262
 
258
- ### Backend: ctx.ui.html()
263
+ ### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
259
264
 
260
265
  ```ts
261
- import { ui, serveStatic } from 'weifuwu'
266
+ import { ui } from 'weifuwu'
267
+
268
+ app.use(ui())
269
+
270
+ // SSR page — ctx.ui.html`` returns complete HTML Response
271
+ app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
272
+ <!DOCTYPE html>
273
+ <html>
274
+ <head>
275
+ <title>${post.title}</title>
276
+ <link rel="stylesheet" href="/static/style.css">
277
+ </head>
278
+ <body>
279
+ <div id="root">
280
+ <h1>${post.title}</h1>
281
+ <div>${ctx.ui.html.unsafe(post.body)}</div>
282
+ <div data-hydrate="like"></div>
283
+ </div>
284
+ <script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
285
+ <script src="/static/app.js"></script>
286
+ </body>
287
+ </html>
288
+ `)
289
+
290
+ // Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
291
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
292
+
293
+ // Dynamic CSS serving — ctx.ui.css() reads and serves CSS
294
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
262
295
 
263
- app.use(ui({ title: 'My App', script: '/static/app.js' }))
264
- app.get('/static/*', serveStatic('./dist/client'))
296
+ // Client hydrates interactive sections, skips SSR content
297
+ const app = createApp()
298
+ app.use(api())
299
+
300
+ const root = document.getElementById('root')
301
+ if (root && root.children.length > 0) {
302
+ // SSR page — hydrate only interactive areas
303
+ app.hydrate('[data-hydrate="like"]', LikeButton)
304
+ } else {
305
+ // SPA page — full mount
306
+ app.mount('#root', AppShell)
307
+ }
308
+ ```
265
309
 
266
- // SPA route
267
- app.get('/', async (req, ctx) => ctx.ui.html())
310
+ ### wrap() — Third-party Library Integration
311
+
312
+ ```tsx
313
+ import { wrap, effect } from 'weifuwu/client'
314
+ import * as echarts from 'echarts'
315
+
316
+ // wrap(tagName, setup) creates a component:
317
+ // - Creates a <div> container
318
+ // - Calls setup(el, props, ctx) when element enters the document
319
+ // - Runs cleanup when element is removed
320
+ const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
321
+ const chart = echarts.init(el)
322
+ chart.setOption({ series: [{ type: 'pie', data: props.data }] })
323
+ effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
324
+ return () => chart.dispose()
325
+ })
268
326
 
269
- // With initial props (accessible via window.__WFUI_PROPS__):
270
- app.get('/', async (req, ctx) => ctx.ui.html({ title: 'Custom', props: { user: userData } }))
327
+ // Use in JSX like any component
328
+ <Dashboard>
329
+ <PieChart data={salesData} />
330
+ </Dashboard>
271
331
  ```
272
332
 
273
333
  ### Show / For
@@ -796,12 +856,12 @@ src/
796
856
  ├── queue/ ← Job queue + cron
797
857
  ├── graphql.ts ← GraphQL
798
858
  ├── hub.ts ← WebSocket hub
799
- ├── ui/ ← SPA HTML shell (`ctx.ui.html()`)
800
- ├── client/ ← Frontend framework (~600 lines)
801
- │ ├── index.ts ← Entry
859
+ ├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
860
+ ├── client/ ← Frontend framework
861
+ │ ├── index.ts ← Entry (exports signal, wrap, createApp, ...)
802
862
  │ ├── signal.ts ← Signal / effect / computed
803
- │ ├── jsx-runtime.ts ← JSX → DOM / Show / For
804
- │ ├── app.ts ← createApp / middleware chain
863
+ │ ├── jsx-runtime.ts ← JSX → DOM / Show / For / wrap / onMount
864
+ │ ├── app.ts ← createApp / hydrate / middleware chain
805
865
  │ ├── router.ts ← Route matching / RouteView
806
866
  │ ├── types.ts ← WfuiContext / RouteDef
807
867
  │ ├── middleware/
@@ -811,14 +871,15 @@ src/
811
871
  │ └── components/
812
872
  │ ├── LoginForm.ts ← Login / register form
813
873
  │ └── Chat.ts ← Real-time messaging
814
- └── test/ ← 281 tests
874
+ └── test/ ← Tests
815
875
 
816
876
  apps/demo/ ← Full-stack demo
817
- ├── src/main.tsx ← SPA demo pages
877
+ ├── src/main.tsx ← SPA + SSR hydrate demo pages
818
878
  ├── server.ts ← weifuwu server
819
- ├── public/index.html
820
- ├── tsconfig.json
821
- └── scripts/build.mjs
879
+ ├── public/
880
+ ├── index.html ← HTML skeleton with placeholders
881
+ └── style.css ← Demo styles
882
+ └── tsconfig.json
822
883
 
823
884
  docker-compose.yml ← postgres (pgvector) + redis
824
885
  ```
@@ -21,4 +21,16 @@ export declare function createApp(): {
21
21
  ctx: WfuiContext;
22
22
  use: (mw: AppMiddleware) => any;
23
23
  mount: (rootSelector: string, RootComponent: Component) => Promise<void>;
24
+ /**
25
+ * Hydrate 一个已由 SSR 渲染的区域。
26
+ * 不清除目标容器内的内容,只附加组件输出。
27
+ *
28
+ * ```ts
29
+ * const app = createApp()
30
+ * app.use(api())
31
+ * app.use(auth())
32
+ * app.hydrate('#comments', CommentSection, { postId: '123' })
33
+ * ```
34
+ */
35
+ hydrate: (selector: string, Component: Component, props?: Record<string, unknown>) => void;
24
36
  };
@@ -15,7 +15,7 @@
15
15
  */
16
16
  export { signal, computed, effect, isSignal } from './signal.ts';
17
17
  export type { Signal } from './signal.ts';
18
- export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount } from './jsx-runtime.ts';
18
+ export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount, wrap } from './jsx-runtime.ts';
19
19
  export type { Component } from './jsx-runtime.ts';
20
20
  export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
21
21
  export { createApp } from './app.ts';
@@ -51,6 +51,60 @@ var currentCtx = null;
51
51
  function setCtx(ctx) {
52
52
  currentCtx = ctx;
53
53
  }
54
+ var _entries = /* @__PURE__ */ new Map();
55
+ function _ensure(el) {
56
+ let entry = _entries.get(el);
57
+ if (entry) return entry;
58
+ entry = {
59
+ mounted: document.contains(el),
60
+ observer: null,
61
+ mountFns: [],
62
+ disposeFns: []
63
+ };
64
+ _entries.set(el, entry);
65
+ const obs = new MutationObserver(() => {
66
+ const now = document.contains(el);
67
+ if (now && !entry.mounted) {
68
+ entry.mounted = true;
69
+ const fns = entry.mountFns.slice();
70
+ entry.mountFns = [];
71
+ for (const fn of fns) {
72
+ const dispose = fn();
73
+ if (typeof dispose === "function") {
74
+ entry.disposeFns.push(dispose);
75
+ }
76
+ }
77
+ } else if (!now && entry.mounted) {
78
+ entry.mounted = false;
79
+ for (const fn of entry.disposeFns) fn();
80
+ entry.disposeFns = [];
81
+ entry.mountFns = [];
82
+ obs.disconnect();
83
+ _entries.delete(el);
84
+ }
85
+ });
86
+ obs.observe(document.body, { childList: true, subtree: true });
87
+ entry.observer = obs;
88
+ return entry;
89
+ }
90
+ function onMount(el, fn) {
91
+ const entry = _ensure(el);
92
+ if (entry.mounted) {
93
+ const dispose = fn();
94
+ if (typeof dispose === "function") {
95
+ entry.disposeFns.push(dispose);
96
+ }
97
+ } else {
98
+ entry.mountFns.push(fn);
99
+ }
100
+ }
101
+ function wrap(tagName, setup) {
102
+ return (props, ctx) => {
103
+ const el = document.createElement(tagName);
104
+ onMount(el, () => setup(el, props, ctx));
105
+ return el;
106
+ };
107
+ }
54
108
  function setProp(el, key, value) {
55
109
  if (key === "class" || key === "className") {
56
110
  if (isSignal(value)) {
@@ -223,6 +277,18 @@ function createApp() {
223
277
  const app = jsx(RootComponent, {});
224
278
  domMount(rootSelector, app);
225
279
  setCtx(null);
280
+ },
281
+ hydrate(selector, Component, props) {
282
+ const root = document.querySelector(selector);
283
+ if (!root) {
284
+ console.warn(`hydrate target not found: ${selector}`);
285
+ return;
286
+ }
287
+ const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
288
+ setCtx(ctx);
289
+ const vnode = jsx(Component, mergedProps);
290
+ root.appendChild(vnode);
291
+ setCtx(null);
226
292
  }
227
293
  };
228
294
  }
@@ -690,5 +756,6 @@ export {
690
756
  jsxs,
691
757
  router,
692
758
  signal,
759
+ wrap,
693
760
  ws
694
761
  };
@@ -20,6 +20,33 @@ declare global {
20
20
  export declare function setCtx(ctx: WfuiContext | null): void;
21
21
  export declare function getCtx(): WfuiContext | null;
22
22
  export type Component<P = {}> = (props: P, ctx: WfuiContext) => Node;
23
+ /**
24
+ * 包装第三方库为组件 — 元素挂载后执行 setup,返回函数在元素移除时自动清理。
25
+ *
26
+ * 自动创建容器元素 + 自动管理生命周期。
27
+ * 返回标准的 (props, ctx) => Node 组件。
28
+ *
29
+ * ```tsx
30
+ * const PieChart = wrap('div', (el, props: { data: any }, ctx) => {
31
+ * el.style.cssText = 'width:100%;height:300px'
32
+ * const chart = echarts.init(el)
33
+ * chart.setOption(props.data)
34
+ * effect(() => chart.setOption(props.data))
35
+ * return () => chart.dispose()
36
+ * })
37
+ *
38
+ * const CanvasChart = wrap('canvas', (el, props, ctx) => {
39
+ * const chart = new Chart(el, { type: 'line', data: props.data })
40
+ * return () => chart.destroy()
41
+ * })
42
+ *
43
+ * // 在 JSX 中使用
44
+ * function Dashboard() {
45
+ * return <div><PieChart data={salesData} /><CanvasChart data={trendData} /></div>
46
+ * }
47
+ * ```
48
+ */
49
+ export declare function wrap<P = {}>(tagName: string, setup: (el: HTMLElement, props: P, ctx: WfuiContext) => (() => void) | void): Component<P>;
23
50
  export declare function jsx(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
24
51
  export declare function jsxs(type: string | Component, props: Record<string, unknown> | null, ...children: unknown[]): Node;
25
52
  export declare function jsxDEV(type: string | Component, props: Record<string, unknown> | null, _key: string | null, _isStatic: boolean, _source: {
@@ -51,6 +51,60 @@ var currentCtx = null;
51
51
  function setCtx(ctx) {
52
52
  currentCtx = ctx;
53
53
  }
54
+ var _entries = /* @__PURE__ */ new Map();
55
+ function _ensure(el) {
56
+ let entry = _entries.get(el);
57
+ if (entry) return entry;
58
+ entry = {
59
+ mounted: document.contains(el),
60
+ observer: null,
61
+ mountFns: [],
62
+ disposeFns: []
63
+ };
64
+ _entries.set(el, entry);
65
+ const obs = new MutationObserver(() => {
66
+ const now = document.contains(el);
67
+ if (now && !entry.mounted) {
68
+ entry.mounted = true;
69
+ const fns = entry.mountFns.slice();
70
+ entry.mountFns = [];
71
+ for (const fn of fns) {
72
+ const dispose = fn();
73
+ if (typeof dispose === "function") {
74
+ entry.disposeFns.push(dispose);
75
+ }
76
+ }
77
+ } else if (!now && entry.mounted) {
78
+ entry.mounted = false;
79
+ for (const fn of entry.disposeFns) fn();
80
+ entry.disposeFns = [];
81
+ entry.mountFns = [];
82
+ obs.disconnect();
83
+ _entries.delete(el);
84
+ }
85
+ });
86
+ obs.observe(document.body, { childList: true, subtree: true });
87
+ entry.observer = obs;
88
+ return entry;
89
+ }
90
+ function onMount(el, fn) {
91
+ const entry = _ensure(el);
92
+ if (entry.mounted) {
93
+ const dispose = fn();
94
+ if (typeof dispose === "function") {
95
+ entry.disposeFns.push(dispose);
96
+ }
97
+ } else {
98
+ entry.mountFns.push(fn);
99
+ }
100
+ }
101
+ function wrap(tagName, setup) {
102
+ return (props, ctx) => {
103
+ const el = document.createElement(tagName);
104
+ onMount(el, () => setup(el, props, ctx));
105
+ return el;
106
+ };
107
+ }
54
108
  function setProp(el, key, value) {
55
109
  if (key === "class" || key === "className") {
56
110
  if (isSignal(value)) {
@@ -223,6 +277,18 @@ function createApp() {
223
277
  const app = jsx(RootComponent, {});
224
278
  domMount(rootSelector, app);
225
279
  setCtx(null);
280
+ },
281
+ hydrate(selector, Component, props) {
282
+ const root = document.querySelector(selector);
283
+ if (!root) {
284
+ console.warn(`hydrate target not found: ${selector}`);
285
+ return;
286
+ }
287
+ const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
288
+ setCtx(ctx);
289
+ const vnode = jsx(Component, mergedProps);
290
+ root.appendChild(vnode);
291
+ setCtx(null);
226
292
  }
227
293
  };
228
294
  }
@@ -690,5 +756,6 @@ export {
690
756
  jsxs,
691
757
  router,
692
758
  signal,
759
+ wrap,
693
760
  ws
694
761
  };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export { compress } from './middleware/compress.ts';
23
23
  export type { CompressOptions } from './middleware/compress.ts';
24
24
  export { helmet } from './middleware/helmet.ts';
25
25
  export type { HelmetOptions } from './middleware/helmet.ts';
26
+ export { ui } from './ui/index.ts';
26
27
  export { graphql } from './graphql.ts';
27
28
  export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
28
29
  export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts';
@@ -47,6 +48,4 @@ export type { CMSAPI, CMSOptions, Content, ContentStatus, ContentType, Tag, TagW
47
48
  export { kb, KB } from './kb/index.ts';
48
49
  export type { KBAPI, KBOptions, Document, Chunk, SearchResult, ImportOptions, SearchOptions, } from './kb/types.ts';
49
50
  export type { BaseAPI, BaseDef, BaseOptions, TableSchema, FieldSchema, FieldType, ColumnMap, CreateBaseInput, UpdateBaseInput, QueryOptions, } from './base/types.ts';
50
- export { ui } from './ui/index.ts';
51
- export type { UiOptions, UiRenderOptions } from './ui/index.ts';
52
51
  export { requireRole } from './user/types.ts';
package/dist/index.js CHANGED
@@ -225,14 +225,14 @@ function serve(router, options) {
225
225
  if (!server.listening) return;
226
226
  server.close();
227
227
  server.closeIdleConnections();
228
- return new Promise((resolve3) => {
228
+ return new Promise((resolve4) => {
229
229
  const timer = setTimeout(() => {
230
230
  server.closeAllConnections();
231
- resolve3();
231
+ resolve4();
232
232
  }, timeoutMs);
233
233
  server.on("close", () => {
234
234
  clearTimeout(timer);
235
- resolve3();
235
+ resolve4();
236
236
  });
237
237
  });
238
238
  }
@@ -1384,6 +1384,90 @@ var DEFAULTS = {
1384
1384
  permissionsPolicy: "camera=(),display-capture=(),fullscreen=(),geolocation=(),microphone=()"
1385
1385
  };
1386
1386
 
1387
+ // src/ui/index.ts
1388
+ import { build } from "esbuild";
1389
+ import { readFile as readFile2 } from "node:fs/promises";
1390
+ import { resolve as resolve3 } from "node:path";
1391
+ var HtmlSafe = class {
1392
+ constructor(value) {
1393
+ this.value = value;
1394
+ }
1395
+ value;
1396
+ toString() {
1397
+ return this.value;
1398
+ }
1399
+ };
1400
+ function escape(s) {
1401
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1402
+ }
1403
+ function stringify(v) {
1404
+ if (v == null || v === false || v === true) return "";
1405
+ if (Array.isArray(v)) return v.map(stringify).join("");
1406
+ if (v instanceof HtmlSafe) return v.value;
1407
+ return escape(String(v));
1408
+ }
1409
+ function unsafe(s) {
1410
+ return new HtmlSafe(s);
1411
+ }
1412
+ var jsCache = /* @__PURE__ */ new Map();
1413
+ var cssCache = /* @__PURE__ */ new Map();
1414
+ function ui() {
1415
+ return async (_req, ctx, next) => {
1416
+ function htmlTag(strings, ...values) {
1417
+ let body = "";
1418
+ for (let i = 0; i < strings.length; i++) {
1419
+ body += strings[i];
1420
+ if (i < values.length) body += stringify(values[i]);
1421
+ }
1422
+ return new Response(body, {
1423
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1424
+ });
1425
+ }
1426
+ ctx.ui = {
1427
+ html: Object.assign(htmlTag, { unsafe }),
1428
+ async js(entryPath) {
1429
+ const absPath = resolve3(entryPath);
1430
+ const cached = jsCache.get(absPath);
1431
+ if (cached) {
1432
+ return new Response(cached.code, {
1433
+ headers: { "Content-Type": "application/javascript" }
1434
+ });
1435
+ }
1436
+ const result = await build({
1437
+ entryPoints: [absPath],
1438
+ bundle: true,
1439
+ format: "esm",
1440
+ platform: "browser",
1441
+ jsx: "automatic",
1442
+ jsxImportSource: "weifuwu/client",
1443
+ write: false
1444
+ });
1445
+ const code = result.outputFiles[0].text;
1446
+ jsCache.set(absPath, { code });
1447
+ return new Response(code, {
1448
+ headers: { "Content-Type": "application/javascript" }
1449
+ });
1450
+ },
1451
+ async css(entryPath) {
1452
+ const absPath = resolve3(entryPath);
1453
+ const stat = await import("node:fs").then((fs) => fs.promises.stat(absPath));
1454
+ const cached = cssCache.get(absPath);
1455
+ if (cached && cached.mtime === stat.mtimeMs) {
1456
+ return new Response(cached.code, {
1457
+ headers: { "Content-Type": "text/css; charset=utf-8" }
1458
+ });
1459
+ }
1460
+ const code = await readFile2(absPath, "utf-8");
1461
+ cssCache.set(absPath, { code, mtime: stat.mtimeMs });
1462
+ return new Response(code, {
1463
+ headers: { "Content-Type": "text/css; charset=utf-8" }
1464
+ });
1465
+ }
1466
+ };
1467
+ return next(_req, ctx);
1468
+ };
1469
+ }
1470
+
1387
1471
  // src/graphql.ts
1388
1472
  import {
1389
1473
  buildSchema,
@@ -4316,51 +4400,6 @@ function kb(opts) {
4316
4400
  mw.__meta = { injects: ["kb"], depends: ["sql"] };
4317
4401
  return mw;
4318
4402
  }
4319
-
4320
- // src/ui/index.ts
4321
- function defaultTemplate(title, script, propsJson) {
4322
- return `<!DOCTYPE html>
4323
- <html lang="zh-CN">
4324
- <head>
4325
- <meta charset="UTF-8">
4326
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4327
- <title>${escapeHtml(title)}</title>
4328
- </head>
4329
- <body>
4330
- <div id="root"></div>
4331
- ${propsJson ? `<script>window.__WFUI_PROPS__=${propsJson}</script>` : ""}
4332
- <script src="${escapeHtml(script)}"></script>
4333
- </body>
4334
- </html>`;
4335
- }
4336
- function escapeHtml(s) {
4337
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4338
- }
4339
- function ui(opts = {}) {
4340
- const defaultTitle = opts.title ?? "weifuwu";
4341
- const defaultScript = opts.script ?? "/static/app.js";
4342
- const template = opts.template;
4343
- return async (req, ctx, next) => {
4344
- ctx.ui = {
4345
- html(renderOpts = {}) {
4346
- const title = renderOpts.title ?? defaultTitle;
4347
- const script = renderOpts.script ?? defaultScript;
4348
- let propsJson = "";
4349
- if (renderOpts.props) {
4350
- try {
4351
- propsJson = JSON.stringify(renderOpts.props);
4352
- } catch {
4353
- }
4354
- }
4355
- const body = template ? template.replace(/\{\{title\}\}/g, escapeHtml(title)).replace(/\{\{script\}\}/g, escapeHtml(script)).replace(/\{\{props\}\}/g, propsJson) : defaultTemplate(title, script, propsJson);
4356
- return new Response(body, {
4357
- headers: { "Content-Type": "text/html; charset=utf-8" }
4358
- });
4359
- }
4360
- };
4361
- return next(req, ctx);
4362
- };
4363
- }
4364
4403
  export {
4365
4404
  Base,
4366
4405
  CMS,
@@ -1,37 +1,46 @@
1
1
  /**
2
- * ui 中间件 — 注入 ctx.ui.html()
2
+ * ui 中间件 — 注入 ctx.ui.html / ctx.ui.js / ctx.ui.css
3
3
  *
4
- * 返回 SPA HTML shell,加载前端 client bundle
4
+ * ctx.ui.html 是 tagged template,返回完整 HTML Response
5
+ * ctx.ui.js 编译 TSX 入口,返回 JS bundle Response。
6
+ * ctx.ui.css 读取/编译 CSS 入口,返回 CSS Response。
5
7
  *
6
8
  * ```ts
7
- * import { ui, serveStatic } from 'weifuwu'
9
+ * import { ui } from 'weifuwu'
8
10
  *
9
- * app.use(ui({ title: 'My App', script: '/static/app.js' }))
10
- * app.get('/static/*', serveStatic('./dist/client'))
11
- * app.get('*', async (req, ctx) => ctx.ui.html())
11
+ * app.use(ui())
12
+ *
13
+ * app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
14
+ * <!DOCTYPE html>
15
+ * <html>
16
+ * <head><title>${post.title}</title></head>
17
+ * <body>
18
+ * <div id="root"><article>...</article></div>
19
+ * <script src="/static/app.js"></script>
20
+ * </body>
21
+ * </html>
22
+ * `)
23
+ *
24
+ * app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
25
+ * app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
12
26
  * ```
13
27
  */
14
28
  import type { Middleware } from '../types.ts';
15
29
  declare module '../types.ts' {
16
30
  interface Context {
17
31
  ui: {
18
- /** 返回 SPA HTML 页面 */
19
- html: (opts?: UiRenderOptions) => Response;
32
+ /** Tagged template HTML Response */
33
+ html: UiHtmlTag;
34
+ /** 编译 TSX → JS bundle Response */
35
+ js: (entryPath: string) => Promise<Response>;
36
+ /** 读取/编译 CSS → CSS Response */
37
+ css: (entryPath: string) => Promise<Response>;
20
38
  };
21
39
  }
22
40
  }
23
- export interface UiOptions {
24
- /** 页面标题,默认 'weifuwu' */
25
- title?: string;
26
- /** Client bundle JS 路径,默认 '/static/app.js' */
27
- script?: string;
28
- /** 自定义 HTML 模板,覆盖默认 */
29
- template?: string;
30
- }
31
- export interface UiRenderOptions {
32
- title?: string;
33
- script?: string;
34
- /** 内嵌到页面的初始数据(通过 window.__WFUI_PROPS__ 访问) */
35
- props?: Record<string, unknown>;
41
+ interface UiHtmlTag {
42
+ (strings: TemplateStringsArray, ...values: unknown[]): Response;
43
+ unsafe: (s: string) => string;
36
44
  }
37
- export declare function ui(opts?: UiOptions): Middleware;
45
+ export declare function ui(): Middleware;
46
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.33.4",
4
+ "version": "0.33.5",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {