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 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 5 patterns. The pattern letter is marked in each module's heading.
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', 'docs')
112
- a.run(agentId, { task: 'summarize' })
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' }) })) // custom verify
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 })) // only > 2KB
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 available in handlers
175
- // Auto-validates X-CSRF-Token header on POST/PUT/DELETE/PATCH
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({ apps: [{ name: 'api', dir: './api', domain: 'api.example.com', port: 3001 }] })
190
- await deploy(config)
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()) // GET /health → 200
197
- app.use(health({ checks: { db: async () => { await pg.sql`SELECT 1`; return { ok: true } } } }))
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
- 13 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
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
- | `.trigger({ function_id, payload, action? })` | Invoke a function (sync or void) |
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({ host: 'smtp.example.com', port: 587, auth: { user, pass } })
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', u.middleware(), msg.wsHandler())
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({ pg, permissions: { bash: { allow: true }, write: { allow: false } } })
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() // reads REDIS_URL
354
- app.use(r) // injects ctx.redis
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({ params, query }: { params: { slug: string }; query: Record<string, string> }) {
443
- const { t } = useCtx()
444
- return <h1>{t('title')}</h1>
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, req }: { children: React.ReactNode; req: Request }) {
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, useData, useQueryState, createStore, Head, setCtx } from 'weifuwu/react'
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', { onMessage: (d) => console.log(d), reconnect: { maxRetries: 10, delay: 3000 } })
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', { method: 'POST' })
490
- // Auto-reads _csrf cookie, sends as X-CSRF-Token
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 } = useData('/api/posts', { fallback: loadData })
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` | Translation function (same as `useCtx().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
- // Client (tsx)
569
- function Toast() { const { prefs } = useCtx(); const flash = prefs?.flash ? JSON.parse(prefs.flash) : null; ... }
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`",
@@ -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 UseDataResult<T> {
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 UseDataOptions<T> {
16
+ interface UseFetchOptions<T> {
17
17
  fallback?: T;
18
18
  ttl?: number;
19
19
  }
20
- export declare function useData<T = unknown>(url: string | null, options?: UseDataOptions<T>): UseDataResult<T>;
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, useCtx, setCtx } from './tsx.ts';
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 { useSyncExternalStore, createContext } from "react";
579
- var fallbackT = (key, _params, fallback) => fallback ?? key;
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
- t: ctx.t ?? ((key) => key),
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,props:window.__WEIFUWU_PROPS});`,
1045
- `useEffect(()=>{window.__WFW_SET_PAGE=(C,props)=>setP({C,props})},[]);`,
1046
- `const ctx=window.__WEIFUWU_CTX||{params:{},query:{}};`,
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,p.props))`,
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,window.__WEIFUWU_PROPS);`,
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
- t: ctx.t ?? ((key) => key),
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, allProps)
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
- isRoot ? { children: element, req } : { children: element }
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
- allProps
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.allProps) {
1462
- parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.allProps)}</script>`);
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, useCtx, setCtx } from './tsx-context.ts';
6
+ export { TsxContext, useLoaderData } from './tsx-context.ts';
7
7
  export { Head } from './head.tsx';
8
- export { createStore, useData, useQueryState } from './client-state.ts';
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 { useSyncExternalStore, createContext } from "react";
335
- var fallbackT = (key, _params, fallback) => fallback ?? key;
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 _buildT() {
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, t: _buildT() };
362
+ return { ...base, ...data };
384
363
  }
385
- function useCtx() {
386
- useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
387
- return _readCtx();
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 subscribe2 = (listener) => {
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
- subscribe2,
396
+ subscribe,
416
397
  () => selector ? selector(state) : state
417
398
  ));
418
399
  useStore.getState = getState;
419
400
  useStore.setState = setState;
420
- useStore.subscribe = subscribe2;
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 useData(url, options) {
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 getSnapshot2() {
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
- getSnapshot2,
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(getSnapshot2()) : 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: ctx.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
- useCtx,
621
- useData,
628
+ useFetch,
629
+ useFlashMessage,
630
+ useLoaderData,
622
631
  useLocale,
623
632
  useNavigate,
624
633
  useNavigating,
@@ -1,4 +1,4 @@
1
- export interface CtxValue {
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
- t: (key: string, params?: Record<string, string>, fallback?: string) => string;
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: () => CtxValue | undefined): void;
14
- export declare function setCtx(value: Partial<CtxValue>): void;
15
- export declare function useCtx(): CtxValue;
16
- export declare const TsxContext: import("react").Context<CtxValue>;
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 };
@@ -1,6 +1,6 @@
1
1
  import { Router } from './router.ts';
2
- import { TsxContext, useCtx, setCtx } from './tsx-context.ts';
3
- export { TsxContext, useCtx, setCtx };
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, useCtx, setCtx } from './tsx-instance.ts';
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, useCtx, setCtx };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.17.25",
3
+ "version": "0.18.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",