weifuwu 0.2.1 → 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 +117 -0
- package/index.ts +1 -1
- package/package.json +1 -1
- package/router.ts +8 -9
- package/tsx.ts +26 -6
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/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
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/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
|
|
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 =
|
|
196
|
-
const idx =
|
|
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
|
-
|
|
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
|
|