@faapi/faapi 1.4.0 → 2.0.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/dist/index.d.ts CHANGED
@@ -1,498 +1,9 @@
1
+ import { R as RouteManifest, C as CorsOptions, F as FaapiContext, H as HelmetOptions, L as LoggerOptions, a as FaapiMiddleware, I as InjectorMap, W as WsRouteManifest } from './routeTypes-FtbRkpVF.js';
2
+ export { b as FaapiContextConfig, c as FailOptions, d as Injector, e as RouteInfo, f as RouteInputSchema, g as RouteOutputSchema, h as RouteParamSchema, S as SseEvent, i as SseWriter, j as cors, k as helmet, l as logger } from './routeTypes-FtbRkpVF.js';
1
3
  import * as node_http from 'node:http';
2
4
  import { Server, IncomingMessage, ServerResponse } from 'node:http';
3
5
  import { Socket } from 'node:net';
4
6
  import ts from 'typescript';
5
- import { WebSocket } from 'ws';
6
-
7
- /**
8
- * SSE(Server-Sent Events)支持
9
- *
10
- * 让 handler 能向客户端推送流式事件,用于 LLM token 流、进度通知等场景。
11
- *
12
- * 核心导出:
13
- * - `encodeSseEvent(event)`:把 SSE 事件对象编码为符合 HTML5 SSE 规范的字符串
14
- * - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/sendError API
15
- * - `SseWriter`:writer 类型,ctx.sse() 返回此类型
16
- *
17
- * 设计要点:
18
- * - writer 内部用 ReadableStream + TextEncoder,send 时 enqueue,close 时 close controller
19
- * - response 预设 text/event-stream、no-cache、keep-alive 头,状态码默认 200
20
- * - close 后再 send 静默忽略,避免 handler 异步流程中误写已关闭的流
21
- * - sendError 向流写入 event: error 后关闭,用于流式输出中报错的优雅终止
22
- *
23
- * 与 ctx 的集成:
24
- * - ctx.sse() 调用 createSseWriter(),并把 response 缓存到 ctx 内部字段
25
- * - invokeHandler 在 handler 返回后检查 ctx 是否持有 SSE response,有则优先使用
26
- */
27
- /**
28
- * SSE 事件字段
29
- *
30
- * 遵循 HTML5 SSE 规范:
31
- * - `data`:消息数据,多行时每行加 `data: ` 前缀;对象自动 JSON.stringify
32
- * - `event`:事件类型,客户端可用 addEventListener(event) 监听
33
- * - `id`:事件 ID,客户端断线重连时通过 Last-Event-ID 头发送
34
- * - `retry`:重连等待时间(毫秒)
35
- * - `comment`:注释行(以 `:` 开头),用于 keep-alive 心跳,不传递给客户端消息
36
- */
37
- interface SseEvent {
38
- /** 消息数据。字符串原样输出;对象自动 JSON.stringify;多行时每行加 data: 前缀 */
39
- data?: unknown;
40
- /** 事件类型,客户端可用 addEventListener 监听 */
41
- event?: string;
42
- /** 事件 ID,客户端断线重连时通过 Last-Event-ID 头发送 */
43
- id?: string | number;
44
- /** 重连等待时间(毫秒) */
45
- retry?: number;
46
- /** 注释行(以 : 开头),用于 keep-alive 心跳 */
47
- comment?: string;
48
- }
49
- /**
50
- * SSE writer:封装流式推送 API
51
- *
52
- * 通过 `ctx.sse()` 创建,handler 调用 `send` 推送事件,`close` 关闭流。
53
- * 框架在 handler 返回后,自动使用 writer.response 作为 HTTP 响应。
54
- */
55
- interface SseWriter {
56
- /** 推送一个 SSE 事件 */
57
- send(event: SseEvent): void;
58
- /**
59
- * 直接写入原始字节/字符串,不做任何 SSE 序列化
60
- *
61
- * 用于透传上游已有的 SSE 原文(如 LLM 中转平台逐 chunk 转发 OpenAI 响应)。
62
- * 调用方负责保证内容符合 HTML5 SSE 规范;`send` 会再次加 `data: ` 前缀,
63
- * 不适用于原文透传场景。
64
- *
65
- * 接受 string 或 Uint8Array(Buffer 是 Uint8Array 子类,自然兼容)。
66
- * 与 `send` 一致:close/aborted 后静默忽略,不抛错。
67
- */
68
- sendRaw(chunk: string | Uint8Array): void;
69
- /** 推送一个 error 事件并关闭流(用于流式输出中报错的优雅终止) */
70
- sendError(error: unknown): void;
71
- /** 关闭流(多次调用安全) */
72
- close(): void;
73
- /** 流是否已关闭(handler 主动 close 或框架自动 close) */
74
- readonly closed: boolean;
75
- /** 客户端是否已断开(ReadableStream 被 cancel) */
76
- readonly aborted: boolean;
77
- /** 对应的 HTTP Response(由框架使用,用户一般不需要直接访问) */
78
- readonly response: Response;
79
- }
80
-
81
- interface CookieOptions {
82
- domain?: string;
83
- path?: string;
84
- maxAge?: number;
85
- expires?: Date;
86
- httpOnly?: boolean;
87
- secure?: boolean;
88
- sameSite?: 'Strict' | 'Lax' | 'None';
89
- }
90
- /**
91
- * ctx.fail() 的参数类型(对象形式,status 和 code 均可省略)
92
- *
93
- * - status: HTTP 状态码(可选,省略时默认 500)
94
- * - code: 业务错误码(可选,省略时响应 body 里不含 code 字段)
95
- * - message: 人类可读错误描述(必填)
96
- *
97
- * status 和 code 是两个独立维度,无关联:
98
- * - status 控制 HTTP 状态码
99
- * - code 是 body 里的业务错误码字段
100
- *
101
- * ```ts
102
- * ctx.fail({ message: '出错' }) // HTTP 500, { error: { message: '出错' } }
103
- * ctx.fail({ status: 404, message: '用户不存在' }) // HTTP 404, { error: { message: '用户不存在' } }
104
- * ctx.fail({ status: 404, code: 'USER_NOT_FOUND', message: '用户不存在' }) // HTTP 404, { error: { code: 'USER_NOT_FOUND', message: '用户不存在' } }
105
- * ```
106
- */
107
- interface FailOptions {
108
- /** HTTP 状态码(可选,省略时默认 500) */
109
- status?: number;
110
- /** 业务错误码(可选,省略时响应 body 里不含 code 字段) */
111
- code?: string;
112
- /** 人类可读错误描述 */
113
- message: string;
114
- }
115
- /**
116
- * ctx.config 的类型:用户自定义业务配置
117
- *
118
- * 默认是 Record<string, unknown>(宽松)。用户可通过 `declare module '@faapi/faapi'` 增强:
119
- *
120
- * ```ts
121
- * declare module '@faapi/faapi' {
122
- * interface FaapiContextConfig {
123
- * db: { host: string; port: number };
124
- * }
125
- * }
126
- * ```
127
- *
128
- * 增强后 `ctx.config.db.host` 即有类型提示。
129
- */
130
- interface FaapiContextConfig extends Record<string, unknown> {
131
- }
132
- interface FaapiContext {
133
- request: Request;
134
- params: Record<string, string>;
135
- query: URLSearchParams;
136
- headers: Headers;
137
- method: string;
138
- path: string;
139
- /**
140
- * 客户端 IP
141
- *
142
- * 优先 `x-forwarded-for` 第一个 IP(反向代理场景),回退到 socket.remoteAddress。
143
- * IPv6 形式 `::ffff:1.2.3.4` 会被规整为 IPv4 形式 `1.2.3.4`。
144
- * 无法获取时为空字符串。
145
- */
146
- ip: string;
147
- /**
148
- * 客户端 User-Agent(请求头 `user-agent` 原值)
149
- *
150
- * 在 createContext 内部从 request.headers 读取(与 ip 不同,无需调用方传入)。
151
- * 不做解析/规整,仅原样透传;无该请求头时为空字符串。
152
- */
153
- ua: string;
154
- /** 解析后的所有 cookie 键值对 */
155
- cookies: Record<string, string>;
156
- /** 配置文件中的自定义业务配置(类型可通过 declare module '@faapi/faapi' 增强 FaapiContextConfig) */
157
- config: FaapiContextConfig;
158
- /**
159
- * 设置响应状态码
160
- */
161
- setStatus(status: number): void;
162
- /**
163
- * 设置响应头
164
- */
165
- setHeader(key: string, value: string): void;
166
- /**
167
- * 设置 ETag 响应头
168
- *
169
- * handler 中基于业务数据(如 updatedAt / version / contentHash)设置 ETag:
170
- * ```ts
171
- * export function GET(ctx) {
172
- * const data = await fetchData();
173
- * ctx.setETag(`"${data.version}-${data.updatedAt}"`);
174
- * return data;
175
- * }
176
- * ```
177
- */
178
- setETag(value: string): void;
179
- /**
180
- * 返回 JSON 响应(handler 直接 return)
181
- *
182
- * ```ts
183
- * return ctx.json({ error: 'Not found' }, 404);
184
- * ```
185
- */
186
- json(data: unknown, status?: number): Response;
187
- /**
188
- * 返回 HTML 响应(handler 直接 return)
189
- *
190
- * ```ts
191
- * return ctx.html('<h1>Hello</h1>');
192
- * ```
193
- */
194
- html(html: string, status?: number): Response;
195
- /**
196
- * 返回重定向响应(handler 直接 return)
197
- *
198
- * ```ts
199
- * return ctx.redirect('/login');
200
- * ```
201
- */
202
- redirect(url: string, status?: number): Response;
203
- /**
204
- * 创建 SSE writer,用于流式推送事件(LLM token 流、进度通知等)
205
- *
206
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
207
- * 作为 HTTP 响应(Content-Type: text/event-stream)。与 ctx.json / ctx.html 互斥。
208
- *
209
- * ```ts
210
- * export async function POST(ctx) {
211
- * const sse = ctx.sse();
212
- * for await (const chunk of stream) {
213
- * sse.send({ data: chunk.text });
214
- * }
215
- * sse.close();
216
- * }
217
- * ```
218
- */
219
- sse(): SseWriter;
220
- /**
221
- * 显式包装成功响应(返回 Response 对象)
222
- *
223
- * 用 config.response.ok 包裹 data 并返回 Response。
224
- * 等价于 handler 直接 `return data`(框架自动包裹),但显式调用语义更清晰。
225
- *
226
- * 返回 Response 对象,不会被框架自动包裹再次包装(避免双重包裹)。
227
- *
228
- * ```ts
229
- * // 以下两种写法等价(假设配置了 response.ok = (data) => ({ data })):
230
- * export function GET() {
231
- * return { id: 1 }; // 自动包裹 → { data: { id: 1 } }
232
- * }
233
- * export function GET2(ctx) {
234
- * return ctx.ok({ id: 1 }); // 显式包裹 → { data: { id: 1 } }
235
- * }
236
- * ```
237
- */
238
- ok(data: unknown): Response;
239
- /**
240
- * 返回错误响应(对象形式参数,status 和 code 均可省略)
241
- *
242
- * @param options.status HTTP 状态码(可选,省略时默认 500)
243
- * @param options.code 业务错误码(可选,省略时响应 body 里不含 code 字段)
244
- * @param options.message 人类可读错误描述(必填)
245
- *
246
- * status 和 code 独立无关联:status 控制 HTTP 状态码,code 是 body 里的业务错误码字段。
247
- *
248
- * ```ts
249
- * return ctx.fail({ message: '出错' }); // HTTP 500, { error: { message: '出错' } }
250
- * return ctx.fail({ status: 404, message: '用户不存在' }); // HTTP 404, { error: { message: '用户不存在' } }
251
- * return ctx.fail({ status: 404, code: 'USER_NOT_FOUND', message: '用户不存在' }); // HTTP 404, { error: { code: 'USER_NOT_FOUND', message: '用户不存在' } }
252
- * ```
253
- */
254
- fail(options: FailOptions): Response;
255
- /**
256
- * 读取 cookie 值
257
- */
258
- getCookie(name: string): string | undefined;
259
- /**
260
- * 设置 cookie
261
- */
262
- setCookie(name: string, value: string, options?: CookieOptions): void;
263
- /**
264
- * 删除 cookie(设置过期)
265
- */
266
- deleteCookie(name: string): void;
267
- }
268
-
269
- /**
270
- * faapi 中间件(洋葱模型)
271
- *
272
- * 单一 async 函数,通过 `await next()` 衔接前置/后置逻辑:
273
- * - `await next()` 之前的代码:前置处理(鉴权、日志开始计时等)
274
- * - `await next()` 之后的代码:后置处理(日志输出、响应修改等)
275
- * - 不调用 `next()` 即拦截请求(如鉴权失败直接返回 Response)
276
- * - `next()` 返回内层 Response,中间件可选择使用或替换
277
- * - 返回 `Response`:作为响应返回(可用于拦截或错误处理)
278
- * - 返回 `void`:使用 `await next()` 返回的内层响应
279
- *
280
- * 错误处理用 try/catch 包裹 `await next()`,而非独立的 error 钩子。
281
- *
282
- * 执行顺序(洋葱模型):
283
- * ```
284
- * mw1.before → mw2.before → handler → mw2.after → mw1.after
285
- * ```
286
- *
287
- * 示例 middlewares.ts:
288
- * ```ts
289
- * import type { FaapiMiddleware } from '@faapi/faapi';
290
- *
291
- * export default [
292
- * // 鉴权:不调 next() 即拦截
293
- * async (ctx, next) => {
294
- * const token = ctx.headers.get('authorization');
295
- * if (!token) return new Response('Unauthorized', { status: 401 });
296
- * ctx.user = await verifyToken(token);
297
- * await next();
298
- * },
299
- * // 日志:before/after 一体,闭包共享状态
300
- * async (ctx, next) => {
301
- * const start = Date.now();
302
- * await next();
303
- * console.log(`${ctx.method} ${ctx.path} ${Date.now() - start}ms`);
304
- * },
305
- * // 错误处理:try/catch 语义
306
- * async (ctx, next) => {
307
- * try {
308
- * await next();
309
- * } catch (err) {
310
- * return new Response(JSON.stringify({ error: String(err) }), { status: 500 });
311
- * }
312
- * },
313
- * ] satisfies FaapiMiddleware[];
314
- * ```
315
- */
316
- type FaapiMiddleware = (ctx: FaapiContext, next: () => Promise<Response>) => Promise<void | Response>;
317
-
318
- /**
319
- * 注入器:按参数名匹配,提供 handler 所需的依赖
320
- *
321
- * 注入器是 faapi 的依赖注入扩展点,与中间件解耦:
322
- * - 中间件只管请求流程(鉴权、日志、错误处理)
323
- * - 注入器只管提供依赖(数据库连接、用户对象等)
324
- *
325
- * 注入器可以读取中间件塞进 ctx 的值(如鉴权中间件塞的 ctx.user),
326
- * 也可以独立提供依赖(如数据库连接池)。
327
- *
328
- * 注入器按需执行:只对 handler 声明的参数执行对应的注入器,避免无谓计算。
329
- *
330
- * 在 middlewares.ts 中通过命名导出 `injectors` 注册:
331
- * ```ts
332
- * import type { InjectorMap } from '@faapi/faapi';
333
- *
334
- * export const injectors: InjectorMap = {
335
- * db: () => getDbConnection(),
336
- * user: (ctx) => ctx.user, // 取中间件塞的值
337
- * };
338
- * ```
339
- */
340
- type Injector = (ctx: FaapiContext) => unknown | Promise<unknown>;
341
- /**
342
- * 注入器映射表:参数名 → 注入器函数
343
- *
344
- * key 必须与 handler 参数名一致,运行时按参数名匹配执行。
345
- */
346
- type InjectorMap = Record<string, Injector>;
347
-
348
- interface CorsOptions {
349
- origin?: string | string[] | true;
350
- methods?: string[];
351
- allowedHeaders?: string[];
352
- exposeHeaders?: string[];
353
- credentials?: boolean;
354
- maxAge?: number;
355
- }
356
- /**
357
- * 创建 CORS 中间件(洋葱模型)
358
- *
359
- * - origin=true: 允许所有来源(反射请求的 Origin)
360
- * - origin=string: 允许指定来源
361
- * - origin=string[]: 允许多个来源
362
- *
363
- * OPTIONS 预检请求直接返回 204,不调用 next()。
364
- */
365
- declare function cors(options?: CorsOptions): FaapiMiddleware;
366
-
367
- type LoggerFn = (messageOrObj: string | Record<string, unknown>, message?: string) => void;
368
- interface LoggerOptions {
369
- /**
370
- * 自定义日志函数
371
- *
372
- * - 传入 `console.log`(默认):纯文本格式 `GET /api/users 200 12ms`
373
- * - 传入 pino logger:结构化日志 `logger.info({ method, path, status, durationMs }, 'request completed')`
374
- * - 传入 winston logger:`logger.info('GET /api/users 200 12ms', { method, path })`
375
- */
376
- log?: LoggerFn;
377
- }
378
- /**
379
- * 创建请求日志中间件(洋葱模型)
380
- *
381
- * 日志格式(文本模式):GET /api/users 200 12ms
382
- * 错误格式(文本模式):POST /api/users 400 45ms - Error: ...
383
- *
384
- * 结构化模式:传入 pino/winston 等 logger 实例时,会自动传递结构化字段。
385
- *
386
- * before/after 一体,闭包变量共享开始时间,无需污染 ctx。
387
- * 错误用 try/catch 捕获,记录后重新抛出(让上层处理)。
388
- * 成功时从 next() 返回的 Response 读取状态码。
389
- *
390
- * log 函数每次请求时读取(options.log ?? console.log),运行时替换 console.log 会生效。
391
- */
392
- declare function logger(options?: LoggerOptions): FaapiMiddleware;
393
-
394
- interface HelmetOptions {
395
- contentSecurityPolicy?: string | false;
396
- xFrameOptions?: 'DENY' | 'SAMEORIGIN' | false;
397
- xContentTypeOptions?: boolean;
398
- referrerPolicy?: string | false;
399
- strictTransportSecurity?: string | false;
400
- xDnsPrefetchControl?: boolean;
401
- xDownloadOptions?: boolean;
402
- xPermittedCrossDomainPolicies?: string | false;
403
- crossOriginOpenerPolicy?: string | false;
404
- crossOriginResourcePolicy?: string | false;
405
- crossOriginEmbedderPolicy?: string | false;
406
- originAgentCluster?: boolean;
407
- xPoweredBy?: boolean;
408
- }
409
- declare function helmet(options?: HelmetOptions): FaapiMiddleware;
410
-
411
- declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
412
- type HttpMethod = (typeof HTTP_METHODS)[number];
413
-
414
- interface RouteRecord {
415
- method: HttpMethod;
416
- urlPath: string;
417
- filePath: string;
418
- paramNames: string[];
419
- isDynamic: boolean;
420
- /** 是否为 catch-all 路由([...slug]) */
421
- isCatchAll?: boolean;
422
- /** 中间件文件绝对路径列表(根在前,路由目录在后;按需加载用) */
423
- middlewarePaths?: string[];
424
- /** 路由对应的中间件集合(从根到路由目录合并,按需加载后缓存) */
425
- middlewares?: FaapiMiddleware[];
426
- /** 路由对应的注入器映射表(从根到路由目录合并,按需加载后缓存) */
427
- injectors?: InjectorMap;
428
- }
429
- /**
430
- * WebSocket 路由记录
431
- *
432
- * 与 HTTP RouteRecord 类似,但不绑定 HTTP 方法(WS 是协议升级,不区分 GET/POST)。
433
- * 一个 handler.ts 中导出 WS 即生成一条 WS 路由记录。
434
- */
435
- interface WsRouteRecord {
436
- urlPath: string;
437
- filePath: string;
438
- paramNames: string[];
439
- isDynamic: boolean;
440
- /** 是否为 catch-all 路由([...slug]) */
441
- isCatchAll?: boolean;
442
- /** 中间件文件绝对路径列表(根在前,路由目录在后;按需加载用) */
443
- middlewarePaths?: string[];
444
- /** 路由对应的中间件集合(握手阶段执行,复用鉴权/CORS/日志;按需加载后缓存) */
445
- middlewares?: FaapiMiddleware[];
446
- /** 路由对应的注入器映射表 */
447
- injectors?: InjectorMap;
448
- }
449
- type RouteManifest = RouteRecord[];
450
- type WsRouteManifest = WsRouteRecord[];
451
- /**
452
- * 路由单个参数的 schema 描述
453
- *
454
- * 供 @faapi/schema 扩展包消费,通过 MCP 暴露给 LLM。
455
- */
456
- interface RouteParamSchema {
457
- name: string;
458
- type: string;
459
- required: boolean;
460
- }
461
- /**
462
- * 路由单个输入源的 schema 描述
463
- */
464
- interface RouteInputSchema {
465
- source: 'query' | 'body' | 'params';
466
- schemaName: string | null;
467
- properties: RouteParamSchema[];
468
- }
469
- /**
470
- * 路由响应类型的 schema 描述
471
- *
472
- * 由 @faapi/schema 扩展包的 buildRouteSchemas 生成。
473
- * output 为 null 表示无显式返回类型注解、void/Promise<void>、或解析失败降级。
474
- */
475
- interface RouteOutputSchema {
476
- /** 命名类型名(如 'UserResponse'),内联类型为 null */
477
- schemaName: string | null;
478
- /** 顶层属性列表 */
479
- properties: RouteParamSchema[];
480
- }
481
- /**
482
- * 路由的完整 schema 描述
483
- *
484
- * 由 @faapi/schema 扩展包的 buildRouteSchemas 生成。
485
- * 主包只定义类型契约,逻辑实现在扩展包。
486
- */
487
- interface RouteInfo {
488
- method: string;
489
- path: string;
490
- filePath: string;
491
- isDynamic: boolean;
492
- inputs: RouteInputSchema[];
493
- /** 响应类型描述(null 表示无返回类型注解/void/解析失败) */
494
- output: RouteOutputSchema | null;
495
- }
496
7
 
497
8
  /** HTTP 请求 handler 类型 */
498
9
  type RequestHandler = (req: IncomingMessage, res: ServerResponse) => void;
@@ -1237,208 +748,6 @@ interface ValidationIssue {
1237
748
  }
1238
749
  type ValidationErrorCode = 'TYPE_MISMATCH' | 'MISSING_FIELD' | 'INVALID_FORMAT' | 'INVALID_VALUE' | 'COERCE_FAILED';
1239
750
 
1240
- /**
1241
- * 从 Request 对象创建 FaapiContext
1242
- * @param request Web Request 对象
1243
- * @param params 动态路由参数
1244
- * @param config 自定义业务配置(来自 faapi.config.ts)
1245
- * @param ip 客户端 IP(由调用方从 IncomingMessage 提取,HTTP/WS 握手均通过 utils/getClientIp)
1246
- *
1247
- * ua 不作为参数传入:User-Agent 是标准 HTTP 请求头,createContext 内部直接从
1248
- * request.headers 读取(与 ip 不同,ip 需要从 IncomingMessage 提取故由调用方传入)。
1249
- */
1250
- declare function createContext(request: Request, params: Record<string, string>, config?: Record<string, unknown>, ip?: string): FaapiContext;
1251
-
1252
- /**
1253
- * 调用路由 handler 并将返回值转为 Response
1254
- *
1255
- * 流程(洋葱模型):
1256
- * 1. 中间件按洋葱模型执行:mw1.before → mw2.before → ... → handler → ... → mw2.after → mw1.after
1257
- * 2. 中间件不调用 next() 即拦截请求(必须返回 Response)
1258
- * 3. 中间件可用 try/catch 捕获内层错误
1259
- * 4. 最内层执行注入器(按需)→ handler
1260
- *
1261
- * 注入器与中间件解耦:
1262
- * - 注入器按 handler 参数名匹配,只执行需要的
1263
- * - 注入器可读取中间件塞进 ctx 的值
1264
- */
1265
- declare function invokeHandler(handler: (...args: unknown[]) => unknown, ctx: FaapiContext, body?: unknown, middlewares?: FaapiMiddleware[], injectors?: InjectorMap): Promise<Response>;
1266
-
1267
- /**
1268
- * createTestServer 入参
1269
- *
1270
- * 业务方一行代码启动带 schema 校验的 E2E 测试服务器。
1271
- * 详见 src/testServer.md。
1272
- */
1273
- interface TestServerOptions {
1274
- /** 项目根目录(路由源码所在,必填) */
1275
- rootDir: string;
1276
- patterns?: string[];
1277
- /**
1278
- * schema 产物输出目录(绝对路径或相对 rootDir)。
1279
- * 不传时自动 mkdtemp 生成临时目录,close() 时清理。
1280
- * 传值时 close() 仍会清理该目录。
1281
- */
1282
- dist?: string;
1283
- /** CORS 中间件配置,默认 false(禁用,避免污染断言) */
1284
- cors?: CorsOptions | boolean;
1285
- /** 安全头配置,默认 false */
1286
- helmet?: HelmetOptions | boolean;
1287
- /** 请求日志配置,默认 false(避免污染测试输出) */
1288
- logger?: LoggerOptions | boolean;
1289
- /** 全局中间件(外层洋葱) */
1290
- middlewares?: FaapiMiddleware[];
1291
- /** 全局注入器 */
1292
- injectors?: InjectorMap;
1293
- /** 请求错误钩子(在错误响应生成后调用,用于副作用) */
1294
- onError?: (error: unknown, ctx: FaapiContext) => Promise<void> | void;
1295
- /** 业务配置(注入到 ctx.config) */
1296
- config?: Record<string, unknown>;
1297
- /** 请求体大小限制(字节),默认 10MB */
1298
- bodyLimit?: number;
1299
- }
1300
- /**
1301
- * createTestServer 返回值
1302
- *
1303
- * 业务方通过 baseUrl 发 fetch 请求,close() 一行完成 teardown。
1304
- */
1305
- interface TestServer {
1306
- /** Node.js HTTP Server 实例(已 listen) */
1307
- server: Server;
1308
- /** 形如 http://localhost:<随机端口> */
1309
- baseUrl: string;
1310
- /** 排序后的路由清单 */
1311
- routes: RouteManifest;
1312
- /** WebSocket 路由清单 */
1313
- wsRoutes: WsRouteManifest;
1314
- /** schema 临时目录绝对路径(业务方调试时可查看生成的 zod.js) */
1315
- schemaDist: string;
1316
- /**
1317
- * 关闭 server + 清理 schema 目录 + 清空 schema 模块缓存
1318
- *
1319
- * 内部顺序:
1320
- * 1. server.closeAllConnections?.()(Node 18+,强制断开 WS / 长连接)
1321
- * 2. server.close()
1322
- * 3. fs.rm(schemaDist, { recursive, force })
1323
- * 4. invalidateSchemaCache()
1324
- *
1325
- * 幂等:重复调用不会重复清理。
1326
- */
1327
- close(): Promise<void>;
1328
- }
1329
- /**
1330
- * 一键启动带 schema 校验的 E2E 测试服务器
1331
- *
1332
- * 内部流程:
1333
- * 1. scanRoutes 扫描路由
1334
- * 2. sortRoutes 排序
1335
- * 3. mkdtemp 创建临时 schema 目录(或用传入的 dist)
1336
- * 4. generateSchemaFiles 生成 zod.js
1337
- * 5. createServer 创建 server(默认禁用 CORS/Helmet/Logger,避免污染断言)
1338
- * 6. server.listen(0) 随机端口
1339
- * 7. 返回 TestServer
1340
- *
1341
- * 详见 src/testServer.md。
1342
- *
1343
- * @param options rootDir 必填,其余可选
1344
- * @returns TestServer 实例
1345
- */
1346
- declare function createTestServer(options: TestServerOptions): Promise<TestServer>;
1347
-
1348
- /**
1349
- * WebSocket 测试客户端
1350
- *
1351
- * 公开导出 connectWs + MessageQueue + waitForWsOpen,业务方测试 WS 路由时
1352
- * 免去手写"消息竞态防护 + 三事件监听 + 端口拼接"样板代码。
1353
- *
1354
- * 详见 src/wsTestClient.md。
1355
- */
1356
- /**
1357
- * connectWs 入参
1358
- */
1359
- interface WsTestClientOptions {
1360
- /** 等待 open 的超时(ms),默认 2000 */
1361
- timeout?: number;
1362
- /** 握手请求头(如 authorization) */
1363
- headers?: Record<string, string>;
1364
- /** WS 子协议 */
1365
- protocols?: string | string[];
1366
- }
1367
- /**
1368
- * connectWs 返回值
1369
- *
1370
- * 业务方通过 ws.send() 发消息,queue.next() 取消息,close() 关闭。
1371
- */
1372
- interface WsTestClient {
1373
- /** ws 库原生实例,业务方可直接 ws.send() / ws.close() */
1374
- ws: WebSocket;
1375
- /** 已开始缓冲的消息队列,调 next(timeout?) 取下一条 */
1376
- queue: MessageQueue;
1377
- /**
1378
- * 关闭 ws 并等待 'close' 事件
1379
- *
1380
- * 内部:
1381
- * 1. 若 ws 仍 OPEN/CLOSING,调 ws.close()
1382
- * 2. 等待 'close' 事件(超时 1000ms 强制 resolve)
1383
- *
1384
- * 幂等:重复调用不抛错。
1385
- */
1386
- close(): Promise<void>;
1387
- }
1388
- /**
1389
- * 消息队列:避免 once('message') 与服务端 onOpen 推送的竞态
1390
- *
1391
- * 服务端在 handleUpgrade 回调里同步触发 onOpen 并 send('connected'),
1392
- * 客户端 'open' 事件触发后到注册 once('message') 之间存在窗口,
1393
- * 若 'connected' 在此窗口内到达,once 会错过。
1394
- *
1395
- * 队列在创建 ws 时立即监听 'message',按 FIFO 顺序消费。
1396
- */
1397
- declare class MessageQueue {
1398
- private queue;
1399
- private waiters;
1400
- private listener;
1401
- constructor(ws: WebSocket);
1402
- /**
1403
- * 取下一条消息
1404
- *
1405
- * 队列有则立即 resolve,无则注册 waiter 等待下一条 'message' 事件。
1406
- * 超时未到 → reject('WebSocket message timeout'),waiter 被清理。
1407
- *
1408
- * @param timeout 超时毫秒,默认 2000
1409
- */
1410
- next(timeout?: number): Promise<string>;
1411
- }
1412
- /**
1413
- * Promise 化等待 ws 'open' 事件
1414
- *
1415
- * 同时监听 'open' / 'error' / 'close' 三事件,任一触发都清理 timer,
1416
- * 避免 timer 泄漏。
1417
- *
1418
- * @param ws WebSocket 实例
1419
- * @param timeout 超时毫秒,默认 2000
1420
- * @returns 'open' → resolve;'error' → reject(err);'close' → reject;超时 → reject
1421
- */
1422
- declare function waitForWsOpen(ws: WebSocket, timeout?: number): Promise<void>;
1423
- /**
1424
- * 一键连接 WS server
1425
- *
1426
- * 内部流程:
1427
- * 1. baseUrl 协议转换(http → ws,https → wss)
1428
- * 2. new WebSocket(url, protocols, { headers })
1429
- * 3. 立即创建 MessageQueue(开始缓冲消息,避免竞态)
1430
- * 4. waitForWsOpen 等待连接建立(三事件监听 + 超时清理)
1431
- * 5. 返回 WsTestClient
1432
- *
1433
- * 连接失败(中间件拦截 / 路径未匹配 / 超时)→ reject。
1434
- *
1435
- * @param baseUrl createTestServer().baseUrl(http://...)
1436
- * @param pathname WS 路径,如 '/api/chat',可含 query
1437
- * @param options timeout / headers / protocols
1438
- * @returns WsTestClient 实例
1439
- */
1440
- declare function connectWs(baseUrl: string, pathname: string, options?: WsTestClientOptions): Promise<WsTestClient>;
1441
-
1442
751
  interface InjectOptions {
1443
752
  method?: string;
1444
753
  path?: string;
@@ -1451,6 +760,31 @@ interface InjectResponse {
1451
760
  headers: Headers;
1452
761
  body: unknown;
1453
762
  }
763
+ /**
764
+ * 获取当前 faapi app 单例
765
+ *
766
+ * 用于在无法直接拿到 app 引用的场景(如 Next.js Server Component)中访问 app。
767
+ *
768
+ * @returns 当前 app 实例
769
+ * @throws 未初始化时抛错(需先调 `createProdApp()` / `createDevApp()`,或 `faapi dev` / `node dist/main` 启动)
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * // Next.js RSC 中调用 faapi API(同进程,跳过 HTTP loopback)
774
+ * import { getApp } from '@faapi/faapi';
775
+ * import { headers } from 'next/headers';
776
+ *
777
+ * const app = getApp();
778
+ * const h = await headers();
779
+ * const res = await app.inject({
780
+ * method: 'GET',
781
+ * path: '/api/user',
782
+ * headers: { cookie: h.get('cookie') ?? '', authorization: h.get('authorization') ?? '' },
783
+ * });
784
+ * const data = res.body; // 已解析
785
+ * ```
786
+ */
787
+ declare function getApp(): AppBase;
1454
788
  interface CreateAppOptions {
1455
789
  /** 项目根目录,默认 process.cwd() */
1456
790
  rootDir?: string;
@@ -1476,8 +810,11 @@ interface AppBase {
1476
810
  /**
1477
811
  * 无服务器测试注入
1478
812
  *
1479
- * 构建一个模拟请求直接走完整请求链路,不绑定端口。
1480
- * 需要在 listen() 之前调用(server 未启动时)。
813
+ * 构建一个模拟请求直接走完整请求链路(CORS / helmet / logger / 全局中间件 / 路由匹配 /
814
+ * schema 校验 / 目录中间件 / handler),不绑定端口,返回已解析的 `{ status, headers, body }`。
815
+ *
816
+ * `listen()` 前后均可调用——`listen()` 后调用常用于 Next.js Server Component 等同进程场景
817
+ * (配合 `getApp()` 拿到 app 实例)。
1481
818
  */
1482
819
  inject(options?: InjectOptions): Promise<InjectResponse>;
1483
820
  }
@@ -1532,4 +869,4 @@ type ProdApp = AppBase;
1532
869
  */
1533
870
  declare function createProdApp(options?: CreateAppOptions): Promise<ProdApp>;
1534
871
 
1535
- export { type ProdApp as App, type CorsOptions, type CreateAppOptions, type DevApp, type FaapiConfig, type FaapiContext, type FaapiContextConfig, FaapiError, type FaapiMiddleware, type FaapiPlugin, type FailOptions, type HandlerTypeInfo, type HelmetOptions, type InjectOptions, type InjectResponse, type Injector, type InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LoggerOptions, MessageQueue, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type ResponseConfig, type RouteInfo, type RouteInputSchema, type RouteManifest, RouteNotFoundError, type RouteOutputSchema, type RouteParamSchema, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, type SseEvent, type SseWriter, type TestServer, type TestServerOptions, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, type WsTestClient, type WsTestClientOptions, collectRouteSchemaSources, connectWs, cors, createProdApp as createApp, createContext, createDevApp, createProdApp, createProgram, createTestServer, extractTypeInfo, getInputTypeForMethod, helmet, invalidateProgramCache, invokeHandler, loadConfig, loadEnv, logger, resolveTypeNode, waitForWsOpen };
872
+ export { type ProdApp as App, CorsOptions, type CreateAppOptions, type DevApp, type FaapiConfig, FaapiContext, FaapiError, FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, HelmetOptions, type InjectOptions, type InjectResponse, InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, LoggerOptions, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type ResponseConfig, RouteManifest, RouteNotFoundError, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, collectRouteSchemaSources, createProdApp as createApp, createDevApp, createProdApp, createProgram, extractTypeInfo, getApp, getInputTypeForMethod, invalidateProgramCache, loadConfig, loadEnv, resolveTypeNode };