@faapi/faapi 6.19.0 → 6.21.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 +339 -138
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +16 -21
- package/dist/index.js +341 -142
- package/dist/index.js.map +1 -1
- package/dist/{routeTypes-DTqdo28V.d.ts → routeTypes-gyz4PMh7.d.ts} +46 -6
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +304 -108
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/testing.js
CHANGED
|
@@ -154,17 +154,24 @@ function encodeSseEvent(event) {
|
|
|
154
154
|
out += "\n";
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
|
-
function createSseWriter() {
|
|
157
|
+
function createSseWriter(options = {}) {
|
|
158
|
+
const { onFirstWrite } = options;
|
|
158
159
|
const encoder = new TextEncoder();
|
|
159
160
|
let controller = null;
|
|
160
161
|
let closed = false;
|
|
161
162
|
let aborted = false;
|
|
163
|
+
let firstWriteTriggered = false;
|
|
162
164
|
let drainWaiters = [];
|
|
163
165
|
const resolveDrainWaiters = () => {
|
|
164
166
|
const waiters = drainWaiters;
|
|
165
167
|
drainWaiters = [];
|
|
166
168
|
for (const resolve of waiters) resolve();
|
|
167
169
|
};
|
|
170
|
+
const triggerFirstWrite = () => {
|
|
171
|
+
if (firstWriteTriggered) return;
|
|
172
|
+
firstWriteTriggered = true;
|
|
173
|
+
onFirstWrite?.(response);
|
|
174
|
+
};
|
|
168
175
|
const stream = new ReadableStream(
|
|
169
176
|
{
|
|
170
177
|
start(c) {
|
|
@@ -198,11 +205,13 @@ function createSseWriter() {
|
|
|
198
205
|
if (closed || !controller) return;
|
|
199
206
|
const text = encodeSseEvent(event);
|
|
200
207
|
controller.enqueue(encoder.encode(text));
|
|
208
|
+
triggerFirstWrite();
|
|
201
209
|
},
|
|
202
210
|
sendRaw(chunk) {
|
|
203
211
|
if (closed || !controller) return;
|
|
204
212
|
const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
|
|
205
213
|
controller.enqueue(bytes);
|
|
214
|
+
triggerFirstWrite();
|
|
206
215
|
},
|
|
207
216
|
sendError(error) {
|
|
208
217
|
if (closed || !controller) return;
|
|
@@ -210,6 +219,7 @@ function createSseWriter() {
|
|
|
210
219
|
const text = encodeSseEvent({ event: "error", data: message });
|
|
211
220
|
try {
|
|
212
221
|
controller.enqueue(encoder.encode(text));
|
|
222
|
+
triggerFirstWrite();
|
|
213
223
|
} finally {
|
|
214
224
|
writer.close();
|
|
215
225
|
}
|
|
@@ -497,6 +507,26 @@ function createLogger(scope, options) {
|
|
|
497
507
|
return makeLogger(void 0);
|
|
498
508
|
}
|
|
499
509
|
|
|
510
|
+
// src/utils/queryToObject.ts
|
|
511
|
+
var queryCache = /* @__PURE__ */ new WeakMap();
|
|
512
|
+
function queryToObject(params) {
|
|
513
|
+
const cached = queryCache.get(params);
|
|
514
|
+
if (cached) return cached;
|
|
515
|
+
const result = {};
|
|
516
|
+
for (const [key, value] of params) {
|
|
517
|
+
const existing = result[key];
|
|
518
|
+
if (existing === void 0) {
|
|
519
|
+
result[key] = value;
|
|
520
|
+
} else if (Array.isArray(existing)) {
|
|
521
|
+
existing.push(value);
|
|
522
|
+
} else {
|
|
523
|
+
result[key] = [existing, value];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
queryCache.set(params, result);
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
|
|
500
530
|
// src/runtime/createContext.ts
|
|
501
531
|
function resolveRequestId(request) {
|
|
502
532
|
const header = request.headers.get("x-request-id");
|
|
@@ -546,7 +576,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
546
576
|
log,
|
|
547
577
|
request,
|
|
548
578
|
params,
|
|
549
|
-
|
|
579
|
+
rawParams: params,
|
|
580
|
+
// ctx.query 为对象口径(createContext 挂原始 string 值对象;handler 声明
|
|
581
|
+
// query 类型时管线校验后替换为转换值)。原始 URLSearchParams 恒在 rawQuery。
|
|
582
|
+
query: queryToObject(url.searchParams),
|
|
583
|
+
rawQuery: url.searchParams,
|
|
550
584
|
headers: request.headers,
|
|
551
585
|
method: request.method,
|
|
552
586
|
path: url.pathname,
|
|
@@ -594,13 +628,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
594
628
|
/**
|
|
595
629
|
* 创建 SSE writer,用于流式推送事件
|
|
596
630
|
*
|
|
597
|
-
* handler 调用此方法后,通过返回的 writer
|
|
598
|
-
*
|
|
631
|
+
* handler 调用此方法后,通过返回的 writer 推送事件。HTTP 服务场景下首次
|
|
632
|
+
* 写入(send/sendRaw/sendError)即触发 server 层提前接管钩子,把 writer.response
|
|
633
|
+
* 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
|
|
634
|
+
* 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
|
|
599
635
|
*
|
|
600
636
|
* 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
|
|
601
637
|
*/
|
|
602
638
|
sse() {
|
|
603
|
-
const writer = createSseWriter(
|
|
639
|
+
const writer = createSseWriter({
|
|
640
|
+
onFirstWrite: (response) => {
|
|
641
|
+
ctx.__earlyRespond?.(response);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
604
644
|
const ctxWithSse = ctx;
|
|
605
645
|
ctxWithSse.__sseResponse = writer.response;
|
|
606
646
|
ctxWithSse.__sseWriter = writer;
|
|
@@ -825,8 +865,16 @@ var PARAM_TYPE_MAP = {
|
|
|
825
865
|
// Phase 2.3
|
|
826
866
|
tasks: "tasks",
|
|
827
867
|
// 任务子系统:TaskClient(入队/查询)
|
|
828
|
-
log: "log"
|
|
868
|
+
log: "log",
|
|
829
869
|
// 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
|
|
870
|
+
// raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
|
|
871
|
+
// 的「校验转换后」口径二分互补
|
|
872
|
+
rawQuery: "rawQuery",
|
|
873
|
+
// URLSearchParams(原始查询参数)
|
|
874
|
+
rawParams: "rawParams",
|
|
875
|
+
// Record<string, string>(URL 原始路径段)
|
|
876
|
+
rawBody: "rawBody"
|
|
877
|
+
// string | undefined(请求体原始文本)
|
|
830
878
|
};
|
|
831
879
|
var injectionCache = /* @__PURE__ */ new WeakMap();
|
|
832
880
|
function resolveInjection(fn) {
|
|
@@ -904,26 +952,6 @@ function extractParamName(param, names) {
|
|
|
904
952
|
}
|
|
905
953
|
}
|
|
906
954
|
|
|
907
|
-
// src/utils/queryToObject.ts
|
|
908
|
-
var queryCache = /* @__PURE__ */ new WeakMap();
|
|
909
|
-
function queryToObject(params) {
|
|
910
|
-
const cached = queryCache.get(params);
|
|
911
|
-
if (cached) return cached;
|
|
912
|
-
const result = {};
|
|
913
|
-
for (const [key, value] of params) {
|
|
914
|
-
const existing = result[key];
|
|
915
|
-
if (existing === void 0) {
|
|
916
|
-
result[key] = value;
|
|
917
|
-
} else if (Array.isArray(existing)) {
|
|
918
|
-
existing.push(value);
|
|
919
|
-
} else {
|
|
920
|
-
result[key] = [existing, value];
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
queryCache.set(params, result);
|
|
924
|
-
return result;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
955
|
// src/task/taskRegistry.ts
|
|
928
956
|
function createTaskRegistry() {
|
|
929
957
|
let registry = /* @__PURE__ */ new Map();
|
|
@@ -1108,12 +1136,17 @@ function getAgentHandle(ctx) {
|
|
|
1108
1136
|
// src/injection/injectParams.ts
|
|
1109
1137
|
function getBuiltinInjectionValue(type, ctx, body) {
|
|
1110
1138
|
switch (type) {
|
|
1111
|
-
case "query":
|
|
1112
|
-
|
|
1113
|
-
return validated ?? queryToObject(ctx.query);
|
|
1114
|
-
}
|
|
1139
|
+
case "query":
|
|
1140
|
+
return ctx.query;
|
|
1115
1141
|
case "params":
|
|
1116
1142
|
return ctx.params;
|
|
1143
|
+
// raw 系:恒原始,管线永不覆盖(ctx 字段即注入值)
|
|
1144
|
+
case "rawQuery":
|
|
1145
|
+
return ctx.rawQuery;
|
|
1146
|
+
case "rawParams":
|
|
1147
|
+
return ctx.rawParams;
|
|
1148
|
+
case "rawBody":
|
|
1149
|
+
return ctx.rawBody;
|
|
1117
1150
|
case "headers":
|
|
1118
1151
|
return ctx.headers;
|
|
1119
1152
|
case "context":
|
|
@@ -2629,7 +2662,19 @@ function extractSchema(typeNode, sourceFile) {
|
|
|
2629
2662
|
|
|
2630
2663
|
// src/cli/collectRouteSchemaSources.ts
|
|
2631
2664
|
import path4 from "path";
|
|
2632
|
-
|
|
2665
|
+
import ts6 from "typescript";
|
|
2666
|
+
function findExportedTypeName(sourceFile, name) {
|
|
2667
|
+
for (const statement of sourceFile.statements) {
|
|
2668
|
+
const modifiers = ts6.canHaveModifiers(statement) ? ts6.getModifiers(statement) : void 0;
|
|
2669
|
+
const isExported = modifiers?.some((m) => m.kind === ts6.SyntaxKind.ExportKeyword);
|
|
2670
|
+
if (!isExported) continue;
|
|
2671
|
+
if ((ts6.isInterfaceDeclaration(statement) || ts6.isTypeAliasDeclaration(statement)) && statement.name?.text === name) {
|
|
2672
|
+
return true;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return false;
|
|
2676
|
+
}
|
|
2677
|
+
function collectRouteSchemaSources(routes, rootDir, wsRoutes) {
|
|
2633
2678
|
const methodsByFile = /* @__PURE__ */ new Map();
|
|
2634
2679
|
for (const route of routes) {
|
|
2635
2680
|
const filePath = rootDir ? path4.resolve(rootDir, route.filePath) : route.filePath;
|
|
@@ -2640,9 +2685,17 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
2640
2685
|
}
|
|
2641
2686
|
entry.methods.add(route.method);
|
|
2642
2687
|
}
|
|
2643
|
-
const
|
|
2688
|
+
const wsFiles = [];
|
|
2689
|
+
for (const wsRoute of wsRoutes ?? []) {
|
|
2690
|
+
wsFiles.push({
|
|
2691
|
+
filePath: rootDir ? path4.resolve(rootDir, wsRoute.filePath) : wsRoute.filePath,
|
|
2692
|
+
urlPath: wsRoute.urlPath
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
const allFiles = [.../* @__PURE__ */ new Set([...methodsByFile.keys(), ...wsFiles.map((f) => f.filePath)])];
|
|
2696
|
+
const programByFile = createPrograms(allFiles);
|
|
2644
2697
|
const resolversByFile = /* @__PURE__ */ new Map();
|
|
2645
|
-
for (const filePath of
|
|
2698
|
+
for (const filePath of allFiles) {
|
|
2646
2699
|
resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
|
|
2647
2700
|
}
|
|
2648
2701
|
const sources = [];
|
|
@@ -2674,6 +2727,50 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
2674
2727
|
typeInfo: resolver.resolve(paramsArg.typeName) ?? null
|
|
2675
2728
|
});
|
|
2676
2729
|
}
|
|
2730
|
+
if (inputType === "body") {
|
|
2731
|
+
const queryArg = meta.params.find((p) => p.type === "query");
|
|
2732
|
+
if (queryArg?.typeName) {
|
|
2733
|
+
sources.push({
|
|
2734
|
+
urlPath: entry.urlPath,
|
|
2735
|
+
filePath,
|
|
2736
|
+
schemaName: getSchemaName(method, "query"),
|
|
2737
|
+
typeInfo: resolver.resolve(queryArg.typeName) ?? null
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
} else if (hasBody(method)) {
|
|
2741
|
+
const bodyArg = meta.params.find((p) => p.type === "body") ?? meta.params.find((p) => p.type === "form");
|
|
2742
|
+
if (bodyArg?.typeName) {
|
|
2743
|
+
sources.push({
|
|
2744
|
+
urlPath: entry.urlPath,
|
|
2745
|
+
filePath,
|
|
2746
|
+
schemaName: getSchemaName(method, "body"),
|
|
2747
|
+
typeInfo: resolver.resolve(bodyArg.typeName) ?? null,
|
|
2748
|
+
coerce: bodyArg.type === "form" || void 0
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
for (const wsFile of wsFiles) {
|
|
2755
|
+
const program = programByFile.get(wsFile.filePath);
|
|
2756
|
+
const sourceFile = program?.getSourceFile(wsFile.filePath);
|
|
2757
|
+
if (!sourceFile) continue;
|
|
2758
|
+
const resolver = resolversByFile.get(wsFile.filePath);
|
|
2759
|
+
if (findExportedTypeName(sourceFile, "Query")) {
|
|
2760
|
+
sources.push({
|
|
2761
|
+
urlPath: wsFile.urlPath,
|
|
2762
|
+
filePath: wsFile.filePath,
|
|
2763
|
+
schemaName: getSchemaName("WS", "query"),
|
|
2764
|
+
typeInfo: resolver.resolve("Query") ?? null
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
if (findExportedTypeName(sourceFile, "Params")) {
|
|
2768
|
+
sources.push({
|
|
2769
|
+
urlPath: wsFile.urlPath,
|
|
2770
|
+
filePath: wsFile.filePath,
|
|
2771
|
+
schemaName: getSchemaName("WS", "params"),
|
|
2772
|
+
typeInfo: resolver.resolve("Params") ?? null
|
|
2773
|
+
});
|
|
2677
2774
|
}
|
|
2678
2775
|
}
|
|
2679
2776
|
return { sources, resolversByFile };
|
|
@@ -3106,9 +3203,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
3106
3203
|
lines.push(...schemaBlocks);
|
|
3107
3204
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
3108
3205
|
}
|
|
3109
|
-
async function generateSchemaFiles(routes, rootDir, dist) {
|
|
3110
|
-
if (routes.length === 0) return;
|
|
3111
|
-
const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
|
|
3206
|
+
async function generateSchemaFiles(routes, rootDir, dist, wsRoutes) {
|
|
3207
|
+
if (routes.length === 0 && (wsRoutes?.length ?? 0) === 0) return;
|
|
3208
|
+
const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir, wsRoutes);
|
|
3112
3209
|
await generateZodArtifacts(sources, {
|
|
3113
3210
|
generateFileSource: generateSchemaFileSource,
|
|
3114
3211
|
resolversByFile,
|
|
@@ -3127,7 +3224,7 @@ import fs8 from "fs";
|
|
|
3127
3224
|
import fg2 from "fast-glob";
|
|
3128
3225
|
|
|
3129
3226
|
// src/cli/aliasPlugin.ts
|
|
3130
|
-
import
|
|
3227
|
+
import ts8 from "typescript";
|
|
3131
3228
|
import path9 from "path";
|
|
3132
3229
|
import fs7 from "fs";
|
|
3133
3230
|
|
|
@@ -3155,7 +3252,7 @@ function resolveAlias(specifier, config) {
|
|
|
3155
3252
|
}
|
|
3156
3253
|
|
|
3157
3254
|
// src/utils/readTsconfig.ts
|
|
3158
|
-
import
|
|
3255
|
+
import ts7 from "typescript";
|
|
3159
3256
|
import path8 from "path";
|
|
3160
3257
|
import fs6 from "fs";
|
|
3161
3258
|
var tsconfigCache = /* @__PURE__ */ new Map();
|
|
@@ -3172,9 +3269,9 @@ function readTsconfig(rootDir) {
|
|
|
3172
3269
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
3173
3270
|
return cached.config;
|
|
3174
3271
|
}
|
|
3175
|
-
const configFile =
|
|
3272
|
+
const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
|
|
3176
3273
|
if (configFile.error || !configFile.config) return null;
|
|
3177
|
-
const parsed =
|
|
3274
|
+
const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
|
|
3178
3275
|
const baseUrl = parsed.options.baseUrl ?? rootDir;
|
|
3179
3276
|
const rawPaths = parsed.options.paths;
|
|
3180
3277
|
const config = rawPaths ? (() => {
|
|
@@ -3274,23 +3371,23 @@ function createAliasPlugin(config, options) {
|
|
|
3274
3371
|
}
|
|
3275
3372
|
const importer = args.path;
|
|
3276
3373
|
const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
|
|
3277
|
-
const sourceFile =
|
|
3374
|
+
const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
|
|
3278
3375
|
const literals = [];
|
|
3279
3376
|
const visit = (node) => {
|
|
3280
|
-
if (
|
|
3377
|
+
if (ts8.isImportDeclaration(node)) {
|
|
3281
3378
|
if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
|
|
3282
|
-
if (
|
|
3379
|
+
if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
|
|
3283
3380
|
}
|
|
3284
|
-
} else if (
|
|
3381
|
+
} else if (ts8.isExportDeclaration(node)) {
|
|
3285
3382
|
if (!node.isTypeOnly && node.moduleSpecifier) {
|
|
3286
|
-
if (
|
|
3383
|
+
if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
|
|
3287
3384
|
}
|
|
3288
|
-
} else if (
|
|
3385
|
+
} else if (ts8.isCallExpression(node) && node.expression.kind === ts8.SyntaxKind.ImportKeyword && node.arguments[0] && ts8.isStringLiteral(node.arguments[0])) {
|
|
3289
3386
|
literals.push(node.arguments[0]);
|
|
3290
3387
|
}
|
|
3291
|
-
|
|
3388
|
+
ts8.forEachChild(node, visit);
|
|
3292
3389
|
};
|
|
3293
|
-
|
|
3390
|
+
ts8.forEachChild(sourceFile, visit);
|
|
3294
3391
|
const replacements = [];
|
|
3295
3392
|
const unresolvable = [];
|
|
3296
3393
|
for (const lit of literals) {
|
|
@@ -3585,7 +3682,7 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
|
3585
3682
|
const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
|
|
3586
3683
|
return path12.resolve(rootDir, dist, jsRel);
|
|
3587
3684
|
}
|
|
3588
|
-
async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
|
|
3685
|
+
async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
|
|
3589
3686
|
const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
|
|
3590
3687
|
if (inFlight2) {
|
|
3591
3688
|
await inFlight2;
|
|
@@ -3604,13 +3701,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
|
|
|
3604
3701
|
return false;
|
|
3605
3702
|
}
|
|
3606
3703
|
const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
|
|
3607
|
-
|
|
3704
|
+
const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
|
|
3705
|
+
if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
|
|
3608
3706
|
return false;
|
|
3609
3707
|
}
|
|
3610
3708
|
const sourceRelPath = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
3611
3709
|
const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
|
|
3710
|
+
const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
|
|
3612
3711
|
const generatePromise = (async () => {
|
|
3613
|
-
await generateSchemaFiles(sourceRoutes, rootDir, dist);
|
|
3712
|
+
await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
|
|
3614
3713
|
state.generatedSchemas.add(schemaPath);
|
|
3615
3714
|
})();
|
|
3616
3715
|
state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
|
|
@@ -4008,46 +4107,29 @@ async function parseMultipart(request) {
|
|
|
4008
4107
|
async function resolveInputFromUrl(method, request, url) {
|
|
4009
4108
|
const inputType = getInputTypeForMethod(method);
|
|
4010
4109
|
if (inputType === "body") {
|
|
4011
|
-
|
|
4012
|
-
if (contentType.includes("multipart/form-data")) {
|
|
4013
|
-
return parseMultipart(request);
|
|
4014
|
-
}
|
|
4015
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
4016
|
-
const text2 = await request.text();
|
|
4017
|
-
if (isBlankText(text2)) return null;
|
|
4018
|
-
const params = new URLSearchParams(text2);
|
|
4019
|
-
const obj = {};
|
|
4020
|
-
for (const [key, value] of params) {
|
|
4021
|
-
obj[key] = value;
|
|
4022
|
-
}
|
|
4023
|
-
return obj;
|
|
4024
|
-
}
|
|
4025
|
-
const text = await request.text();
|
|
4026
|
-
if (isBlankText(text)) {
|
|
4027
|
-
return null;
|
|
4028
|
-
}
|
|
4029
|
-
const result = parseJsonBody(text);
|
|
4030
|
-
if (!result.success) {
|
|
4031
|
-
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
4032
|
-
{
|
|
4033
|
-
path: "body",
|
|
4034
|
-
code: "INVALID_FORMAT",
|
|
4035
|
-
expected: "JSON",
|
|
4036
|
-
received: "text",
|
|
4037
|
-
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
4038
|
-
}
|
|
4039
|
-
]);
|
|
4040
|
-
}
|
|
4041
|
-
return result.data;
|
|
4110
|
+
return parseBodyByContentType(request);
|
|
4042
4111
|
}
|
|
4043
|
-
return queryToObject(url.searchParams);
|
|
4044
|
-
}
|
|
4045
|
-
function isBlankText(text) {
|
|
4046
|
-
return text.length === 0 || !/\S/.test(text);
|
|
4112
|
+
return { input: queryToObject(url.searchParams) };
|
|
4047
4113
|
}
|
|
4048
|
-
async function
|
|
4114
|
+
async function parseBodyByContentType(request) {
|
|
4115
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
4116
|
+
if (contentType.includes("multipart/form-data")) {
|
|
4117
|
+
return { input: await parseMultipart(request) };
|
|
4118
|
+
}
|
|
4119
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
4120
|
+
const text2 = await request.text();
|
|
4121
|
+
if (isBlankText(text2)) return { input: null };
|
|
4122
|
+
const params = new URLSearchParams(text2);
|
|
4123
|
+
const obj = {};
|
|
4124
|
+
for (const [key, value] of params) {
|
|
4125
|
+
obj[key] = value;
|
|
4126
|
+
}
|
|
4127
|
+
return { input: obj, rawBody: text2 };
|
|
4128
|
+
}
|
|
4049
4129
|
const text = await request.text();
|
|
4050
|
-
if (isBlankText(text))
|
|
4130
|
+
if (isBlankText(text)) {
|
|
4131
|
+
return { input: null };
|
|
4132
|
+
}
|
|
4051
4133
|
const result = parseJsonBody(text);
|
|
4052
4134
|
if (!result.success) {
|
|
4053
4135
|
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
@@ -4060,7 +4142,13 @@ async function resolveBodyForQueryMethod(request) {
|
|
|
4060
4142
|
}
|
|
4061
4143
|
]);
|
|
4062
4144
|
}
|
|
4063
|
-
return result.data;
|
|
4145
|
+
return { input: result.data, rawBody: text };
|
|
4146
|
+
}
|
|
4147
|
+
function isBlankText(text) {
|
|
4148
|
+
return text.length === 0 || !/\S/.test(text);
|
|
4149
|
+
}
|
|
4150
|
+
async function resolveBodyForQueryMethod(request) {
|
|
4151
|
+
return parseBodyByContentType(request);
|
|
4064
4152
|
}
|
|
4065
4153
|
|
|
4066
4154
|
// src/response/sendNodeResponse.ts
|
|
@@ -4645,6 +4733,7 @@ function attachWebSocket(options) {
|
|
|
4645
4733
|
server,
|
|
4646
4734
|
routesRef,
|
|
4647
4735
|
rootDir,
|
|
4736
|
+
dist,
|
|
4648
4737
|
config,
|
|
4649
4738
|
globalMiddlewares,
|
|
4650
4739
|
trustedProxy = false,
|
|
@@ -4679,12 +4768,50 @@ function attachWebSocket(options) {
|
|
|
4679
4768
|
const { route, params } = match;
|
|
4680
4769
|
const headers = nodeHttpToWebHeaders(req);
|
|
4681
4770
|
const host = req.headers.host ?? "localhost";
|
|
4682
|
-
const
|
|
4683
|
-
const request = new Request(
|
|
4771
|
+
const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
|
|
4772
|
+
const request = new Request(fullUrl.toString(), { method: "GET", headers });
|
|
4684
4773
|
const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
|
|
4685
4774
|
const meta = ctx.meta;
|
|
4686
4775
|
let upgraded = false;
|
|
4687
4776
|
const finalHandler = async () => {
|
|
4777
|
+
const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
|
|
4778
|
+
if (fs11.existsSync(schemaPath)) {
|
|
4779
|
+
if (isDevOnDemandEnabled()) {
|
|
4780
|
+
const devDist = getDevDist();
|
|
4781
|
+
if (devDist) {
|
|
4782
|
+
await ensureSchemaGenerated(
|
|
4783
|
+
schemaPath,
|
|
4784
|
+
route.filePath,
|
|
4785
|
+
routesRef.current,
|
|
4786
|
+
rootDir,
|
|
4787
|
+
dist,
|
|
4788
|
+
routesRef.wsCurrent
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
const searchParams = fullUrl.searchParams;
|
|
4793
|
+
const rawQueryObject = queryToObject(searchParams);
|
|
4794
|
+
const paramsResult = await validateInput(schemaPath, "WS", "params", params);
|
|
4795
|
+
if (!paramsResult.valid) {
|
|
4796
|
+
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", paramsResult.issues);
|
|
4797
|
+
}
|
|
4798
|
+
if (paramsResult.data !== params && typeof paramsResult.data === "object" && paramsResult.data !== null) {
|
|
4799
|
+
ctx.params = {
|
|
4800
|
+
...params,
|
|
4801
|
+
...paramsResult.data
|
|
4802
|
+
};
|
|
4803
|
+
}
|
|
4804
|
+
const queryResult = await validateInput(schemaPath, "WS", "query", rawQueryObject);
|
|
4805
|
+
if (!queryResult.valid) {
|
|
4806
|
+
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
|
|
4807
|
+
}
|
|
4808
|
+
if (queryResult.data !== rawQueryObject && typeof queryResult.data === "object" && queryResult.data !== null) {
|
|
4809
|
+
ctx.query = {
|
|
4810
|
+
...rawQueryObject,
|
|
4811
|
+
...queryResult.data
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4688
4815
|
let handlers;
|
|
4689
4816
|
try {
|
|
4690
4817
|
const absoluteFilePath = path13.resolve(rootDir, route.filePath);
|
|
@@ -4891,7 +5018,8 @@ function createServer(options) {
|
|
|
4891
5018
|
globalInjectors,
|
|
4892
5019
|
bodyLimit,
|
|
4893
5020
|
trustedProxy,
|
|
4894
|
-
registries
|
|
5021
|
+
registries,
|
|
5022
|
+
routesRef.wsCurrent
|
|
4895
5023
|
).catch((err) => {
|
|
4896
5024
|
console.error("[faapi] Request pipeline failed after error response attempt:", err);
|
|
4897
5025
|
try {
|
|
@@ -4909,6 +5037,7 @@ function createServer(options) {
|
|
|
4909
5037
|
server,
|
|
4910
5038
|
routesRef,
|
|
4911
5039
|
rootDir,
|
|
5040
|
+
dist,
|
|
4912
5041
|
config,
|
|
4913
5042
|
globalMiddlewares,
|
|
4914
5043
|
trustedProxy,
|
|
@@ -4916,14 +5045,13 @@ function createServer(options) {
|
|
|
4916
5045
|
});
|
|
4917
5046
|
return { server, routesRef };
|
|
4918
5047
|
}
|
|
4919
|
-
function prepareRequest(req,
|
|
4920
|
-
const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
|
|
5048
|
+
function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
|
|
4921
5049
|
const method = request.method.toUpperCase();
|
|
4922
5050
|
const urlPath = url.pathname;
|
|
4923
5051
|
const ctx = createContextFromUrl(
|
|
4924
5052
|
request,
|
|
4925
5053
|
url,
|
|
4926
|
-
{},
|
|
5054
|
+
preMatched?.params ?? {},
|
|
4927
5055
|
config,
|
|
4928
5056
|
getClientIp(req, trustedProxy),
|
|
4929
5057
|
registries
|
|
@@ -4954,19 +5082,33 @@ function getRoutePaths(route, rootDir, dist) {
|
|
|
4954
5082
|
}
|
|
4955
5083
|
var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
|
|
4956
5084
|
function createRoutePipeline(opts) {
|
|
4957
|
-
const {
|
|
5085
|
+
const {
|
|
5086
|
+
routes,
|
|
5087
|
+
method,
|
|
5088
|
+
urlPath,
|
|
5089
|
+
url,
|
|
5090
|
+
ctx,
|
|
5091
|
+
request,
|
|
5092
|
+
rootDir,
|
|
5093
|
+
dist,
|
|
5094
|
+
globalInjectors,
|
|
5095
|
+
preMatched,
|
|
5096
|
+
wsRoutes
|
|
5097
|
+
} = opts;
|
|
4958
5098
|
return async () => {
|
|
4959
|
-
const match = resolveRouteOrThrow(routes, method, urlPath);
|
|
4960
|
-
ctx.params = match.params;
|
|
5099
|
+
const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
|
|
4961
5100
|
const { route } = match;
|
|
4962
5101
|
const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
|
|
4963
5102
|
const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
|
|
4964
|
-
const input = await resolveInputFromUrl(route.method, request, url);
|
|
5103
|
+
const { input, rawBody } = await resolveInputFromUrl(route.method, request, url);
|
|
5104
|
+
if (rawBody !== void 0) {
|
|
5105
|
+
ctx.rawBody = rawBody;
|
|
5106
|
+
}
|
|
4965
5107
|
const inputType = getInputTypeForMethod(route.method);
|
|
4966
5108
|
if (isDevOnDemandEnabled()) {
|
|
4967
5109
|
const devDist = getDevDist();
|
|
4968
5110
|
if (devDist) {
|
|
4969
|
-
await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
|
|
5111
|
+
await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
|
|
4970
5112
|
}
|
|
4971
5113
|
}
|
|
4972
5114
|
const result = await validateInput(schemaPath, route.method, inputType, input);
|
|
@@ -4974,7 +5116,7 @@ function createRoutePipeline(opts) {
|
|
|
4974
5116
|
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
|
|
4975
5117
|
}
|
|
4976
5118
|
if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
|
|
4977
|
-
ctx.
|
|
5119
|
+
ctx.query = {
|
|
4978
5120
|
...input,
|
|
4979
5121
|
...result.data
|
|
4980
5122
|
};
|
|
@@ -4989,7 +5131,36 @@ function createRoutePipeline(opts) {
|
|
|
4989
5131
|
...paramsResult.data
|
|
4990
5132
|
} : match.params;
|
|
4991
5133
|
}
|
|
4992
|
-
|
|
5134
|
+
if (inputType === "body") {
|
|
5135
|
+
const rawQuery = queryToObject(url.searchParams);
|
|
5136
|
+
const queryResult = await validateInput(schemaPath, route.method, "query", rawQuery);
|
|
5137
|
+
if (!queryResult.valid) {
|
|
5138
|
+
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
|
|
5139
|
+
}
|
|
5140
|
+
if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
|
|
5141
|
+
ctx.query = {
|
|
5142
|
+
...rawQuery,
|
|
5143
|
+
...queryResult.data
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
let body;
|
|
5148
|
+
if (inputType === "query" && hasBody(route.method)) {
|
|
5149
|
+
const parsed = await resolveBodyForQueryMethod(request);
|
|
5150
|
+
if (parsed.rawBody !== void 0) {
|
|
5151
|
+
ctx.rawBody = parsed.rawBody;
|
|
5152
|
+
}
|
|
5153
|
+
const bodyResult = await validateInput(schemaPath, route.method, "body", parsed.input);
|
|
5154
|
+
if (!bodyResult.valid) {
|
|
5155
|
+
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
|
|
5156
|
+
}
|
|
5157
|
+
body = bodyResult.data;
|
|
5158
|
+
} else if (hasBody(route.method)) {
|
|
5159
|
+
body = result.data;
|
|
5160
|
+
}
|
|
5161
|
+
if (body !== void 0 && body !== null) {
|
|
5162
|
+
ctx.body = body;
|
|
5163
|
+
}
|
|
4993
5164
|
if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
|
|
4994
5165
|
await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
|
|
4995
5166
|
const bundle = await loadMergedMiddlewares(route.middlewarePaths);
|
|
@@ -5025,25 +5196,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
5025
5196
|
}
|
|
5026
5197
|
}
|
|
5027
5198
|
}
|
|
5028
|
-
async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
|
|
5199
|
+
async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes) {
|
|
5029
5200
|
let meta = { headers: {}, setCookies: [] };
|
|
5030
5201
|
let ctx;
|
|
5202
|
+
let sseEarlySent = false;
|
|
5031
5203
|
const abortController = new AbortController();
|
|
5032
5204
|
res.on("close", () => {
|
|
5033
5205
|
if (!res.writableEnded) abortController.abort();
|
|
5034
5206
|
});
|
|
5035
5207
|
try {
|
|
5208
|
+
const { request, url } = toWebRequest(req, bodyLimit, abortController.signal);
|
|
5209
|
+
const method = request.method.toUpperCase();
|
|
5210
|
+
const urlPath = url.pathname;
|
|
5211
|
+
const preMatched = matchRoute(routes, method, urlPath);
|
|
5036
5212
|
const prepared = prepareRequest(
|
|
5037
5213
|
req,
|
|
5214
|
+
request,
|
|
5215
|
+
url,
|
|
5038
5216
|
config,
|
|
5039
|
-
bodyLimit,
|
|
5040
5217
|
trustedProxy,
|
|
5041
5218
|
registries,
|
|
5042
|
-
|
|
5219
|
+
preMatched
|
|
5043
5220
|
);
|
|
5044
5221
|
ctx = prepared.ctx;
|
|
5045
5222
|
meta = prepared.meta;
|
|
5046
|
-
|
|
5223
|
+
ctx.__earlyRespond = (response2) => {
|
|
5224
|
+
if (sseEarlySent) return;
|
|
5225
|
+
sseEarlySent = true;
|
|
5226
|
+
void sendNodeResponse(mergeMeta(response2, meta), res).catch(() => {
|
|
5227
|
+
if (!res.writableEnded) res.destroy();
|
|
5228
|
+
});
|
|
5229
|
+
};
|
|
5047
5230
|
const routePipeline = createRoutePipeline({
|
|
5048
5231
|
routes,
|
|
5049
5232
|
method,
|
|
@@ -5053,12 +5236,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
|
|
|
5053
5236
|
request,
|
|
5054
5237
|
rootDir,
|
|
5055
5238
|
dist,
|
|
5056
|
-
globalInjectors
|
|
5239
|
+
globalInjectors,
|
|
5240
|
+
preMatched,
|
|
5241
|
+
wsRoutes
|
|
5057
5242
|
});
|
|
5058
5243
|
const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
|
|
5059
|
-
|
|
5244
|
+
if (!sseEarlySent) {
|
|
5245
|
+
await sendSuccessResponse(response, res);
|
|
5246
|
+
}
|
|
5060
5247
|
} catch (err) {
|
|
5061
5248
|
if (res.destroyed || res.writableEnded) return;
|
|
5249
|
+
if (sseEarlySent) {
|
|
5250
|
+
if (onError && ctx) {
|
|
5251
|
+
try {
|
|
5252
|
+
await onError(err, ctx);
|
|
5253
|
+
} catch {
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
return;
|
|
5257
|
+
}
|
|
5062
5258
|
if (err instanceof PayloadTooLargeError) {
|
|
5063
5259
|
res.setHeader("Connection", "close");
|
|
5064
5260
|
res.once("finish", () => req.destroy());
|