weifuwu 0.18.1 → 0.18.2
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 +127 -61
- package/cli.ts +7 -6
- package/dist/cli.js +7 -6
- package/dist/index.d.ts +1 -2
- package/dist/index.js +511 -1002
- package/dist/router.d.ts +1 -0
- package/dist/ssr/compile.d.ts +2 -0
- package/dist/ssr/error-boundary.d.ts +2 -0
- package/dist/ssr/index.d.ts +7 -0
- package/dist/ssr/index.js +936 -0
- package/dist/ssr/layout.d.ts +2 -0
- package/dist/ssr/live.d.ts +6 -0
- package/dist/ssr/not-found.d.ts +2 -0
- package/dist/ssr/ssr.d.ts +3 -0
- package/dist/ssr/stream.d.ts +14 -0
- package/dist/ssr/tailwind.d.ts +2 -0
- package/dist/types.d.ts +5 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -15,11 +15,14 @@ serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { serve, Router,
|
|
18
|
+
import { serve, Router, preferences } from 'weifuwu'
|
|
19
|
+
import { ssr, layout, liveReload } from 'weifuwu/ssr'
|
|
19
20
|
const app = new Router()
|
|
20
21
|
app.use(preferences({ dir: './locales' }))
|
|
21
|
-
app.use('/
|
|
22
|
-
|
|
22
|
+
app.use(layout('./layouts/root.tsx'))
|
|
23
|
+
app.get('/', ssr('./pages/home.tsx'))
|
|
24
|
+
app.use(liveReload({ dirs: ['./pages', './layouts'] }))
|
|
25
|
+
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
```bash
|
|
@@ -107,13 +110,18 @@ app.use(rateLimit({ max: 100 })) // with .stop()
|
|
|
107
110
|
### Pattern β — Router
|
|
108
111
|
|
|
109
112
|
```ts
|
|
110
|
-
app.use('/health', health()) //
|
|
113
|
+
app.use('/health', health()) // with path
|
|
111
114
|
app.use('/graphql', graphql(handler))
|
|
112
115
|
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
|
|
113
116
|
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
|
|
114
117
|
app.ws('/ws', messager({ pg }).wsHandler())
|
|
115
118
|
```
|
|
116
119
|
|
|
120
|
+
β modules can also be mounted **without a path** — internal routes (`/__xxx`) are inaccessible to the user:
|
|
121
|
+
```ts
|
|
122
|
+
app.use(liveReload({ dirs: ['./pages'] })) // no path, /__weifuwu/livereload
|
|
123
|
+
```
|
|
124
|
+
|
|
117
125
|
β modules that need **separate middleware** use `.middleware()`:
|
|
118
126
|
```ts
|
|
119
127
|
const a = analytics()
|
|
@@ -558,6 +566,102 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
|
|
|
558
566
|
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
559
567
|
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
560
568
|
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## React SSR (weifuwu/ssr)
|
|
572
|
+
|
|
573
|
+
Import from `'weifuwu/ssr'`:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { ssr, layout, liveReload, errorBoundary, notFound, tailwind } from 'weifuwu/ssr'
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### ssr(path) [β]
|
|
580
|
+
|
|
581
|
+
Compiles a `.tsx` file and returns a Router handler that renders the React component to HTML with streaming, client bundle injection, and context serialization.
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
app.get('/about', ssr('./pages/about.tsx'))
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
- Compiles via esbuild at runtime (no build step)
|
|
588
|
+
- Reads `ctx.layoutStack` (set by `layout()` middleware) and wraps the component from outer to inner
|
|
589
|
+
- Injects hydration script pointing to the auto-generated client bundle at `/__ssr/[hash].js`
|
|
590
|
+
- Serializes middleware-injected `ctx` data to `window.__WEIFUWU_CTX` for client-side hydration
|
|
591
|
+
- Dev mode: injects live reload WebSocket script
|
|
592
|
+
|
|
593
|
+
### layout(path) [β]
|
|
594
|
+
|
|
595
|
+
Compiles a `.tsx` file and returns middleware that pushes the layout component onto `ctx.layoutStack`. Pages rendered by `ssr()` consume this stack.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
app.use(layout('./layouts/root.tsx')) // outermost
|
|
599
|
+
app.use('/blog', layout('./layouts/blog.tsx')) // inner
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Layout components receive `{ children }` (the child page or nested layout). Multiple layouts wrap from outer to inner in `use()` order.
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
// layouts/root.tsx
|
|
606
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
607
|
+
return <html><head/><body><main>{children}</main></body></html>
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### liveReload(opts) [β]
|
|
612
|
+
|
|
613
|
+
Returns a `Router` that registers a WebSocket endpoint at `/__weifuwu/livereload` and starts a file watcher on the given directories. When a `.tsx` file changes, it clears the compile cache and broadcasts a reload to all connected browsers.
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
617
|
+
app.use(liveReload({ dirs: ['./pages', './layouts'] }))
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Mount without a path — the internal `/__weifuwu/livereload` route is invisible to the user. The `ssr()` function automatically injects the client-side WS script in dev mode.
|
|
622
|
+
|
|
623
|
+
| Option | Type | Default | Description |
|
|
624
|
+
|--------|------|---------|-------------|
|
|
625
|
+
| `dirs` | `string[]` | — | Directories to watch for `.tsx` changes |
|
|
626
|
+
|
|
627
|
+
Returns `Router & { close: () => void }` — call `.close()` to stop the watcher.
|
|
628
|
+
|
|
629
|
+
### errorBoundary(path) [β]
|
|
630
|
+
|
|
631
|
+
Wraps child routes in an error boundary. If a page or middleware throws, the error component is rendered instead.
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
app.use('/blog', errorBoundary('./blog-error.tsx'))
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
The error component receives `{ error, reset }` as props:
|
|
638
|
+
|
|
639
|
+
```tsx
|
|
640
|
+
export default function BlogError({ error, reset }: { error: Error; reset: () => void }) {
|
|
641
|
+
return <div><h2>Error</h2><p>{error.message}</p></div>
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Error boundaries nest — the nearest one up the middleware chain catches the error.
|
|
646
|
+
|
|
647
|
+
### notFound(path) [β]
|
|
648
|
+
|
|
649
|
+
Returns a catch-all handler for 404 pages. Typically registered last:
|
|
650
|
+
|
|
651
|
+
```ts
|
|
652
|
+
app.all('/*', notFound('./not-found.tsx'))
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### tailwind(path) [α]
|
|
656
|
+
|
|
657
|
+
Compiles Tailwind CSS v4 via `@tailwindcss/postcss` and serves it at `/__wfw/style.css`. In dev mode, watches the CSS file for changes.
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
app.use(tailwind('./app.css'))
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
When `tailwind()` middleware is detected, `ssr()` automatically injects `<link rel="stylesheet" href="/__wfw/style.css" />` into the HTML `<head>`.
|
|
664
|
+
|
|
561
665
|
### seo [β] + seoMiddleware [α]
|
|
562
666
|
|
|
563
667
|
```ts
|
|
@@ -648,58 +752,6 @@ app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string(
|
|
|
648
752
|
})
|
|
649
753
|
// Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
|
|
650
754
|
```
|
|
651
|
-
|
|
652
|
-
---
|
|
653
|
-
|
|
654
|
-
## React SSR (tsx)
|
|
655
|
-
|
|
656
|
-
```ts
|
|
657
|
-
app.use('/', await tsx({ dir: './ui/' }))
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
```
|
|
661
|
-
ui/
|
|
662
|
-
├── pages/
|
|
663
|
-
│ ├── page.tsx → GET /
|
|
664
|
-
│ ├── layout.tsx → root layout
|
|
665
|
-
│ ├── not-found.tsx → 404
|
|
666
|
-
│ ├── about/page.tsx → GET /about
|
|
667
|
-
│ ├── blog/[slug]/
|
|
668
|
-
│ │ ├── page.tsx → GET /blog/:slug
|
|
669
|
-
│ │ ├── load.ts → server data fetching
|
|
670
|
-
│ │ └── route.ts → API (named exports: POST, PUT...)
|
|
671
|
-
│ ├── blog/layout.tsx → nested layout
|
|
672
|
-
│ └── api/search/
|
|
673
|
-
│ └── route.ts → GET /api/search
|
|
674
|
-
└── components/
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
```tsx
|
|
678
|
-
// page.tsx
|
|
679
|
-
export default function Page() {
|
|
680
|
-
const { t } = useLocale()
|
|
681
|
-
const data = useLoaderData()
|
|
682
|
-
return <h1>{t('title') ?? data.title}</h1>
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
```tsx
|
|
687
|
-
// layout.tsx
|
|
688
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
689
|
-
return <html><head/><body><main>{children}</main></body></html>
|
|
690
|
-
}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
```ts
|
|
694
|
-
// load.ts — server-only data fetching
|
|
695
|
-
export default async function load({ params, query }) { return { data: await db.query(params.slug) } }
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
```ts
|
|
699
|
-
// route.ts — API co-located with page
|
|
700
|
-
export const POST: Handler = async (req, ctx) => Response.json({ slug: ctx.params.slug })
|
|
701
|
-
```
|
|
702
|
-
|
|
703
755
|
### Client-side navigation
|
|
704
756
|
|
|
705
757
|
```tsx
|
|
@@ -710,7 +762,7 @@ const navigate = useNavigate() // programmatic: navigate('/contact
|
|
|
710
762
|
const loading = useNavigating() // reactive loading state
|
|
711
763
|
```
|
|
712
764
|
|
|
713
|
-
`navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place.
|
|
765
|
+
`navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. Middleware runs on server each nav — data is always fresh.
|
|
714
766
|
|
|
715
767
|
**Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
|
|
716
768
|
|
|
@@ -795,16 +847,18 @@ function ThemeToggle() {
|
|
|
795
847
|
|
|
796
848
|
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
797
849
|
|
|
798
|
-
**`useLoaderData()`** — Returns
|
|
850
|
+
**`useLoaderData()`** — Returns middleware-injected data from the request context. Works identically on server (SSR) and client (hydration/SPA). Re-renders on SPA navigation.
|
|
799
851
|
|
|
800
852
|
```tsx
|
|
801
853
|
import { useLoaderData } from 'weifuwu/react'
|
|
802
854
|
function Page() {
|
|
803
|
-
const data = useLoaderData<{
|
|
804
|
-
return <
|
|
855
|
+
const data = useLoaderData<{ posts: Post[] }>()
|
|
856
|
+
return <ul>{data.posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
|
|
805
857
|
}
|
|
806
858
|
```
|
|
807
859
|
|
|
860
|
+
On the server, data flows from middleware → `ctx` → `ctx.loaderData` (serialized). On the client, it's restored from `window.__WEIFUWU_CTX`. Under the hood, `useLoaderData()` uses `AsyncLocalStorage` on the server and `window.__WEIFUWU_CTX` on the client — no SSR-specific code needed in your components.
|
|
861
|
+
|
|
808
862
|
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
809
863
|
|
|
810
864
|
```ts
|
|
@@ -836,7 +890,19 @@ function Toast() {
|
|
|
836
890
|
|
|
837
891
|
### Dev mode
|
|
838
892
|
|
|
839
|
-
Auto-detected when `NODE_ENV !== 'production'`. File watching
|
|
893
|
+
Auto-detected when `NODE_ENV !== 'production'`. File watching + live reload via `liveReload()`:
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
import { liveReload } from 'weifuwu/ssr'
|
|
897
|
+
|
|
898
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
899
|
+
app.use(liveReload({ dirs: ['./pages', './layouts'] }))
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
When a `.tsx` file changes, `ssr()` clears its compile cache and the browser auto-refreshes. No process restart needed.
|
|
904
|
+
|
|
905
|
+
Tailwind v4 auto-compile via `tailwind()` middleware:
|
|
840
906
|
|
|
841
907
|
---
|
|
842
908
|
|
package/cli.ts
CHANGED
|
@@ -94,14 +94,15 @@ async function cmdInit(name: string) {
|
|
|
94
94
|
].join('\n'))
|
|
95
95
|
|
|
96
96
|
await writeFile(join(targetDir, 'app.ts'), [
|
|
97
|
-
"import { serve, Router, loadEnv
|
|
97
|
+
"import { serve, Router, loadEnv } from 'weifuwu'",
|
|
98
|
+
"import { ssr, layout } from 'weifuwu/ssr'",
|
|
98
99
|
'',
|
|
99
100
|
"loadEnv()",
|
|
100
101
|
"const port = Number(process.env.PORT) || 3000",
|
|
101
102
|
'',
|
|
102
103
|
"const app = new Router()",
|
|
103
|
-
"
|
|
104
|
-
"app.
|
|
104
|
+
"app.use(layout('./ui/layout.tsx'))",
|
|
105
|
+
"app.get('/', ssr('./ui/page.tsx'))",
|
|
105
106
|
'',
|
|
106
107
|
"app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))",
|
|
107
108
|
'',
|
|
@@ -113,11 +114,11 @@ async function cmdInit(name: string) {
|
|
|
113
114
|
'',
|
|
114
115
|
].join('\n'))
|
|
115
116
|
|
|
116
|
-
await mkdir(join(targetDir, 'ui'
|
|
117
|
+
await mkdir(join(targetDir, 'ui'), { recursive: true })
|
|
117
118
|
|
|
118
119
|
await writeFile(join(targetDir, 'ui', 'app.css'), '@import "tailwindcss";\n')
|
|
119
120
|
|
|
120
|
-
await writeFile(join(targetDir, 'ui', '
|
|
121
|
+
await writeFile(join(targetDir, 'ui', 'layout.tsx'), [
|
|
121
122
|
"import { ReactNode } from 'react'",
|
|
122
123
|
'',
|
|
123
124
|
'export default function RootLayout({ children }: { children: ReactNode }) {',
|
|
@@ -136,7 +137,7 @@ async function cmdInit(name: string) {
|
|
|
136
137
|
'',
|
|
137
138
|
].join('\n'))
|
|
138
139
|
|
|
139
|
-
await writeFile(join(targetDir, 'ui', '
|
|
140
|
+
await writeFile(join(targetDir, 'ui', 'page.tsx'), [
|
|
140
141
|
"import { useState } from 'react'",
|
|
141
142
|
"import { useWebsocket } from 'weifuwu/react'",
|
|
142
143
|
'',
|
package/dist/cli.js
CHANGED
|
@@ -84,14 +84,15 @@ async function cmdInit(name) {
|
|
|
84
84
|
""
|
|
85
85
|
].join("\n"));
|
|
86
86
|
await writeFile(join(targetDir, "app.ts"), [
|
|
87
|
-
"import { serve, Router, loadEnv
|
|
87
|
+
"import { serve, Router, loadEnv } from 'weifuwu'",
|
|
88
|
+
"import { ssr, layout } from 'weifuwu/ssr'",
|
|
88
89
|
"",
|
|
89
90
|
"loadEnv()",
|
|
90
91
|
"const port = Number(process.env.PORT) || 3000",
|
|
91
92
|
"",
|
|
92
93
|
"const app = new Router()",
|
|
93
|
-
"
|
|
94
|
-
"app.
|
|
94
|
+
"app.use(layout('./ui/layout.tsx'))",
|
|
95
|
+
"app.get('/', ssr('./ui/page.tsx'))",
|
|
95
96
|
"",
|
|
96
97
|
"app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))",
|
|
97
98
|
"",
|
|
@@ -102,9 +103,9 @@ async function cmdInit(name) {
|
|
|
102
103
|
"console.log(`Listening on http://localhost:${server.port}`)",
|
|
103
104
|
""
|
|
104
105
|
].join("\n"));
|
|
105
|
-
await mkdir(join(targetDir, "ui"
|
|
106
|
+
await mkdir(join(targetDir, "ui"), { recursive: true });
|
|
106
107
|
await writeFile(join(targetDir, "ui", "app.css"), '@import "tailwindcss";\n');
|
|
107
|
-
await writeFile(join(targetDir, "ui", "
|
|
108
|
+
await writeFile(join(targetDir, "ui", "layout.tsx"), [
|
|
108
109
|
"import { ReactNode } from 'react'",
|
|
109
110
|
"",
|
|
110
111
|
"export default function RootLayout({ children }: { children: ReactNode }) {",
|
|
@@ -122,7 +123,7 @@ async function cmdInit(name) {
|
|
|
122
123
|
"}",
|
|
123
124
|
""
|
|
124
125
|
].join("\n"));
|
|
125
|
-
await writeFile(join(targetDir, "ui", "
|
|
126
|
+
await writeFile(join(targetDir, "ui", "page.tsx"), [
|
|
126
127
|
"import { useState } from 'react'",
|
|
127
128
|
"import { useWebsocket } from 'weifuwu/react'",
|
|
128
129
|
"",
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,7 @@ export { serve, createTestServer } from './serve.ts';
|
|
|
4
4
|
export type { ServeOptions, Server } from './serve.ts';
|
|
5
5
|
export { Router } from './router.ts';
|
|
6
6
|
export type { WebSocketHandler } from './router.ts';
|
|
7
|
-
export {
|
|
8
|
-
export type { TsxOptions } from './tsx.ts';
|
|
7
|
+
export { TsxContext } from './tsx-context.ts';
|
|
9
8
|
export { auth, cors, logger } from './middleware.ts';
|
|
10
9
|
export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts';
|
|
11
10
|
export { serveStatic } from './static.ts';
|