@faapi/faapi 6.20.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 +297 -144
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +16 -21
- package/dist/index.js +300 -148
- 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 +263 -114
- 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 = [];
|
|
@@ -2698,6 +2751,28 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
2698
2751
|
}
|
|
2699
2752
|
}
|
|
2700
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
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2701
2776
|
return { sources, resolversByFile };
|
|
2702
2777
|
}
|
|
2703
2778
|
|
|
@@ -3128,9 +3203,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
3128
3203
|
lines.push(...schemaBlocks);
|
|
3129
3204
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
3130
3205
|
}
|
|
3131
|
-
async function generateSchemaFiles(routes, rootDir, dist) {
|
|
3132
|
-
if (routes.length === 0) return;
|
|
3133
|
-
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);
|
|
3134
3209
|
await generateZodArtifacts(sources, {
|
|
3135
3210
|
generateFileSource: generateSchemaFileSource,
|
|
3136
3211
|
resolversByFile,
|
|
@@ -3149,7 +3224,7 @@ import fs8 from "fs";
|
|
|
3149
3224
|
import fg2 from "fast-glob";
|
|
3150
3225
|
|
|
3151
3226
|
// src/cli/aliasPlugin.ts
|
|
3152
|
-
import
|
|
3227
|
+
import ts8 from "typescript";
|
|
3153
3228
|
import path9 from "path";
|
|
3154
3229
|
import fs7 from "fs";
|
|
3155
3230
|
|
|
@@ -3177,7 +3252,7 @@ function resolveAlias(specifier, config) {
|
|
|
3177
3252
|
}
|
|
3178
3253
|
|
|
3179
3254
|
// src/utils/readTsconfig.ts
|
|
3180
|
-
import
|
|
3255
|
+
import ts7 from "typescript";
|
|
3181
3256
|
import path8 from "path";
|
|
3182
3257
|
import fs6 from "fs";
|
|
3183
3258
|
var tsconfigCache = /* @__PURE__ */ new Map();
|
|
@@ -3194,9 +3269,9 @@ function readTsconfig(rootDir) {
|
|
|
3194
3269
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
3195
3270
|
return cached.config;
|
|
3196
3271
|
}
|
|
3197
|
-
const configFile =
|
|
3272
|
+
const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
|
|
3198
3273
|
if (configFile.error || !configFile.config) return null;
|
|
3199
|
-
const parsed =
|
|
3274
|
+
const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
|
|
3200
3275
|
const baseUrl = parsed.options.baseUrl ?? rootDir;
|
|
3201
3276
|
const rawPaths = parsed.options.paths;
|
|
3202
3277
|
const config = rawPaths ? (() => {
|
|
@@ -3296,23 +3371,23 @@ function createAliasPlugin(config, options) {
|
|
|
3296
3371
|
}
|
|
3297
3372
|
const importer = args.path;
|
|
3298
3373
|
const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
|
|
3299
|
-
const sourceFile =
|
|
3374
|
+
const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
|
|
3300
3375
|
const literals = [];
|
|
3301
3376
|
const visit = (node) => {
|
|
3302
|
-
if (
|
|
3377
|
+
if (ts8.isImportDeclaration(node)) {
|
|
3303
3378
|
if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
|
|
3304
|
-
if (
|
|
3379
|
+
if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
|
|
3305
3380
|
}
|
|
3306
|
-
} else if (
|
|
3381
|
+
} else if (ts8.isExportDeclaration(node)) {
|
|
3307
3382
|
if (!node.isTypeOnly && node.moduleSpecifier) {
|
|
3308
|
-
if (
|
|
3383
|
+
if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
|
|
3309
3384
|
}
|
|
3310
|
-
} else if (
|
|
3385
|
+
} else if (ts8.isCallExpression(node) && node.expression.kind === ts8.SyntaxKind.ImportKeyword && node.arguments[0] && ts8.isStringLiteral(node.arguments[0])) {
|
|
3311
3386
|
literals.push(node.arguments[0]);
|
|
3312
3387
|
}
|
|
3313
|
-
|
|
3388
|
+
ts8.forEachChild(node, visit);
|
|
3314
3389
|
};
|
|
3315
|
-
|
|
3390
|
+
ts8.forEachChild(sourceFile, visit);
|
|
3316
3391
|
const replacements = [];
|
|
3317
3392
|
const unresolvable = [];
|
|
3318
3393
|
for (const lit of literals) {
|
|
@@ -3607,7 +3682,7 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
|
3607
3682
|
const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
|
|
3608
3683
|
return path12.resolve(rootDir, dist, jsRel);
|
|
3609
3684
|
}
|
|
3610
|
-
async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
|
|
3685
|
+
async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
|
|
3611
3686
|
const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
|
|
3612
3687
|
if (inFlight2) {
|
|
3613
3688
|
await inFlight2;
|
|
@@ -3626,13 +3701,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
|
|
|
3626
3701
|
return false;
|
|
3627
3702
|
}
|
|
3628
3703
|
const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
|
|
3629
|
-
|
|
3704
|
+
const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
|
|
3705
|
+
if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
|
|
3630
3706
|
return false;
|
|
3631
3707
|
}
|
|
3632
3708
|
const sourceRelPath = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
3633
3709
|
const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
|
|
3710
|
+
const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
|
|
3634
3711
|
const generatePromise = (async () => {
|
|
3635
|
-
await generateSchemaFiles(sourceRoutes, rootDir, dist);
|
|
3712
|
+
await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
|
|
3636
3713
|
state.generatedSchemas.add(schemaPath);
|
|
3637
3714
|
})();
|
|
3638
3715
|
state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
|
|
@@ -4030,46 +4107,29 @@ async function parseMultipart(request) {
|
|
|
4030
4107
|
async function resolveInputFromUrl(method, request, url) {
|
|
4031
4108
|
const inputType = getInputTypeForMethod(method);
|
|
4032
4109
|
if (inputType === "body") {
|
|
4033
|
-
|
|
4034
|
-
if (contentType.includes("multipart/form-data")) {
|
|
4035
|
-
return parseMultipart(request);
|
|
4036
|
-
}
|
|
4037
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
4038
|
-
const text2 = await request.text();
|
|
4039
|
-
if (isBlankText(text2)) return null;
|
|
4040
|
-
const params = new URLSearchParams(text2);
|
|
4041
|
-
const obj = {};
|
|
4042
|
-
for (const [key, value] of params) {
|
|
4043
|
-
obj[key] = value;
|
|
4044
|
-
}
|
|
4045
|
-
return obj;
|
|
4046
|
-
}
|
|
4047
|
-
const text = await request.text();
|
|
4048
|
-
if (isBlankText(text)) {
|
|
4049
|
-
return null;
|
|
4050
|
-
}
|
|
4051
|
-
const result = parseJsonBody(text);
|
|
4052
|
-
if (!result.success) {
|
|
4053
|
-
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
4054
|
-
{
|
|
4055
|
-
path: "body",
|
|
4056
|
-
code: "INVALID_FORMAT",
|
|
4057
|
-
expected: "JSON",
|
|
4058
|
-
received: "text",
|
|
4059
|
-
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
4060
|
-
}
|
|
4061
|
-
]);
|
|
4062
|
-
}
|
|
4063
|
-
return result.data;
|
|
4110
|
+
return parseBodyByContentType(request);
|
|
4064
4111
|
}
|
|
4065
|
-
return queryToObject(url.searchParams);
|
|
4066
|
-
}
|
|
4067
|
-
function isBlankText(text) {
|
|
4068
|
-
return text.length === 0 || !/\S/.test(text);
|
|
4112
|
+
return { input: queryToObject(url.searchParams) };
|
|
4069
4113
|
}
|
|
4070
|
-
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
|
+
}
|
|
4071
4129
|
const text = await request.text();
|
|
4072
|
-
if (isBlankText(text))
|
|
4130
|
+
if (isBlankText(text)) {
|
|
4131
|
+
return { input: null };
|
|
4132
|
+
}
|
|
4073
4133
|
const result = parseJsonBody(text);
|
|
4074
4134
|
if (!result.success) {
|
|
4075
4135
|
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
@@ -4082,7 +4142,13 @@ async function resolveBodyForQueryMethod(request) {
|
|
|
4082
4142
|
}
|
|
4083
4143
|
]);
|
|
4084
4144
|
}
|
|
4085
|
-
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);
|
|
4086
4152
|
}
|
|
4087
4153
|
|
|
4088
4154
|
// src/response/sendNodeResponse.ts
|
|
@@ -4667,6 +4733,7 @@ function attachWebSocket(options) {
|
|
|
4667
4733
|
server,
|
|
4668
4734
|
routesRef,
|
|
4669
4735
|
rootDir,
|
|
4736
|
+
dist,
|
|
4670
4737
|
config,
|
|
4671
4738
|
globalMiddlewares,
|
|
4672
4739
|
trustedProxy = false,
|
|
@@ -4701,12 +4768,50 @@ function attachWebSocket(options) {
|
|
|
4701
4768
|
const { route, params } = match;
|
|
4702
4769
|
const headers = nodeHttpToWebHeaders(req);
|
|
4703
4770
|
const host = req.headers.host ?? "localhost";
|
|
4704
|
-
const
|
|
4705
|
-
const request = new Request(
|
|
4771
|
+
const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
|
|
4772
|
+
const request = new Request(fullUrl.toString(), { method: "GET", headers });
|
|
4706
4773
|
const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
|
|
4707
4774
|
const meta = ctx.meta;
|
|
4708
4775
|
let upgraded = false;
|
|
4709
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
|
+
}
|
|
4710
4815
|
let handlers;
|
|
4711
4816
|
try {
|
|
4712
4817
|
const absoluteFilePath = path13.resolve(rootDir, route.filePath);
|
|
@@ -4913,7 +5018,8 @@ function createServer(options) {
|
|
|
4913
5018
|
globalInjectors,
|
|
4914
5019
|
bodyLimit,
|
|
4915
5020
|
trustedProxy,
|
|
4916
|
-
registries
|
|
5021
|
+
registries,
|
|
5022
|
+
routesRef.wsCurrent
|
|
4917
5023
|
).catch((err) => {
|
|
4918
5024
|
console.error("[faapi] Request pipeline failed after error response attempt:", err);
|
|
4919
5025
|
try {
|
|
@@ -4931,6 +5037,7 @@ function createServer(options) {
|
|
|
4931
5037
|
server,
|
|
4932
5038
|
routesRef,
|
|
4933
5039
|
rootDir,
|
|
5040
|
+
dist,
|
|
4934
5041
|
config,
|
|
4935
5042
|
globalMiddlewares,
|
|
4936
5043
|
trustedProxy,
|
|
@@ -4938,14 +5045,13 @@ function createServer(options) {
|
|
|
4938
5045
|
});
|
|
4939
5046
|
return { server, routesRef };
|
|
4940
5047
|
}
|
|
4941
|
-
function prepareRequest(req,
|
|
4942
|
-
const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
|
|
5048
|
+
function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
|
|
4943
5049
|
const method = request.method.toUpperCase();
|
|
4944
5050
|
const urlPath = url.pathname;
|
|
4945
5051
|
const ctx = createContextFromUrl(
|
|
4946
5052
|
request,
|
|
4947
5053
|
url,
|
|
4948
|
-
{},
|
|
5054
|
+
preMatched?.params ?? {},
|
|
4949
5055
|
config,
|
|
4950
5056
|
getClientIp(req, trustedProxy),
|
|
4951
5057
|
registries
|
|
@@ -4976,19 +5082,33 @@ function getRoutePaths(route, rootDir, dist) {
|
|
|
4976
5082
|
}
|
|
4977
5083
|
var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
|
|
4978
5084
|
function createRoutePipeline(opts) {
|
|
4979
|
-
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;
|
|
4980
5098
|
return async () => {
|
|
4981
|
-
const match = resolveRouteOrThrow(routes, method, urlPath);
|
|
4982
|
-
ctx.params = match.params;
|
|
5099
|
+
const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
|
|
4983
5100
|
const { route } = match;
|
|
4984
5101
|
const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
|
|
4985
5102
|
const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
|
|
4986
|
-
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
|
+
}
|
|
4987
5107
|
const inputType = getInputTypeForMethod(route.method);
|
|
4988
5108
|
if (isDevOnDemandEnabled()) {
|
|
4989
5109
|
const devDist = getDevDist();
|
|
4990
5110
|
if (devDist) {
|
|
4991
|
-
await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
|
|
5111
|
+
await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
|
|
4992
5112
|
}
|
|
4993
5113
|
}
|
|
4994
5114
|
const result = await validateInput(schemaPath, route.method, inputType, input);
|
|
@@ -4996,7 +5116,7 @@ function createRoutePipeline(opts) {
|
|
|
4996
5116
|
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
|
|
4997
5117
|
}
|
|
4998
5118
|
if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
|
|
4999
|
-
ctx.
|
|
5119
|
+
ctx.query = {
|
|
5000
5120
|
...input,
|
|
5001
5121
|
...result.data
|
|
5002
5122
|
};
|
|
@@ -5018,7 +5138,7 @@ function createRoutePipeline(opts) {
|
|
|
5018
5138
|
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
|
|
5019
5139
|
}
|
|
5020
5140
|
if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
|
|
5021
|
-
ctx.
|
|
5141
|
+
ctx.query = {
|
|
5022
5142
|
...rawQuery,
|
|
5023
5143
|
...queryResult.data
|
|
5024
5144
|
};
|
|
@@ -5027,16 +5147,20 @@ function createRoutePipeline(opts) {
|
|
|
5027
5147
|
let body;
|
|
5028
5148
|
if (inputType === "query" && hasBody(route.method)) {
|
|
5029
5149
|
const parsed = await resolveBodyForQueryMethod(request);
|
|
5030
|
-
if (parsed !== void 0) {
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
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);
|
|
5036
5156
|
}
|
|
5157
|
+
body = bodyResult.data;
|
|
5037
5158
|
} else if (hasBody(route.method)) {
|
|
5038
5159
|
body = result.data;
|
|
5039
5160
|
}
|
|
5161
|
+
if (body !== void 0 && body !== null) {
|
|
5162
|
+
ctx.body = body;
|
|
5163
|
+
}
|
|
5040
5164
|
if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
|
|
5041
5165
|
await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
|
|
5042
5166
|
const bundle = await loadMergedMiddlewares(route.middlewarePaths);
|
|
@@ -5072,25 +5196,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
5072
5196
|
}
|
|
5073
5197
|
}
|
|
5074
5198
|
}
|
|
5075
|
-
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) {
|
|
5076
5200
|
let meta = { headers: {}, setCookies: [] };
|
|
5077
5201
|
let ctx;
|
|
5202
|
+
let sseEarlySent = false;
|
|
5078
5203
|
const abortController = new AbortController();
|
|
5079
5204
|
res.on("close", () => {
|
|
5080
5205
|
if (!res.writableEnded) abortController.abort();
|
|
5081
5206
|
});
|
|
5082
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);
|
|
5083
5212
|
const prepared = prepareRequest(
|
|
5084
5213
|
req,
|
|
5214
|
+
request,
|
|
5215
|
+
url,
|
|
5085
5216
|
config,
|
|
5086
|
-
bodyLimit,
|
|
5087
5217
|
trustedProxy,
|
|
5088
5218
|
registries,
|
|
5089
|
-
|
|
5219
|
+
preMatched
|
|
5090
5220
|
);
|
|
5091
5221
|
ctx = prepared.ctx;
|
|
5092
5222
|
meta = prepared.meta;
|
|
5093
|
-
|
|
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
|
+
};
|
|
5094
5230
|
const routePipeline = createRoutePipeline({
|
|
5095
5231
|
routes,
|
|
5096
5232
|
method,
|
|
@@ -5100,12 +5236,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
|
|
|
5100
5236
|
request,
|
|
5101
5237
|
rootDir,
|
|
5102
5238
|
dist,
|
|
5103
|
-
globalInjectors
|
|
5239
|
+
globalInjectors,
|
|
5240
|
+
preMatched,
|
|
5241
|
+
wsRoutes
|
|
5104
5242
|
});
|
|
5105
5243
|
const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
|
|
5106
|
-
|
|
5244
|
+
if (!sseEarlySent) {
|
|
5245
|
+
await sendSuccessResponse(response, res);
|
|
5246
|
+
}
|
|
5107
5247
|
} catch (err) {
|
|
5108
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
|
+
}
|
|
5109
5258
|
if (err instanceof PayloadTooLargeError) {
|
|
5110
5259
|
res.setHeader("Connection", "close");
|
|
5111
5260
|
res.once("finish", () => req.destroy());
|