weifuwu 0.21.0 → 0.22.1

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
@@ -141,6 +141,8 @@ The `ctx` object accumulates properties as it passes through the middleware chai
141
141
  | `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
142
142
  | `csrfToken` | `csrf()` | `string` | CSRF token |
143
143
  | `requestId` | `requestId()` | `string` | Request ID |
144
+ | `session` | `session()` | `Session` | Session data object |
145
+ | `sessionId` | `session()` | `string` | Session ID |
144
146
  | `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
145
147
  | `redis` | `redis()` | `Redis` | Redis client |
146
148
  | `queue` | `queue()` | `Queue` | Job queue |
@@ -154,6 +156,8 @@ The `ctx` object accumulates properties as it passes through the middleware chai
154
156
  | `setPref` | `preferences()` | `(key, val) => Response` | Set preference cookie + redirect |
155
157
  | `compiledTailwindCss` | `ssr()` internal | `string` | Compiled CSS content (internal) |
156
158
  | `tailwindCssUrl` | `ssr()` internal | `string` | Compiled CSS route URL (internal) |
159
+ | `session` | `session()` | `Session` | Session data object |
160
+ | `sessionId` | `session()` | `string` | Session ID |
157
161
 
158
162
  ### Type-Safe Context
159
163
 
@@ -303,6 +307,31 @@ assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
303
307
  | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
304
308
  | `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
305
309
 
310
+ ### Database test isolation
311
+
312
+ ```ts
313
+ import { createTestDb, withTestDb } from 'weifuwu'
314
+
315
+ // Isolated schema — each test gets its own schema, destroyed after
316
+ const db = await createTestDb()
317
+ await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
318
+ await db.sql`INSERT INTO users (name) VALUES ('Alice')`
319
+ await db.destroy() // DROP SCHEMA ... CASCADE
320
+
321
+ // Transaction rollback — all changes are rolled back after callback
322
+ await withTestDb(async (sql) => {
323
+ await sql`INSERT INTO users ...`
324
+ // Automatically rolled back
325
+ })
326
+ ```
327
+
328
+ | Function | Description |
329
+ |----------|-------------|
330
+ | `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
331
+ | `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
332
+
333
+ Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
334
+
306
335
  ---
307
336
 
308
337
  ## Module Reference
@@ -414,6 +443,46 @@ app.use(cors({ credentials: true, maxAge: 3600 }))
414
443
  | `credentials` | `boolean` | `false` | Allow cookies/credentials |
415
444
  | `maxAge` | `number` | — | Preflight cache duration (seconds) |
416
445
 
446
+ ### cache [α]
447
+
448
+ Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
449
+
450
+ ```ts
451
+ app.use(cache()) // in-memory, 5min TTL
452
+ app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
453
+ app.use(cache({
454
+ ttl: 30_000,
455
+ tag: (req, ctx) => ctx.user ? `user:${ctx.user.id}` : undefined, // per-user invalidation
456
+ }))
457
+
458
+ // Programmatic invalidation
459
+ const c = cache({ store: 'redis', redis: ctx.redis })
460
+ app.use(c)
461
+ await c.invalidate('users') // invalidate all entries tagged with 'users'
462
+ await c.flush() // clear entire cache
463
+ ```
464
+
465
+ | Option | Type | Default | Description |
466
+ |--------|------|---------|-------------|
467
+ | `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
468
+ | `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
469
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
470
+ | `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
471
+ | `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
472
+ | `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
473
+ | `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
474
+ | `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
475
+
476
+ Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Authorization` or `Cookie` headers are never cached. Binary content types (image, audio, video) are skipped.
477
+
478
+ ```ts
479
+ import { MemoryCache, RedisCache } from 'weifuwu'
480
+
481
+ const mem = new MemoryCache()
482
+ await mem.set('key', { status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] }, 300_000)
483
+ mem.close()
484
+ ```
485
+
417
486
  ### csrf [α]
418
487
 
419
488
  ```ts
@@ -871,6 +940,42 @@ class MyModule extends PgModule {
871
940
 
872
941
  Where helpers + `and`/`or`/`not` can be imported from `'weifuwu'` alongside `postgres`. Full column builders and table helpers are in the same barrel.
873
942
 
943
+ ### fts — Full-Text Search (PostgreSQL)
944
+
945
+ Utilities for PostgreSQL full-text search: create GIN indexes, search with ranking, and generate highlighted snippets.
946
+
947
+ ```ts
948
+ import { fts } from 'weifuwu'
949
+
950
+ const articles = pg.table('articles', {
951
+ id: serial('id').primaryKey(),
952
+ title: text('title'),
953
+ body: text('body'),
954
+ })
955
+
956
+ // Create search index
957
+ await fts.createIndex(pg.sql, articles, ['title', 'body'], { language: 'english' })
958
+
959
+ // Search with ranking
960
+ const results = await fts.search(pg.sql, articles, 'node.js framework', {
961
+ fields: ['title', 'body'],
962
+ limit: 20,
963
+ headline: true, // highlighted snippets via ts_headline
964
+ })
965
+ // → [{ id, rank: 0.8, row: { title, body, ... }, headline: '...<b>Node.js</b> framework...' }]
966
+
967
+ // Drop index
968
+ await fts.dropIndex(pg.sql, articles)
969
+ ```
970
+
971
+ | Function | Description |
972
+ |----------|-------------|
973
+ | `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
974
+ | `search(sql, table, query, opts?)` | Search with ts_rank ordering |
975
+ | `dropIndex(sql, table, opts?)` | Drop the index |
976
+
977
+ Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
978
+
874
979
  ### preferences [α]
875
980
 
876
981
  Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
@@ -923,16 +1028,34 @@ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
923
1028
  | `.process(handler)` | Register job processor |
924
1029
  | `.run()` | Start processing |
925
1030
  | `.stop()` | Stop processing |
1031
+ | `.jobs(limit?)` | List pending jobs |
1032
+ | `.failedJobs(limit?)` | List failed jobs with error messages |
1033
+ | `.retryFailed(jobId)` | Retry a specific failed job |
1034
+ | `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
1035
+ | `.dashboard()` | Returns a Router with management endpoints |
926
1036
  | `.close()` | Cleanup |
927
1037
 
1038
+ **Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
1039
+
1040
+ | Method | Path | Description |
1041
+ |--------|------|-------------|
1042
+ | GET | `/` | Queue stats + pending/failed counts by type |
1043
+ | GET | `/:type/failed` | List failed jobs for a type |
1044
+ | POST | `/:type/retry` | Retry all failed jobs of a type |
1045
+ | POST | `/retry/:id` | Retry a specific failed job by ID |
1046
+
928
1047
  ### rateLimit [α]
929
1048
 
930
1049
  ```ts
931
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
1050
+ app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
932
1051
  app.get('/api', rateLimit({ max: 10 }), handler) // per-route
933
1052
  app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
1053
+
1054
+ // Multi-process: Redis-backed rate limiting
1055
+ app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
1056
+
934
1057
  // Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
935
- // m.stop() — clear interval
1058
+ // m.stop() — clear interval (memory) or Redis cleanup
936
1059
  ```
937
1060
 
938
1061
  | Option | Type | Default | Description |
@@ -941,6 +1064,11 @@ app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' })
941
1064
  | `window` | `number` | `60_000` | Window duration (ms) |
942
1065
  | `key` | `(req) => string` | IP-based | Key function |
943
1066
  | `message` | `string` | `'Too Many Requests'` | 429 response body |
1067
+ | `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
1068
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1069
+ | `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
1070
+
1071
+ Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
944
1072
 
945
1073
  ### redis [α]
946
1074
 
@@ -989,6 +1117,58 @@ Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML st
989
1117
  | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
990
1118
  | `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
991
1119
 
1120
+ ### session [α]
1121
+
1122
+ Cookie-based server-side session management with memory and Redis stores.
1123
+
1124
+ ```ts
1125
+ app.use(session()) // in-memory store (default)
1126
+ app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
1127
+ app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
1128
+
1129
+ app.get('/login', async (req, ctx) => {
1130
+ ctx.session.userId = 42
1131
+ ctx.session.role = 'admin'
1132
+ // Auto-saved on response — cookie set automatically
1133
+ return Response.json({ ok: true })
1134
+ })
1135
+
1136
+ app.get('/logout', async (req, ctx) => {
1137
+ ctx.session.destroy() // or ctx.session = null
1138
+ return Response.json({ ok: true })
1139
+ })
1140
+
1141
+ // ctx.session.id — readonly session ID
1142
+ // ctx.session.save() — explicit dirty mark (for deep mutations)
1143
+ // ctx.session.destroy() — clear session + remove cookie
1144
+ // Session mutations are auto-detected on property set/delete
1145
+ ```
1146
+
1147
+ | Option | Type | Default | Description |
1148
+ |--------|------|---------|-------------|
1149
+ | `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
1150
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1151
+ | `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
1152
+ | `cookieName` | `string` | `'__session'` | Cookie name |
1153
+ | `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
1154
+ | `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
1155
+ | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
1156
+ | `cookie.path` | `string` | `'/'` | Cookie path |
1157
+ | `cookie.domain` | `string` | — | Cookie domain |
1158
+
1159
+ **Stores** are also exported for standalone use:
1160
+
1161
+ ```ts
1162
+ import { MemoryStore, RedisStore } from 'weifuwu'
1163
+
1164
+ const mem = new MemoryStore() // auto-cleanup every 60s
1165
+ await mem.set('sid', { userId: 1 }, 86400000)
1166
+ mem.close()
1167
+
1168
+ const redis = new RedisStore(redisClient, 'myapp:session:')
1169
+ await redis.destroy('sid')
1170
+ ```
1171
+
992
1172
  ### ssr({ dir }) [β]
993
1173
 
994
1174
  One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration bundles, and livereload — using Next.js-style file conventions.
@@ -1119,7 +1299,67 @@ app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string(
1119
1299
  // ctx.parsed.headers — typed & validated
1120
1300
  })
1121
1301
  // Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
1302
+
1303
+ **Form body auto-parsing** — `application/x-www-form-urlencoded` bodies are automatically parsed into `Record<string, string>` via `URLSearchParams`, even without a Zod schema:
1304
+
1305
+ ```ts
1306
+ // No schema needed — just parse the form
1307
+ app.post('/contact', validate(), (req, ctx) => {
1308
+ const email = ctx.parsed.body.email // string
1309
+ const msg = ctx.parsed.body.message // string
1310
+ return Response.json({ received: true })
1311
+ })
1312
+
1313
+ // Or validate with Zod
1314
+ app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
1122
1315
  ```
1316
+
1317
+ | Option | Type | Default | Description |
1318
+ |--------|------|---------|-------------|
1319
+ | `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
1320
+ | `query` | `ZodSchema` | — | Query validation schema |
1321
+ | `params` | `ZodSchema` | — | URL params validation schema |
1322
+ | `headers` | `ZodSchema` | — | Header validation schema |
1323
+
1324
+ ### webhook [β]
1325
+
1326
+ Webhook receiver with built-in signature verification for Stripe, GitHub, and Slack. Event-based dispatch with replay protection.
1327
+
1328
+ ```ts
1329
+ import { webhook } from 'weifuwu'
1330
+
1331
+ const wh = webhook({
1332
+ stripe: { secret: process.env.STRIPE_WEBHOOK_SECRET! },
1333
+ github: { secret: process.env.GITHUB_WEBHOOK_SECRET! },
1334
+ slack: { secret: process.env.SLACK_WEBHOOK_SECRET! },
1335
+ })
1336
+
1337
+ app.use('/webhooks', wh)
1338
+
1339
+ wh.on('checkout.session.completed', async (event, ctx) => {
1340
+ await fulfillOrder(event.payload.data.object)
1341
+ })
1342
+
1343
+ wh.on('push', async (event, ctx) => {
1344
+ await triggerCI(event.payload)
1345
+ })
1346
+
1347
+ wh.on('*', (event) => {
1348
+ console.log(`Received ${event.provider}.${event.event}`)
1349
+ })
1350
+ ```
1351
+
1352
+ | Option | Type | Default | Description |
1353
+ |--------|------|---------|-------------|
1354
+ | `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
1355
+ | `github` | `PlatformConfig` | — | GitHub webhook config |
1356
+ | `slack` | `PlatformConfig` | — | Slack webhook config |
1357
+ | `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
1358
+ | `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
1359
+ | `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
1360
+
1361
+ Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min window), and Stripe's `t=` / `v1=` signature format. Slack URL verification challenges are auto-responded.
1362
+
1123
1363
  ### Client-side navigation
1124
1364
 
1125
1365
  ```tsx
@@ -1309,7 +1549,8 @@ currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
1309
1549
 
1310
1550
  ```ts
1311
1551
  auth, cors, csrf, compress, helmet, logger, rateLimit, requestId, validate, upload,
1312
- preferences, serveStatic
1552
+ preferences, serveStatic, session, MemoryStore, RedisStore, SessionStore,
1553
+ cache, MemoryCache, RedisCache, CacheStore
1313
1554
  ```
1314
1555
 
1315
1556
  ### Database
@@ -1353,13 +1594,15 @@ openai, createOpenAI
1353
1594
  preferences, health, analytics, seo, seoMiddleware, seoTags,
1354
1595
  user, mailer, graphql, aiStream, runWorkflow,
1355
1596
  logdb, messager, agent, iii, createWorker, registerWorker,
1356
- opencode, deploy, defineConfig,
1597
+ opencode, deploy, defineConfig, webhook,
1357
1598
  testApp, TestApp, TestRequest, TestResponse,
1599
+ createTestDb, withTestDb,
1358
1600
  getCookies, setCookie, deleteCookie,
1359
1601
  createSSEStream, formatSSE, formatSSEData,
1360
1602
  currentTraceId, currentTrace, runWithTrace, traceElapsed,
1361
1603
  createHub, Hub, HubOptions,
1362
1604
  DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
1605
+ fts,
1363
1606
  ```
1364
1607
 
1365
1608
  ---
package/cli.ts CHANGED
@@ -63,8 +63,6 @@ async function cmdInit(name: string, opts: { minimal?: boolean; skipInstall?: bo
63
63
 
64
64
  // Minimal mode: strip SSR/i18n/theme, keep only HTTP core
65
65
  if (opts.minimal) {
66
- // Remove UI directory
67
- await cp(join(templateDir, '..', '..', '..'), '/dev/null') // noop
68
66
  try { await rmrf(join(targetDir, 'ui')) } catch {}
69
67
  try { await rmrf(join(targetDir, 'locales')) } catch {}
70
68
 
@@ -245,6 +243,8 @@ async function rmrf(dir: string) {
245
243
  } catch { /* ignore */ }
246
244
  }
247
245
 
246
+ import { parseArgs } from 'node:util'
247
+
248
248
  // ── CLI dispatcher ──────────────────────────────────────────────────────
249
249
 
250
250
  const cmd = process.argv[2]
@@ -253,11 +253,12 @@ const HELP = `
253
253
  weifuwu — Web-standard HTTP framework for Node.js
254
254
 
255
255
  Usage:
256
- npx weifuwu init <name> Create a new project (SSR + i18n + theme)
257
- npx weifuwu init <name> --minimal Create a minimal HTTP project
258
- npx weifuwu dev Start dev server in current directory
259
- npx weifuwu generate module <name> Scaffold a new module
260
- npx weifuwu version Print version
256
+ npx weifuwu init <name> Create a new project (SSR + i18n + theme)
257
+ npx weifuwu init <name> --minimal Create a minimal HTTP project
258
+ npx weifuwu init <name> --skip-install Create project, skip npm install
259
+ npx weifuwu dev Start dev server in current directory
260
+ npx weifuwu generate module <name> Scaffold a new module
261
+ npx weifuwu version Print version
261
262
  `
262
263
 
263
264
  if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
@@ -265,19 +266,32 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
265
266
  } else if (cmd === 'skill') {
266
267
  cmdSkill().catch(console.error)
267
268
  } else if (cmd === 'init') {
268
- const name = process.argv[3]
269
+ const { values, positionals } = parseArgs({
270
+ args: process.argv.slice(3),
271
+ options: {
272
+ minimal: { type: 'boolean', short: 'm' },
273
+ 'skip-install': { type: 'boolean' },
274
+ },
275
+ strict: false,
276
+ allowPositionals: true,
277
+ })
278
+ const name = positionals[0]
269
279
  if (!name) {
270
- console.error('Usage: npx weifuwu init <name> [--minimal]')
280
+ console.error('Usage: npx weifuwu init <name> [--minimal] [--skip-install]')
271
281
  process.exit(1)
272
282
  }
273
- const minimal = process.argv.includes('--minimal')
274
- const skipInstall = process.argv.includes('--skip-install')
275
- cmdInit(name, { minimal, skipInstall }).catch(console.error)
283
+ cmdInit(name, { minimal: !!values.minimal, skipInstall: !!values['skip-install'] }).catch(console.error)
276
284
  } else if (cmd === 'dev') {
277
285
  cmdDev()
278
286
  } else if (cmd === 'generate' || cmd === 'g') {
279
- const type = process.argv[3]
280
- const name = process.argv[4]
287
+ const { positionals } = parseArgs({
288
+ args: process.argv.slice(3),
289
+ options: {},
290
+ strict: false,
291
+ allowPositionals: true,
292
+ })
293
+ const type = positionals[0]
294
+ const name = positionals[1]
281
295
  if (!type || !name) {
282
296
  console.error('Usage: npx weifuwu generate module <name>')
283
297
  process.exit(1)
@@ -0,0 +1,74 @@
1
+ import type { Context, Middleware } from './types.ts';
2
+ import type { Redis } from './vendor.ts';
3
+ export interface CachedResponse {
4
+ status: number;
5
+ statusText: string;
6
+ headers: Record<string, string>;
7
+ body: string;
8
+ createdAt: number;
9
+ tags: string[];
10
+ }
11
+ export interface CacheStore {
12
+ get(key: string): Promise<CachedResponse | null>;
13
+ set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
14
+ delete(key: string): Promise<void>;
15
+ invalidate(tag: string): Promise<void>;
16
+ flush(): Promise<void>;
17
+ }
18
+ export interface CacheOptions {
19
+ /** TTL in milliseconds. Default: 300_000 (5 min). */
20
+ ttl?: number;
21
+ /** Cache store. 'memory' (default) or 'redis'. */
22
+ store?: 'memory' | 'redis' | CacheStore;
23
+ /** Redis client (required when store: 'redis'). */
24
+ redis?: Redis;
25
+ /** Custom cache key function. Default: SHA256 of method + URL. */
26
+ key?: (req: Request) => string;
27
+ /** Tag function for grouped invalidation. Called after handler runs (ctx available). */
28
+ tag?: (req: Request, ctx: Context) => string | string[] | undefined;
29
+ /** Whether to cache responses with Set-Cookie. Default: false. */
30
+ cacheCookies?: boolean;
31
+ /** Status codes to cache. Default: [200]. */
32
+ cacheStatus?: number[];
33
+ /** Maximum number of bytes per cached body. Default: 1MB. Larger bodies are skipped. */
34
+ maxBodySize?: number;
35
+ }
36
+ export interface CacheMiddleware extends Middleware {
37
+ /** Invalidate all entries with a given tag. */
38
+ invalidate(tag: string): Promise<void>;
39
+ /** Flush all cached entries. */
40
+ flush(): Promise<void>;
41
+ /** Cleanup. */
42
+ close(): void;
43
+ /** Store reference (for testing). */
44
+ store: CacheStore;
45
+ }
46
+ export declare class MemoryCache implements CacheStore {
47
+ private store;
48
+ private tagIndex;
49
+ private interval;
50
+ constructor(cleanupMs?: number);
51
+ get(key: string): Promise<CachedResponse | null>;
52
+ set(key: string, data: CachedResponse, ttl: number): Promise<void>;
53
+ delete(key: string): Promise<void>;
54
+ invalidate(tag: string): Promise<void>;
55
+ flush(): Promise<void>;
56
+ private cleanup;
57
+ close(): void;
58
+ /** Testing only. */
59
+ get size(): number;
60
+ }
61
+ export declare class RedisCache implements CacheStore {
62
+ private redis;
63
+ private prefix;
64
+ private tagPrefix;
65
+ constructor(redis: Redis, prefix?: string);
66
+ private key;
67
+ private tagKey;
68
+ get(key: string): Promise<CachedResponse | null>;
69
+ set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
70
+ delete(key: string): Promise<void>;
71
+ invalidate(tag: string): Promise<void>;
72
+ flush(): Promise<void>;
73
+ }
74
+ export declare function cache(options?: CacheOptions): CacheMiddleware;
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { execSync } from "node:child_process";
7
7
  import { homedir } from "node:os";
8
8
  import { join, dirname, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { parseArgs } from "node:util";
10
11
  var __filename = fileURLToPath(import.meta.url);
11
12
  var __dirname = dirname(__filename);
12
13
  var pkgRoot = existsSync(join(__dirname, "package.json")) ? __dirname : resolve(__dirname, "..");
@@ -44,7 +45,6 @@ async function cmdInit(name, opts) {
44
45
  await writeFile(uiPage, content);
45
46
  }
46
47
  if (opts.minimal) {
47
- await cp(join(templateDir, "..", "..", ".."), "/dev/null");
48
48
  try {
49
49
  await rmrf(join(targetDir, "ui"));
50
50
  } catch {
@@ -213,30 +213,44 @@ var HELP = `
213
213
  weifuwu \u2014 Web-standard HTTP framework for Node.js
214
214
 
215
215
  Usage:
216
- npx weifuwu init <name> Create a new project (SSR + i18n + theme)
217
- npx weifuwu init <name> --minimal Create a minimal HTTP project
218
- npx weifuwu dev Start dev server in current directory
219
- npx weifuwu generate module <name> Scaffold a new module
220
- npx weifuwu version Print version
216
+ npx weifuwu init <name> Create a new project (SSR + i18n + theme)
217
+ npx weifuwu init <name> --minimal Create a minimal HTTP project
218
+ npx weifuwu init <name> --skip-install Create project, skip npm install
219
+ npx weifuwu dev Start dev server in current directory
220
+ npx weifuwu generate module <name> Scaffold a new module
221
+ npx weifuwu version Print version
221
222
  `;
222
223
  if (cmd === "version" || cmd === "-v" || cmd === "--version") {
223
224
  cmdVersion().catch(console.error);
224
225
  } else if (cmd === "skill") {
225
226
  cmdSkill().catch(console.error);
226
227
  } else if (cmd === "init") {
227
- const name = process.argv[3];
228
+ const { values, positionals } = parseArgs({
229
+ args: process.argv.slice(3),
230
+ options: {
231
+ minimal: { type: "boolean", short: "m" },
232
+ "skip-install": { type: "boolean" }
233
+ },
234
+ strict: false,
235
+ allowPositionals: true
236
+ });
237
+ const name = positionals[0];
228
238
  if (!name) {
229
- console.error("Usage: npx weifuwu init <name> [--minimal]");
239
+ console.error("Usage: npx weifuwu init <name> [--minimal] [--skip-install]");
230
240
  process.exit(1);
231
241
  }
232
- const minimal = process.argv.includes("--minimal");
233
- const skipInstall = process.argv.includes("--skip-install");
234
- cmdInit(name, { minimal, skipInstall }).catch(console.error);
242
+ cmdInit(name, { minimal: !!values.minimal, skipInstall: !!values["skip-install"] }).catch(console.error);
235
243
  } else if (cmd === "dev") {
236
244
  cmdDev();
237
245
  } else if (cmd === "generate" || cmd === "g") {
238
- const type = process.argv[3];
239
- const name = process.argv[4];
246
+ const { positionals } = parseArgs({
247
+ args: process.argv.slice(3),
248
+ options: {},
249
+ strict: false,
250
+ allowPositionals: true
251
+ });
252
+ const type = positionals[0];
253
+ const name = positionals[1];
240
254
  if (!type || !name) {
241
255
  console.error("Usage: npx weifuwu generate module <name>");
242
256
  process.exit(1);
package/dist/fts.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Sql } from './vendor.ts';
2
+ import type { BoundTable } from './postgres/schema/index.ts';
3
+ export interface FTSSearchResult {
4
+ /** Primary key value */
5
+ id: unknown;
6
+ /** Relevance score (0–1) */
7
+ rank: number;
8
+ /** The row data (all columns) */
9
+ row: Record<string, unknown>;
10
+ /** ts_headline highlighted snippet, if requested */
11
+ headline?: string;
12
+ }
13
+ export interface FTSCreateIndexOptions {
14
+ language?: string;
15
+ indexName?: string;
16
+ indexType?: 'gin' | 'gist';
17
+ }
18
+ export interface FTSSearchOptions {
19
+ fields?: string[];
20
+ limit?: number;
21
+ offset?: number;
22
+ headline?: boolean;
23
+ language?: string;
24
+ rankColumn?: string;
25
+ minRank?: number;
26
+ }
27
+ export declare function createIndex(sql: Sql<{}>, table: BoundTable<any>, fields: string[], options?: FTSCreateIndexOptions): Promise<void>;
28
+ export declare function dropIndex(sql: Sql<{}>, table: BoundTable<any>, options?: {
29
+ indexName?: string;
30
+ }): Promise<void>;
31
+ export declare function search<T extends Record<string, unknown>>(sql: Sql<{}>, table: BoundTable<T>, query: string, options?: FTSSearchOptions): Promise<FTSSearchResult[]>;
32
+ export declare function suggest(sql: Sql<{}>, table: BoundTable<any>, prefix: string, options?: {
33
+ field?: string;
34
+ language?: string;
35
+ limit?: number;
36
+ }): Promise<string[]>;
package/dist/index.d.ts CHANGED
@@ -31,8 +31,8 @@ export { requestId } from './request-id.ts';
31
31
  export type { RequestIdOptions } from './request-id.ts';
32
32
  export { createSSEStream, formatSSE, formatSSEData } from './sse.ts';
33
33
  export type { SSEEvent } from './sse.ts';
34
- export { testApp, TestApp, TestRequest } from './test-utils.ts';
35
- export type { TestResponse } from './test-utils.ts';
34
+ export { testApp, TestApp, TestRequest, createTestDb, withTestDb } from './test-utils.ts';
35
+ export type { TestResponse, TestDb } from './test-utils.ts';
36
36
  export { graphql } from './graphql.ts';
37
37
  export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
38
38
  export { aiStream } from './ai.ts';
@@ -77,3 +77,10 @@ export type { LogdbOptions, LogdbModule, LogEntry, LogEntryInput, } from './logd
77
77
  export { iii, createWorker, registerWorker } from './iii/index.ts';
78
78
  export type { IIIModule, IIIOptions, WorkerInfo, FunctionInfo, TriggerInfo, FunctionHandler, FunctionContext, TriggerInput, RemoteWorker, TriggerRequest, } from './iii/types.ts';
79
79
  export { ssr } from './ssr.ts';
80
+ export { session, MemoryStore, RedisStore } from './session.ts';
81
+ export type { Session, SessionOptions, SessionStore, SessionData, SessionInjected } from './session.ts';
82
+ export { cache, MemoryCache, RedisCache } from './cache.ts';
83
+ export type { CacheOptions, CacheStore, CacheMiddleware, CachedResponse } from './cache.ts';
84
+ export { webhook } from './webhook.ts';
85
+ export type { WebhookOptions, WebhookModule, WebhookEvent, WebhookHandler, PlatformConfig, CustomVerifierConfig } from './webhook.ts';
86
+ export * as fts from './fts.ts';