weifuwu 0.31.1 → 0.31.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 CHANGED
@@ -21,7 +21,7 @@ serve(app, { port: 3000 })
21
21
  | Export | Description |
22
22
  |---|---|
23
23
  | `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
24
- | `Router` | Trie-based HTTP router with WebSocket support. |
24
+ | `Router` | Trie-based HTTP router with WebSocket support and `plugin()` method. |
25
25
  | `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
26
26
  | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
27
27
 
@@ -42,6 +42,7 @@ app.ws(path, ...middlewares, handler)
42
42
  // Middleware & mounting
43
43
  app.use(middleware) // global middleware
44
44
  app.mount(prefix, router) // sub-router at prefix
45
+ app.plugin(fn) // extension: (app) => { app.get(), app.use(), ... }
45
46
  app.onError(handler) // error handler: (error, req, ctx) => Response
46
47
  app.routes() // debug: list all registered routes
47
48
 
@@ -178,111 +179,139 @@ npm install react react-dom
178
179
  ```
179
180
 
180
181
  ```ts
181
- import { react, Link, useServerData, ErrorBoundary } from 'weifuwu'
182
- import { createElement as h } from 'react'
182
+ // server.ts the only file you need
183
+ import { serve, Router } from 'weifuwu'
184
+ import { react } from 'weifuwu/react'
183
185
 
184
- // ── Server ──────────────────────────────────────────────
186
+ const app = new Router()
187
+ .use(trace())
188
+ .use(logger())
189
+ .plugin(react({
190
+ pages: {
191
+ '/': './pages/Home.tsx',
192
+ '/users': './pages/Users.tsx',
193
+ '/users/:id': './pages/UserDetail.tsx',
194
+ },
195
+ layout: './layouts/Root.tsx',
196
+ notFound: './pages/NotFound.tsx',
197
+ tailwind: { entry: './styles/input.css' },
198
+ }))
185
199
 
186
- app.use(react({
187
- layout: ({ children }) =>
188
- h('html', null,
189
- h('head', null, h('title', null, 'My App')),
190
- h('body', null, h('div', { id: 'root' }, children)),
191
- ),
192
- }))
200
+ app.get('/api/hello', () => Response.json({ message: 'hi' }))
201
+ serve(app, { port: 3000 })
202
+ ```
203
+
204
+ **One `react()` call handles:** SSR rendering, routing, data loading, Tailwind CSS, client bundle auto-generation, and error pages. No `client.ts`, `routes.ts`, or manual middleware setup needed.
193
205
 
194
- // Render React components to HTML
195
- app.get('/', (_req, ctx) =>
196
- ctx.render(h('h1', null, 'Hello SSR'), {
197
- head: { title: 'Home' },
198
- data: { greeting: 'Hello' },
199
- })
200
- )
201
-
202
- // Streaming SSR
203
- app.get('/dashboard', (_req, ctx) =>
204
- ctx.renderStream(h(Dashboard))
205
- )
206
-
207
- // Layout nesting via mount
208
- const admin = new Router()
209
- admin.use(react({ layout: AdminLayout }))
210
- admin.get('/dashboard', (_req, ctx) => ctx.render(h(AdminDashboard)))
211
- app.mount('/admin', admin)
212
-
213
- // ── Shared components ──────────────────────────────────
214
-
215
- // Use the same component on server and client
216
- function Page() {
217
- const { greeting } = useServerData<{ greeting: string }>()
218
- return h('h1', null, greeting)
206
+ #### Page components
207
+
208
+ ```tsx
209
+ // pages/UserDetail.tsx
210
+ import type { Context } from 'weifuwu'
211
+ import { HttpError } from 'weifuwu'
212
+ import { useServerData } from 'weifuwu/react'
213
+
214
+ export async function loader(ctx: Context) {
215
+ const user = await db.find(ctx.params.id)
216
+ if (!user) throw new HttpError('Not found', 404)
217
+ return { user }
219
218
  }
220
219
 
221
- // Link works on both sides: <a> on server, SPA <a> on client
222
- function Nav() {
223
- return h('nav', null,
224
- h(Link, { href: '/' }, 'Home'),
225
- h(Link, { href: '/users' }, 'Users'),
220
+ export default function UserDetailPage() {
221
+ const { user } = useServerData<{ user: User }>()
222
+ return (
223
+ <div>
224
+ <title>{`${user.name} My App`}</title>
225
+ <h1>{user.name}</h1>
226
+ <p>{user.email}</p>
227
+ </div>
226
228
  )
227
229
  }
230
+ ```
231
+
232
+ - `export async function loader(ctx)` — runs on the server, returns data for `useServerData()`
233
+ - `throw new HttpError('Not found', 404)` — renders the NotFound page with correct status
234
+ - `<title>` auto-hoists to `<head>` via React 19
235
+ - `export default` is auto-detected; named exports work too
236
+
237
+ #### Layout with shared data
228
238
 
229
- // ErrorBoundary catches client-side render errors
230
- function SafeUserProfile() {
231
- return h(ErrorBoundary, { fallback: h('div', null, 'Error') },
232
- h(UserProfile),
239
+ ```tsx
240
+ // layouts/Root.tsx
241
+ export async function loader(ctx: Context) {
242
+ return { currentUser: await getCurrentUser(ctx) }
243
+ }
244
+
245
+ export function Root({ children }: { children: ReactNode }) {
246
+ const { currentUser } = useServerData()
247
+ return (
248
+ <>
249
+ <nav>
250
+ <a href="/">Home</a>
251
+ <a href="/users">Users</a>
252
+ <span>{currentUser?.name}</span>
253
+ </nav>
254
+ <main>{children}</main>
255
+ </>
233
256
  )
234
257
  }
258
+ ```
235
259
 
236
- // ── Client (SPA navigation) ────────────────────────────
260
+ Layout `loader` data merges with page data. Page loader overrides same keys.
237
261
 
238
- // client.ts — bundled separately with esbuild
239
- import { hydrate, createClientRouter, defineRoute } from 'weifuwu/react/client'
262
+ #### Data flow
240
263
 
241
- const userRoute = defineRoute({
242
- path: '/users/:id',
243
- component: UserPage,
244
- loader: (params) => fetch(`/users/${params.id}?_data`).then(r => r.json()),
245
- })
264
+ ```
265
+ layout loader ──┐
266
+ ├──→ merge → useServerData() ──→ Layout + Page
267
+ page loader ───┘
268
+ ```
246
269
 
247
- const router = createClientRouter([
248
- { path: '/', component: HomePage },
249
- userRoute,
250
- ])
270
+ #### Client-side SPA
251
271
 
252
- hydrate(router.App)
272
+ Every page is automatically code-split into a separate chunk. On first visit the server renders HTML and the client hydrates. Subsequent navigations intercept `<a>` clicks, `import()` the page chunk, and fetch fresh server data — all automatic, zero config.
273
+
274
+ #### Streaming SSR
275
+
276
+ ```tsx
277
+ import { Suspense, use } from 'react'
278
+
279
+ export default function StreamingPage() {
280
+ return (
281
+ <div>
282
+ <h1>Instant shell</h1>
283
+ <Suspense fallback={<Spinner />}>
284
+ <SlowData promise={fetchSlowData()} />
285
+ </Suspense>
286
+ </div>
287
+ )
288
+ }
253
289
  ```
254
290
 
255
- **Key concepts:**
291
+ `<Suspense>` boundaries stream to the browser as data resolves.
256
292
 
257
293
  | Feature | Description |
258
294
  |---|---|
259
- | `ctx.render(el, opts)` | Render React to HTML. Auto-detects `?_data` returns JSON. |
260
- | `ctx.renderStream(el)` | Streaming SSR via `renderToReadableStream`. |
261
- | `useServerData<T>()` | Access data on both server (via `ctx.render({ data })`) and client (via `loader`). |
262
- | `Link` | Shared component — `<a>` on server, SPA `<a>` on client. |
263
- | `Form` | Shared `<form>` on server, `fetch` + revalidate on client. |
264
- | `ErrorBoundary` | Catches render errors on client (SSR uses `onError`). |
265
- | `useNavigation()` | `{ state: 'idle' \| 'loading' }` for progress indicators. |
266
- | `useParams()` / `useNavigate()` / `useRevalidate()` | Router hooks. |
267
- | `defineRoute()` | Type-safe route config — captures loader return type. |
268
- | `createClientRouter()` | Client-side SPA router with loader-based data fetching. |
269
- | `hydrate(App)` | Hydrate server-rendered HTML on the client. |
270
- | `head: { title, meta }` | Inject dynamic `<title>` and `<meta>` tags. |
295
+ | `react({ pages, layout, notFound, tailwind })` | One call: SSR + routing + client bundle + error handling |
296
+ | `export async function loader(ctx)` | Server-side data loading, auto-detected by the framework |
297
+ | `useServerData<T>()` | Type-safe access to loader data in any component |
298
+ | `Link` | `<a>` that does SPA navigation on the client |
299
+ | `ErrorBoundary` | Catches render errors on server and client |
300
+ | `<title>`, `<meta>` | Auto-hoisted to `<head>` via React 19 |
301
+ | `<Suspense>` | Streaming SSR, works out of the box |
302
+ | `client: false` | Disable client JS for static SSR only |
271
303
 
272
304
  **Import paths:**
273
305
 
274
306
  ```ts
275
307
  // Server-side
276
- import { react, Link, Form, ErrorBoundary, useServerData } from 'weifuwu'
277
-
278
- // Client-side (for browser bundles)
279
- import { hydrate, createClientRouter, defineRoute, Link } from 'weifuwu/react/client'
308
+ import { react, Link, ErrorBoundary, useServerData } from 'weifuwu'
280
309
 
281
- // Shared primitives (safe for both, pure React — no react-dom import)
282
- import { Link, Form, useServerData, useParams, useNavigation } from 'weifuwu/react/navigation'
310
+ // Client-side (for custom advanced setups)
311
+ import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
283
312
  ```
284
313
 
285
- See [examples/react-ssr/](examples/react-ssr/) for a complete demo.
314
+ See [examples/react-ssr/](examples/react-ssr/) for the full demo.
286
315
 
287
316
  ### Types
288
317
 
@@ -25,6 +25,17 @@ export declare class Router<T extends Context = Context> {
25
25
  private get hub();
26
26
  wsHub(hub: Hub): this;
27
27
  use(mw: Middleware<Context, Context>): Router<T>;
28
+ /**
29
+ * Install a plugin — a function that configures the Router with
30
+ * routes, middleware, and error handlers. Use when `.use()` isn't
31
+ * enough because you need to call `app.get()`, `app.onError()`, etc.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * app.plugin(app => createReactApp(app, { pages: {...}, layout: '...', tailwind: {...} }))
36
+ * ```
37
+ */
38
+ plugin(fn: (app: this) => void): this;
28
39
  mount(path: string, router: Router<Context>): Router<T>;
29
40
  onError(handler: ErrorHandler<T>): Router<T>;
30
41
  get(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,10 @@ export { serveStatic } from './middleware/static.ts';
14
14
  export type { ServeStaticOptions } from './middleware/static.ts';
15
15
  export { upload } from './middleware/upload.ts';
16
16
  export type { UploadOptions, UploadedFile, UploadModule } from './middleware/upload.ts';
17
+ export { esbuildDev } from './middleware/esbuild-dev.ts';
18
+ export type { EsbuildDevEntry, EsbuildDevOptions } from './middleware/esbuild-dev.ts';
19
+ export { tailwindDev } from './middleware/tailwind-dev.ts';
20
+ export type { TailwindDevEntry, TailwindDevOptions } from './middleware/tailwind-dev.ts';
17
21
  export { rateLimit } from './middleware/rate-limit.ts';
18
22
  export type { RateLimitOptions } from './middleware/rate-limit.ts';
19
23
  export { compress } from './middleware/compress.ts';
@@ -30,10 +34,6 @@ export { createHub } from './hub.ts';
30
34
  export type { Hub, HubOptions } from './hub.ts';
31
35
  export { queue } from './queue/index.ts';
32
36
  export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types.ts';
33
- export { react } from './react/index.ts';
34
- export type { ReactOptions, RenderOptions, ReactInjected } from './react/types.ts';
35
- export { useServerData, ServerDataContext } from './react/index.ts';
36
- export { Link, useParams, useNavigate, useRevalidate, Form, useNavigation } from './react/index.ts';
37
- export type { LinkProps, FormProps, NavigationState } from './react/index.ts';
38
- export { ErrorBoundary } from './react/index.ts';
39
- export type { ErrorBoundaryProps } from './react/index.ts';
37
+ export { react, reactRouter } from './react/index.ts';
38
+ export type { ReactOptions, RenderOptions, ReactRouterOptions, ReactAppOptions } from './react/types.ts';
39
+ export { useServerData, ServerDataContext, Link, ErrorBoundary } from './react/index.ts';