@faapi/faapi 0.0.0-canary.e2ee43b → 0.0.0-canary.f08a14e
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 +7 -1
- package/dist/cli/index.js +5705 -5493
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +426 -66
- package/dist/index.js +2833 -168
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,17 @@ interface SseEvent {
|
|
|
54
54
|
interface SseWriter {
|
|
55
55
|
/** 推送一个 SSE 事件 */
|
|
56
56
|
send(event: SseEvent): void;
|
|
57
|
+
/**
|
|
58
|
+
* 直接写入原始字节/字符串,不做任何 SSE 序列化
|
|
59
|
+
*
|
|
60
|
+
* 用于透传上游已有的 SSE 原文(如 LLM 中转平台逐 chunk 转发 OpenAI 响应)。
|
|
61
|
+
* 调用方负责保证内容符合 HTML5 SSE 规范;`send` 会再次加 `data: ` 前缀,
|
|
62
|
+
* 不适用于原文透传场景。
|
|
63
|
+
*
|
|
64
|
+
* 接受 string 或 Uint8Array(Buffer 是 Uint8Array 子类,自然兼容)。
|
|
65
|
+
* 与 `send` 一致:close/aborted 后静默忽略,不抛错。
|
|
66
|
+
*/
|
|
67
|
+
sendRaw(chunk: string | Uint8Array): void;
|
|
57
68
|
/** 推送一个 error 事件并关闭流(用于流式输出中报错的优雅终止) */
|
|
58
69
|
sendError(error: unknown): void;
|
|
59
70
|
/** 关闭流(多次调用安全) */
|
|
@@ -119,6 +130,19 @@ interface FaapiContext {
|
|
|
119
130
|
* 设置响应头
|
|
120
131
|
*/
|
|
121
132
|
setHeader(key: string, value: string): void;
|
|
133
|
+
/**
|
|
134
|
+
* 设置 ETag 响应头
|
|
135
|
+
*
|
|
136
|
+
* handler 中基于业务数据(如 updatedAt / version / contentHash)设置 ETag:
|
|
137
|
+
* ```ts
|
|
138
|
+
* export function GET(ctx) {
|
|
139
|
+
* const data = await fetchData();
|
|
140
|
+
* ctx.setETag(`"${data.version}-${data.updatedAt}"`);
|
|
141
|
+
* return data;
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
setETag(value: string): void;
|
|
122
146
|
/**
|
|
123
147
|
* 返回 JSON 响应(handler 直接 return)
|
|
124
148
|
*
|
|
@@ -272,22 +296,50 @@ interface CorsOptions {
|
|
|
272
296
|
*/
|
|
273
297
|
declare function cors(options?: CorsOptions): FaapiMiddleware;
|
|
274
298
|
|
|
299
|
+
type LoggerFn = (messageOrObj: string | Record<string, unknown>, message?: string) => void;
|
|
275
300
|
interface LoggerOptions {
|
|
276
|
-
/**
|
|
277
|
-
|
|
301
|
+
/**
|
|
302
|
+
* 自定义日志函数
|
|
303
|
+
*
|
|
304
|
+
* - 传入 `console.log`(默认):纯文本格式 `GET /api/users 200 12ms`
|
|
305
|
+
* - 传入 pino logger:结构化日志 `logger.info({ method, path, status, durationMs }, 'request completed')`
|
|
306
|
+
* - 传入 winston logger:`logger.info('GET /api/users 200 12ms', { method, path })`
|
|
307
|
+
*/
|
|
308
|
+
log?: LoggerFn;
|
|
278
309
|
}
|
|
279
310
|
/**
|
|
280
311
|
* 创建请求日志中间件(洋葱模型)
|
|
281
312
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
313
|
+
* 日志格式(文本模式):GET /api/users 200 12ms
|
|
314
|
+
* 错误格式(文本模式):POST /api/users 400 45ms - Error: ...
|
|
315
|
+
*
|
|
316
|
+
* 结构化模式:传入 pino/winston 等 logger 实例时,会自动传递结构化字段。
|
|
284
317
|
*
|
|
285
318
|
* before/after 一体,闭包变量共享开始时间,无需污染 ctx。
|
|
286
319
|
* 错误用 try/catch 捕获,记录后重新抛出(让上层处理)。
|
|
287
320
|
* 成功时从 next() 返回的 Response 读取状态码。
|
|
321
|
+
*
|
|
322
|
+
* log 函数每次请求时读取(options.log ?? console.log),运行时替换 console.log 会生效。
|
|
288
323
|
*/
|
|
289
324
|
declare function logger(options?: LoggerOptions): FaapiMiddleware;
|
|
290
325
|
|
|
326
|
+
interface HelmetOptions {
|
|
327
|
+
contentSecurityPolicy?: string | false;
|
|
328
|
+
xFrameOptions?: 'DENY' | 'SAMEORIGIN' | false;
|
|
329
|
+
xContentTypeOptions?: boolean;
|
|
330
|
+
referrerPolicy?: string | false;
|
|
331
|
+
strictTransportSecurity?: string | false;
|
|
332
|
+
xDnsPrefetchControl?: boolean;
|
|
333
|
+
xDownloadOptions?: boolean;
|
|
334
|
+
xPermittedCrossDomainPolicies?: string | false;
|
|
335
|
+
crossOriginOpenerPolicy?: string | false;
|
|
336
|
+
crossOriginResourcePolicy?: string | false;
|
|
337
|
+
crossOriginEmbedderPolicy?: string | false;
|
|
338
|
+
originAgentCluster?: boolean;
|
|
339
|
+
xPoweredBy?: boolean;
|
|
340
|
+
}
|
|
341
|
+
declare function helmet(options?: HelmetOptions): FaapiMiddleware;
|
|
342
|
+
|
|
291
343
|
declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
292
344
|
type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
293
345
|
|
|
@@ -304,7 +356,26 @@ interface RouteRecord {
|
|
|
304
356
|
/** 路由对应的注入器映射表(从根到路由目录合并,构建时加载) */
|
|
305
357
|
injectors?: InjectorMap;
|
|
306
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* WebSocket 路由记录
|
|
361
|
+
*
|
|
362
|
+
* 与 HTTP RouteRecord 类似,但不绑定 HTTP 方法(WS 是协议升级,不区分 GET/POST)。
|
|
363
|
+
* 一个 handler.ts 中导出 WS 即生成一条 WS 路由记录。
|
|
364
|
+
*/
|
|
365
|
+
interface WsRouteRecord {
|
|
366
|
+
urlPath: string;
|
|
367
|
+
filePath: string;
|
|
368
|
+
paramNames: string[];
|
|
369
|
+
isDynamic: boolean;
|
|
370
|
+
/** 是否为 catch-all 路由([...slug]) */
|
|
371
|
+
isCatchAll?: boolean;
|
|
372
|
+
/** 路由对应的中间件集合(握手阶段执行,复用鉴权/CORS/日志) */
|
|
373
|
+
middlewares?: FaapiMiddleware[];
|
|
374
|
+
/** 路由对应的注入器映射表 */
|
|
375
|
+
injectors?: InjectorMap;
|
|
376
|
+
}
|
|
307
377
|
type RouteManifest = RouteRecord[];
|
|
378
|
+
type WsRouteManifest = WsRouteRecord[];
|
|
308
379
|
/**
|
|
309
380
|
* 路由单个参数的 schema 描述
|
|
310
381
|
*
|
|
@@ -323,6 +394,18 @@ interface RouteInputSchema {
|
|
|
323
394
|
schemaName: string | null;
|
|
324
395
|
properties: RouteParamSchema[];
|
|
325
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* 路由响应类型的 schema 描述
|
|
399
|
+
*
|
|
400
|
+
* 由 @faapi/schema 扩展包的 buildRouteSchemas 生成。
|
|
401
|
+
* output 为 null 表示无显式返回类型注解、void/Promise<void>、或解析失败降级。
|
|
402
|
+
*/
|
|
403
|
+
interface RouteOutputSchema {
|
|
404
|
+
/** 命名类型名(如 'UserResponse'),内联类型为 null */
|
|
405
|
+
schemaName: string | null;
|
|
406
|
+
/** 顶层属性列表 */
|
|
407
|
+
properties: RouteParamSchema[];
|
|
408
|
+
}
|
|
326
409
|
/**
|
|
327
410
|
* 路由的完整 schema 描述
|
|
328
411
|
*
|
|
@@ -335,6 +418,8 @@ interface RouteInfo {
|
|
|
335
418
|
filePath: string;
|
|
336
419
|
isDynamic: boolean;
|
|
337
420
|
inputs: RouteInputSchema[];
|
|
421
|
+
/** 响应类型描述(null 表示无返回类型注解/void/解析失败) */
|
|
422
|
+
output: RouteOutputSchema | null;
|
|
338
423
|
}
|
|
339
424
|
|
|
340
425
|
/** HTTP 请求 handler 类型 */
|
|
@@ -352,8 +437,10 @@ type UpgradeHandler = (req: IncomingMessage, socket: Socket, head: Buffer) => vo
|
|
|
352
437
|
interface PluginContext {
|
|
353
438
|
/** 项目根目录 */
|
|
354
439
|
rootDir: string;
|
|
355
|
-
/**
|
|
440
|
+
/** 当前路由清单(setup 时的快照,reloadRoutes 后不会更新;需最新路由用 getRoutes()) */
|
|
356
441
|
routes: RouteManifest;
|
|
442
|
+
/** 获取最新路由清单(reloadRoutes 后返回更新后的数组) */
|
|
443
|
+
getRoutes: () => RouteManifest;
|
|
357
444
|
/** HTTP 服务器实例(未 listen) */
|
|
358
445
|
server: Server;
|
|
359
446
|
/** 自定义业务配置(faapi.config.ts 中的自定义 key) */
|
|
@@ -424,40 +511,31 @@ type PluginDeclaration = string | [string, unknown] | {
|
|
|
424
511
|
options?: unknown;
|
|
425
512
|
};
|
|
426
513
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
*/
|
|
433
|
-
type ResponseFormatFn = (data: unknown, ctx: FaapiContext) => unknown;
|
|
434
|
-
/**
|
|
435
|
-
* 错误响应格式化函数
|
|
436
|
-
*
|
|
437
|
-
* 优先于内置 formatErrorResponse 处理错误。返回 Response 表示已处理;
|
|
438
|
-
* 返回 null/undefined 表示不处理,由内置 formatErrorResponse 兜底。
|
|
439
|
-
*/
|
|
440
|
-
type ErrorFormatFn = (error: unknown, ctx?: FaapiContext) => Response | null | undefined;
|
|
514
|
+
interface Http2Options {
|
|
515
|
+
key?: string;
|
|
516
|
+
cert?: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
441
519
|
/**
|
|
442
520
|
* 生命周期钩子
|
|
443
521
|
*/
|
|
444
522
|
interface LifecycleHooks {
|
|
445
|
-
/**
|
|
523
|
+
/** 服务器启动后调用(适合初始化数据库连接等) */
|
|
446
524
|
onReady?: (ctx: LifecycleContext) => Promise<void> | void;
|
|
447
525
|
/** 服务器关闭时调用(适合清理资源、优雅关闭) */
|
|
448
526
|
onClose?: (ctx: LifecycleContext) => Promise<void> | void;
|
|
449
527
|
/**
|
|
450
|
-
*
|
|
528
|
+
* 请求错误已被处理为响应后调用(参考 Fastify onError 语义)
|
|
451
529
|
*
|
|
452
|
-
*
|
|
530
|
+
* 时机:handler 抛错 → 全局中间件 try/catch(若有) → 框架内置 formatErrorResponse 兜底
|
|
453
531
|
* → 响应发出后 → onError 触发副作用
|
|
454
532
|
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
533
|
+
* 职责:日志上报、告警、链路追踪等副作用。**不修改、不替换已生成的响应**。
|
|
534
|
+
* 自身抛错会被捕获并忽略,不影响响应已发送的事实。
|
|
457
535
|
*
|
|
458
|
-
*
|
|
459
|
-
* -
|
|
460
|
-
* - onError
|
|
536
|
+
* 与全局错误中间件的区别:
|
|
537
|
+
* - 全局错误中间件:把 error 翻译成 Response(主入口,决定响应内容)
|
|
538
|
+
* - onError:响应发出后的副作用(不能改响应)
|
|
461
539
|
*/
|
|
462
540
|
onError?: (error: unknown, ctx: FaapiContext) => Promise<void> | void;
|
|
463
541
|
}
|
|
@@ -479,7 +557,6 @@ interface LifecycleContext {
|
|
|
479
557
|
* ```ts
|
|
480
558
|
* import type { FaapiConfig } from '@faapi/faapi';
|
|
481
559
|
* export default {
|
|
482
|
-
* port: 3000,
|
|
483
560
|
* cors: { origin: '*' },
|
|
484
561
|
* } satisfies FaapiConfig;
|
|
485
562
|
* ```
|
|
@@ -488,7 +565,6 @@ interface LifecycleContext {
|
|
|
488
565
|
* ```ts
|
|
489
566
|
* import type { FaapiConfig } from '@faapi/faapi';
|
|
490
567
|
* export default {
|
|
491
|
-
* port: 3000,
|
|
492
568
|
* cors: { origin: '*' },
|
|
493
569
|
* // 自定义业务配置(任意 key)
|
|
494
570
|
* db: { host: 'localhost', port: 5432 },
|
|
@@ -496,20 +572,24 @@ interface LifecycleContext {
|
|
|
496
572
|
* ```
|
|
497
573
|
*
|
|
498
574
|
* 环境覆盖通过 faapi.config.{NODE_ENV}.ts 实现(如 faapi.config.production.ts)
|
|
575
|
+
*
|
|
576
|
+
* 框架元信息通过环境变量配置(不放在 config 内):
|
|
577
|
+
* - `PORT`:服务端口,默认 3000
|
|
578
|
+
* - `FAAPI_DIST`:产物输出目录,dev 固定为 `.faapi`(不可修改),prod 默认为 `dist`(可通过 `--dist` 修改)
|
|
499
579
|
*/
|
|
500
580
|
interface FaapiConfig {
|
|
501
|
-
/** 服务端口,默认 3000(可被 --port / PORT 环境变量覆盖) */
|
|
502
|
-
port?: number;
|
|
503
|
-
/** 静态文件目录 */
|
|
504
|
-
staticDir?: string;
|
|
505
581
|
/** CORS 配置,false 禁用 */
|
|
506
582
|
cors?: CorsOptions | boolean;
|
|
507
|
-
/** 统一响应格式化函数 */
|
|
508
|
-
responseFormat?: ResponseFormatFn;
|
|
509
|
-
/** 错误响应格式化函数 */
|
|
510
|
-
errorFormat?: ErrorFormatFn;
|
|
511
583
|
/** 生命周期钩子 */
|
|
512
584
|
lifecycle?: LifecycleHooks;
|
|
585
|
+
/** 安全头配置,false 禁用 */
|
|
586
|
+
helmet?: HelmetOptions | boolean;
|
|
587
|
+
/** 请求体大小限制(字节),默认 10MB(10 * 1024 * 1024) */
|
|
588
|
+
bodyLimit?: number;
|
|
589
|
+
/** 日志中间件配置 */
|
|
590
|
+
logger?: LoggerOptions | boolean;
|
|
591
|
+
/** HTTP/2 配置,false 禁用(默认 http/1.1) */
|
|
592
|
+
http2?: Http2Options | boolean;
|
|
513
593
|
/**
|
|
514
594
|
* 全局中间件:对所有路由(HTTP + WebSocket 握手)生效
|
|
515
595
|
*
|
|
@@ -672,6 +752,15 @@ interface WsContext {
|
|
|
672
752
|
*/
|
|
673
753
|
type WsHandler = (ctx: WsContext) => WsEventHandlers | void;
|
|
674
754
|
|
|
755
|
+
/**
|
|
756
|
+
* 清理所有 Program 缓存(watch 模式下文件变化时调用)
|
|
757
|
+
*
|
|
758
|
+
* 全量清理而非增量清理,理由:
|
|
759
|
+
* - 简单可靠,无状态一致性问题
|
|
760
|
+
* - 跨文件类型引用需要所有文件的 Program 同步更新
|
|
761
|
+
* - dev 模式文件量有限,全量重建在百毫秒级
|
|
762
|
+
*/
|
|
763
|
+
declare function invalidateProgramCache(): void;
|
|
675
764
|
/**
|
|
676
765
|
* 为指定文件创建 TypeScript Program(带缓存)
|
|
677
766
|
*
|
|
@@ -733,6 +822,13 @@ type RuntimeType = {
|
|
|
733
822
|
kind: 'record';
|
|
734
823
|
key: RuntimeType;
|
|
735
824
|
value: RuntimeType;
|
|
825
|
+
} | {
|
|
826
|
+
kind: 'map';
|
|
827
|
+
key: RuntimeType;
|
|
828
|
+
value: RuntimeType;
|
|
829
|
+
} | {
|
|
830
|
+
kind: 'set';
|
|
831
|
+
element: RuntimeType;
|
|
736
832
|
} | {
|
|
737
833
|
kind: 'ref';
|
|
738
834
|
name: string;
|
|
@@ -752,7 +848,79 @@ interface PropertyType {
|
|
|
752
848
|
name: string;
|
|
753
849
|
type: RuntimeType;
|
|
754
850
|
optional: boolean;
|
|
851
|
+
/**
|
|
852
|
+
* 字段级 JSDoc 约束标签(@max/@min/@maxLength 等)
|
|
853
|
+
*
|
|
854
|
+
* 来自字段 JSDoc 注释,由 generateZodSchema 转为 zod 链式调用。
|
|
855
|
+
* 约束与字段类型不匹配时在提取阶段抛 SchemaExtractionError。
|
|
856
|
+
*/
|
|
857
|
+
constraints?: TypeConstraint[];
|
|
755
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* JSDoc 约束标签的运行时描述
|
|
861
|
+
*
|
|
862
|
+
* 由字段 JSDoc 注释提取,对应 zod schema 的链式约束方法。
|
|
863
|
+
* 仅在 PropertyType.constraints 中出现,不挂在嵌套类型(array 元素、tuple 元素等)上。
|
|
864
|
+
*/
|
|
865
|
+
type TypeConstraint = {
|
|
866
|
+
kind: 'max';
|
|
867
|
+
value: number;
|
|
868
|
+
} | {
|
|
869
|
+
kind: 'min';
|
|
870
|
+
value: number;
|
|
871
|
+
} | {
|
|
872
|
+
kind: 'int';
|
|
873
|
+
} | {
|
|
874
|
+
kind: 'positive';
|
|
875
|
+
} | {
|
|
876
|
+
kind: 'negative';
|
|
877
|
+
} | {
|
|
878
|
+
kind: 'nonnegative';
|
|
879
|
+
} | {
|
|
880
|
+
kind: 'nonpositive';
|
|
881
|
+
} | {
|
|
882
|
+
kind: 'maxLength';
|
|
883
|
+
value: number;
|
|
884
|
+
} | {
|
|
885
|
+
kind: 'minLength';
|
|
886
|
+
value: number;
|
|
887
|
+
} | {
|
|
888
|
+
kind: 'length';
|
|
889
|
+
value: number;
|
|
890
|
+
} | {
|
|
891
|
+
kind: 'regex';
|
|
892
|
+
pattern: string;
|
|
893
|
+
flags?: string;
|
|
894
|
+
} | {
|
|
895
|
+
kind: 'email';
|
|
896
|
+
} | {
|
|
897
|
+
kind: 'url';
|
|
898
|
+
} | {
|
|
899
|
+
kind: 'uuid';
|
|
900
|
+
};
|
|
901
|
+
/**
|
|
902
|
+
* 将 TypeScript 类型节点解析为运行时类型描述
|
|
903
|
+
*
|
|
904
|
+
* 支持的类型:
|
|
905
|
+
* - 基础类型:string / number / boolean / null / undefined / any / unknown / void
|
|
906
|
+
* - bigint:不支持(HTTP/JSON 不能传输),AST 提取阶段抛 SchemaExtractionError
|
|
907
|
+
* - 字面量类型:'foo' / 42 / true
|
|
908
|
+
* - 数组类型:T[] / Array<T> / ReadonlyArray<T> / readonly T[]
|
|
909
|
+
* - 元组类型:[string, number] / [string, number?] / [string, ...number[]] / readonly [T, U](按位置校验)
|
|
910
|
+
* - 对象类型:{ name: string; age?: number }(含 readonly 字段修饰符,忽略 readonly)
|
|
911
|
+
* - 联合类型:string | null
|
|
912
|
+
* - 交叉类型:A & B(按对象合并处理)
|
|
913
|
+
* - 引用类型:Date / 其他 interface(递归解析)
|
|
914
|
+
* - 工具类型:Record<K, V> / Partial<T> / Readonly<T>(best effort)
|
|
915
|
+
* - Pick<T, K> / Omit<T, K>:K 支持字面量联合、类型别名、keyof T
|
|
916
|
+
*
|
|
917
|
+
* readonly 是编译期约束,运行时不产生校验语义,所有 readonly 修饰符统一忽略。
|
|
918
|
+
*
|
|
919
|
+
* @param typeNode TypeScript 类型节点
|
|
920
|
+
* @param checker 类型 checker(用于解析引用类型)
|
|
921
|
+
* @param visited 防止递归循环
|
|
922
|
+
*/
|
|
923
|
+
declare function resolveTypeNode(typeNode: ts.TypeNode, checker?: ts.TypeChecker, visited?: Set<string>): RuntimeType;
|
|
756
924
|
|
|
757
925
|
interface HandlerTypeInfo {
|
|
758
926
|
name: string;
|
|
@@ -788,48 +956,240 @@ declare function extractTypeInfo(program: ts.Program, filePath: string, typeName
|
|
|
788
956
|
declare function getInputTypeForMethod(method: string): 'query' | 'body';
|
|
789
957
|
|
|
790
958
|
/**
|
|
791
|
-
*
|
|
959
|
+
* 单个路由的 schema 提取结果
|
|
960
|
+
*
|
|
961
|
+
* key 使用 urlPath(如 '/api/hello')而非 filePath,因为 urlPath 在 dev/prod 完全一致,
|
|
962
|
+
* 无需 remapManifestKeys 桥接 .ts/.js 路径差异。
|
|
792
963
|
*/
|
|
793
|
-
interface
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
964
|
+
interface RouteSchemaSource {
|
|
965
|
+
/** 路由 URL 路径(如 '/api/hello'),作为 schema key */
|
|
966
|
+
urlPath: string;
|
|
967
|
+
/** 源文件绝对路径(用于 generateSchemaFiles 按文件分组生成 zod.js) */
|
|
968
|
+
filePath: string;
|
|
969
|
+
schemaName: string;
|
|
970
|
+
typeInfo: HandlerTypeInfo | null;
|
|
971
|
+
/**
|
|
972
|
+
* 是否对 number/boolean 字段生成 z.preprocess 字符串转换(coerce)。
|
|
973
|
+
*
|
|
974
|
+
* - query/params:始终 coerce=true(URL 来源均为 string)
|
|
975
|
+
* - body:始终 coerce=false(JSON 解析已是天然 JS 类型)
|
|
976
|
+
* - form:coerce=true(form-urlencoded 来源均为 string),由本函数在提取时
|
|
977
|
+
* 检测到 handler 声明 `form` 参数时显式设置。schema 名仍为 `POSTBody`
|
|
978
|
+
* (与 body 共享运行时 schema key),运行时 validateInput 无需感知 form/body 差异。
|
|
979
|
+
*
|
|
980
|
+
* 未设置时由 generateSchemaFileSource 回退到 schemaName 后缀正则推断(Query/Params → true)。
|
|
981
|
+
*/
|
|
982
|
+
coerce?: boolean;
|
|
797
983
|
}
|
|
798
984
|
/**
|
|
799
|
-
*
|
|
985
|
+
* 从路由清单收集 schema 提取所需的原始数据
|
|
986
|
+
*
|
|
987
|
+
* dev 和 prd 共享的核心提取流程:
|
|
988
|
+
* 1. 按文件分组遍历路由
|
|
989
|
+
* 2. 对每个文件 createProgram + extractAllTypes 收集所有类型
|
|
990
|
+
* 3. 用 analyzeInjection + extractTypeInfo 提取每个路由的 schema 类型
|
|
991
|
+
* 4. 同时返回按文件分组的 allTypesMap 和合并后的全局 allTypes
|
|
992
|
+
*
|
|
993
|
+
* 调用方基于返回的 sources 和 allTypes 各自做最终转换:
|
|
994
|
+
* - dev:生成 JS 模块文件 → import 加载(用 allTypesByFile)
|
|
995
|
+
* - prd:生成 JS 模块代码 → SchemaModuleEntry[](用 allTypesByFile)
|
|
800
996
|
*/
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
997
|
+
declare function collectRouteSchemaSources(routes: RouteManifest, rootDir?: string): {
|
|
998
|
+
sources: RouteSchemaSource[];
|
|
999
|
+
/** 按文件分组的类型映射(prd writeSchemaModule 用) */
|
|
1000
|
+
allTypesByFile: Map<string, Map<string, HandlerTypeInfo>>;
|
|
1001
|
+
/** 合并后的全局类型映射(兼容旧调用方保留,新路径使用 allTypesByFile) */
|
|
1002
|
+
mergedAllTypes: Map<string, HandlerTypeInfo>;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* 加载 faapi 配置文件
|
|
1007
|
+
*
|
|
1008
|
+
* 统一读取 `<dist>/faapi-config.js` 产物:
|
|
1009
|
+
* - dev 模式:`faapi dev` 启动时由 `compileConfig` 生成 `.faapi/faapi-config.js`
|
|
1010
|
+
* - prod 模式:`faapi build` 时由 `compileConfig` 生成 `dist/faapi-config.js`
|
|
1011
|
+
*
|
|
1012
|
+
* 产物由 `compileConfig` 在构建阶段合并 env 后固化,运行时不读源码、不现场编译、不按 env 合并。
|
|
1013
|
+
*
|
|
1014
|
+
* - 产物存在 → import 并返回 default
|
|
1015
|
+
* - 产物不存在但源码有配置文件 → 抛错(强制 rebuild)
|
|
1016
|
+
* - 源码也无配置文件 → 返回 `null`(配置可选)
|
|
1017
|
+
*
|
|
1018
|
+
* @param rootDir 项目根目录
|
|
1019
|
+
* @param dist 产物目录(如 'dist' 或 '.faapi')
|
|
1020
|
+
* @returns 配置对象,无配置文件时返回 null
|
|
1021
|
+
*/
|
|
1022
|
+
declare function loadConfig(rootDir: string, dist: string): Promise<Partial<FaapiConfig> | null>;
|
|
1023
|
+
|
|
1024
|
+
declare const VALIDATION_ERROR = "VALIDATION_ERROR";
|
|
1025
|
+
declare const ROUTE_NOT_FOUND = "ROUTE_NOT_FOUND";
|
|
1026
|
+
declare const METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
|
1027
|
+
declare const INTERNAL_ERROR = "INTERNAL_ERROR";
|
|
1028
|
+
declare const MODULE_LOAD_ERROR = "MODULE_LOAD_ERROR";
|
|
1029
|
+
type ErrorCode = typeof VALIDATION_ERROR | typeof ROUTE_NOT_FOUND | typeof METHOD_NOT_ALLOWED | typeof INTERNAL_ERROR | typeof MODULE_LOAD_ERROR;
|
|
1030
|
+
|
|
1031
|
+
declare class FaapiError extends Error {
|
|
1032
|
+
readonly code: ErrorCode;
|
|
1033
|
+
readonly statusCode: number;
|
|
1034
|
+
constructor(code: ErrorCode, message: string, statusCode: number);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
declare class ValidationError extends FaapiError {
|
|
1038
|
+
readonly issues: ValidationIssue[];
|
|
1039
|
+
constructor(message: string, issues: ValidationIssue[]);
|
|
1040
|
+
}
|
|
1041
|
+
declare class RouteNotFoundError extends FaapiError {
|
|
1042
|
+
constructor(path: string);
|
|
1043
|
+
}
|
|
1044
|
+
declare class MethodNotAllowedError extends FaapiError {
|
|
1045
|
+
readonly allowedMethods: string[];
|
|
1046
|
+
constructor(method: string, path: string, allowedMethods: string[]);
|
|
1047
|
+
}
|
|
1048
|
+
declare class InternalError extends FaapiError {
|
|
1049
|
+
constructor(message: string);
|
|
1050
|
+
}
|
|
1051
|
+
declare class ModuleLoadError extends FaapiError {
|
|
1052
|
+
constructor(filePath: string, reason: string);
|
|
806
1053
|
}
|
|
807
1054
|
/**
|
|
808
|
-
*
|
|
1055
|
+
* 校验问题类型
|
|
809
1056
|
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
1057
|
+
* 结构化错误信息,便于上层(全局错误中间件/前端)按 code 做不同处理,
|
|
1058
|
+
* 不依赖字符串解析。message 仅为人类可读的兜底描述。
|
|
812
1059
|
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
1060
|
+
* code 与 HTTP 状态码的映射(由 ValidationError 推导):
|
|
1061
|
+
* - INVALID_FORMAT / MISSING_FIELD → 400 Bad Request
|
|
1062
|
+
* - TYPE_MISMATCH / INVALID_VALUE / COERCE_FAILED → 422 Unprocessable Entity
|
|
816
1063
|
*/
|
|
817
|
-
|
|
1064
|
+
interface ValidationIssue {
|
|
1065
|
+
/** 字段路径,如 'user.address.city' */
|
|
1066
|
+
path: string;
|
|
1067
|
+
/** 错误码,机器可读的契约 */
|
|
1068
|
+
code: ValidationErrorCode;
|
|
1069
|
+
/** 期望类型/值,如 'number' / '"admin" | "user"' */
|
|
1070
|
+
expected: string;
|
|
1071
|
+
/** 实际类型/值,如 'string' / 'undefined' */
|
|
1072
|
+
received: string;
|
|
1073
|
+
/** 人类可读的本地化消息(兜底,不保证稳定) */
|
|
1074
|
+
message: string;
|
|
1075
|
+
}
|
|
1076
|
+
type ValidationErrorCode = 'TYPE_MISMATCH' | 'MISSING_FIELD' | 'INVALID_FORMAT' | 'INVALID_VALUE' | 'COERCE_FAILED';
|
|
818
1077
|
|
|
819
1078
|
/**
|
|
820
|
-
*
|
|
1079
|
+
* 从 Request 对象创建 FaapiContext
|
|
1080
|
+
* @param request Web Request 对象
|
|
1081
|
+
* @param params 动态路由参数
|
|
1082
|
+
* @param config 自定义业务配置(来自 faapi.config.ts)
|
|
1083
|
+
* @param ip 客户端 IP(由调用方从 IncomingMessage 提取,HTTP/WS 握手均通过 utils/getClientIp)
|
|
1084
|
+
*/
|
|
1085
|
+
declare function createContext(request: Request, params: Record<string, string>, config?: Record<string, unknown>, ip?: string): FaapiContext;
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* 调用路由 handler 并将返回值转为 Response
|
|
821
1089
|
*
|
|
822
|
-
*
|
|
823
|
-
* 1.
|
|
824
|
-
* 2.
|
|
825
|
-
* 3.
|
|
1090
|
+
* 流程(洋葱模型):
|
|
1091
|
+
* 1. 中间件按洋葱模型执行:mw1.before → mw2.before → ... → handler → ... → mw2.after → mw1.after
|
|
1092
|
+
* 2. 中间件不调用 next() 即拦截请求(必须返回 Response)
|
|
1093
|
+
* 3. 中间件可用 try/catch 捕获内层错误
|
|
1094
|
+
* 4. 最内层执行注入器(按需)→ handler
|
|
826
1095
|
*
|
|
827
|
-
*
|
|
1096
|
+
* 注入器与中间件解耦:
|
|
1097
|
+
* - 注入器按 handler 参数名匹配,只执行需要的
|
|
1098
|
+
* - 注入器可读取中间件塞进 ctx 的值
|
|
1099
|
+
*/
|
|
1100
|
+
declare function invokeHandler(handler: (...args: unknown[]) => unknown, ctx: FaapiContext, body?: unknown, middlewares?: FaapiMiddleware[], injectors?: InjectorMap): Promise<Response>;
|
|
1101
|
+
|
|
1102
|
+
interface InjectOptions {
|
|
1103
|
+
method?: string;
|
|
1104
|
+
path?: string;
|
|
1105
|
+
headers?: Record<string, string>;
|
|
1106
|
+
query?: Record<string, string>;
|
|
1107
|
+
body?: unknown;
|
|
1108
|
+
}
|
|
1109
|
+
interface InjectResponse {
|
|
1110
|
+
status: number;
|
|
1111
|
+
headers: Headers;
|
|
1112
|
+
body: unknown;
|
|
1113
|
+
}
|
|
1114
|
+
interface CreateAppOptions {
|
|
1115
|
+
/** 项目根目录,默认 process.cwd() */
|
|
1116
|
+
rootDir?: string;
|
|
1117
|
+
/** 产物输出目录(如 dist 或 .faapi),覆盖环境变量 FAAPI_DIST,默认 'dist' */
|
|
1118
|
+
dist?: string;
|
|
1119
|
+
/** 端口号,也可在 listen() 时传入;默认环境变量 PORT 或 3000 */
|
|
1120
|
+
port?: number;
|
|
1121
|
+
}
|
|
1122
|
+
/** 应用基础接口(dev/prod 共用,不含 reloadRoutes) */
|
|
1123
|
+
interface AppBase {
|
|
1124
|
+
/** Node.js Server 实例(listen 后可用,close 后置 null) */
|
|
1125
|
+
server: Server | null;
|
|
1126
|
+
/** 排序后的路由清单 */
|
|
1127
|
+
routes: RouteManifest;
|
|
1128
|
+
/** WebSocket 路由清单 */
|
|
1129
|
+
wsRoutes: WsRouteManifest;
|
|
1130
|
+
/** 项目根目录 */
|
|
1131
|
+
rootDir: string;
|
|
1132
|
+
/** 启动 HTTP server,打印路由表,执行 onReady 钩子 */
|
|
1133
|
+
listen(port?: number): Promise<Server>;
|
|
1134
|
+
/** 关闭 server,执行 onClose 钩子 */
|
|
1135
|
+
close(): Promise<void>;
|
|
1136
|
+
/**
|
|
1137
|
+
* 无服务器测试注入
|
|
1138
|
+
*
|
|
1139
|
+
* 构建一个模拟请求直接走完整请求链路,不绑定端口。
|
|
1140
|
+
* 需要在 listen() 之前调用(server 未启动时)。
|
|
1141
|
+
*/
|
|
1142
|
+
inject(options?: InjectOptions): Promise<InjectResponse>;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** dev 应用接口(AppBase + reloadRoutes 热替换) */
|
|
1146
|
+
interface DevApp extends AppBase {
|
|
1147
|
+
/** 重新水合路由清单 + 清 schema 缓存 + 更新 server 路由引用(dev 热替换用) */
|
|
1148
|
+
reloadRoutes(): Promise<void>;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* dev 模式应用启动 API
|
|
828
1152
|
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
1153
|
+
* 在 createAppBase(共享逻辑)基础上增加 `reloadRoutes` 热替换能力,供 `faapi dev` watcher 调用。
|
|
1154
|
+
*
|
|
1155
|
+
* 与 createProdApp 的区别:
|
|
1156
|
+
* - dev:含 reloadRoutes(重新扫描路由 + 重新生成 schema + 清缓存 + 更新 server 路由引用)
|
|
1157
|
+
* - prod:精简,无 reloadRoutes(产物已固化,运行时不重建)
|
|
1158
|
+
*
|
|
1159
|
+
* 由 `devCommand` 直接调用,devCommand 持有 app 引用并传给 watcher。
|
|
1160
|
+
*
|
|
1161
|
+
* @example
|
|
1162
|
+
* ```ts
|
|
1163
|
+
* // devCommand 内部
|
|
1164
|
+
* const app = await createDevApp();
|
|
1165
|
+
* await app.listen();
|
|
1166
|
+
* startWatcher({ rootDir, app, devDist });
|
|
1167
|
+
* ```
|
|
1168
|
+
*/
|
|
1169
|
+
declare function createDevApp(options?: CreateAppOptions): Promise<DevApp>;
|
|
1170
|
+
|
|
1171
|
+
/** prod 应用接口(AppBase,无 reloadRoutes) */
|
|
1172
|
+
type ProdApp = AppBase;
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* prod 模式应用启动 API
|
|
1176
|
+
*
|
|
1177
|
+
* 直接返回 createAppBase 结果(共享逻辑),不含 dev 专用能力(reloadRoutes、缓存失效)。
|
|
1178
|
+
* 产物在 `faapi build` 阶段已固化,运行时不重建。
|
|
1179
|
+
*
|
|
1180
|
+
* 框架采用零入口设计——用户无需编写 main.ts:
|
|
1181
|
+
* - `faapi build` 自动生成 `dist/main.js` 启动入口,内部调用 `createProdApp()` + `listen()` 启动生产服务器
|
|
1182
|
+
* - 用户自定义启动逻辑通过 `faapi.config.ts` 的 `lifecycle.onReady` / `onClose` 钩子实现
|
|
1183
|
+
*
|
|
1184
|
+
* 编程式调用场景(如自定义 CLI 启动器)也可直接调用:
|
|
1185
|
+
*
|
|
1186
|
+
* @example
|
|
1187
|
+
* ```ts
|
|
1188
|
+
* import { createProdApp } from '@faapi/faapi';
|
|
1189
|
+
* const app = await createProdApp();
|
|
1190
|
+
* await app.listen();
|
|
1191
|
+
* ```
|
|
832
1192
|
*/
|
|
833
|
-
declare function
|
|
1193
|
+
declare function createProdApp(options?: CreateAppOptions): Promise<ProdApp>;
|
|
834
1194
|
|
|
835
|
-
export { type CorsOptions, type
|
|
1195
|
+
export { type ProdApp as App, type CorsOptions, type CreateAppOptions, type DevApp, type FaapiConfig, type FaapiContext, type FaapiContextConfig, FaapiError, type FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, type HelmetOptions, type InjectOptions, type InjectResponse, type Injector, type InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LoggerOptions, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type RouteInfo, type RouteInputSchema, type RouteManifest, RouteNotFoundError, type RouteOutputSchema, type RouteParamSchema, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, type SseEvent, type SseWriter, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, collectRouteSchemaSources, cors, createProdApp as createApp, createContext, createDevApp, createProdApp, createProgram, extractTypeInfo, getInputTypeForMethod, helmet, invalidateProgramCache, invokeHandler, loadConfig, logger, resolveTypeNode };
|