weifuwu 0.17.25 → 0.18.0
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 +252 -45
- package/cli.ts +4 -0
- package/dist/cli.js +4 -0
- package/dist/client-state.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23 -53
- package/dist/react.d.ts +3 -2
- package/dist/react.js +48 -39
- package/dist/tsx-context.d.ts +8 -6
- package/dist/tsx-instance.d.ts +2 -2
- package/dist/tsx.d.ts +2 -2
- package/dist/types.d.ts +1 -4
- package/dist/use-flash-message.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ app.get('/admin', mw, handler) // route-level
|
|
|
88
88
|
|
|
89
89
|
## Module Patterns
|
|
90
90
|
|
|
91
|
-
All modules follow one of
|
|
91
|
+
All modules follow one of 6 patterns. The pattern letter is marked in each module's heading.
|
|
92
92
|
|
|
93
93
|
| Pattern | How to mount | Example |
|
|
94
94
|
|---------|-------------|---------|
|
|
@@ -105,13 +105,28 @@ All modules follow one of 5 patterns. The pattern letter is marked in each modul
|
|
|
105
105
|
### agent [C]
|
|
106
106
|
|
|
107
107
|
```ts
|
|
108
|
-
const a = agent({ pg })
|
|
108
|
+
const a = agent({ pg, model: openai('gpt-4o'), embeddingModel: openai.embedding('text-embedding-3-small') })
|
|
109
109
|
await a.migrate()
|
|
110
110
|
app.use('/api', a.router())
|
|
111
|
-
await a.addKnowledge(agentId, 'Title', '
|
|
112
|
-
a.run(agentId, {
|
|
111
|
+
await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
112
|
+
a.run(agentId, { input: 'summarize the data', stream: true })
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
| Option | Type | Default | Description |
|
|
116
|
+
|--------|------|---------|-------------|
|
|
117
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
118
|
+
| `model` | `object` | — | AI model (e.g. `openai('gpt-4o')`) |
|
|
119
|
+
| `embeddingModel` | `object` | — | Embedding model for knowledge search |
|
|
120
|
+
| `embeddingDimension` | `number` | `1536` | Embedding vector dimension |
|
|
121
|
+
| `tools` | `object[]` | — | Custom tool definitions |
|
|
122
|
+
|
|
123
|
+
| Method | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
|
|
126
|
+
| `.addKnowledge(agentId, title, content)` | Add knowledge document |
|
|
127
|
+
| `.router()` | REST + WS API |
|
|
128
|
+
| `.close()` | Cleanup |
|
|
129
|
+
|
|
115
130
|
### aiStream [E]
|
|
116
131
|
|
|
117
132
|
```ts
|
|
@@ -147,17 +162,29 @@ app.use('/', a.router())
|
|
|
147
162
|
```ts
|
|
148
163
|
app.use(auth({ token: 'sk-123' })) // static token
|
|
149
164
|
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
150
|
-
app.use(auth({ verify: async (token) => ({ sub: 'abc' }) }))
|
|
165
|
+
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
151
166
|
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
152
167
|
```
|
|
153
168
|
|
|
169
|
+
| Option | Type | Default | Description |
|
|
170
|
+
|--------|------|---------|-------------|
|
|
171
|
+
| `token` | `string` | — | Static token to match |
|
|
172
|
+
| `header` | `string` | `'Authorization'` | Header name |
|
|
173
|
+
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
174
|
+
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
175
|
+
|
|
154
176
|
### compress [A]
|
|
155
177
|
|
|
156
178
|
```ts
|
|
157
|
-
app.use(compress()) // brotli > gzip > deflate
|
|
158
|
-
app.use(compress({ threshold: 2048 })) //
|
|
179
|
+
app.use(compress()) // brotli > gzip > deflate (min 1KB)
|
|
180
|
+
app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
|
|
159
181
|
```
|
|
160
182
|
|
|
183
|
+
| Option | Type | Default | Description |
|
|
184
|
+
|--------|------|---------|-------------|
|
|
185
|
+
| `threshold` | `number` | `1024` | Minimum byte size to compress |
|
|
186
|
+
| `level` | `number` | `6` | Compression level (zlib) |
|
|
187
|
+
|
|
161
188
|
### cors [A]
|
|
162
189
|
|
|
163
190
|
```ts
|
|
@@ -167,18 +194,28 @@ app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
|
|
|
167
194
|
app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
168
195
|
```
|
|
169
196
|
|
|
197
|
+
| Option | Type | Default | Description |
|
|
198
|
+
|--------|------|---------|-------------|
|
|
199
|
+
| `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
|
|
200
|
+
| `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
|
|
201
|
+
| `allowedHeaders` | `string[]` | — | Custom allowed headers |
|
|
202
|
+
| `exposedHeaders` | `string[]` | — | Response headers exposed to client |
|
|
203
|
+
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
204
|
+
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
205
|
+
|
|
170
206
|
### csrf [A]
|
|
171
207
|
|
|
172
208
|
```ts
|
|
173
209
|
app.use(csrf())
|
|
174
|
-
// ctx.csrfToken
|
|
175
|
-
// Auto-validates
|
|
210
|
+
// ctx.csrfToken — set on GET/HEAD/OPTIONS
|
|
211
|
+
// Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
|
|
212
|
+
// Falls back to body field matching the key name
|
|
176
213
|
```
|
|
177
214
|
|
|
178
215
|
| Option | Default | Description |
|
|
179
216
|
|--------|---------|-------------|
|
|
180
217
|
| `cookie` | `'_csrf'` | Cookie name |
|
|
181
|
-
| `header` | `'x-csrf-token'` | Header name |
|
|
218
|
+
| `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
|
|
182
219
|
| `key` | `'_csrf'` | Body field fallback |
|
|
183
220
|
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
|
|
184
221
|
|
|
@@ -186,31 +223,57 @@ app.use(csrf())
|
|
|
186
223
|
|
|
187
224
|
```ts
|
|
188
225
|
import { deploy, defineConfig } from 'weifuwu'
|
|
189
|
-
const config = defineConfig({
|
|
190
|
-
|
|
226
|
+
const config = defineConfig({
|
|
227
|
+
domain: 'example.com',
|
|
228
|
+
apps: {
|
|
229
|
+
api: { repo: 'git@github.com:user/api.git', entry: 'app.ts', port: 3001, subdomain: 'api' },
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
const server = await deploy(config)
|
|
233
|
+
// server.close(), server.ready, server.url
|
|
234
|
+
// server.apps.list(), server.apps.status(name), server.apps.deploy(name)
|
|
191
235
|
```
|
|
192
236
|
|
|
193
237
|
### health [D]
|
|
194
238
|
|
|
195
239
|
```ts
|
|
196
|
-
app.use(health(
|
|
197
|
-
|
|
240
|
+
app.use(health({ path: '/health' }))
|
|
241
|
+
// Returns 200 on success, 503 when check throws
|
|
198
242
|
```
|
|
199
243
|
|
|
244
|
+
| Option | Type | Default | Description |
|
|
245
|
+
|--------|------|---------|-------------|
|
|
246
|
+
| `path` | `string` | `'/health'` | Health check endpoint |
|
|
247
|
+
| `check` | `() => Promise<void>` | — | Async function; throws → 503 |
|
|
248
|
+
|
|
200
249
|
### helmet [A]
|
|
201
250
|
|
|
202
|
-
|
|
251
|
+
15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
|
|
203
252
|
|
|
204
253
|
```ts
|
|
205
254
|
app.use(helmet())
|
|
206
255
|
app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
|
|
207
256
|
```
|
|
208
257
|
|
|
258
|
+
| Option | Default | Description |
|
|
259
|
+
|--------|---------|-------------|
|
|
260
|
+
| `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
|
|
261
|
+
| `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
|
|
262
|
+
| `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
|
|
263
|
+
| `referrerPolicy` | `'no-referrer'` | Referrer header |
|
|
264
|
+
| `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
|
|
265
|
+
| `permissionsPolicy` | — | Feature permissions policy |
|
|
266
|
+
| `crossOriginEmbedderPolicy` | — | COEP header |
|
|
267
|
+
| `crossOriginOpenerPolicy` | — | COOP header |
|
|
268
|
+
| `crossOriginResourcePolicy` | — | CORP header |
|
|
269
|
+
|
|
209
270
|
### iii [C] — Worker / Function / Trigger
|
|
210
271
|
|
|
211
272
|
```ts
|
|
273
|
+
import { createWorker } from 'weifuwu'
|
|
212
274
|
const engine = iii({ pg, redis })
|
|
213
275
|
app.use('/iii', engine.router())
|
|
276
|
+
app.ws('/iii', engine.wsHandler())
|
|
214
277
|
|
|
215
278
|
const w = createWorker('orders')
|
|
216
279
|
w.registerFunction('orders::create', async (payload) => db.query('INSERT INTO orders ...', [payload.items]))
|
|
@@ -221,8 +284,15 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
|
|
|
221
284
|
| Method | Description |
|
|
222
285
|
|--------|-------------|
|
|
223
286
|
| `.addWorker(w)` | Register a worker |
|
|
224
|
-
| `.
|
|
287
|
+
| `.removeWorker(w)` | Remove a worker |
|
|
288
|
+
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
|
|
289
|
+
| `.listWorkers()` | List registered workers |
|
|
290
|
+
| `.listFunctions()` | List registered functions |
|
|
291
|
+
| `.listTriggers()` | List registered triggers |
|
|
225
292
|
| `.router()` | REST + WS API |
|
|
293
|
+
| `.wsHandler()` | WebSocket handler |
|
|
294
|
+
| `.migrate()` | DB setup |
|
|
295
|
+
| `.shutdown()` | Clean shutdown |
|
|
226
296
|
|
|
227
297
|
### logdb [C]
|
|
228
298
|
|
|
@@ -233,9 +303,14 @@ const logger = logdb({ pg })
|
|
|
233
303
|
await logger.migrate()
|
|
234
304
|
app.use('/logs', logger.router())
|
|
235
305
|
await logger.clean(12) // drop partitions older than 12 months
|
|
236
|
-
await logger.log({ level: 'info', source: 'app', message: 'hello' })
|
|
306
|
+
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
|
|
237
307
|
```
|
|
238
308
|
|
|
309
|
+
| Option | Type | Default | Description |
|
|
310
|
+
|--------|------|---------|-------------|
|
|
311
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
312
|
+
| `table` | `string` | `'_log_entries'` | Table name |
|
|
313
|
+
|
|
239
314
|
| Method | Path | Description |
|
|
240
315
|
|--------|------|-------------|
|
|
241
316
|
| POST | `/` | Create log entry |
|
|
@@ -252,10 +327,16 @@ app.use(logger({ format: 'combined' })) // with query params
|
|
|
252
327
|
### mailer
|
|
253
328
|
|
|
254
329
|
```ts
|
|
255
|
-
const mail = mailer({
|
|
256
|
-
await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>' })
|
|
330
|
+
const mail = mailer({ from: 'noreply@example.com', transport: 'smtp://user:pass@smtp.example.com:587' })
|
|
331
|
+
await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>', cc: 'admin@test.com' })
|
|
257
332
|
```
|
|
258
333
|
|
|
334
|
+
| Option | Type | Default | Description |
|
|
335
|
+
|--------|------|---------|-------------|
|
|
336
|
+
| `transport` | `string\|object` | — | Nodemailer transport config or connection string |
|
|
337
|
+
| `from` | `string` | — | Default sender address |
|
|
338
|
+
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
339
|
+
|
|
259
340
|
### messager [C]
|
|
260
341
|
|
|
261
342
|
Real-time chat with channels, WebSocket, agent routing.
|
|
@@ -263,21 +344,44 @@ Real-time chat with channels, WebSocket, agent routing.
|
|
|
263
344
|
```ts
|
|
264
345
|
const msg = messager({ pg, agents, redis: redis() })
|
|
265
346
|
await msg.migrate()
|
|
266
|
-
app.ws('/ws',
|
|
267
|
-
await msg.send(channelId, 'System message', { sender_type: 'system' })
|
|
347
|
+
app.ws('/ws', msg.wsHandler())
|
|
348
|
+
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
|
|
268
349
|
```
|
|
269
350
|
|
|
351
|
+
| Method | Description |
|
|
352
|
+
|--------|-------------|
|
|
353
|
+
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
354
|
+
| `.send(channel, content, opts?)` | Send message to channel |
|
|
355
|
+
| `.router()` | REST API |
|
|
356
|
+
| `.close()` | Cleanup |
|
|
357
|
+
|
|
270
358
|
### opencode [C]
|
|
271
359
|
|
|
272
360
|
AI programming assistant.
|
|
273
361
|
|
|
274
362
|
```ts
|
|
275
|
-
const oc = await opencode({
|
|
363
|
+
const oc = await opencode({
|
|
364
|
+
pg,
|
|
365
|
+
model: openai('gpt-4o'),
|
|
366
|
+
workspace: '/home/user/project',
|
|
367
|
+
permissions: { bash: { allow: true }, write: { allow: false } },
|
|
368
|
+
})
|
|
276
369
|
await oc.migrate()
|
|
277
370
|
app.use('/opencode', await oc.router())
|
|
278
371
|
app.ws('/opencode', oc.wsHandler())
|
|
279
372
|
```
|
|
280
373
|
|
|
374
|
+
| Option | Type | Default | Description |
|
|
375
|
+
|--------|------|---------|-------------|
|
|
376
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
377
|
+
| `model` | `object` | — | AI model |
|
|
378
|
+
| `baseURL` | `string` | — | OpenAI-compatible API base URL |
|
|
379
|
+
| `apiKey` | `string` | — | API key for the model |
|
|
380
|
+
| `workspace` | `string` | — | Project directory |
|
|
381
|
+
| `systemPrompt` | `string` | — | Custom system prompt |
|
|
382
|
+
| `skills` | `object[]` | — | Custom skill definitions |
|
|
383
|
+
| `permissions` | `object` | — | Tool permission rules |
|
|
384
|
+
|
|
281
385
|
### postgres [B]
|
|
282
386
|
|
|
283
387
|
```ts
|
|
@@ -285,6 +389,14 @@ const pg = postgres() // reads DATABASE_URL
|
|
|
285
389
|
app.use(pg) // injects ctx.sql
|
|
286
390
|
```
|
|
287
391
|
|
|
392
|
+
| Option | Type | Default | Description |
|
|
393
|
+
|--------|------|---------|-------------|
|
|
394
|
+
| `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
|
|
395
|
+
| `max` | `number` | `10` | Max pool connections |
|
|
396
|
+
| `ssl` | `boolean\|object` | — | SSL options |
|
|
397
|
+
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
398
|
+
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
399
|
+
|
|
288
400
|
```ts
|
|
289
401
|
// Type-safe DDL
|
|
290
402
|
const users = pgTable('_users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').unique().notNull(), active: boolean('active').default(true), ...timestamps() })
|
|
@@ -308,6 +420,7 @@ Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme`
|
|
|
308
420
|
```ts
|
|
309
421
|
app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { default: 'system' } }))
|
|
310
422
|
// ctx.prefs.locale, ctx.prefs.theme, ctx.t('key'), ctx.setPref('locale', 'zh')
|
|
423
|
+
// ctx.setPref() returns a 302 Response with Set-Cookie — return it from your handler
|
|
311
424
|
// GET /__lang/zh → 302 + Set-Cookie (or JSON if Accept: application/json)
|
|
312
425
|
// GET /__theme/dark → same pattern
|
|
313
426
|
```
|
|
@@ -335,34 +448,69 @@ const { theme, resolvedTheme, setTheme } = useTheme()
|
|
|
335
448
|
|
|
336
449
|
```ts
|
|
337
450
|
const q = queue({ redis })
|
|
451
|
+
app.use(q) // injects ctx.queue
|
|
338
452
|
await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
|
|
339
453
|
```
|
|
340
454
|
|
|
455
|
+
| Option | Type | Default | Description |
|
|
456
|
+
|--------|------|---------|-------------|
|
|
457
|
+
| `redis` | `object` | — | Redis client |
|
|
458
|
+
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
459
|
+
| `prefix` | `string` | `'queue:'` | Redis key prefix |
|
|
460
|
+
| `pollInterval` | `number` | `1000` | Poll interval (ms) |
|
|
461
|
+
|
|
462
|
+
| Method | Description |
|
|
463
|
+
|--------|-------------|
|
|
464
|
+
| `.add(name, data, opts?)` | Add job to queue |
|
|
465
|
+
| `.process(handler)` | Register job processor |
|
|
466
|
+
| `.run()` | Start processing |
|
|
467
|
+
| `.stop()` | Stop processing |
|
|
468
|
+
| `.close()` | Cleanup |
|
|
469
|
+
|
|
341
470
|
### rateLimit [B]
|
|
342
471
|
|
|
343
472
|
```ts
|
|
344
473
|
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
|
|
345
474
|
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
346
475
|
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
476
|
+
// Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
|
|
347
477
|
// m.stop() — clear interval
|
|
348
478
|
```
|
|
349
479
|
|
|
480
|
+
| Option | Type | Default | Description |
|
|
481
|
+
|--------|------|---------|-------------|
|
|
482
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
483
|
+
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
484
|
+
| `key` | `(req) => string` | IP-based | Key function |
|
|
485
|
+
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
486
|
+
|
|
350
487
|
### redis [B]
|
|
351
488
|
|
|
352
489
|
```ts
|
|
353
|
-
const r = redis()
|
|
354
|
-
app.use(r)
|
|
490
|
+
const r = redis() // reads REDIS_URL
|
|
491
|
+
app.use(r) // injects ctx.redis
|
|
355
492
|
await ctx.redis.set('key', 'value')
|
|
356
493
|
// r.close() — cleanup
|
|
357
494
|
```
|
|
358
495
|
|
|
496
|
+
| Option | Type | Default | Description |
|
|
497
|
+
|--------|------|---------|-------------|
|
|
498
|
+
| `url` | `string` | `REDIS_URL` env | Redis connection string |
|
|
499
|
+
| (all ioredis options) | — | — | Passed directly to ioredis |
|
|
500
|
+
|
|
359
501
|
### requestId [A]
|
|
360
502
|
|
|
361
503
|
```ts
|
|
362
504
|
app.use(requestId())
|
|
505
|
+
app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID() }))
|
|
363
506
|
// Sets X-Request-ID header on responses, available as ctx.requestId
|
|
364
507
|
```
|
|
365
508
|
|
|
509
|
+
| Option | Type | Default | Description |
|
|
510
|
+
|--------|------|---------|-------------|
|
|
511
|
+
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
512
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
513
|
+
|
|
366
514
|
### seo [D] + seoMiddleware [A]
|
|
367
515
|
|
|
368
516
|
```ts
|
|
@@ -372,6 +520,8 @@ app.use(seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow:
|
|
|
372
520
|
app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
|
|
373
521
|
```
|
|
374
522
|
|
|
523
|
+
Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
|
|
524
|
+
|
|
375
525
|
### tenant [C]
|
|
376
526
|
|
|
377
527
|
Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
@@ -387,29 +537,58 @@ app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
|
387
537
|
### upload [A]
|
|
388
538
|
|
|
389
539
|
```ts
|
|
390
|
-
app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760 }), (req, ctx) => {
|
|
391
|
-
// ctx.parsed.files.avatar → { name, type, size, path }
|
|
540
|
+
app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }), (req, ctx) => {
|
|
541
|
+
// ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
|
|
542
|
+
// Multiple files with same field name → array
|
|
392
543
|
// ctx.parsed.fields.title → 'hello'
|
|
393
544
|
})
|
|
394
545
|
```
|
|
395
546
|
|
|
547
|
+
| Option | Type | Default | Description |
|
|
548
|
+
|--------|------|---------|-------------|
|
|
549
|
+
| `dir` | `string` | — | Write files to disk (omit for in-memory) |
|
|
550
|
+
| `maxFileSize` | `number` | — | Max bytes per file |
|
|
551
|
+
| `allowedTypes` | `string[]` | — | Allowed MIME types |
|
|
552
|
+
|
|
396
553
|
### user [C]
|
|
397
554
|
|
|
555
|
+
Authentication: register, login, JWT, OAuth2.
|
|
556
|
+
|
|
398
557
|
```ts
|
|
399
558
|
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
|
|
400
559
|
await auth.migrate()
|
|
401
|
-
app.use('/auth', auth.router()) // POST /register, POST /login
|
|
560
|
+
app.use('/auth', auth.router()) // POST /register, POST /login, OAuth2 routes
|
|
402
561
|
app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
|
|
403
562
|
```
|
|
404
563
|
|
|
564
|
+
| Option | Type | Default | Description |
|
|
565
|
+
|--------|------|---------|-------------|
|
|
566
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
567
|
+
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
568
|
+
| `table` | `string` | `'_users'` | Users table name |
|
|
569
|
+
| `expiresIn` | `string` | `'7d'` | JWT expiration |
|
|
570
|
+
| `oauth2` | `object` | — | OAuth2 client config (PKCE flow) |
|
|
571
|
+
|
|
572
|
+
| Method | Description |
|
|
573
|
+
|--------|-------------|
|
|
574
|
+
| `.register(data)` | Register a new user programmatically |
|
|
575
|
+
| `.login(data)` | Log in programmatically |
|
|
576
|
+
| `.verify(token)` | Verify JWT token |
|
|
577
|
+
| `.router()` | REST routes (register, login, OAuth2) |
|
|
578
|
+
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
579
|
+
|
|
405
580
|
### validate [A]
|
|
406
581
|
|
|
407
582
|
```ts
|
|
408
583
|
import { z } from 'zod'
|
|
409
584
|
const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
|
|
410
|
-
app.post('/users', validate({ body: CreateUser }), (req, ctx) => {
|
|
585
|
+
app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
|
|
411
586
|
// ctx.parsed.body — typed & validated
|
|
587
|
+
// ctx.parsed.query — typed & validated
|
|
588
|
+
// ctx.parsed.params — typed & validated (for dynamic routes)
|
|
589
|
+
// ctx.parsed.headers — typed & validated
|
|
412
590
|
})
|
|
591
|
+
// Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
|
|
413
592
|
```
|
|
414
593
|
|
|
415
594
|
---
|
|
@@ -439,15 +618,16 @@ ui/
|
|
|
439
618
|
|
|
440
619
|
```tsx
|
|
441
620
|
// page.tsx
|
|
442
|
-
export default function Page(
|
|
443
|
-
const { t } =
|
|
444
|
-
|
|
621
|
+
export default function Page() {
|
|
622
|
+
const { t } = useLocale()
|
|
623
|
+
const data = useLoaderData()
|
|
624
|
+
return <h1>{t('title') ?? data.title}</h1>
|
|
445
625
|
}
|
|
446
626
|
```
|
|
447
627
|
|
|
448
628
|
```tsx
|
|
449
629
|
// layout.tsx
|
|
450
|
-
export default function RootLayout({ children
|
|
630
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
451
631
|
return <html><head/><body><main>{children}</main></body></html>
|
|
452
632
|
}
|
|
453
633
|
```
|
|
@@ -479,18 +659,28 @@ const loading = useNavigating() // reactive loading state
|
|
|
479
659
|
### Client-side hooks
|
|
480
660
|
|
|
481
661
|
```tsx
|
|
482
|
-
import { useWebsocket, useAction,
|
|
483
|
-
import { useLocale, useTheme, applyTheme, addInterceptor } from 'weifuwu/react'
|
|
662
|
+
import { useWebsocket, useAction, useFetch, useQueryState, createStore, Head } from 'weifuwu/react'
|
|
663
|
+
import { useLocale, useTheme, applyTheme, addInterceptor, useLoaderData, useFlashMessage } from 'weifuwu/react'
|
|
484
664
|
|
|
485
665
|
// WebSocket — auto-reconnecting
|
|
486
|
-
const { send, lastMessage, readyState } = useWebsocket('/ws/chat', {
|
|
666
|
+
const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
|
|
667
|
+
onMessage: (d) => console.log(d),
|
|
668
|
+
reconnect: { maxRetries: 10, delay: 3000 },
|
|
669
|
+
protocols: [], // optional sub-protocols
|
|
670
|
+
enabled: true, // pause/resume connection
|
|
671
|
+
})
|
|
487
672
|
|
|
488
673
|
// Form action
|
|
489
|
-
const { submit, data, error, pending } = useAction('/api/feedback', {
|
|
490
|
-
|
|
674
|
+
const { submit, data, error, pending, reset } = useAction('/api/feedback', {
|
|
675
|
+
method: 'POST',
|
|
676
|
+
headers: { 'X-Custom': 'value' },
|
|
677
|
+
onSuccess: (data) => console.log(data),
|
|
678
|
+
onError: (err) => console.error(err),
|
|
679
|
+
})
|
|
680
|
+
// Auto-reads _csrf cookie, sends as x-csrf-token or x-xsrf-token
|
|
491
681
|
|
|
492
682
|
// Data fetching — cache + dedup + mutate
|
|
493
|
-
const { data, error, loading, mutate } =
|
|
683
|
+
const { data, error, loading, mutate } = useFetch('/api/posts', { fallback: loadData, ttl: 30_000 })
|
|
494
684
|
|
|
495
685
|
// URL query state
|
|
496
686
|
const [q, setQ] = useQueryState('q', '')
|
|
@@ -502,11 +692,10 @@ const count = useStore(s => s.count)
|
|
|
502
692
|
|
|
503
693
|
// Per-page meta tags
|
|
504
694
|
<Head><title>Page Title</title><meta name="description" content="..." /></Head>
|
|
505
|
-
|
|
506
|
-
// Update context (locale switch etc.)
|
|
507
|
-
setCtx({ locale: 'en', prefs: { locale: 'en' } })
|
|
508
695
|
```
|
|
509
696
|
|
|
697
|
+
**`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `prefs`, `env`). Used internally by hooks; rarely needed directly.
|
|
698
|
+
|
|
510
699
|
### Locale & Theme
|
|
511
700
|
|
|
512
701
|
```tsx
|
|
@@ -521,7 +710,7 @@ function LangSwitch() {
|
|
|
521
710
|
|--------|-------------|
|
|
522
711
|
| `locale` | Current locale string (from `ctx.prefs.locale`) |
|
|
523
712
|
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
524
|
-
| `t` |
|
|
713
|
+
| `t` | Translate a key using loaded locale messages |
|
|
525
714
|
|
|
526
715
|
```tsx
|
|
527
716
|
import { useTheme } from 'weifuwu/react'
|
|
@@ -548,6 +737,16 @@ function ThemeToggle() {
|
|
|
548
737
|
|
|
549
738
|
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
550
739
|
|
|
740
|
+
**`useLoaderData()`** — Returns the data returned by `load.ts`. Update-triggered; re-renders on SPA navigation.
|
|
741
|
+
|
|
742
|
+
```tsx
|
|
743
|
+
import { useLoaderData } from 'weifuwu/react'
|
|
744
|
+
function Page() {
|
|
745
|
+
const data = useLoaderData<{ post: { title: string } }>()
|
|
746
|
+
return <h1>{data.post.title}</h1>
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
551
750
|
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
552
751
|
|
|
553
752
|
```ts
|
|
@@ -562,11 +761,19 @@ addInterceptor(async (url) => {
|
|
|
562
761
|
### Flash messages
|
|
563
762
|
|
|
564
763
|
```ts
|
|
565
|
-
// Server
|
|
764
|
+
// Server — set flash cookie on redirect, auto-cleared after first read
|
|
566
765
|
return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })) // 302 + Set-Cookie
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
// Client
|
|
770
|
+
import { useFlashMessage } from 'weifuwu/react'
|
|
567
771
|
|
|
568
|
-
|
|
569
|
-
|
|
772
|
+
function Toast() {
|
|
773
|
+
const flash = useFlashMessage<{ type: string; message: string }>()
|
|
774
|
+
if (!flash) return null
|
|
775
|
+
return <div className={`toast toast-${flash.type}`}>{flash.message}</div>
|
|
776
|
+
}
|
|
570
777
|
```
|
|
571
778
|
|
|
572
779
|
### Dev mode
|
package/cli.ts
CHANGED
|
@@ -70,6 +70,10 @@ async function cmdInit(name: string) {
|
|
|
70
70
|
'',
|
|
71
71
|
'This is a [weifuwu](https://weifuwu.io) HTTP application — pure Node.js, no build step.',
|
|
72
72
|
'',
|
|
73
|
+
'## Before you start',
|
|
74
|
+
'',
|
|
75
|
+
'Read `node_modules/weifuwu/README.md` first. Understand every module and API before writing any code. The weifuwu framework has its own patterns, types, and conventions — do not guess or assume.',
|
|
76
|
+
'',
|
|
73
77
|
'## Commands',
|
|
74
78
|
'',
|
|
75
79
|
'- `npm run dev` — start dev server with `--watch`',
|
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,10 @@ async function cmdInit(name) {
|
|
|
61
61
|
"",
|
|
62
62
|
"This is a [weifuwu](https://weifuwu.io) HTTP application \u2014 pure Node.js, no build step.",
|
|
63
63
|
"",
|
|
64
|
+
"## Before you start",
|
|
65
|
+
"",
|
|
66
|
+
"Read `node_modules/weifuwu/README.md` first. Understand every module and API before writing any code. The weifuwu framework has its own patterns, types, and conventions \u2014 do not guess or assume.",
|
|
67
|
+
"",
|
|
64
68
|
"## Commands",
|
|
65
69
|
"",
|
|
66
70
|
"- `npm run dev` \u2014 start dev server with `--watch`",
|
package/dist/client-state.d.ts
CHANGED
|
@@ -7,16 +7,16 @@ export interface StoreApi<T> {
|
|
|
7
7
|
subscribe: (listener: () => void) => () => void;
|
|
8
8
|
}
|
|
9
9
|
export declare function createStore<T extends Record<string, unknown>>(initial: T): StoreApi<T>;
|
|
10
|
-
interface
|
|
10
|
+
interface UseFetchResult<T> {
|
|
11
11
|
data: T | undefined;
|
|
12
12
|
error: Error | undefined;
|
|
13
13
|
loading: boolean;
|
|
14
14
|
mutate: (data?: T) => Promise<void>;
|
|
15
15
|
}
|
|
16
|
-
interface
|
|
16
|
+
interface UseFetchOptions<T> {
|
|
17
17
|
fallback?: T;
|
|
18
18
|
ttl?: number;
|
|
19
19
|
}
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function useFetch<T = unknown>(url: string | null, options?: UseFetchOptions<T>): UseFetchResult<T>;
|
|
21
21
|
export declare function useQueryState(key: string, defaultValue?: string): [string, (val: string | ((prev: string) => string)) => void];
|
|
22
22
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +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
|
|
7
|
+
export { tsx, TsxContext } from './tsx.ts';
|
|
8
8
|
export type { TsxOptions } from './tsx.ts';
|
|
9
9
|
export { auth, cors, logger } from './middleware.ts';
|
|
10
10
|
export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts';
|
package/dist/index.js
CHANGED
|
@@ -575,9 +575,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
575
575
|
import chokidar from "chokidar";
|
|
576
576
|
|
|
577
577
|
// tsx-context.ts
|
|
578
|
-
import {
|
|
579
|
-
var
|
|
580
|
-
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
|
|
578
|
+
import { createContext } from "react";
|
|
579
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
|
|
581
580
|
var KEY = "__WEIFUWU_CTX_STORE";
|
|
582
581
|
function getStore() {
|
|
583
582
|
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
@@ -595,14 +594,6 @@ function getStore() {
|
|
|
595
594
|
return s;
|
|
596
595
|
}
|
|
597
596
|
var store = getStore();
|
|
598
|
-
var subscribe = (cb) => {
|
|
599
|
-
store._listeners.add(cb);
|
|
600
|
-
return () => {
|
|
601
|
-
store._listeners.delete(cb);
|
|
602
|
-
};
|
|
603
|
-
};
|
|
604
|
-
var getSnapshot = () => store._snapshot;
|
|
605
|
-
var getServerSnapshot = getSnapshot;
|
|
606
597
|
function __registerAls(getStore2) {
|
|
607
598
|
store._alsGetStore = getStore2;
|
|
608
599
|
}
|
|
@@ -611,28 +602,6 @@ function setCtx(value) {
|
|
|
611
602
|
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
|
|
612
603
|
store._listeners.forEach((fn) => fn());
|
|
613
604
|
}
|
|
614
|
-
function _buildT() {
|
|
615
|
-
const messages2 = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
616
|
-
if (!messages2) return fallbackT;
|
|
617
|
-
return (key, params, fallback) => {
|
|
618
|
-
const msg = key.split(".").reduce((o, k) => o?.[k], messages2);
|
|
619
|
-
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
620
|
-
if (!params) return String(msg);
|
|
621
|
-
let result = String(msg);
|
|
622
|
-
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
623
|
-
return result;
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function _readCtx() {
|
|
627
|
-
const alsStore = store._alsGetStore?.();
|
|
628
|
-
const base = alsStore ?? store._ctx;
|
|
629
|
-
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
630
|
-
return { ...base, ...data, t: _buildT() };
|
|
631
|
-
}
|
|
632
|
-
function useCtx() {
|
|
633
|
-
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
634
|
-
return _readCtx();
|
|
635
|
-
}
|
|
636
605
|
var TsxContext = createContext(DEFAULT_CTX);
|
|
637
606
|
|
|
638
607
|
// tsx-instance.ts
|
|
@@ -929,7 +898,7 @@ var TsxInstance = class {
|
|
|
929
898
|
user: ctx.user ?? {},
|
|
930
899
|
parsed: ctx.parsed ?? {},
|
|
931
900
|
prefs: ctx.prefs ?? {},
|
|
932
|
-
|
|
901
|
+
loaderData: {},
|
|
933
902
|
env: ctx.env ?? {}
|
|
934
903
|
};
|
|
935
904
|
return als.run(ctxValue, async () => {
|
|
@@ -1032,24 +1001,27 @@ ${src}`;
|
|
|
1032
1001
|
async buildClientBundle(entryPath, layoutPaths, pagesDir) {
|
|
1033
1002
|
try {
|
|
1034
1003
|
const layoutImports = layoutPaths.map((p) => `import${JSON.stringify(p)};`).join("");
|
|
1004
|
+
const _sc = `(function(){var k='__WEIFUWU_CTX_STORE';var s=typeof globalThis!='undefined'&&globalThis[k];if(!s)return function(){};return function(v){s._ctx={...s._ctx,...v};s._snapshot={params:s._ctx.params,query:s._ctx.query,user:s._ctx.user,parsed:s._ctx.parsed,prefs:s._ctx.prefs,env:s._ctx.env};s._listeners.forEach(function(fn){fn()})}})()`;
|
|
1035
1005
|
const code = [
|
|
1036
1006
|
layoutImports,
|
|
1037
1007
|
`import{hydrateRoot}from'react-dom/client';`,
|
|
1038
1008
|
`import{createElement,useState,useEffect}from'react';`,
|
|
1039
1009
|
`import{TsxContext}from'weifuwu/react';`,
|
|
1040
1010
|
`import P from${JSON.stringify(entryPath)};`,
|
|
1011
|
+
`var setCtx=${_sc};`,
|
|
1041
1012
|
`const c=document.getElementById('__weifuwu_root');`,
|
|
1013
|
+
`if(window.__WEIFUWU_PROPS)setCtx({loaderData:window.__WEIFUWU_PROPS});`,
|
|
1042
1014
|
`if(!window.__WFW_ROOT){`,
|
|
1043
1015
|
`function App(){`,
|
|
1044
|
-
`const[p,setP]=useState({C:P
|
|
1045
|
-
`useEffect(()=>{window.__WFW_SET_PAGE=(C
|
|
1046
|
-
`const ctx=window.__WEIFUWU_CTX||{
|
|
1016
|
+
`const[p,setP]=useState({C:P});`,
|
|
1017
|
+
`useEffect(()=>{window.__WFW_SET_PAGE=(C)=>{setCtx({loaderData:window.__WEIFUWU_PROPS});setP({C})}},[]);`,
|
|
1018
|
+
`const ctx=window.__WEIFUWU_CTX||{};`,
|
|
1047
1019
|
`return createElement(TsxContext.Provider,{value:ctx},`,
|
|
1048
|
-
`createElement(p.C,
|
|
1020
|
+
`createElement(p.C,null))`,
|
|
1049
1021
|
`}`,
|
|
1050
1022
|
`window.__WFW_ROOT=hydrateRoot(c,createElement(App));`,
|
|
1051
1023
|
`}else{`,
|
|
1052
|
-
`window.__WFW_SET_PAGE?.(P
|
|
1024
|
+
`window.__WFW_SET_PAGE?.(P);`,
|
|
1053
1025
|
`}`
|
|
1054
1026
|
].join("");
|
|
1055
1027
|
const publicEnv = {};
|
|
@@ -1119,28 +1091,27 @@ ${src}`;
|
|
|
1119
1091
|
const pageMod = this.pageModules.get(entryPath);
|
|
1120
1092
|
if (!pageMod) return new Response("", { status: 500 });
|
|
1121
1093
|
const Component = pageMod.default;
|
|
1094
|
+
const loadMod = loadPath ? this.loadModules.get(loadPath) : void 0;
|
|
1095
|
+
const loadFn = loadMod?.default;
|
|
1096
|
+
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
1122
1097
|
const ctxValue = {
|
|
1123
1098
|
params: ctx.params,
|
|
1124
1099
|
query: ctx.query,
|
|
1125
1100
|
user: ctx.user ?? {},
|
|
1126
1101
|
parsed: ctx.parsed ?? {},
|
|
1127
1102
|
prefs: ctx.prefs ?? {},
|
|
1128
|
-
|
|
1103
|
+
loaderData: loadProps,
|
|
1129
1104
|
env: ctx.env ?? {}
|
|
1130
1105
|
};
|
|
1131
1106
|
return als.run(ctxValue, async () => {
|
|
1132
1107
|
setCtx(ctxValue);
|
|
1133
|
-
const loadMod = loadPath ? this.loadModules.get(loadPath) : void 0;
|
|
1134
|
-
const loadFn = loadMod?.default;
|
|
1135
|
-
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
1136
|
-
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
1137
1108
|
let element = createElement(
|
|
1138
1109
|
TsxContext.Provider,
|
|
1139
1110
|
{ value: ctxValue },
|
|
1140
1111
|
createElement(
|
|
1141
1112
|
"div",
|
|
1142
1113
|
{ id: "__weifuwu_root" },
|
|
1143
|
-
createElement(Component,
|
|
1114
|
+
createElement(Component, null)
|
|
1144
1115
|
)
|
|
1145
1116
|
);
|
|
1146
1117
|
if (layoutPaths.length === 0) {
|
|
@@ -1165,7 +1136,7 @@ ${src}`;
|
|
|
1165
1136
|
const isRoot = i === 0;
|
|
1166
1137
|
element = createElement(
|
|
1167
1138
|
Layout,
|
|
1168
|
-
|
|
1139
|
+
{ children: element }
|
|
1169
1140
|
);
|
|
1170
1141
|
}
|
|
1171
1142
|
}
|
|
@@ -1177,7 +1148,7 @@ ${src}`;
|
|
|
1177
1148
|
compiledTailwindCss: this.compiledTailwindCss,
|
|
1178
1149
|
isDev,
|
|
1179
1150
|
bundle,
|
|
1180
|
-
|
|
1151
|
+
loaderData: loadProps
|
|
1181
1152
|
});
|
|
1182
1153
|
});
|
|
1183
1154
|
};
|
|
@@ -1456,12 +1427,13 @@ function buildHeadPayload(opts) {
|
|
|
1456
1427
|
return result;
|
|
1457
1428
|
}
|
|
1458
1429
|
function buildBodyScripts(opts) {
|
|
1459
|
-
if (!opts.bundle) return "";
|
|
1460
1430
|
const parts = [];
|
|
1461
|
-
if (opts.
|
|
1462
|
-
parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.
|
|
1431
|
+
if (opts.loaderData && Object.keys(opts.loaderData).length > 0) {
|
|
1432
|
+
parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
|
|
1433
|
+
}
|
|
1434
|
+
if (opts.bundle) {
|
|
1435
|
+
parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
|
|
1463
1436
|
}
|
|
1464
|
-
parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
|
|
1465
1437
|
return parts.join("\n");
|
|
1466
1438
|
}
|
|
1467
1439
|
|
|
@@ -8665,7 +8637,6 @@ export {
|
|
|
8665
8637
|
serve,
|
|
8666
8638
|
serveStatic,
|
|
8667
8639
|
setCookie,
|
|
8668
|
-
setCtx,
|
|
8669
8640
|
smoothStream,
|
|
8670
8641
|
streamObject,
|
|
8671
8642
|
streamText,
|
|
@@ -8673,7 +8644,6 @@ export {
|
|
|
8673
8644
|
tool2 as tool,
|
|
8674
8645
|
tsx,
|
|
8675
8646
|
upload,
|
|
8676
|
-
useCtx,
|
|
8677
8647
|
user,
|
|
8678
8648
|
validate
|
|
8679
8649
|
};
|
package/dist/react.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts
|
|
|
3
3
|
export { useAction } from './use-action.ts';
|
|
4
4
|
export type { UseActionOptions, UseActionReturn } from './use-action.ts';
|
|
5
5
|
export { Link, useNavigate, navigate, useNavigating, addInterceptor } from './client-router.ts';
|
|
6
|
-
export { TsxContext,
|
|
6
|
+
export { TsxContext, useLoaderData } from './tsx-context.ts';
|
|
7
7
|
export { Head } from './head.tsx';
|
|
8
|
-
export { createStore,
|
|
8
|
+
export { createStore, useFetch, useQueryState } from './client-state.ts';
|
|
9
9
|
export type { StoreApi } from './client-state.ts';
|
|
10
10
|
export { useLocale } from './client-locale.ts';
|
|
11
11
|
export { useTheme, applyTheme } from './client-theme.ts';
|
|
12
|
+
export { useFlashMessage } from './use-flash-message.ts';
|
package/dist/react.js
CHANGED
|
@@ -331,9 +331,8 @@ async function prefetchPage(href) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
// tsx-context.ts
|
|
334
|
-
import {
|
|
335
|
-
var
|
|
336
|
-
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
|
|
334
|
+
import { createContext } from "react";
|
|
335
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
|
|
337
336
|
var KEY = "__WEIFUWU_CTX_STORE";
|
|
338
337
|
function getStore() {
|
|
339
338
|
if (typeof globalThis !== "undefined" && globalThis[KEY]) {
|
|
@@ -351,40 +350,22 @@ function getStore() {
|
|
|
351
350
|
return s;
|
|
352
351
|
}
|
|
353
352
|
var store = getStore();
|
|
354
|
-
var subscribe = (cb) => {
|
|
355
|
-
store._listeners.add(cb);
|
|
356
|
-
return () => {
|
|
357
|
-
store._listeners.delete(cb);
|
|
358
|
-
};
|
|
359
|
-
};
|
|
360
|
-
var getSnapshot = () => store._snapshot;
|
|
361
|
-
var getServerSnapshot = getSnapshot;
|
|
362
353
|
function setCtx(value) {
|
|
363
354
|
store._ctx = { ...store._ctx, ...value };
|
|
364
355
|
store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
|
|
365
356
|
store._listeners.forEach((fn) => fn());
|
|
366
357
|
}
|
|
367
|
-
function
|
|
368
|
-
const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
369
|
-
if (!messages) return fallbackT;
|
|
370
|
-
return (key, params, fallback) => {
|
|
371
|
-
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
372
|
-
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
373
|
-
if (!params) return String(msg);
|
|
374
|
-
let result = String(msg);
|
|
375
|
-
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
376
|
-
return result;
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
function _readCtx() {
|
|
358
|
+
function useCtx() {
|
|
380
359
|
const alsStore = store._alsGetStore?.();
|
|
381
360
|
const base = alsStore ?? store._ctx;
|
|
382
361
|
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
383
|
-
return { ...base, ...data
|
|
362
|
+
return { ...base, ...data };
|
|
384
363
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
364
|
+
function useLoaderData() {
|
|
365
|
+
const alsStore = store._alsGetStore?.();
|
|
366
|
+
const base = alsStore ?? store._ctx;
|
|
367
|
+
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
368
|
+
return { ...base, ...data }.loaderData;
|
|
388
369
|
}
|
|
389
370
|
var TsxContext = createContext(DEFAULT_CTX);
|
|
390
371
|
|
|
@@ -405,25 +386,25 @@ function createStore(initial) {
|
|
|
405
386
|
state = { ...state, ...next };
|
|
406
387
|
listeners.forEach((fn) => fn());
|
|
407
388
|
};
|
|
408
|
-
const
|
|
389
|
+
const subscribe = (listener) => {
|
|
409
390
|
listeners.add(listener);
|
|
410
391
|
return () => {
|
|
411
392
|
listeners.delete(listener);
|
|
412
393
|
};
|
|
413
394
|
};
|
|
414
395
|
const useStore = ((selector) => useSyncExternalStore2(
|
|
415
|
-
|
|
396
|
+
subscribe,
|
|
416
397
|
() => selector ? selector(state) : state
|
|
417
398
|
));
|
|
418
399
|
useStore.getState = getState;
|
|
419
400
|
useStore.setState = setState;
|
|
420
|
-
useStore.subscribe =
|
|
401
|
+
useStore.subscribe = subscribe;
|
|
421
402
|
return useStore;
|
|
422
403
|
}
|
|
423
404
|
var dataCache = /* @__PURE__ */ new Map();
|
|
424
405
|
var inflight = /* @__PURE__ */ new Map();
|
|
425
406
|
var CACHE_TTL = 6e4;
|
|
426
|
-
function
|
|
407
|
+
function useFetch(url, options) {
|
|
427
408
|
const ttl = options?.ttl ?? CACHE_TTL;
|
|
428
409
|
const [state, setState] = useState4({
|
|
429
410
|
data: options?.fallback,
|
|
@@ -497,7 +478,7 @@ function notifyQueryListeners() {
|
|
|
497
478
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
498
479
|
}
|
|
499
480
|
function useQueryState(key, defaultValue = "") {
|
|
500
|
-
function
|
|
481
|
+
function getSnapshot() {
|
|
501
482
|
if (typeof window === "undefined") return defaultValue;
|
|
502
483
|
const params = new URLSearchParams(window.location.search);
|
|
503
484
|
return params.get(key) ?? defaultValue;
|
|
@@ -509,12 +490,12 @@ function useQueryState(key, defaultValue = "") {
|
|
|
509
490
|
window.addEventListener("popstate", cb);
|
|
510
491
|
return () => window.removeEventListener("popstate", cb);
|
|
511
492
|
},
|
|
512
|
-
|
|
493
|
+
getSnapshot,
|
|
513
494
|
() => defaultValue
|
|
514
495
|
);
|
|
515
496
|
const setValue = useCallback4((val) => {
|
|
516
497
|
if (typeof window === "undefined") return;
|
|
517
|
-
const resolved = typeof val === "function" ? val(
|
|
498
|
+
const resolved = typeof val === "function" ? val(getSnapshot()) : val;
|
|
518
499
|
const url = new URL(window.location.href);
|
|
519
500
|
if (resolved === defaultValue || resolved === "") {
|
|
520
501
|
url.searchParams.delete(key);
|
|
@@ -528,6 +509,18 @@ function useQueryState(key, defaultValue = "") {
|
|
|
528
509
|
}
|
|
529
510
|
|
|
530
511
|
// client-locale.ts
|
|
512
|
+
function buildT() {
|
|
513
|
+
const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
514
|
+
if (!messages) return (key, _p, fb) => fb ?? key;
|
|
515
|
+
return (key, params, fallback) => {
|
|
516
|
+
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
517
|
+
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
518
|
+
if (!params) return String(msg);
|
|
519
|
+
let result = String(msg);
|
|
520
|
+
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
521
|
+
return result;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
531
524
|
addInterceptor(async (url) => {
|
|
532
525
|
const m = url.pathname.match(/^\/__lang\/(\w+)$/);
|
|
533
526
|
if (!m) return false;
|
|
@@ -551,11 +544,12 @@ function useLocale() {
|
|
|
551
544
|
return {
|
|
552
545
|
locale: ctx.prefs.locale,
|
|
553
546
|
setLocale: (locale) => navigate("/__lang/" + locale),
|
|
554
|
-
t:
|
|
547
|
+
t: buildT()
|
|
555
548
|
};
|
|
556
549
|
}
|
|
557
550
|
|
|
558
551
|
// client-theme.ts
|
|
552
|
+
import { useEffect as useEffect4 } from "react";
|
|
559
553
|
function resolveTheme(theme) {
|
|
560
554
|
if (theme === "system") {
|
|
561
555
|
if (typeof window === "undefined") return "light";
|
|
@@ -601,12 +595,27 @@ addInterceptor(async (url) => {
|
|
|
601
595
|
function useTheme() {
|
|
602
596
|
const ctx = useCtx();
|
|
603
597
|
const theme = ctx.prefs.theme ?? "system";
|
|
598
|
+
useEffect4(() => {
|
|
599
|
+
applyTheme(theme);
|
|
600
|
+
}, [theme]);
|
|
604
601
|
return {
|
|
605
602
|
theme,
|
|
606
603
|
resolvedTheme: resolveTheme(theme),
|
|
607
604
|
setTheme: (t) => navigate("/__theme/" + t)
|
|
608
605
|
};
|
|
609
606
|
}
|
|
607
|
+
|
|
608
|
+
// use-flash-message.ts
|
|
609
|
+
import { useState as useState5 } from "react";
|
|
610
|
+
function useFlashMessage() {
|
|
611
|
+
const [flash] = useState5(() => {
|
|
612
|
+
if (typeof window === "undefined") return null;
|
|
613
|
+
const raw = window.__WEIFUWU_CTX?.prefs?.flash;
|
|
614
|
+
if (!raw) return null;
|
|
615
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
616
|
+
});
|
|
617
|
+
return flash;
|
|
618
|
+
}
|
|
610
619
|
export {
|
|
611
620
|
Head,
|
|
612
621
|
Link,
|
|
@@ -615,10 +624,10 @@ export {
|
|
|
615
624
|
applyTheme,
|
|
616
625
|
createStore,
|
|
617
626
|
navigate,
|
|
618
|
-
setCtx,
|
|
619
627
|
useAction,
|
|
620
|
-
|
|
621
|
-
|
|
628
|
+
useFetch,
|
|
629
|
+
useFlashMessage,
|
|
630
|
+
useLoaderData,
|
|
622
631
|
useLocale,
|
|
623
632
|
useNavigate,
|
|
624
633
|
useNavigating,
|
package/dist/tsx-context.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface PageContext {
|
|
2
2
|
params: Record<string, string>;
|
|
3
3
|
query: Record<string, string>;
|
|
4
4
|
user: {
|
|
@@ -6,11 +6,13 @@ export interface CtxValue {
|
|
|
6
6
|
};
|
|
7
7
|
parsed: Record<string, unknown>;
|
|
8
8
|
prefs: Record<string, string>;
|
|
9
|
-
|
|
9
|
+
loaderData: Record<string, unknown>;
|
|
10
10
|
env: Record<string, string>;
|
|
11
11
|
}
|
|
12
12
|
/** @internal Injected by tsx-instance.ts for async-safe context isolation */
|
|
13
|
-
export declare function __registerAls(getStore: () =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export declare
|
|
13
|
+
export declare function __registerAls(getStore: () => PageContext | undefined): void;
|
|
14
|
+
declare function setCtx(value: Partial<PageContext>): void;
|
|
15
|
+
declare function useCtx(): PageContext;
|
|
16
|
+
export declare function useLoaderData<T = Record<string, unknown>>(): T;
|
|
17
|
+
export declare const TsxContext: import("react").Context<PageContext>;
|
|
18
|
+
export { useCtx, setCtx };
|
package/dist/tsx-instance.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from './router.ts';
|
|
2
|
-
import { TsxContext
|
|
3
|
-
export { TsxContext
|
|
2
|
+
import { TsxContext } from './tsx-context.ts';
|
|
3
|
+
export { TsxContext };
|
|
4
4
|
export interface TsxOptions {
|
|
5
5
|
dir: string;
|
|
6
6
|
}
|
package/dist/tsx.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { TsxContext
|
|
1
|
+
import { TsxContext } from './tsx-instance.ts';
|
|
2
2
|
import type { TsxOptions } from './tsx-instance.ts';
|
|
3
3
|
import type { Router } from './router.ts';
|
|
4
|
-
export { TsxContext
|
|
4
|
+
export { TsxContext };
|
|
5
5
|
export type { TsxOptions };
|
|
6
6
|
export declare function tsx(options: TsxOptions): Promise<Router & {
|
|
7
7
|
stop: () => void;
|
package/dist/types.d.ts
CHANGED
|
@@ -4,12 +4,9 @@ export interface Context {
|
|
|
4
4
|
user?: unknown;
|
|
5
5
|
parsed?: Record<string, unknown>;
|
|
6
6
|
mountPath?: string;
|
|
7
|
-
locale?: string;
|
|
8
7
|
t?: (key: string, params?: Record<string, string>, fallback?: string) => string;
|
|
9
|
-
requestId?: string;
|
|
10
|
-
prefs?: Record<string, string>;
|
|
11
|
-
theme?: string;
|
|
12
8
|
setPref?: (name: string, value: string) => Response;
|
|
9
|
+
prefs?: Record<string, string>;
|
|
13
10
|
env?: Record<string, string>;
|
|
14
11
|
}
|
|
15
12
|
export type Handler = (req: Request, ctx: Context) => Response | Promise<Response>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useFlashMessage<T = any>(): T | null;
|