weifuwu 0.2.0 → 0.2.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
@@ -8,6 +8,7 @@
8
8
  - **Trie router** — static > param > wildcard, sub-router mounting, path params
9
9
  - **Middleware** — global, path-scoped, route-level — onion model, short-circuit
10
10
  - **Built-in middleware** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`
11
+ - **React SSR + Hydration** — `tsx({ dir })` — page.tsx / load.ts / layout.tsx / route.ts
11
12
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
12
13
  - **GraphQL** — `router.graphql()` with GraphiQL IDE
13
14
  - **AI streaming** — `router.ai()` via Vercel AI SDK
@@ -27,6 +28,110 @@ import { serve } from 'weifuwu'
27
28
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
28
29
  ```
29
30
 
31
+ ## React pages with tsx()
32
+
33
+ ```ts
34
+ import { serve, Router } from 'weifuwu'
35
+ import { tsx } from 'weifuwu/tsx'
36
+
37
+ const app = new Router()
38
+ app.use('/', await tsx({ dir: './pages/' }))
39
+
40
+ serve(app.handler(), { port: 3000 })
41
+ ```
42
+
43
+ ### File conventions
44
+
45
+ ```
46
+ pages/
47
+ page.tsx → GET / (React component, default export)
48
+ layout.tsx → root layout (wraps all pages)
49
+ about/page.tsx → GET /about
50
+ blog/[slug]/
51
+ page.tsx → GET /blog/:slug
52
+ load.ts → data fetching (server-only, default export)
53
+ route.ts → POST /blog/:slug (API, named exports GET/POST/...)
54
+ blog/layout.tsx → /blog/* layout (auto-wraps blog pages)
55
+ ```
56
+
57
+ ### page.tsx — page component
58
+
59
+ ```tsx
60
+ export default function Page({ params, query }: {
61
+ params: { slug: string }
62
+ query: Record<string, string>
63
+ }) {
64
+ return <article><h1>{params.slug}</h1></article>
65
+ }
66
+ ```
67
+
68
+ ### load.ts — data fetching (server-only)
69
+
70
+ ```ts
71
+ import { db } from './db.ts'
72
+
73
+ export default async function load({ params, query }: {
74
+ params: Record<string, string>
75
+ query: Record<string, string>
76
+ }) {
77
+ const data = await db.query(params.slug)
78
+ return { data } // merged into props passed to page.tsx
79
+ }
80
+ ```
81
+
82
+ `load()` runs only on the server. Its return value is merged with `{ params, query }` and passed to the page component. The merged props are serialized as `window.__WEIFUWU_PROPS` for client hydration.
83
+
84
+ ### layout.tsx — nested layouts
85
+
86
+ ```tsx
87
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
88
+ return (
89
+ <html>
90
+ <head><title>App</title></head>
91
+ <body>
92
+ <div id="__weifuwu_root">{children}</div>
93
+ </body>
94
+ </html>
95
+ )
96
+ }
97
+ ```
98
+
99
+ Layouts auto-nest by directory depth — `pages/blog/layout.tsx` wraps `pages/blog/*` pages inside `pages/layout.tsx`.
100
+
101
+ ### route.ts — API (co-located with page)
102
+
103
+ ```ts
104
+ export const POST: Handler = async (req, ctx) => {
105
+ const body = await req.json()
106
+ return Response.json({ ...body, slug: ctx.params.slug })
107
+ }
108
+ ```
109
+
110
+ Route.ts exports `POST`/`PUT`/`DELETE`/`PATCH` (GET is handled by page.tsx). The same `route.ts` file coexists with `page.tsx` in the same directory for handling form submissions or AJAX requests.
111
+
112
+ ### Usage within a full app
113
+
114
+ ```ts
115
+ import { serve, Router } from 'weifuwu'
116
+ import { tsx } from 'weifuwu/tsx'
117
+
118
+ const r = new Router()
119
+ r.use('/', await tsx({ dir: './pages/' }))
120
+
121
+ // Other features coexist in the same process
122
+ r.ws('/chat', { message(ws, _, data) { ws.send(data) } })
123
+ r.graphql('/graphql', { schema: `...`, resolvers: { ... } })
124
+
125
+ serve(r.handler())
126
+ ```
127
+
128
+ ```bash
129
+ node --watch app.ts # development
130
+ node app.ts # production
131
+ ```
132
+
133
+ No build step, no configuration file — just Node.js and React.
134
+
30
135
  ## Router
31
136
 
32
137
  ```ts
@@ -277,6 +382,18 @@ const app = new Router()
277
382
 
278
383
  Returns `{ stop, port, hostname, ready }`.
279
384
 
385
+ ### `tsx(options)`
386
+
387
+ ```ts
388
+ import { tsx } from 'weifuwu/tsx'
389
+ ```
390
+
391
+ | Option | Default | Description |
392
+ |--------|---------|-------------|
393
+ | `dir` | — | Pages directory path |
394
+
395
+ Returns `Promise<Router>`.
396
+
280
397
  ### `Router`
281
398
 
282
399
  | Method | Description |
package/compress.ts CHANGED
@@ -60,7 +60,7 @@ export function compress(options?: CompressOptions): Middleware {
60
60
  headers.delete('Content-Range')
61
61
  headers.set('Vary', 'Accept-Encoding')
62
62
 
63
- return new Response(compressed, {
63
+ return new Response(compressed as BodyInit, {
64
64
  status: res.status,
65
65
  statusText: res.statusText,
66
66
  headers,
package/index.ts CHANGED
@@ -3,7 +3,7 @@ export { serve } from './serve.ts'
3
3
  export type { ServeOptions, Server } from './serve.ts'
4
4
  export { Router } from './router.ts'
5
5
  export type { WebSocketHandler, GraphQLOptions, AIHandler } from './router.ts'
6
- export { tsx } from './tsx.ts'
6
+ export { tsx, TsxContext, useTsx } from './tsx.ts'
7
7
  export type { TsxOptions } from './tsx.ts'
8
8
  export { auth, cors, logger } from './middleware.ts'
9
9
  export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "index.ts",
6
6
  "scripts": {
package/router.ts CHANGED
@@ -385,15 +385,6 @@ export class Router {
385
385
  let wildcardIdx = -1
386
386
 
387
387
  for (let i = 0; i < segments.length; i++) {
388
- if (node.subRouter) {
389
- return {
390
- pathMws,
391
- params,
392
- middlewares: [],
393
- subRouter: { router: node.subRouter, remainingIdx: i },
394
- }
395
- }
396
-
397
388
  pathMws.push(...node.pathMws)
398
389
 
399
390
  if (node.wildcard) {
@@ -410,6 +401,14 @@ export class Router {
410
401
 
411
402
  const next = matchTrieNode(node, segment, params)
412
403
  if (!next) {
404
+ if (node.subRouter) {
405
+ return {
406
+ pathMws,
407
+ params,
408
+ middlewares: [],
409
+ subRouter: { router: node.subRouter, remainingIdx: i },
410
+ }
411
+ }
413
412
  if (wildcardHandler) {
414
413
  params['*'] = segments.slice(wildcardIdx).join('/')
415
414
  return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
package/serve.ts CHANGED
@@ -39,7 +39,7 @@ export function createRequest(req: IncomingMessage, body: Buffer): [Request, Rec
39
39
  method: req.method?.toUpperCase() ?? 'GET',
40
40
  headers,
41
41
  body: (req.method !== 'GET' && req.method !== 'HEAD' && body.length > 0)
42
- ? body
42
+ ? body as BodyInit
43
43
  : null,
44
44
  })
45
45
 
package/static.ts CHANGED
@@ -73,7 +73,7 @@ export function serveStatic(root: string, options?: ServeStaticOptions): Handler
73
73
  }
74
74
 
75
75
  const stream = fileHandle.readableWebStream()
76
- return new Response(stream, { headers })
76
+ return new Response(stream as unknown as BodyInit, { headers })
77
77
  } catch (err) {
78
78
  if (fileHandle) await fileHandle.close().catch(() => {})
79
79
  if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
package/tsx.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createElement } from 'react'
1
+ import { createElement, createContext, useContext } from 'react'
2
2
  import { renderToReadableStream } from 'react-dom/server'
3
3
  import * as esbuild from 'esbuild'
4
4
  import { readdirSync, statSync, existsSync, mkdirSync } from 'node:fs'
@@ -6,12 +6,23 @@ import { join, relative, resolve, sep, dirname } from 'node:path'
6
6
  import { pathToFileURL } from 'node:url'
7
7
  import { createHash } from 'node:crypto'
8
8
  import { Router } from './router.ts'
9
- import type { Handler } from './types.ts'
9
+ import type { Context, Handler } from './types.ts'
10
10
 
11
11
  export interface TsxOptions {
12
12
  dir: string
13
13
  }
14
14
 
15
+ export const TsxContext = createContext<{
16
+ params: Record<string, string>
17
+ query: Record<string, string>
18
+ user?: unknown
19
+ parsed?: Record<string, unknown>
20
+ }>({ params: {}, query: {} })
21
+
22
+ export function useTsx() {
23
+ return useContext(TsxContext)
24
+ }
25
+
15
26
  type PageEntry = {
16
27
  route: string
17
28
  entryPath: string
@@ -189,11 +200,12 @@ async function getOrBuildClientBundle(
189
200
 
190
201
  if (!buf) {
191
202
  try {
192
- const layoutsImport = layoutPaths.map((p, i) =>
203
+ const nested = layoutPaths.slice(1)
204
+ const layoutsImport = nested.map((p, i) =>
193
205
  `import L${i} from${JSON.stringify(p)};`,
194
206
  ).join('')
195
- const layoutsWrap = layoutPaths.map((_, i) => {
196
- const idx = layoutPaths.length - 1 - i
207
+ const layoutsWrap = nested.map((_, i) => {
208
+ const idx = nested.length - 1 - i
197
209
  return `el=createElement(L${idx},null,el);`
198
210
  }).join('')
199
211
 
@@ -255,9 +267,17 @@ function makeSsrHandler(
255
267
 
256
268
  let element = createElement(Component, allProps)
257
269
  for (let i = layouts.length - 1; i >= 0; i--) {
258
- element = createElement(layouts[i], null, element)
270
+ const isRoot = i === 0
271
+ element = createElement(
272
+ layouts[i],
273
+ isRoot ? { children: element, req, ctx } : { children: element },
274
+ )
259
275
  }
260
276
 
277
+ element = createElement(TsxContext.Provider, {
278
+ value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed },
279
+ }, element)
280
+
261
281
  const stream = await renderToReadableStream(element)
262
282
  const body = await readStream(stream)
263
283