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 +217 -4
- package/dist/cli.js +241 -23
- package/dist/index.d.ts +9 -0
- package/dist/index.js +327 -507
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/module.d.ts +1 -5
- package/dist/postgres/types.d.ts +0 -7
- package/dist/ssr/assets.d.ts +20 -0
- package/dist/ssr/compile.d.ts +21 -0
- package/dist/ssr/css.d.ts +16 -0
- package/dist/ssr/html.d.ts +53 -0
- package/dist/ssr/layout.d.ts +2 -0
- package/dist/ssr/view.d.ts +36 -0
- package/dist/types.d.ts +6 -0
- package/package.json +6 -1
- package/dist/mailer.d.ts +0 -51
- package/dist/postgres/schema/columns.d.ts +0 -99
- package/dist/postgres/schema/index.d.ts +0 -6
- package/dist/postgres/schema/sql.d.ts +0 -22
- package/dist/postgres/schema/table.d.ts +0 -141
- package/dist/postgres/schema/where.d.ts +0 -29
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><script>...</script></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-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
101
|
-
console.log(` cd ${
|
|
102
|
-
if (
|
|
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>
|
|
111
|
-
npx weifuwu init <name> --
|
|
112
|
-
npx weifuwu
|
|
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: {
|
|
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(
|
|
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';
|