weifuwu 0.18.1 → 0.18.3

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
@@ -15,11 +15,14 @@ serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
15
15
  ```
16
16
 
17
17
  ```ts
18
- import { serve, Router, tsx, preferences } from 'weifuwu'
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('/', await tsx({ dir: './ui' }))
22
- serve(app.handler(), { port: 3000 })
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()) // no extras
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. `load.ts` runs on server each nav.
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 the data returned by `load.ts`. Update-triggered; re-renders on SPA navigation.
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<{ post: { title: string } }>()
804
- return <h1>{data.post.title}</h1>
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, live reload, Tailwind v4 auto-compile.
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, tsx } from 'weifuwu'",
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
- "const ui = await tsx({ dir: './ui/' })",
104
- "app.use('/', ui)",
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', 'pages'), { recursive: true })
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', 'pages', 'layout.tsx'), [
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', 'pages', 'page.tsx'), [
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, tsx } from 'weifuwu'",
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
- "const ui = await tsx({ dir: './ui/' })",
94
- "app.use('/', ui)",
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", "pages"), { recursive: true });
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", "pages", "layout.tsx"), [
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", "pages", "page.tsx"), [
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 { tsx, TsxContext } from './tsx.ts';
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';