weifuwu 0.27.4 → 0.27.6

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
@@ -63,6 +63,137 @@ app.use(rateLimit({ window: 60 }))
63
63
 
64
64
  ---
65
65
 
66
+ ## Full-stack SSR
67
+
68
+ Server-rendered HTML with zero frontend build tools. Uses `html()` tagged templates
69
+ for safe HTML rendering, HTMX for dynamic interactions, and Alpine.js for client-side state.
70
+
71
+ ```ts
72
+ import { Router, serve, html, raw, layout, view, cssContext, cssRouter, assetRouter } from 'weifuwu'
73
+
74
+ const app = new Router()
75
+
76
+ // Middleware
77
+ app.use(theme())
78
+ app.use(i18n({ dir: './locales' }))
79
+ app.use(cssContext('./ui')) // compile globals.css → ctx.css
80
+
81
+ // Layout (wraps all pages)
82
+ app.use(layout('./ui/app/layout.ts'))
83
+
84
+ // Static assets (HTMX, Alpine — served locally, no CDN)
85
+ app.use(assetRouter())
86
+
87
+ // CSS serving
88
+ app.use('/', cssRouter('./ui'))
89
+
90
+ // Page
91
+ app.get('/', view('./ui/app/page.ts'))
92
+
93
+ // HTMX fragment handler
94
+ app.get('/users/table', async (req, ctx) => {
95
+ const users = await ctx.sql`SELECT * FROM users`
96
+ return html`${users.map((u) => html`<div>${u.name}</div>`)}`
97
+ })
98
+
99
+ // API
100
+ app.get('/api/ping', () => Response.json({ pong: true }))
101
+
102
+ serve(app.handler(), { port: 3000 })
103
+ ```
104
+
105
+ ### html() — Tagged template HTML
106
+
107
+ Safe HTML rendering with automatic escaping. Zero dependencies — uses JavaScript
108
+ tagged template literals.
109
+
110
+ ```ts
111
+ import { html, raw } from 'weifuwu'
112
+
113
+ // Auto-escaped
114
+ html`<h1>${userInput}</h1>`
115
+ // `<h1>&lt;script&gt;...&lt;/script&gt;</h1>`
116
+
117
+ // raw() bypasses escaping (for trusted HTML)
118
+ html`<div>${raw(body)}</div>`
119
+
120
+ // Arrays (from map)
121
+ html`<ul>
122
+ ${items.map((i) => html`<li>${i}</li>`)}
123
+ </ul>`
124
+
125
+ // Conditionals
126
+ html`${isAdmin && html`<button>Admin</button>`}`
127
+
128
+ // Nested html() is safe from double-escaping
129
+ html`<div>${html`<span>nested</span>`}</div>`
130
+ ```
131
+
132
+ ### layout() — Layout middleware
133
+
134
+ Wraps page HTML in a layout template. Multiple layouts nest naturally.
135
+
136
+ ```ts
137
+ import { html, raw } from 'weifuwu'
138
+
139
+ // ui/app/layout.ts
140
+ export default function (body: string, ctx: any) {
141
+ return html`<!DOCTYPE html>
142
+ <html>
143
+ <head>
144
+ <meta charset="utf-8" />
145
+ <script src="/__wfw/js/htmx.min.js"></script>
146
+ <script defer src="/__wfw/js/alpine.min.js"></script>
147
+ </head>
148
+ <body class="min-h-screen bg-white dark:bg-gray-950">
149
+ ${raw(body)}
150
+ <!-- ← use raw() for page content -->
151
+ </body>
152
+ </html>`
153
+ }
154
+ ```
155
+
156
+ ### view() — Page handler factory
157
+
158
+ Loads a `.ts` file and calls its default export to produce an HTML Response.
159
+
160
+ ```ts
161
+ // app.ts
162
+ app.get('/', view('./ui/app/page.ts'))
163
+
164
+ // ui/app/page.ts
165
+ export default function (ctx: any) {
166
+ return html`<h1 class="text-3xl font-bold">${ctx.i18n?.t('title')}</h1>`
167
+ }
168
+ ```
169
+
170
+ ### CSS pipeline (Tailwind v4)
171
+
172
+ Compiles `globals.css` via `@tailwindcss/postcss`. Cached and served with content hash.
173
+
174
+ ```css
175
+ /* ui/app/globals.css */
176
+ @import 'tailwindcss';
177
+ @custom-variant dark (&:is(.dark *));
178
+ ```
179
+
180
+ ```ts
181
+ app.use(cssContext('./ui')) // compile → ctx.css.url
182
+ app.use(cssRouter('./ui')) // serve /__wfw/style/:hash.css
183
+ ```
184
+
185
+ ### Local assets (no CDN)
186
+
187
+ HTMX and Alpine.js are npm dependencies, served from the weifuwu server.
188
+ No external network requests.
189
+
190
+ ```ts
191
+ app.use(assetRouter()) // serve /__wfw/js/htmx.min.js, alpine.min.js
192
+ // In layout: ${assetScripts()}
193
+ ```
194
+
195
+ ---
196
+
66
197
  ## Public API
67
198
 
68
199
  ### serve
@@ -405,6 +536,57 @@ app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
405
536
 
406
537
  Options: `dir`, `defaultLocale`, `cookie`, `param`, `header`
407
538
 
539
+ ### SSR utilities
540
+
541
+ #### html()
542
+
543
+ Tagged template literal for safe HTML. See [Full-stack SSR](#full-stack-ssr).
544
+
545
+ ```ts
546
+ import { html, raw } from 'weifuwu'
547
+
548
+ html`<h1>${title}</h1>` // auto-escaped
549
+ html`<div>${raw(html)}</div>` // unescaped
550
+ ```
551
+
552
+ #### layout()
553
+
554
+ Middleware that wraps page content in a layout template.
555
+
556
+ ```ts
557
+ import { layout } from 'weifuwu'
558
+ app.use(layout('./ui/app/layout.ts'))
559
+ ```
560
+
561
+ #### view()
562
+
563
+ Handler factory that loads a `.ts` file as a page.
564
+
565
+ ```ts
566
+ import { view } from 'weifuwu'
567
+ app.get('/', view('./ui/app/page.ts'))
568
+ ```
569
+
570
+ #### cssContext() / cssRouter()
571
+
572
+ Tailwind v4 CSS compilation and serving.
573
+
574
+ ```ts
575
+ import { cssContext, cssRouter } from 'weifuwu'
576
+ app.use(cssContext('./ui')) // compile → ctx.css
577
+ app.use(cssRouter('./ui')) // serve /__wfw/style/:hash.css
578
+ ```
579
+
580
+ #### assetRouter() / assetScripts()
581
+
582
+ Serve HTMX and Alpine.js from node_modules (no CDN).
583
+
584
+ ```ts
585
+ import { assetRouter, assetScripts } from 'weifuwu'
586
+ app.use(assetRouter())
587
+ // In layout: ${assetScripts()}
588
+ ```
589
+
408
590
  ### Standalone utilities
409
591
 
410
592
  #### SSE
@@ -484,17 +666,42 @@ throw new HttpError('Not found', 404) // caught by serve(), returns 404
484
666
  ## CLI
485
667
 
486
668
  ```bash
487
- npx weifuwu init my-api
488
- cd my-api
489
- npm run dev
669
+ npx weifuwu init my-app # Full-stack project (SSR + Tailwind + HTMX + Alpine)
670
+ npx weifuwu init my-app --minimal # Minimal API-only project
671
+ npx weifuwu version # Print version
490
672
  ```
491
673
 
674
+ ### Full-stack template (`init`)
675
+
676
+ Generates a complete project with SSR, Tailwind CSS compilation, HTMX + Alpine served
677
+ locally, theme switching, internationalization, and a demo home page.
678
+
679
+ ```
680
+ my-app/
681
+ index.ts — server entry
682
+ app.ts — Router setup
683
+ ui/
684
+ app/
685
+ globals.css — Tailwind v4
686
+ layout.ts — root layout (HTMX + Alpine + theme script)
687
+ page.ts — home page (theme/i18n demo)
688
+ locales/
689
+ en.json
690
+ zh-CN.json
691
+ package.json
692
+ tsconfig.json — with @/ path alias
693
+ ```
694
+
695
+ ### Minimal template (`init --minimal`)
696
+
492
697
  Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
493
698
 
494
699
  ---
495
700
 
496
701
  ## Dependencies
497
702
 
703
+ ### Backend
704
+
498
705
  - `postgres` — PostgreSQL client
499
706
  - `ioredis` — Redis client
500
707
  - `ai`, `@ai-sdk/openai` — AI SDK
@@ -502,4 +709,10 @@ Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
502
709
  - `ws` — WebSocket
503
710
  - `zod` — Schema validation
504
711
 
505
- Zero build tools. Zero frontend framework dependencies.
712
+ ### Frontend (served locally, no CDN)
713
+
714
+ - `tailwindcss`, `@tailwindcss/postcss`, `postcss` — Tailwind v4 CSS compilation
715
+ - `htmx.org` — HTML-over-the-wire dynamic interactions
716
+ - `alpinejs` — Lightweight client interactivity
717
+
718
+ Zero build tools. Zero frontend framework compilation.
package/dist/cli.js CHANGED
@@ -28,6 +28,13 @@ async function cmdInit(name, opts) {
28
28
  process.exit(1);
29
29
  }
30
30
  const pkg = await readPkg();
31
+ if (opts.minimal) {
32
+ await generateMinimal(targetDir, name, pkg.version, opts.skipInstall);
33
+ } else {
34
+ await generateFull(targetDir, name, pkg.version, opts.skipInstall);
35
+ }
36
+ }
37
+ async function generateMinimal(targetDir, name, version, skipInstall) {
31
38
  await mkdir(targetDir, { recursive: true });
32
39
  await writeFile(
33
40
  join(targetDir, "app.ts"),
@@ -53,24 +60,208 @@ async function cmdInit(name, opts) {
53
60
  ``
54
61
  ].join("\n")
55
62
  );
63
+ await writePackageJson(targetDir, name, version, {});
64
+ await writeCommonFiles(targetDir);
65
+ await finishInit(targetDir, skipInstall);
66
+ }
67
+ async function generateFull(targetDir, name, version, skipInstall) {
68
+ await mkdir(targetDir, { recursive: true });
69
+ await mkdir(join(targetDir, "ui", "app"), { recursive: true });
70
+ await mkdir(join(targetDir, "ui", "lib"), { recursive: true });
71
+ await mkdir(join(targetDir, "locales"), { recursive: true });
72
+ await writeFile(
73
+ join(targetDir, "app.ts"),
74
+ [
75
+ `import { Router, layout, view, theme, i18n, cssContext, cssRouter, assetRouter } from 'weifuwu'`,
76
+ ``,
77
+ `export const app = new Router()`,
78
+ ``,
79
+ `// Middleware`,
80
+ `app.use(theme())`,
81
+ `app.use(i18n({ dir: './locales' }))`,
82
+ `app.use(cssContext('./ui'))`,
83
+ ``,
84
+ `// Layout \u2014 wraps all pages`,
85
+ `app.use(layout('./ui/app/layout.ts'))`,
86
+ ``,
87
+ `// Static assets (HTMX, Alpine)`,
88
+ `app.use(assetRouter())`,
89
+ ``,
90
+ `// CSS serving`,
91
+ `app.use('/', cssRouter('./ui'))`,
92
+ ``,
93
+ `// Pages`,
94
+ `app.get('/', view('./ui/app/page.ts'))`,
95
+ ``,
96
+ `// API route`,
97
+ `app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))`,
98
+ ``
99
+ ].join("\n")
100
+ );
101
+ await writeFile(
102
+ join(targetDir, "index.ts"),
103
+ [
104
+ `import { loadEnv, serve } from 'weifuwu'`,
105
+ `import { app } from './app.ts'`,
106
+ ``,
107
+ `loadEnv()`,
108
+ `const port = Number(process.env.PORT) || 3000`,
109
+ `serve(app.handler(), { port })`,
110
+ ``
111
+ ].join("\n")
112
+ );
56
113
  await writeFile(
57
- join(targetDir, "package.json"),
114
+ join(targetDir, "ui", "app", "globals.css"),
115
+ [`@import "tailwindcss";`, `@custom-variant dark (&:is(.dark *));`, ``].join("\n")
116
+ );
117
+ await writeFile(
118
+ join(targetDir, "ui", "app", "layout.ts"),
119
+ [
120
+ `import { html, raw, assetScripts } from 'weifuwu'`,
121
+ ``,
122
+ `export default function(body: string, ctx: any) {`,
123
+ ` // Theme: resolve before paint to prevent flash`,
124
+ ` const themeScript = raw(\`<script>`,
125
+ `!function(){`,
126
+ `var t=(document.cookie.match(/(?:^|;\\s*)theme=([^;]+)/)||[])[1]||'system';`,
127
+ `if(t==='system')t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';`,
128
+ `document.documentElement.classList.toggle('dark',t==='dark');`,
129
+ `}()`,
130
+ `</script>\`)`,
131
+ ``,
132
+ ` // i18n: set lang attribute`,
133
+ ` const lang = ctx.i18n?.locale || 'en'`,
134
+ ``,
135
+ ` // CSS: include compiled stylesheet`,
136
+ ` const cssLink = ctx.css?.url`,
137
+ ` ? raw(\`<link rel="stylesheet" href="\${ctx.css.url}">\`)`,
138
+ ` : ''`,
139
+ ``,
140
+ ` return html\`<!DOCTYPE html>`,
141
+ `<html lang="\${lang}">`,
142
+ `<head>`,
143
+ ` <meta charset="utf-8" />`,
144
+ ` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
145
+ ` \${themeScript}`,
146
+ ` \${assetScripts()}`,
147
+ ` \${cssLink}`,
148
+ `</head>`,
149
+ `<body class="min-h-screen bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">`,
150
+ ` \${raw(body)}`,
151
+ `</body>`,
152
+ `</html>\``,
153
+ `}`,
154
+ ``
155
+ ].join("\n")
156
+ );
157
+ await writeFile(
158
+ join(targetDir, "ui", "app", "page.ts"),
159
+ [
160
+ `import { html } from 'weifuwu'`,
161
+ ``,
162
+ `export default function(ctx: any) {`,
163
+ ` const t = ctx.i18n?.t || ((k: string) => k)`,
164
+ ` const theme = ctx.theme?.value || 'system'`,
165
+ ` const locale = ctx.i18n?.locale || 'en'`,
166
+ ``,
167
+ ` return html\`<div x-data="{ open: false }" class="min-h-screen">`,
168
+ ` <!-- Navbar -->`,
169
+ ` <nav class="border-b border-gray-200 dark:border-gray-800">`,
170
+ ` <div class="max-w-5xl mx-auto flex items-center justify-between h-14 px-4">`,
171
+ ` <span class="font-bold text-lg">weifuwu</span>`,
172
+ ` <div class="flex items-center gap-3 text-sm">`,
173
+ ` <!-- Locale toggle -->`,
174
+ ` <a href="/__lang/\${locale === 'en' ? 'zh-CN' : 'en'}"`,
175
+ ` class="px-2 py-1 rounded border border-gray-300 dark:border-gray-600`,
176
+ ` hover:bg-gray-100 dark:hover:bg-gray-800 transition">`,
177
+ ` \${locale === 'en' ? '\u4E2D\u6587' : 'EN'}`,
178
+ ` </a>`,
179
+ ` <!-- Theme toggle -->`,
180
+ ` <a href="/__theme/\${theme === 'dark' ? 'light' : 'dark'}"`,
181
+ ` class="px-2 py-1 rounded border border-gray-300 dark:border-gray-600`,
182
+ ` hover:bg-gray-100 dark:hover:bg-gray-800 transition">`,
183
+ ` \${theme === 'dark' ? '\u2600\uFE0F' : '\u{1F319}'}`,
184
+ ` </a>`,
185
+ ` </div>`,
186
+ ` </div>`,
187
+ ` </nav>`,
188
+ ``,
189
+ ` <!-- Hero -->`,
190
+ ` <section class="max-w-3xl mx-auto px-4 py-16 text-center">`,
191
+ ` <h1 class="text-4xl font-bold tracking-tight mb-3">\${t('hero.title')}</h1>`,
192
+ ` <p class="text-gray-500 dark:text-gray-400 text-lg mb-8">`,
193
+ ` Pure Node.js, no build step`,
194
+ ` </p>`,
195
+ ``,
196
+ ` <div class="flex justify-center gap-3">`,
197
+ ` <button @click="open = !open"`,
198
+ ` class="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white`,
199
+ ` hover:bg-gray-700 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-300">`,
200
+ ` \${t('hero.cta')}`,
201
+ ` </button>`,
202
+ ` <a href="/docs"`,
203
+ ` class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium`,
204
+ ` hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800">`,
205
+ ` \${t('hero.docs')}`,
206
+ ` </a>`,
207
+ ` </div>`,
208
+ ``,
209
+ ` <!-- Alpine demo: click to reveal -->`,
210
+ ` <div x-show="open" x-cloak`,
211
+ ` class="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-sm text-left">`,
212
+ ` \${t('demo.alpine')}`,
213
+ ` </div>`,
214
+ ` </section>`,
215
+ ` </div>\``,
216
+ `}`,
217
+ ``
218
+ ].join("\n")
219
+ );
220
+ await writeFile(
221
+ join(targetDir, "ui", "lib", "utils.ts"),
222
+ [
223
+ `/**`,
224
+ ` * cn() \u2014 Merge class names, handling conditional and array inputs.`,
225
+ ` * Lightweight alternative to clsx + tailwind-merge.`,
226
+ ` */`,
227
+ `export function cn(...classes: (string | false | null | undefined)[]): string {`,
228
+ ` return classes.filter(Boolean).join(' ')`,
229
+ `}`,
230
+ ``
231
+ ].join("\n")
232
+ );
233
+ await writeFile(
234
+ join(targetDir, "locales", "en.json"),
58
235
  JSON.stringify(
59
236
  {
60
- name,
61
- type: "module",
62
- scripts: {
63
- dev: "node --watch index.ts",
64
- start: "node index.ts"
65
- },
66
- dependencies: {
67
- weifuwu: `^${pkg.version}`
68
- }
237
+ "hero.title": "Build APIs & UI, Zero Build Step",
238
+ "hero.cta": "Try Alpine",
239
+ "hero.docs": "Documentation",
240
+ "demo.alpine": "This is Alpine.js in action \u2014 click-toggled content, zero JavaScript written."
69
241
  },
70
242
  null,
71
243
  2
72
244
  ) + "\n"
73
245
  );
246
+ await writeFile(
247
+ join(targetDir, "locales", "zh-CN.json"),
248
+ JSON.stringify(
249
+ {
250
+ "hero.title": "\u96F6\u7F16\u8BD1\u6784\u5EFA API \u548C UI",
251
+ "hero.cta": "\u4F53\u9A8C Alpine",
252
+ "hero.docs": "\u6587\u6863",
253
+ "demo.alpine": "\u8FD9\u662F Alpine.js \u7684\u6F14\u793A\u2014\u2014\u70B9\u51FB\u5207\u6362\u5185\u5BB9\uFF0C\u4E0D\u9700\u8981\u5199 JavaScript\u3002"
254
+ },
255
+ null,
256
+ 2
257
+ ) + "\n"
258
+ );
259
+ await writePackageJson(targetDir, name, version, {
260
+ dependencies: {
261
+ weifuwu: `^${version}`
262
+ },
263
+ devDependencies: {}
264
+ });
74
265
  await writeFile(
75
266
  join(targetDir, "tsconfig.json"),
76
267
  JSON.stringify(
@@ -82,24 +273,45 @@ async function cmdInit(name, opts) {
82
273
  strict: true,
83
274
  skipLibCheck: true,
84
275
  noEmit: true,
85
- allowImportingTsExtensions: true
276
+ allowImportingTsExtensions: true,
277
+ paths: {
278
+ "@/*": ["./ui/*"]
279
+ }
86
280
  },
87
- include: ["*.ts"]
281
+ include: ["*.ts", "ui/**/*.ts"]
88
282
  },
89
283
  null,
90
284
  2
91
285
  ) + "\n"
92
286
  );
93
- await writeFile(join(targetDir, ".gitignore"), "node_modules\n.env\n");
287
+ await writeCommonFiles(targetDir);
288
+ await finishInit(targetDir, skipInstall);
289
+ }
290
+ async function writePackageJson(targetDir, name, version, extra) {
291
+ const pkg = {
292
+ name,
293
+ type: "module",
294
+ scripts: {
295
+ dev: "node --watch index.ts",
296
+ start: "node index.ts"
297
+ },
298
+ ...extra
299
+ };
300
+ await writeFile(join(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
301
+ }
302
+ async function writeCommonFiles(targetDir) {
303
+ await writeFile(join(targetDir, ".gitignore"), "node_modules\n.env\n.weifuwu\n");
94
304
  await writeFile(join(targetDir, ".env"), "PORT=3000\n");
95
- if (!opts.skipInstall) {
305
+ }
306
+ async function finishInit(targetDir, skipInstall) {
307
+ if (!skipInstall) {
96
308
  console.log("\nInstalling dependencies...");
97
309
  execSync("npm install", { cwd: targetDir, stdio: "inherit" });
98
310
  }
99
311
  console.log(`
100
- \u2705 Created ${name}/`);
101
- console.log(` cd ${name}`);
102
- if (opts.skipInstall) console.log(` npm install`);
312
+ \u2705 Created ${targetDir.split("/").pop()}/`);
313
+ console.log(` cd ${targetDir.split("/").pop()}`);
314
+ if (skipInstall) console.log(` npm install`);
103
315
  console.log(` npm run dev`);
104
316
  }
105
317
  var cmd = process.argv[2];
@@ -107,25 +319,31 @@ var HELP = `
107
319
  weifuwu \u2014 Web-standard HTTP microframework for Node.js
108
320
 
109
321
  Usage:
110
- npx weifuwu init <name> Create a new project
111
- npx weifuwu init <name> --skip-install Create project, skip npm install
112
- npx weifuwu version Print version
322
+ npx weifuwu init <name> Create a new project (SSR + shadcn UI)
323
+ npx weifuwu init <name> --minimal Create a minimal API-only project
324
+ npx weifuwu init <name> --skip-install Skip npm install
325
+ npx weifuwu version Print version
113
326
  `;
114
327
  if (cmd === "version" || cmd === "-v" || cmd === "--version") {
115
328
  cmdVersion().catch(console.error);
116
329
  } else if (cmd === "init") {
117
330
  const { values, positionals } = parseArgs({
118
331
  args: process.argv.slice(3),
119
- options: { "skip-install": { type: "boolean" } },
332
+ options: {
333
+ "skip-install": { type: "boolean" },
334
+ minimal: { type: "boolean" }
335
+ },
120
336
  strict: false,
121
337
  allowPositionals: true
122
338
  });
123
339
  const name = positionals[0];
124
340
  if (!name) {
125
- console.error("Usage: npx weifuwu init <name> [--skip-install]");
341
+ console.error("Usage: npx weifuwu init <name> [--skip-install] [--minimal]");
126
342
  process.exit(1);
127
343
  }
128
- cmdInit(name, { skipInstall: !!values["skip-install"] }).catch(console.error);
344
+ cmdInit(name, { skipInstall: !!values["skip-install"], minimal: !!values["minimal"] }).catch(
345
+ console.error
346
+ );
129
347
  } else {
130
348
  console.log(HELP);
131
349
  }
package/dist/index.d.ts CHANGED
@@ -57,3 +57,12 @@ export { flash } from './middleware/flash.ts';
57
57
  export type { FlashOptions, FlashInjected, FlashModule } from './middleware/flash.ts';
58
58
  export { csrf } from './middleware/csrf.ts';
59
59
  export type { CsrfOptions, CsrfInjected, CsrfModule } from './middleware/csrf.ts';
60
+ export { html, raw } from './ssr/html.ts';
61
+ export type { RawString } from './ssr/html.ts';
62
+ export { layout } from './ssr/layout.ts';
63
+ export { view } from './ssr/view.ts';
64
+ export type { ViewOptions } from './ssr/view.ts';
65
+ export { loadModule, clearModuleCache } from './ssr/compile.ts';
66
+ export { cssContext, cssRouter, clearCSSCache } from './ssr/css.ts';
67
+ export type { CssAsset } from './ssr/css.ts';
68
+ export { assetRouter, assetScripts } from './ssr/assets.ts';