@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/cli/index.js +7 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +33 -696
- package/dist/index.js +1131 -1336
- package/dist/index.js.map +1 -1
- package/dist/routeTypes-FtbRkpVF.d.ts +491 -0
- package/dist/testing.d.ts +232 -0
- package/dist/testing.js +3578 -0
- package/dist/testing.js.map +1 -0
- package/package.json +5 -1
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
|
-
*
|
|
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,
|
|
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 };
|