weifuwu 0.27.10 → 0.27.12

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,36 +66,27 @@ app.use(rateLimit({ window: 60 }))
66
66
  ## Full-stack SSR
67
67
 
68
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.
69
+ for safe HTML rendering, and `weifuwu-ui.js` for client-side interactions.
70
70
 
71
71
  ```ts
72
- import { Router, serve, html, raw, layout, view, cssContext, cssRouter, assetRouter } from 'weifuwu'
72
+ import { Router, serve, html, raw, layout, view, wfuwAssets, theme, i18n, flash } from 'weifuwu'
73
73
 
74
74
  const app = new Router()
75
75
 
76
76
  // Middleware
77
77
  app.use(theme())
78
78
  app.use(i18n({ dir: './locales' }))
79
- app.use(cssContext('./ui')) // compile globals.css → ctx.css
79
+ app.use(flash())
80
+
81
+ // weifuwu-ui frontend runtime
82
+ app.use('/', wfuwAssets())
80
83
 
81
84
  // Layout (wraps all pages)
82
85
  app.use(layout('./ui/app/layout.ts'))
83
86
 
84
- // Static assets (HTMX, Alpine — served locally, no CDN)
85
- app.use(assetRouter())
86
-
87
- // CSS serving
88
- app.use('/', cssRouter('./ui'))
89
-
90
87
  // Page
91
88
  app.get('/', view('./ui/app/page.ts'))
92
89
 
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
90
  // API
100
91
  app.get('/api/ping', () => Response.json({ pong: true }))
101
92
 
@@ -139,15 +130,17 @@ import { html, raw } from 'weifuwu'
139
130
  // ui/app/layout.ts
140
131
  export default function (body: string, ctx: any) {
141
132
  return html`<!DOCTYPE html>
142
- <html>
133
+ <html data-theme="${ctx.theme?.value || 'light'}">
143
134
  <head>
144
135
  <meta charset="utf-8" />
145
- <script src="/__wfw/js/htmx.min.js"></script>
146
- <script defer src="/__wfw/js/alpine.min.js"></script>
136
+ <link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
137
+ <script src="/__wfw/js/weifuwu-ui.js"></script>
138
+ <script id="__wfw-i18n" type="application/json">
139
+ ${raw(JSON.stringify(ctx.i18n?.messages || {}))}
140
+ </script>
147
141
  </head>
148
- <body class="min-h-screen bg-white dark:bg-gray-950">
142
+ <body data-locale="${ctx.i18n?.locale || 'en'}">
149
143
  ${raw(body)}
150
- <!-- ← use raw() for page content -->
151
144
  </body>
152
145
  </html>`
153
146
  }
@@ -167,29 +160,23 @@ export default function (ctx: any) {
167
160
  }
168
161
  ```
169
162
 
170
- ### CSS pipeline (Tailwind v4)
163
+ ### UI frontend runtime (weifuwu-ui)
171
164
 
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
- ```
165
+ weifuwu-ui is a zero-dependency frontend runtime (~5KB) that ships with weifuwu.
166
+ One `<script>` + `<link>` covers AJAX loading, state binding, SSE streaming,
167
+ WebSocket, theme/i18n/flash integration, and UI components.
179
168
 
180
169
  ```ts
181
- app.use(cssContext('./ui')) // compile ctx.css.url
182
- app.use(cssRouter('./ui')) // serve /__wfw/style/:hash.css
183
- ```
170
+ import { wfuwAssets } from 'weifuwu'
184
171
 
185
- ### Local assets (no CDN)
172
+ app.use(wfuwAssets()) // serve /__wfw/js/weifuwu-ui.js + /__wfw/css/weifuwu-ui.css
173
+ ```
186
174
 
187
- HTMX and Alpine.js are npm dependencies, served from the weifuwu server.
188
- No external network requests.
175
+ In your layout:
189
176
 
190
- ```ts
191
- app.use(assetRouter()) // serve /__wfw/js/htmx.min.js, alpine.min.js
192
- // In layout: ${assetScripts()}
177
+ ```html
178
+ <script src="/__wfw/js/weifuwu-ui.js"></script>
179
+ <link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
193
180
  ```
194
181
 
195
182
  ---
@@ -567,24 +554,20 @@ import { view } from 'weifuwu'
567
554
  app.get('/', view('./ui/app/page.ts'))
568
555
  ```
569
556
 
570
- #### cssContext() / cssRouter()
557
+ #### wfuwAssets()
571
558
 
572
- Tailwind v4 CSS compilation and serving.
559
+ Serve weifuwu-ui.js and weifuwu-ui.css zero-dependency frontend runtime (~5KB total).
560
+ Covers: AJAX loading, state binding, SSE streaming, WebSocket, theme/i18n/flash,
561
+ modal/collapse/tabs/dropdown/toast components.
573
562
 
574
563
  ```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
564
+ import { wfuwAssets } from 'weifuwu'
565
+ app.use(wfuwAssets())
578
566
  ```
579
567
 
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()}
568
+ ```html
569
+ <script src="/__wfw/js/weifuwu-ui.js"></script>
570
+ <link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
588
571
  ```
589
572
 
590
573
  ### Standalone utilities
@@ -666,15 +649,15 @@ throw new HttpError('Not found', 404) // caught by serve(), returns 404
666
649
  ## CLI
667
650
 
668
651
  ```bash
669
- npx weifuwu init my-app # Full-stack project (SSR + Tailwind + HTMX + Alpine)
652
+ npx weifuwu init my-app # Full-stack project (SSR + weifuwu-ui)
670
653
  npx weifuwu init my-app --minimal # Minimal API-only project
671
654
  npx weifuwu version # Print version
672
655
  ```
673
656
 
674
657
  ### Full-stack template (`init`)
675
658
 
676
- Generates a complete project with SSR, Tailwind CSS compilation, HTMX + Alpine served
677
- locally, theme switching, internationalization, and a demo home page.
659
+ Generates a complete project with SSR via `html()` tagged templates, weifuwu-ui frontend
660
+ runtime (zero external deps, ~5KB), theme switching, i18n, and flash messages.
678
661
 
679
662
  ```
680
663
  my-app/
@@ -682,9 +665,9 @@ my-app/
682
665
  app.ts — Router setup
683
666
  ui/
684
667
  app/
685
- globals.css — Tailwind v4
686
- layout.ts — root layout (HTMX + Alpine + theme script)
687
- page.ts — home page (theme/i18n demo)
668
+ globals.css — custom styles
669
+ layout.ts — root layout (weifuwu-ui + theme/i18n/flash)
670
+ page.ts — home page (wu-data/wu-theme/wu-lang demo)
688
671
  locales/
689
672
  en.json
690
673
  zh-CN.json
@@ -709,10 +692,11 @@ Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
709
692
  - `ws` — WebSocket
710
693
  - `zod` — Schema validation
711
694
 
712
- ### Frontend (served locally, no CDN)
695
+ ### Frontend
696
+
697
+ - weifuwu-ui.js (~5KB) — built-in, zero external dependencies
713
698
 
714
- - `tailwindcss`, `@tailwindcss/postcss`, `postcss` Tailwind v4 CSS compilation
715
- - `htmx.org` — HTML-over-the-wire dynamic interactions
716
- - `alpinejs` — Lightweight client interactivity
699
+ Covers: AJAX loading, state binding, SSE streaming, WebSocket, theme/i18n/flash,
700
+ modal/collapse/tabs/dropdown/toast components.
717
701
 
718
702
  Zero build tools. Zero frontend framework compilation.
package/dist/index.d.ts CHANGED
@@ -66,3 +66,4 @@ export { loadModule, clearModuleCache } from './ssr/compile.ts';
66
66
  export { cssContext, cssRouter, clearCSSCache } from './ssr/css.ts';
67
67
  export type { CssAsset } from './ssr/css.ts';
68
68
  export { assetRouter, assetScripts } from './ssr/assets.ts';
69
+ export { wfuwAssets } from './ssr/ui/assets.ts';
package/dist/index.js CHANGED
@@ -3594,6 +3594,7 @@ import { createHash } from "node:crypto";
3594
3594
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
3595
3595
  import { join as join3, resolve as resolve6, sep as sep2 } from "node:path";
3596
3596
  import tailwindPlugin from "@tailwindcss/postcss";
3597
+ import postcssNesting from "postcss-nesting";
3597
3598
  import postcss from "postcss";
3598
3599
  var cssCache = /* @__PURE__ */ new Map();
3599
3600
  function findTailwindDir() {
@@ -3625,7 +3626,7 @@ async function compileCSS(cssPath, sourceDir) {
3625
3626
  }
3626
3627
  const src = `@source "${sourceDir}";
3627
3628
  ${raw2}`;
3628
- const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
3629
+ const result = await postcss([tailwindPlugin(), postcssNesting()]).process(src, { from: cssPath });
3629
3630
  const hash = createHash("md5").update(result.css).digest("hex").slice(0, 8);
3630
3631
  const asset = { css: result.css, hash, url: `/__wfw/style/${hash}.css` };
3631
3632
  cssCache.set(cssPath, asset);
@@ -3658,51 +3659,60 @@ function clearCSSCache() {
3658
3659
  cssCache.clear();
3659
3660
  }
3660
3661
 
3661
- // ssr/assets.ts
3662
+ // ssr/ui/assets.ts
3662
3663
  import { readFileSync as readFileSync3 } from "node:fs";
3663
- import { resolve as resolve7 } from "node:path";
3664
- function resolvePackage(name, file) {
3665
- return resolve7(process.cwd(), "node_modules", name, file);
3666
- }
3667
- var HTMX_PATH = resolvePackage("htmx.org", "dist/htmx.min.js");
3668
- var ALPINE_PATH = resolvePackage("alpinejs", "dist/cdn.min.js");
3669
- var htmxContent = null;
3670
- var alpineContent = null;
3671
- function loadAsset(path) {
3672
- try {
3673
- return readFileSync3(path, "utf-8");
3674
- } catch {
3675
- return null;
3676
- }
3677
- }
3678
- function assetRouter() {
3664
+ import { resolve as resolve7, dirname } from "node:path";
3665
+ import { fileURLToPath } from "node:url";
3666
+ var __dirname = dirname(fileURLToPath(import.meta.url));
3667
+ function wfuwAssets() {
3679
3668
  const router = new Router();
3680
- router.get("/__wfw/js/htmx.min.js", () => {
3681
- if (!htmxContent) htmxContent = loadAsset(HTMX_PATH);
3682
- if (!htmxContent) return new Response("HTMX not found", { status: 404 });
3683
- return new Response(htmxContent, {
3669
+ const jsPath = resolve7(__dirname, "weifuwu-ui.js");
3670
+ const cssPath = resolve7(__dirname, "weifuwu-ui.css");
3671
+ let jsContent = null;
3672
+ let cssContent = null;
3673
+ router.get("/__wfw/js/weifuwu-ui.js", () => {
3674
+ if (!jsContent) {
3675
+ try {
3676
+ jsContent = readFileSync3(jsPath, "utf-8");
3677
+ } catch {
3678
+ return new Response("weifuwu-ui.js not found", { status: 404 });
3679
+ }
3680
+ }
3681
+ return new Response(jsContent, {
3684
3682
  headers: {
3685
3683
  "content-type": "application/javascript; charset=utf-8",
3686
3684
  "cache-control": "public, max-age=31536000, immutable"
3687
3685
  }
3688
3686
  });
3689
3687
  });
3690
- router.get("/__wfw/js/alpine.min.js", () => {
3691
- if (!alpineContent) alpineContent = loadAsset(ALPINE_PATH);
3692
- if (!alpineContent) return new Response("Alpine not found", { status: 404 });
3693
- return new Response(alpineContent, {
3688
+ router.get("/__wfw/css/weifuwu-ui.css", () => {
3689
+ if (!cssContent) {
3690
+ try {
3691
+ cssContent = readFileSync3(cssPath, "utf-8");
3692
+ } catch {
3693
+ return new Response("weifuwu-ui.css not found", { status: 404 });
3694
+ }
3695
+ }
3696
+ return new Response(cssContent, {
3694
3697
  headers: {
3695
- "content-type": "application/javascript; charset=utf-8",
3698
+ "content-type": "text/css; charset=utf-8",
3696
3699
  "cache-control": "public, max-age=31536000, immutable"
3697
3700
  }
3698
3701
  });
3699
3702
  });
3700
3703
  return router;
3701
3704
  }
3705
+
3706
+ // ssr/assets.ts
3707
+ function assetRouter() {
3708
+ console.warn("[weifuwu] assetRouter() is deprecated. Use wfuwAssets() instead.");
3709
+ return wfuwAssets();
3710
+ }
3702
3711
  function assetScripts() {
3712
+ console.warn("[weifuwu] assetScripts() is deprecated. Use weifuwu-ui.js directly.");
3703
3713
  return raw(`
3704
- <script src="/__wfw/js/htmx.min.js"></script>
3705
- <script defer src="/__wfw/js/alpine.min.js"></script>
3714
+ <script src="/__wfw/js/weifuwu-ui.js"></script>
3715
+ <link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css">
3706
3716
  `);
3707
3717
  }
3708
3718
  export {
@@ -3775,5 +3785,6 @@ export {
3775
3785
  upload,
3776
3786
  validate,
3777
3787
  view,
3788
+ wfuwAssets,
3778
3789
  withTestDb
3779
3790
  };
@@ -1,20 +1,5 @@
1
- import { Router } from '../core/router.ts';
2
1
  import { type RawString } from './html.ts';
3
- /**
4
- * Create a Router that serves HTMX and Alpine.js at `/__wfw/js/`.
5
- *
6
- * ```ts
7
- * app.use('/', assetRouter())
8
- * ```
9
- */
10
- export declare function assetRouter(): Router;
11
- /**
12
- * Generate `<script>` tags for HTMX and Alpine, pointing to local paths.
13
- *
14
- * ```ts
15
- * <head>
16
- * ${assetScripts()}
17
- * </head>
18
- * ```
19
- */
2
+ /** @deprecated Use {@link wfuwAssets} instead. */
3
+ export declare function assetRouter(): import("../index.ts").Router<import("../types.ts").Context>;
4
+ /** @deprecated Use `<script src="/__wfw/js/weifuwu-ui.js">` instead. */
20
5
  export declare function assetScripts(): RawString;
@@ -0,0 +1,2 @@
1
+ import { Router } from '../../core/router.ts';
2
+ export declare function wfuwAssets(): Router;
@@ -0,0 +1,20 @@
1
+ import { Router, layout, view, theme, i18n, flash, wfuwAssets } from 'weifuwu'
2
+
3
+ export const app = new Router()
4
+
5
+ // Core middleware
6
+ app.use(theme())
7
+ app.use(i18n({ dir: './locales' }))
8
+ app.use(flash())
9
+
10
+ // weifuwu-ui static assets
11
+ app.use('/', wfuwAssets())
12
+
13
+ // Layout
14
+ app.use(layout('./ui/app/layout.ts'))
15
+
16
+ // Pages
17
+ app.get('/', view('./ui/app/page.ts'))
18
+
19
+ // API route
20
+ app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))
@@ -0,0 +1,6 @@
1
+ import { loadEnv, serve } from 'weifuwu'
2
+ import { app } from './app.ts'
3
+
4
+ loadEnv()
5
+ const port = Number(process.env.PORT) || 3000
6
+ serve(app.handler(), { port })
@@ -0,0 +1,6 @@
1
+ {
2
+ "title": "Build APIs & UI, Zero Build Step",
3
+ "cta": "Try Me",
4
+ "docs": "Documentation",
5
+ "demo": "Click toggled content — zero JavaScript written, powered by weifuwu-ui."
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "title": "零编译构建 API 和 UI",
3
+ "cta": "体验一下",
4
+ "docs": "文档",
5
+ "demo": "点击切换内容——不需要写 JavaScript,由 weifuwu-ui 驱动。"
6
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "type": "module",
4
+ "scripts": {
5
+ "dev": "node --watch index.ts",
6
+ "start": "node index.ts"
7
+ },
8
+ "dependencies": {
9
+ "weifuwu": "^__VERSION__"
10
+ }
11
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "paths": {
11
+ "@/*": ["./ui/*"]
12
+ }
13
+ },
14
+ "include": ["*.ts", "ui/**/*.ts"]
15
+ }
@@ -0,0 +1,2 @@
1
+ /* Custom styles for your weifuwu app */
2
+ /* weifuwu-ui.css provides base UI component styles */
@@ -0,0 +1,39 @@
1
+ import { html, raw } from 'weifuwu'
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export default function (body: string, ctx: any) {
5
+ // Theme
6
+ const themeVal = ctx.theme?.value || 'system'
7
+ const resolvedTheme = themeVal === 'dark' ? 'dark' : 'light'
8
+
9
+ // i18n
10
+ const locale = ctx.i18n?.locale || 'en'
11
+ const messages = ctx.i18n?.messages || {}
12
+
13
+ return html`<!DOCTYPE html>
14
+ <html lang="${locale}" data-theme="${resolvedTheme}">
15
+ <head>
16
+ <meta charset="utf-8" />
17
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <title>weifuwu</title>
19
+ <link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
20
+ <script src="/__wfw/js/weifuwu-ui.js"></script>
21
+ <script id="__wfw-i18n" type="application/json">
22
+ ${raw(JSON.stringify(messages))}
23
+ </script>
24
+ ${ctx.flash?.value
25
+ ? raw(
26
+ `<script id="__wfw-flash" type="application/json">${JSON.stringify(ctx.flash.value)}</script>`,
27
+ )
28
+ : ''}
29
+ <style>
30
+ body {
31
+ margin: 0;
32
+ }
33
+ </style>
34
+ </head>
35
+ <body data-locale="${locale}">
36
+ ${raw(body)}
37
+ </body>
38
+ </html>`
39
+ }
@@ -0,0 +1,41 @@
1
+ import { html } from 'weifuwu'
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export default function (ctx: any) {
5
+ const t = ctx.i18n?.t || ((k: string) => k)
6
+ const theme = ctx.theme?.value || 'system'
7
+ const locale = ctx.i18n?.locale || 'en'
8
+
9
+ return html`<div wu-data="{ open: false }">
10
+ <!-- Navbar -->
11
+ <nav class="wu-flex wu-items-center wu-justify-between wu-p-4 wu-border-bottom">
12
+ <strong class="wu-text-lg">weifuwu</strong>
13
+ <div class="wu-flex wu-gap-sm wu-items-center">
14
+ <button wu-theme="${theme === 'dark' ? 'light' : 'dark'}" class="wu-btn wu-btn-sm">
15
+ ${theme === 'dark' ? '☀️' : '🌙'}
16
+ </button>
17
+ <button wu-lang="${locale === 'en' ? 'zh-CN' : 'en'}" class="wu-btn wu-btn-sm">
18
+ ${locale === 'en' ? '中文' : 'EN'}
19
+ </button>
20
+ </div>
21
+ </nav>
22
+
23
+ <!-- Hero -->
24
+ <section class="wu-p-4" style="max-width: 640px; margin: 80px auto; text-align: center;">
25
+ <h1 class="wu-text-2xl" style="margin-bottom: 8px;">${t('title')}</h1>
26
+ <p class="wu-text-secondary wu-text-md" style="margin-bottom: 32px;">
27
+ Pure Node.js, no build step
28
+ </p>
29
+
30
+ <div class="wu-flex wu-justify-center wu-gap-md">
31
+ <button class="wu-btn wu-btn-primary" wu-on="click: open = !open">${t('cta')}</button>
32
+ <a href="https://weifuwu.dev" class="wu-btn" target="_blank"> ${t('docs')} </a>
33
+ </div>
34
+
35
+ <!-- Demo: toggled content -->
36
+ <div wu-show="open" class="wu-card" style="margin-top: 24px; text-align: left;">
37
+ ${t('demo')}
38
+ </div>
39
+ </section>
40
+ </div>`
41
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * cn() — Merge class names, handling conditional and array inputs.
3
+ * Lightweight alternative to clsx + tailwind-merge.
4
+ */
5
+ export function cn(...classes: (string | false | null | undefined)[]): string {
6
+ return classes.filter(Boolean).join(' ')
7
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.27.10",
4
+ "version": "0.27.12",
5
5
  "description": "Web-standard HTTP microframework for Node.js — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": "./dist/index.js"
@@ -35,6 +35,7 @@
35
35
  "htmx.org": "^2",
36
36
  "ioredis": "^5.11.0",
37
37
  "postcss": "^8",
38
+ "postcss-nesting": "^14.0.0",
38
39
  "postgres": "^3.4.9",
39
40
  "tailwindcss": "^4",
40
41
  "ws": "^8",
@@ -57,6 +58,7 @@
57
58
  "eslint": "^10.5.0",
58
59
  "globals": "^17.6.0",
59
60
  "husky": "^9.1.7",
61
+ "jsdom": "^29.1.1",
60
62
  "lint-staged": "^17.0.7",
61
63
  "prettier": "^3.8.4",
62
64
  "typescript-eslint": "^8.61.0"