@faapi/faapi 0.0.0-canary.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.
@@ -0,0 +1,786 @@
1
+ import * as node_http from 'node:http';
2
+ import ts from 'typescript';
3
+
4
+ /**
5
+ * SSE(Server-Sent Events)支持
6
+ *
7
+ * 让 handler 能向客户端推送流式事件,用于 LLM token 流、进度通知等场景。
8
+ *
9
+ * 核心导出:
10
+ * - `encodeSseEvent(event)`:把 SSE 事件对象编码为符合 HTML5 SSE 规范的字符串
11
+ * - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/sendError API
12
+ * - `SseWriter`:writer 类型,ctx.sse() 返回此类型
13
+ *
14
+ * 设计要点:
15
+ * - writer 内部用 ReadableStream + TextEncoder,send 时 enqueue,close 时 close controller
16
+ * - response 预设 text/event-stream、no-cache、keep-alive 头,状态码默认 200
17
+ * - close 后再 send 静默忽略,避免 handler 异步流程中误写已关闭的流
18
+ * - sendError 向流写入 event: error 后关闭,用于流式输出中报错的优雅终止
19
+ *
20
+ * 与 ctx 的集成:
21
+ * - ctx.sse() 调用 createSseWriter(),并把 response 缓存到 ctx 内部字段
22
+ * - invokeHandler 在 handler 返回后检查 ctx 是否持有 SSE response,有则优先使用
23
+ */
24
+ /**
25
+ * SSE 事件字段
26
+ *
27
+ * 遵循 HTML5 SSE 规范:
28
+ * - `data`:消息数据,多行时每行加 `data: ` 前缀;对象自动 JSON.stringify
29
+ * - `event`:事件类型,客户端可用 addEventListener(event) 监听
30
+ * - `id`:事件 ID,客户端断线重连时通过 Last-Event-ID 头发送
31
+ * - `retry`:重连等待时间(毫秒)
32
+ * - `comment`:注释行(以 `:` 开头),用于 keep-alive 心跳,不传递给客户端消息
33
+ */
34
+ interface SseEvent {
35
+ /** 消息数据。字符串原样输出;对象自动 JSON.stringify;多行时每行加 data: 前缀 */
36
+ data?: unknown;
37
+ /** 事件类型,客户端可用 addEventListener 监听 */
38
+ event?: string;
39
+ /** 事件 ID,客户端断线重连时通过 Last-Event-ID 头发送 */
40
+ id?: string | number;
41
+ /** 重连等待时间(毫秒) */
42
+ retry?: number;
43
+ /** 注释行(以 : 开头),用于 keep-alive 心跳 */
44
+ comment?: string;
45
+ }
46
+ /**
47
+ * SSE writer:封装流式推送 API
48
+ *
49
+ * 通过 `ctx.sse()` 创建,handler 调用 `send` 推送事件,`close` 关闭流。
50
+ * 框架在 handler 返回后,自动使用 writer.response 作为 HTTP 响应。
51
+ */
52
+ interface SseWriter {
53
+ /** 推送一个 SSE 事件 */
54
+ send(event: SseEvent): void;
55
+ /** 推送一个 error 事件并关闭流(用于流式输出中报错的优雅终止) */
56
+ sendError(error: unknown): void;
57
+ /** 关闭流(多次调用安全) */
58
+ close(): void;
59
+ /** 流是否已关闭(handler 主动 close 或框架自动 close) */
60
+ readonly closed: boolean;
61
+ /** 客户端是否已断开(ReadableStream 被 cancel) */
62
+ readonly aborted: boolean;
63
+ /** 对应的 HTTP Response(由框架使用,用户一般不需要直接访问) */
64
+ readonly response: Response;
65
+ }
66
+
67
+ interface CookieOptions {
68
+ domain?: string;
69
+ path?: string;
70
+ maxAge?: number;
71
+ expires?: Date;
72
+ httpOnly?: boolean;
73
+ secure?: boolean;
74
+ sameSite?: 'Strict' | 'Lax' | 'None';
75
+ }
76
+ /**
77
+ * ctx.config 的类型:用户自定义业务配置
78
+ *
79
+ * 默认是 Record<string, unknown>(宽松)。用户可通过 `declare module '@faapi/faapi'` 增强:
80
+ *
81
+ * ```ts
82
+ * declare module '@faapi/faapi' {
83
+ * interface FaapiContextConfig {
84
+ * db: { host: string; port: number };
85
+ * }
86
+ * }
87
+ * ```
88
+ *
89
+ * 增强后 `ctx.config.db.host` 即有类型提示。
90
+ */
91
+ interface FaapiContextConfig extends Record<string, unknown> {
92
+ }
93
+ interface FaapiContext {
94
+ request: Request;
95
+ params: Record<string, string>;
96
+ query: URLSearchParams;
97
+ headers: Headers;
98
+ method: string;
99
+ path: string;
100
+ /** 解析后的所有 cookie 键值对 */
101
+ cookies: Record<string, string>;
102
+ /** 配置文件中的自定义业务配置(类型可通过 declare module '@faapi/faapi' 增强 FaapiContextConfig) */
103
+ config: FaapiContextConfig;
104
+ /**
105
+ * 设置响应状态码
106
+ */
107
+ setStatus(status: number): void;
108
+ /**
109
+ * 设置响应头
110
+ */
111
+ setHeader(key: string, value: string): void;
112
+ /**
113
+ * 返回 JSON 响应(handler 直接 return)
114
+ *
115
+ * ```ts
116
+ * return ctx.json({ error: 'Not found' }, 404);
117
+ * ```
118
+ */
119
+ json(data: unknown, status?: number): Response;
120
+ /**
121
+ * 返回 HTML 响应(handler 直接 return)
122
+ *
123
+ * ```ts
124
+ * return ctx.html('<h1>Hello</h1>');
125
+ * ```
126
+ */
127
+ html(html: string, status?: number): Response;
128
+ /**
129
+ * 返回重定向响应(handler 直接 return)
130
+ *
131
+ * ```ts
132
+ * return ctx.redirect('/login');
133
+ * ```
134
+ */
135
+ redirect(url: string, status?: number): Response;
136
+ /**
137
+ * 创建 SSE writer,用于流式推送事件(LLM token 流、进度通知等)
138
+ *
139
+ * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
140
+ * 作为 HTTP 响应(Content-Type: text/event-stream)。与 ctx.json / ctx.html 互斥。
141
+ *
142
+ * ```ts
143
+ * export async function POST(ctx) {
144
+ * const sse = ctx.sse();
145
+ * for await (const chunk of stream) {
146
+ * sse.send({ data: chunk.text });
147
+ * }
148
+ * sse.close();
149
+ * }
150
+ * ```
151
+ */
152
+ sse(): SseWriter;
153
+ /**
154
+ * 读取 cookie 值
155
+ */
156
+ getCookie(name: string): string | undefined;
157
+ /**
158
+ * 设置 cookie
159
+ */
160
+ setCookie(name: string, value: string, options?: CookieOptions): void;
161
+ /**
162
+ * 删除 cookie(设置过期)
163
+ */
164
+ deleteCookie(name: string): void;
165
+ }
166
+
167
+ /**
168
+ * faapi 中间件(洋葱模型)
169
+ *
170
+ * 单一 async 函数,通过 `await next()` 衔接前置/后置逻辑:
171
+ * - `await next()` 之前的代码:前置处理(鉴权、日志开始计时等)
172
+ * - `await next()` 之后的代码:后置处理(日志输出、响应修改等)
173
+ * - 不调用 `next()` 即拦截请求(如鉴权失败直接返回 Response)
174
+ * - `next()` 返回内层 Response,中间件可选择使用或替换
175
+ * - 返回 `Response`:作为响应返回(可用于拦截或错误处理)
176
+ * - 返回 `void`:使用 `await next()` 返回的内层响应
177
+ *
178
+ * 错误处理用 try/catch 包裹 `await next()`,而非独立的 error 钩子。
179
+ *
180
+ * 执行顺序(洋葱模型):
181
+ * ```
182
+ * mw1.before → mw2.before → handler → mw2.after → mw1.after
183
+ * ```
184
+ *
185
+ * 示例 middlewares.ts:
186
+ * ```ts
187
+ * import type { FaapiMiddleware } from '@faapi/faapi';
188
+ *
189
+ * export default [
190
+ * // 鉴权:不调 next() 即拦截
191
+ * async (ctx, next) => {
192
+ * const token = ctx.headers.get('authorization');
193
+ * if (!token) return new Response('Unauthorized', { status: 401 });
194
+ * ctx.user = await verifyToken(token);
195
+ * await next();
196
+ * },
197
+ * // 日志:before/after 一体,闭包共享状态
198
+ * async (ctx, next) => {
199
+ * const start = Date.now();
200
+ * await next();
201
+ * console.log(`${ctx.method} ${ctx.path} ${Date.now() - start}ms`);
202
+ * },
203
+ * // 错误处理:try/catch 语义
204
+ * async (ctx, next) => {
205
+ * try {
206
+ * await next();
207
+ * } catch (err) {
208
+ * return new Response(JSON.stringify({ error: String(err) }), { status: 500 });
209
+ * }
210
+ * },
211
+ * ] satisfies FaapiMiddleware[];
212
+ * ```
213
+ */
214
+ type FaapiMiddleware = (ctx: FaapiContext, next: () => Promise<Response>) => Promise<void | Response>;
215
+
216
+ /**
217
+ * 注入器:按参数名匹配,提供 handler 所需的依赖
218
+ *
219
+ * 注入器是 faapi 的依赖注入扩展点,与中间件解耦:
220
+ * - 中间件只管请求流程(鉴权、日志、错误处理)
221
+ * - 注入器只管提供依赖(数据库连接、用户对象等)
222
+ *
223
+ * 注入器可以读取中间件塞进 ctx 的值(如鉴权中间件塞的 ctx.user),
224
+ * 也可以独立提供依赖(如数据库连接池)。
225
+ *
226
+ * 注入器按需执行:只对 handler 声明的参数执行对应的注入器,避免无谓计算。
227
+ *
228
+ * 在 middlewares.ts 中通过命名导出 `injectors` 注册:
229
+ * ```ts
230
+ * import type { InjectorMap } from '@faapi/faapi';
231
+ *
232
+ * export const injectors: InjectorMap = {
233
+ * db: () => getDbConnection(),
234
+ * user: (ctx) => ctx.user, // 取中间件塞的值
235
+ * };
236
+ * ```
237
+ */
238
+ type Injector = (ctx: FaapiContext) => unknown | Promise<unknown>;
239
+ /**
240
+ * 注入器映射表:参数名 → 注入器函数
241
+ *
242
+ * key 必须与 handler 参数名一致,运行时按参数名匹配执行。
243
+ */
244
+ type InjectorMap = Record<string, Injector>;
245
+
246
+ interface CorsOptions {
247
+ origin?: string | string[] | true;
248
+ methods?: string[];
249
+ allowedHeaders?: string[];
250
+ exposeHeaders?: string[];
251
+ credentials?: boolean;
252
+ maxAge?: number;
253
+ }
254
+ /**
255
+ * 创建 CORS 中间件(洋葱模型)
256
+ *
257
+ * - origin=true: 允许所有来源(反射请求的 Origin)
258
+ * - origin=string: 允许指定来源
259
+ * - origin=string[]: 允许多个来源
260
+ *
261
+ * OPTIONS 预检请求直接返回 204,不调用 next()。
262
+ */
263
+ declare function cors(options?: CorsOptions): FaapiMiddleware;
264
+
265
+ interface LoggerOptions {
266
+ /** 自定义日志输出函数,默认 console.log */
267
+ log?: (message: string) => void;
268
+ }
269
+ /**
270
+ * 创建请求日志中间件(洋葱模型)
271
+ *
272
+ * 日志格式:GET /api/users 200 12ms
273
+ * 错误格式:POST /api/users 400 45ms - Error: ...
274
+ *
275
+ * before/after 一体,闭包变量共享开始时间,无需污染 ctx。
276
+ * 错误用 try/catch 捕获,记录后重新抛出(让上层处理)。
277
+ * 成功时从 next() 返回的 Response 读取状态码。
278
+ */
279
+ declare function logger(options?: LoggerOptions): FaapiMiddleware;
280
+
281
+ declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
282
+ type HttpMethod = (typeof HTTP_METHODS)[number];
283
+
284
+ interface RouteRecord {
285
+ method: HttpMethod;
286
+ urlPath: string;
287
+ filePath: string;
288
+ paramNames: string[];
289
+ isDynamic: boolean;
290
+ /** 是否为 catch-all 路由([...slug]) */
291
+ isCatchAll?: boolean;
292
+ /** 路由对应的中间件集合(从根到路由目录合并,构建时加载) */
293
+ middlewares?: FaapiMiddleware[];
294
+ /** 路由对应的注入器映射表(从根到路由目录合并,构建时加载) */
295
+ injectors?: InjectorMap;
296
+ }
297
+ type RouteManifest = RouteRecord[];
298
+ /**
299
+ * 路由单个参数的 schema 描述
300
+ *
301
+ * 供 @faapi/schema 扩展包消费,通过 MCP 暴露给 LLM。
302
+ */
303
+ interface RouteParamSchema {
304
+ name: string;
305
+ type: string;
306
+ required: boolean;
307
+ }
308
+ /**
309
+ * 路由单个输入源的 schema 描述
310
+ */
311
+ interface RouteInputSchema {
312
+ source: 'query' | 'body' | 'params';
313
+ schemaName: string | null;
314
+ properties: RouteParamSchema[];
315
+ }
316
+ /**
317
+ * 路由的完整 schema 描述
318
+ *
319
+ * 由 @faapi/schema 扩展包的 buildRouteSchemas 生成。
320
+ * 主包只定义类型契约,逻辑实现在扩展包。
321
+ */
322
+ interface RouteInfo {
323
+ method: string;
324
+ path: string;
325
+ filePath: string;
326
+ isDynamic: boolean;
327
+ inputs: RouteInputSchema[];
328
+ }
329
+
330
+ /**
331
+ * 插件上下文:插件 setup 函数接收的框架能力
332
+ *
333
+ * 插件通过 ctx 访问路由、服务器实例等,不需要直接依赖框架内部模块。
334
+ */
335
+ interface PluginContext {
336
+ /** 项目根目录 */
337
+ rootDir: string;
338
+ /** 当前路由清单 */
339
+ routes: RouteManifest;
340
+ /** HTTP 服务器实例 */
341
+ server: node_http.Server;
342
+ /** 自定义业务配置(faapi.config.ts 中的自定义 key) */
343
+ config: Record<string, unknown>;
344
+ /** 插件选项(来自声明中的 options 字段或元组第二个元素) */
345
+ options?: unknown;
346
+ }
347
+ /**
348
+ * faapi 插件接口
349
+ *
350
+ * 插件是一个对象,包含 name 和 setup 函数。
351
+ * 框架在 server 启动后、onReady 之前按声明顺序加载插件,调用 setup(ctx)。
352
+ *
353
+ * ```ts
354
+ * import type { FaapiPlugin, PluginContext } from '@faapi/faapi';
355
+ *
356
+ * export default {
357
+ * name: 'my-plugin',
358
+ * setup(ctx: PluginContext) {
359
+ * console.log(`Plugin loaded, ${ctx.routes.length} routes found`);
360
+ * },
361
+ * } satisfies FaapiPlugin;
362
+ * ```
363
+ */
364
+ interface FaapiPlugin {
365
+ /** 插件名称(用于去重和日志) */
366
+ name: string;
367
+ /** 插件初始化函数,在 server 启动后、onReady 之前调用 */
368
+ setup: (ctx: PluginContext) => Promise<void> | void;
369
+ }
370
+ /**
371
+ * 插件声明:用户在 faapi.config.ts 的 plugins 字段中使用
372
+ *
373
+ * 支持三种形式:
374
+ * - 包名字符串:`'@faapi/schema'`
375
+ * - 带选项的元组:`['@faapi/schema', { stdio: true }]`
376
+ * - 完整声明对象:`{ package: '@faapi/schema', enable: true }`
377
+ * - 本地路径:`{ path: './my-plugin' }`
378
+ */
379
+ type PluginDeclaration = string | [string, unknown] | {
380
+ package: string;
381
+ enable?: boolean;
382
+ options?: unknown;
383
+ } | {
384
+ path: string;
385
+ enable?: boolean;
386
+ options?: unknown;
387
+ };
388
+
389
+ /**
390
+ * 统一响应格式化函数
391
+ *
392
+ * 当配置了 responseFormat 时,handler 返回的非 Response 值会经过此函数包装
393
+ * 例如:{ code: 0, data, message: 'success' }
394
+ */
395
+ type ResponseFormatFn = (data: unknown, ctx: FaapiContext) => unknown;
396
+ /**
397
+ * 错误响应格式化函数
398
+ *
399
+ * 替代内置的 formatErrorResponse,允许自定义错误响应格式
400
+ */
401
+ type ErrorFormatFn = (error: unknown, ctx?: FaapiContext) => Response;
402
+ /**
403
+ * 生命周期钩子
404
+ */
405
+ interface LifecycleHooks {
406
+ /** 路由加载完成、服务器启动前调用(适合初始化数据库连接等) */
407
+ onReady?: (ctx: LifecycleContext) => Promise<void> | void;
408
+ /** 服务器关闭时调用(适合清理资源、优雅关闭) */
409
+ onClose?: (ctx: LifecycleContext) => Promise<void> | void;
410
+ /**
411
+ * 请求错误已被 errorFormat 处理为响应后调用(参考 Fastify onError 语义)
412
+ *
413
+ * 时机:handler 抛错 → errorFormat 生成错误响应(失败则由框架内置 formatErrorResponse 兜底)
414
+ * → 响应发出后 → onError 触发副作用
415
+ *
416
+ * 职责:日志上报、告警、链路追踪等副作用。**不修改、不替换已生成的响应**。
417
+ * 自身抛错会被捕获并忽略,不影响响应已发送的事实。
418
+ *
419
+ * 与 errorFormat 的区别:
420
+ * - errorFormat:把 error 翻译成 Response(主入口,决定响应内容)
421
+ * - onError:响应发出后的副作用(不能改响应)
422
+ */
423
+ onError?: (error: unknown, ctx: FaapiContext) => Promise<void> | void;
424
+ }
425
+ /**
426
+ * 生命周期上下文
427
+ */
428
+ interface LifecycleContext {
429
+ /** 项目根目录 */
430
+ rootDir: string;
431
+ /** 当前路由清单 */
432
+ routes: RouteManifest;
433
+ /** 服务器实例 */
434
+ server: node_http.Server;
435
+ }
436
+ /**
437
+ * faapi 配置文件类型
438
+ *
439
+ * 在项目根目录创建 faapi.config.ts:
440
+ * ```ts
441
+ * import type { FaapiConfig } from '@faapi/faapi';
442
+ * export default {
443
+ * port: 3000,
444
+ * cors: { origin: '*' },
445
+ * } satisfies FaapiConfig;
446
+ * ```
447
+ *
448
+ * 多环境配置:
449
+ * ```ts
450
+ * import type { FaapiConfig } from '@faapi/faapi';
451
+ * export default {
452
+ * port: 3000,
453
+ * cors: { origin: '*' },
454
+ * // 自定义业务配置(任意 key)
455
+ * db: { host: 'localhost', port: 5432 },
456
+ * } satisfies FaapiConfig;
457
+ * ```
458
+ *
459
+ * 环境覆盖通过 faapi.config.{NODE_ENV}.ts 实现(如 faapi.config.production.ts)
460
+ */
461
+ interface FaapiConfig {
462
+ /** 服务端口,默认 3000(可被 --port / PORT 环境变量覆盖) */
463
+ port?: number;
464
+ /** app 目录,默认 'app' */
465
+ appDir?: string;
466
+ /** 路由扫描 patterns,默认扫描 app/api 下所有 ts 文件 */
467
+ patterns?: string[];
468
+ /** 静态文件目录 */
469
+ staticDir?: string;
470
+ /** CORS 配置,false 禁用 */
471
+ cors?: CorsOptions | boolean;
472
+ /** 统一响应格式化函数 */
473
+ responseFormat?: ResponseFormatFn;
474
+ /** 错误响应格式化函数 */
475
+ errorFormat?: ErrorFormatFn;
476
+ /** 生命周期钩子 */
477
+ lifecycle?: LifecycleHooks;
478
+ /**
479
+ * 全局中间件:对所有路由(HTTP + WebSocket 握手)生效
480
+ *
481
+ * 执行顺序:全局中间件在最外层,目录中间件在内层,handler 最内层。
482
+ * 全局中间件拦截(返回 Response)则目录中间件和 handler 不执行。
483
+ * 全局中间件塞入 ctx 的值,目录中间件和 handler 可读取。
484
+ *
485
+ * 与 CORS 的关系:CORS 由 `cors` 字段配置,全局中间件在 CORS 之后执行。
486
+ *
487
+ * ```ts
488
+ * import type { FaapiConfig, FaapiMiddleware } from '@faapi/faapi';
489
+ *
490
+ * const requestId: FaapiMiddleware = async (ctx, next) => {
491
+ * ctx.requestId = crypto.randomUUID();
492
+ * await next();
493
+ * };
494
+ *
495
+ * export default {
496
+ * middlewares: [requestId],
497
+ * } satisfies FaapiConfig;
498
+ * ```
499
+ *
500
+ * 详见 `src/middleware/README.md` 全局中间件章节。
501
+ */
502
+ middlewares?: FaapiMiddleware[];
503
+ /**
504
+ * 全局注入器:对所有路由的 handler 参数注入生效
505
+ *
506
+ * 合并规则:`{ ...全局注入器, ...目录注入器 }`,目录注入器覆盖全局同名。
507
+ * 全局注入器独立于中间件链,仅提供依赖(db、redis 等),不参与请求流程。
508
+ *
509
+ * ```ts
510
+ * import type { FaapiConfig, InjectorMap } from '@faapi/faapi';
511
+ *
512
+ * export default {
513
+ * injectors: {
514
+ * db: () => getDbConnection(),
515
+ * redis: () => getRedis(),
516
+ * },
517
+ * } satisfies FaapiConfig;
518
+ * ```
519
+ *
520
+ * 详见 `src/middleware/README.md` 全局注入器章节。
521
+ */
522
+ injectors?: InjectorMap;
523
+ /**
524
+ * 插件:应用级扩展,在 server 启动后、onReady 之前按声明顺序加载
525
+ *
526
+ * 与中间件的区别:中间件拦截每个请求,插件在启动时初始化(如启动后台服务、注册协议等)
527
+ *
528
+ * ```ts
529
+ * import type { FaapiConfig } from '@faapi/faapi';
530
+ * export default {
531
+ * plugins: [
532
+ * '@faapi/schema', // 包名
533
+ * ['@faapi/schema', { stdio: true }], // 带选项
534
+ * { package: '@faapi/schema', enable: true }, // 完整声明
535
+ * { path: './my-plugin' }, // 本地路径
536
+ * ],
537
+ * } satisfies FaapiConfig;
538
+ * ```
539
+ */
540
+ plugins?: PluginDeclaration[];
541
+ /**
542
+ * 扩展 ctx:在每次请求创建上下文后调用,可挂载自定义方法(如 ctx.xml、ctx.stream)
543
+ *
544
+ * 类型增强:用户通过 `declare module '@faapi/faapi'` 扩展 FaapiContext 接口获得类型提示
545
+ *
546
+ * ```ts
547
+ * // faapi.config.ts
548
+ * declare module '@faapi/faapi' {
549
+ * interface FaapiContext {
550
+ * xml(data: string): Response;
551
+ * }
552
+ * }
553
+ * export default {
554
+ * extendContext(ctx) {
555
+ * ctx.xml = (data) => new Response(data, { headers: { 'Content-Type': 'application/xml' } });
556
+ * },
557
+ * } satisfies FaapiConfig;
558
+ * ```
559
+ */
560
+ extendContext?: (ctx: FaapiContext) => void;
561
+ /**
562
+ * 自定义业务配置(任意 key)
563
+ *
564
+ * 用户可以在这里放数据库连接、Redis 配置等
565
+ * 通过 ctx.config 访问
566
+ *
567
+ * ```ts
568
+ * export default {
569
+ * db: { host: 'localhost', port: 5432 },
570
+ * redis: { host: '127.0.0.1', port: 6379 },
571
+ * } satisfies FaapiConfig;
572
+ * ```
573
+ */
574
+ [key: string]: unknown;
575
+ }
576
+
577
+ /**
578
+ * WebSocket Handler 类型定义与 socket 封装
579
+ *
580
+ * 用户在 handler.ts 中导出 WS 函数,返回事件回调对象。
581
+ * 框架在协议升级成功后调用对应回调,传递封装后的 WsSocket。
582
+ *
583
+ * @see wsHandler.md 功能说明
584
+ */
585
+ /**
586
+ * faapi 封装的 WebSocket socket 抽象
587
+ *
588
+ * 不直接暴露 ws 库的原生 socket,提供更安全、易用的 API。
589
+ * send 对象时自动 JSON.stringify。
590
+ */
591
+ interface WsSocket {
592
+ /** 发送数据(string/Buffer 直发,对象自动 JSON.stringify) */
593
+ send(data: string | Buffer | object): void;
594
+ /** 关闭连接 */
595
+ close(code?: number, reason?: string | Buffer): void;
596
+ /** 连接状态:0=connecting, 1=open, 2=closing, 3=closed */
597
+ readonly readyState: number;
598
+ }
599
+ /**
600
+ * WebSocket 事件回调集合
601
+ *
602
+ * 用户在 WS handler 中返回此对象,框架在对应事件触发时调用。
603
+ * 所有回调可选,未提供则忽略事件。
604
+ */
605
+ interface WsEventHandlers {
606
+ /** 连接建立时触发 */
607
+ onOpen?: (ws: WsSocket) => void;
608
+ /** 收到客户端消息时触发 */
609
+ onMessage?: (ws: WsSocket, message: string | Buffer) => void;
610
+ /** 连接关闭时触发 */
611
+ onClose?: (ws: WsSocket, code: number, reason: string) => void;
612
+ /** 发生错误时触发 */
613
+ onError?: (ws: WsSocket, error: Error) => void;
614
+ }
615
+ /**
616
+ * WebSocket 上下文(握手阶段构造)
617
+ *
618
+ * 与 HTTP FaapiContext 类似但精简,提供路由参数、查询参数、请求头、配置。
619
+ * 可通过 declare module '@faapi/faapi' 增强自定义字段。
620
+ */
621
+ interface WsContext {
622
+ /** 动态路由参数(如 [id] → params.id) */
623
+ params: Record<string, string>;
624
+ /** URL 查询参数 */
625
+ query: URLSearchParams;
626
+ /** 请求头 */
627
+ headers: Headers;
628
+ /** 业务配置(来自 faapi.config.ts) */
629
+ config: Record<string, unknown>;
630
+ /** 中间件塞入的用户信息(鉴权等) */
631
+ user?: unknown;
632
+ /** 允许通过 declare module 扩展 */
633
+ [key: string]: unknown;
634
+ }
635
+ /**
636
+ * WS handler 签名:接收 WsContext,返回事件回调对象(或无返回)
637
+ */
638
+ type WsHandler = (ctx: WsContext) => WsEventHandlers | void;
639
+
640
+ /**
641
+ * 为指定文件创建 TypeScript Program(带缓存)
642
+ *
643
+ * @param filePath 要分析的 .ts 文件绝对路径
644
+ */
645
+ declare function createProgram(filePath: string): ts.Program;
646
+
647
+ /**
648
+ * Schema 提取错误
649
+ *
650
+ * 遇到无法解析或不支持运行时校验的类型时抛出,
651
+ * 避免静默降级为 any 导致用户不知情。
652
+ */
653
+ declare class SchemaExtractionError extends Error {
654
+ readonly typeText: string;
655
+ readonly reason: string;
656
+ constructor(typeText: string, reason: string, options?: ErrorOptions);
657
+ }
658
+ /**
659
+ * 运行时类型描述
660
+ *
661
+ * 用于校验器在运行时判断值的结构是否符合声明类型。
662
+ * 相比单纯的字符串,能描述数组元素类型、嵌套对象、联合类型等。
663
+ */
664
+ type RuntimeType = {
665
+ kind: 'string';
666
+ } | {
667
+ kind: 'number';
668
+ } | {
669
+ kind: 'boolean';
670
+ } | {
671
+ kind: 'bigint';
672
+ } | {
673
+ kind: 'null';
674
+ } | {
675
+ kind: 'undefined';
676
+ } | {
677
+ kind: 'any';
678
+ } | {
679
+ kind: 'unknown';
680
+ } | {
681
+ kind: 'literal';
682
+ value: string | number | boolean;
683
+ } | {
684
+ kind: 'array';
685
+ element: RuntimeType;
686
+ } | {
687
+ kind: 'object';
688
+ properties: PropertyType[];
689
+ } | {
690
+ kind: 'union';
691
+ members: RuntimeType[];
692
+ } | {
693
+ kind: 'date';
694
+ } | {
695
+ kind: 'record';
696
+ key: RuntimeType;
697
+ value: RuntimeType;
698
+ } | {
699
+ kind: 'ref';
700
+ name: string;
701
+ };
702
+ interface PropertyType {
703
+ name: string;
704
+ type: RuntimeType;
705
+ optional: boolean;
706
+ }
707
+
708
+ interface HandlerTypeInfo {
709
+ name: string;
710
+ properties: PropertyType[];
711
+ /** 完整的运行时类型描述(用于嵌套校验) */
712
+ runtimeType: RuntimeType;
713
+ }
714
+ /**
715
+ * 从源文件中提取指定名称的类型信息
716
+ *
717
+ * 支持的类型声明:
718
+ * - interface 声明(含继承)
719
+ * - type 别名(type Query = { ... })
720
+ *
721
+ * 遇到不支持的类型时抛 `SchemaExtractionError`,错误信息包含文件路径和类型名。
722
+ *
723
+ * @param program TypeScript Program
724
+ * @param filePath 源文件路径
725
+ * @param typeName 类型名,如 'GETQuery'
726
+ */
727
+ declare function extractTypeInfo(program: ts.Program, filePath: string, typeName: string): HandlerTypeInfo | null;
728
+
729
+ /**
730
+ * 根据 HTTP 方法判断主输入类型
731
+ *
732
+ * - GET / DELETE / HEAD:query(URL 查询参数)
733
+ * - POST / PUT / PATCH:body(请求体)
734
+ *
735
+ * 注意:所有方法都可能同时有 query 和 body,
736
+ * 这里返回的是"主输入"(用于校验和注入)。
737
+ * DELETE 请求也支持 body(见 resolveInput.ts)。
738
+ */
739
+ declare function getInputTypeForMethod(method: string): 'query' | 'body';
740
+
741
+ /**
742
+ * 单个参数的 schema 描述(简化版,供扩展包消费)
743
+ */
744
+ interface SchemaPropertyDescriptor {
745
+ name: string;
746
+ type: string;
747
+ required: boolean;
748
+ }
749
+ /**
750
+ * 单个输入源的 schema 描述
751
+ */
752
+ interface InputSchemaDescriptor {
753
+ /** 输入源类型名(如 'Query'、'CreateUserBody'),无类型声明时 null */
754
+ schemaName: string | null;
755
+ /** 参数列表 */
756
+ properties: SchemaPropertyDescriptor[];
757
+ }
758
+ /**
759
+ * 查询指定路由 handler 的输入参数 schema
760
+ *
761
+ * 复用 schemaRegistry 已有的类型提取结果,避免重复 AST 分析。
762
+ * 在 schema 尚未注册时返回 undefined。
763
+ *
764
+ * @param filePath handler 文件绝对路径
765
+ * @param method HTTP 方法(GET/POST 等)
766
+ * @param inputType 输入源类型(query/body/params)
767
+ */
768
+ declare function getSchemaProperties(filePath: string, method: string, inputType: 'query' | 'body' | 'params'): InputSchemaDescriptor | undefined;
769
+
770
+ /**
771
+ * 加载 faapi 配置文件
772
+ *
773
+ * 查找顺序:
774
+ * 1. 指定的 configPath
775
+ * 2. faapi.config.ts / faapi.config.js(基础配置)
776
+ * 3. faapi.config.{env}.ts / faapi.config.{env}.js(环境覆盖,深度合并)
777
+ *
778
+ * 环境由 NODE_ENV 或 FAAPI_ENV 决定,默认 'development'
779
+ *
780
+ * @param rootDir 项目根目录
781
+ * @param configPath 指定的配置文件路径(可选)
782
+ * @returns 合并后的配置,如果无配置文件则返回 null
783
+ */
784
+ declare function loadConfig(rootDir: string, configPath?: string): Promise<Partial<FaapiConfig> | null>;
785
+
786
+ export { type CorsOptions, type ErrorFormatFn, type FaapiConfig, type FaapiContext, type FaapiContextConfig, type FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, type Injector, type InjectorMap, type InputSchemaDescriptor, type LifecycleContext, type LifecycleHooks, type LoggerOptions, type PluginContext, type PluginDeclaration, type PropertyType, type ResponseFormatFn, type RouteInfo, type RouteInputSchema, type RouteManifest, type RouteParamSchema, type RuntimeType, SchemaExtractionError, type SchemaPropertyDescriptor, type SseEvent, type SseWriter, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, cors, createProgram, extractTypeInfo, getInputTypeForMethod, getSchemaProperties, loadConfig, logger };