@faapi/faapi 0.0.0-canary.4ab8d97 → 0.0.0-canary.4e89b9b
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 +6056 -5253
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +423 -66
- package/dist/index.js +2828 -168
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/ast/createProgram.ts
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
var programCache = /* @__PURE__ */ new Map();
|
|
4
|
+
function invalidateProgramCache() {
|
|
5
|
+
programCache.clear();
|
|
6
|
+
}
|
|
4
7
|
function createProgram(filePath) {
|
|
5
8
|
const cached = programCache.get(filePath);
|
|
6
9
|
if (cached) {
|
|
@@ -143,6 +146,9 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
143
146
|
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.KeyOfKeyword) {
|
|
144
147
|
return resolveKeyOf(typeNode, checker);
|
|
145
148
|
}
|
|
149
|
+
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.ReadonlyKeyword) {
|
|
150
|
+
return resolveTypeNode(typeNode.type, checker, visited);
|
|
151
|
+
}
|
|
146
152
|
if (ts2.isTypeReferenceNode(typeNode)) {
|
|
147
153
|
return resolveTypeReference(typeNode, checker, visited);
|
|
148
154
|
}
|
|
@@ -155,7 +161,11 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
|
|
|
155
161
|
const name = member.name.getText();
|
|
156
162
|
const optional = !!member.questionToken;
|
|
157
163
|
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
158
|
-
|
|
164
|
+
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
165
|
+
validateConstraints(constraints, type, name);
|
|
166
|
+
properties.push(
|
|
167
|
+
constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
|
|
168
|
+
);
|
|
159
169
|
}
|
|
160
170
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
161
171
|
const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
|
|
@@ -237,7 +247,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
237
247
|
if (typeName === "Date") {
|
|
238
248
|
return { kind: "date" };
|
|
239
249
|
}
|
|
240
|
-
if (typeName === "Array" && typeNode.typeArguments?.length === 1) {
|
|
250
|
+
if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
|
|
241
251
|
return {
|
|
242
252
|
kind: "array",
|
|
243
253
|
element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
|
|
@@ -280,10 +290,35 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
280
290
|
const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
|
|
281
291
|
return { kind: "object", properties };
|
|
282
292
|
}
|
|
283
|
-
if (typeName === "Map"
|
|
293
|
+
if (typeName === "Map") {
|
|
294
|
+
if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
|
|
295
|
+
throw new SchemaExtractionError(
|
|
296
|
+
typeNode.getText(),
|
|
297
|
+
"Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
kind: "map",
|
|
302
|
+
key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
|
|
303
|
+
value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (typeName === "Set") {
|
|
307
|
+
if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
|
|
308
|
+
throw new SchemaExtractionError(
|
|
309
|
+
typeNode.getText(),
|
|
310
|
+
"Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
kind: "set",
|
|
315
|
+
element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (typeName === "WeakMap" || typeName === "WeakSet") {
|
|
284
319
|
throw new SchemaExtractionError(
|
|
285
320
|
typeNode.getText(),
|
|
286
|
-
`${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528\u5BF9\u8C61
|
|
321
|
+
`${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
|
|
287
322
|
);
|
|
288
323
|
}
|
|
289
324
|
if (typeName === "Promise") {
|
|
@@ -362,7 +397,12 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
362
397
|
const name = member.name.getText();
|
|
363
398
|
const optional = !!member.questionToken;
|
|
364
399
|
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
365
|
-
|
|
400
|
+
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
401
|
+
validateConstraints(constraints, type, name);
|
|
402
|
+
propMap.set(
|
|
403
|
+
name,
|
|
404
|
+
constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
|
|
405
|
+
);
|
|
366
406
|
}
|
|
367
407
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
368
408
|
const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
|
|
@@ -375,6 +415,142 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
375
415
|
}
|
|
376
416
|
return { kind: "object", properties };
|
|
377
417
|
}
|
|
418
|
+
var NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
419
|
+
"max",
|
|
420
|
+
"min",
|
|
421
|
+
"int",
|
|
422
|
+
"positive",
|
|
423
|
+
"negative",
|
|
424
|
+
"nonnegative",
|
|
425
|
+
"nonpositive"
|
|
426
|
+
]);
|
|
427
|
+
var LENGTH_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
428
|
+
"maxLength",
|
|
429
|
+
"minLength",
|
|
430
|
+
"length"
|
|
431
|
+
]);
|
|
432
|
+
var STRING_FORMAT_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
433
|
+
"regex",
|
|
434
|
+
"email",
|
|
435
|
+
"url",
|
|
436
|
+
"uuid"
|
|
437
|
+
]);
|
|
438
|
+
function extractConstraintsFromJsDoc(node, fieldName) {
|
|
439
|
+
const jsDocs = ts2.getJSDocCommentsAndTags(node).filter((entry) => ts2.isJSDoc(entry));
|
|
440
|
+
if (jsDocs.length === 0) return [];
|
|
441
|
+
const constraints = [];
|
|
442
|
+
for (const jsDoc of jsDocs) {
|
|
443
|
+
if (!jsDoc.tags) continue;
|
|
444
|
+
for (const tag of jsDoc.tags) {
|
|
445
|
+
const constraint = parseJsDocTag(tag, fieldName);
|
|
446
|
+
if (constraint) constraints.push(constraint);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return constraints;
|
|
450
|
+
}
|
|
451
|
+
function getTagCommentText(tag) {
|
|
452
|
+
const comment = tag.comment;
|
|
453
|
+
if (typeof comment === "string") return comment;
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
function parseJsDocTag(tag, fieldName) {
|
|
457
|
+
const tagName = tag.tagName.text;
|
|
458
|
+
switch (tagName) {
|
|
459
|
+
// 数值约束(带值)
|
|
460
|
+
case "max":
|
|
461
|
+
case "min": {
|
|
462
|
+
const value = parseNumberValue(tag, fieldName, tagName);
|
|
463
|
+
return { kind: tagName, value };
|
|
464
|
+
}
|
|
465
|
+
// 长度约束(带值)
|
|
466
|
+
case "maxLength":
|
|
467
|
+
case "minLength":
|
|
468
|
+
case "length": {
|
|
469
|
+
const value = parseNumberValue(tag, fieldName, tagName);
|
|
470
|
+
return { kind: tagName, value };
|
|
471
|
+
}
|
|
472
|
+
// 正则约束(带 /pattern/flags 值)
|
|
473
|
+
case "regex":
|
|
474
|
+
case "pattern": {
|
|
475
|
+
const text = getTagCommentText(tag);
|
|
476
|
+
if (!text) {
|
|
477
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981 /pattern/flags \u5F62\u5F0F\u7684\u503C`);
|
|
478
|
+
}
|
|
479
|
+
const regex = parseRegexLiteral(text.trim(), fieldName);
|
|
480
|
+
return { kind: "regex", pattern: regex.pattern, flags: regex.flags };
|
|
481
|
+
}
|
|
482
|
+
// 数值约束(无值)
|
|
483
|
+
case "int":
|
|
484
|
+
case "positive":
|
|
485
|
+
case "negative":
|
|
486
|
+
case "nonnegative":
|
|
487
|
+
case "nonpositive":
|
|
488
|
+
return { kind: tagName };
|
|
489
|
+
// 字符串格式约束(无值)
|
|
490
|
+
case "email":
|
|
491
|
+
case "url":
|
|
492
|
+
case "uuid":
|
|
493
|
+
return { kind: tagName };
|
|
494
|
+
default:
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function parseNumberValue(tag, fieldName, tagName) {
|
|
499
|
+
const text = getTagCommentText(tag);
|
|
500
|
+
if (!text) {
|
|
501
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981\u4E00\u4E2A\u6570\u5B57\u503C`);
|
|
502
|
+
}
|
|
503
|
+
const trimmed = text.trim();
|
|
504
|
+
const num = Number(trimmed);
|
|
505
|
+
if (!Number.isFinite(num)) {
|
|
506
|
+
throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u7684\u503C "${trimmed}" \u4E0D\u662F\u6709\u6548\u6570\u5B57`);
|
|
507
|
+
}
|
|
508
|
+
return num;
|
|
509
|
+
}
|
|
510
|
+
function parseRegexLiteral(text, fieldName) {
|
|
511
|
+
const match = /^\/(.+)\/([gimsuy]*)$/.exec(text);
|
|
512
|
+
if (!match) {
|
|
513
|
+
throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u4E0D\u662F /pattern/flags \u5F62\u5F0F`);
|
|
514
|
+
}
|
|
515
|
+
const [, pattern, flags] = match;
|
|
516
|
+
if (!pattern) {
|
|
517
|
+
throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u7684 pattern \u90E8\u5206\u4E3A\u7A7A`);
|
|
518
|
+
}
|
|
519
|
+
return flags ? { pattern, flags } : { pattern };
|
|
520
|
+
}
|
|
521
|
+
function validateConstraints(constraints, type, fieldName) {
|
|
522
|
+
if (constraints.length === 0) return;
|
|
523
|
+
for (const constraint of constraints) {
|
|
524
|
+
const kind = constraint.kind;
|
|
525
|
+
if (NUMBER_CONSTRAINT_KINDS.has(kind)) {
|
|
526
|
+
if (type.kind !== "number") {
|
|
527
|
+
throw new SchemaExtractionError(
|
|
528
|
+
fieldName,
|
|
529
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E number \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (LENGTH_CONSTRAINT_KINDS.has(kind)) {
|
|
535
|
+
if (type.kind !== "string" && type.kind !== "array") {
|
|
536
|
+
throw new SchemaExtractionError(
|
|
537
|
+
fieldName,
|
|
538
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u6216 array \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (STRING_FORMAT_CONSTRAINT_KINDS.has(kind)) {
|
|
544
|
+
if (type.kind !== "string") {
|
|
545
|
+
throw new SchemaExtractionError(
|
|
546
|
+
fieldName,
|
|
547
|
+
`@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
378
554
|
|
|
379
555
|
// src/ast/extractHandlerTypes.ts
|
|
380
556
|
function extractTypeInfo(program, filePath, typeName) {
|
|
@@ -417,6 +593,45 @@ function extractTypeInfo(program, filePath, typeName) {
|
|
|
417
593
|
});
|
|
418
594
|
return result;
|
|
419
595
|
}
|
|
596
|
+
function extractAllTypes(program, filePath) {
|
|
597
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
598
|
+
if (!sourceFile) return /* @__PURE__ */ new Map();
|
|
599
|
+
const checker = program.getTypeChecker();
|
|
600
|
+
const result = /* @__PURE__ */ new Map();
|
|
601
|
+
ts3.forEachChild(sourceFile, (node) => {
|
|
602
|
+
if (ts3.isInterfaceDeclaration(node)) {
|
|
603
|
+
const visited = /* @__PURE__ */ new Set();
|
|
604
|
+
visited.add(node.name.text);
|
|
605
|
+
const runtimeType = withFileContext(
|
|
606
|
+
filePath,
|
|
607
|
+
node.name.text,
|
|
608
|
+
() => resolveInterfaceDeclaration(node, checker, visited)
|
|
609
|
+
);
|
|
610
|
+
result.set(node.name.text, {
|
|
611
|
+
name: node.name.text,
|
|
612
|
+
properties: runtimeType.kind === "object" ? runtimeType.properties : [],
|
|
613
|
+
runtimeType
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (ts3.isTypeAliasDeclaration(node)) {
|
|
618
|
+
const visited = /* @__PURE__ */ new Set();
|
|
619
|
+
visited.add(node.name.text);
|
|
620
|
+
const runtimeType = withFileContext(
|
|
621
|
+
filePath,
|
|
622
|
+
node.name.text,
|
|
623
|
+
() => resolveTypeNode(node.type, checker, visited)
|
|
624
|
+
);
|
|
625
|
+
result.set(node.name.text, {
|
|
626
|
+
name: node.name.text,
|
|
627
|
+
properties: runtimeType.kind === "object" ? runtimeType.properties : [],
|
|
628
|
+
runtimeType
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
420
635
|
function withFileContext(filePath, typeName, fn) {
|
|
421
636
|
try {
|
|
422
637
|
return fn();
|
|
@@ -442,122 +657,195 @@ function getInputTypeForMethod(method) {
|
|
|
442
657
|
}
|
|
443
658
|
return "body";
|
|
444
659
|
}
|
|
660
|
+
function hasBody(method) {
|
|
661
|
+
const upper = method.toUpperCase();
|
|
662
|
+
return upper === "POST" || upper === "PUT" || upper === "PATCH" || upper === "DELETE";
|
|
663
|
+
}
|
|
445
664
|
|
|
446
|
-
// src/validator/
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
665
|
+
// src/validator/schemaName.ts
|
|
666
|
+
function getSchemaName(method, inputType) {
|
|
667
|
+
return `${method.toUpperCase()}${inputType.charAt(0).toUpperCase() + inputType.slice(1)}`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/injection/analyzeInjection.ts
|
|
671
|
+
import ts5 from "typescript";
|
|
672
|
+
|
|
673
|
+
// src/injection/resolveInjection.ts
|
|
674
|
+
import ts4 from "typescript";
|
|
675
|
+
var PARAM_TYPE_MAP = {
|
|
676
|
+
query: "query",
|
|
677
|
+
body: "body",
|
|
678
|
+
form: "form",
|
|
679
|
+
headers: "headers",
|
|
680
|
+
params: "params",
|
|
681
|
+
context: "context",
|
|
682
|
+
ctx: "context",
|
|
683
|
+
// 别名
|
|
684
|
+
cookies: "cookies",
|
|
685
|
+
ip: "ip",
|
|
686
|
+
files: "files",
|
|
687
|
+
fields: "fields"
|
|
688
|
+
};
|
|
689
|
+
function resolveInjection(fn) {
|
|
690
|
+
const fnStr = fn.toString();
|
|
691
|
+
const params = extractParamsWithAst(fnStr);
|
|
692
|
+
return params.map((param) => {
|
|
693
|
+
const type = PARAM_TYPE_MAP[param.name] || "unknown";
|
|
694
|
+
return {
|
|
695
|
+
name: param.name,
|
|
696
|
+
type,
|
|
697
|
+
hasType: false
|
|
698
|
+
// 运行时类型已擦除
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function extractParamsWithAst(fnStr) {
|
|
703
|
+
const sourceFile = ts4.createSourceFile(
|
|
704
|
+
"__faapi_injection__.ts",
|
|
705
|
+
fnStr,
|
|
706
|
+
ts4.ScriptTarget.Latest,
|
|
707
|
+
true
|
|
708
|
+
);
|
|
709
|
+
const paramNames = [];
|
|
710
|
+
function visit(node) {
|
|
711
|
+
if (ts4.isFunctionDeclaration(node) && node.parameters.length > 0) {
|
|
712
|
+
for (const param of node.parameters) {
|
|
713
|
+
extractParamName(param, paramNames);
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
459
716
|
}
|
|
717
|
+
if ((ts4.isArrowFunction(node) || ts4.isFunctionExpression(node)) && node.parameters.length > 0) {
|
|
718
|
+
for (const param of node.parameters) {
|
|
719
|
+
extractParamName(param, paramNames);
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
ts4.forEachChild(node, visit);
|
|
460
724
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const fileSchemas = this.manifest.get(filePath);
|
|
470
|
-
if (!fileSchemas) return void 0;
|
|
471
|
-
return fileSchemas.get(schemaName);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* 设置单个文件的所有 schema
|
|
475
|
-
* 覆盖该文件的已有数据
|
|
476
|
-
*/
|
|
477
|
-
set(filePath, schemas) {
|
|
478
|
-
const copy = /* @__PURE__ */ new Map();
|
|
479
|
-
schemas.forEach((value, key) => copy.set(key, value));
|
|
480
|
-
this.manifest.set(filePath, copy);
|
|
725
|
+
visit(sourceFile);
|
|
726
|
+
return paramNames.map((name) => ({ name }));
|
|
727
|
+
}
|
|
728
|
+
function extractParamName(param, names) {
|
|
729
|
+
const name = param.name;
|
|
730
|
+
if (ts4.isIdentifier(name)) {
|
|
731
|
+
names.push(name.text);
|
|
732
|
+
return;
|
|
481
733
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
734
|
+
if (ts4.isObjectBindingPattern(name)) {
|
|
735
|
+
for (const element of name.elements) {
|
|
736
|
+
if (ts4.isBindingElement(element)) {
|
|
737
|
+
const elemName = element.name;
|
|
738
|
+
if (ts4.isIdentifier(elemName)) {
|
|
739
|
+
names.push(elemName.text);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
487
744
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
745
|
+
if (ts4.isArrayBindingPattern(name)) {
|
|
746
|
+
for (const element of name.elements) {
|
|
747
|
+
if (element && ts4.isBindingElement(element)) {
|
|
748
|
+
const elemName = element.name;
|
|
749
|
+
if (ts4.isIdentifier(elemName)) {
|
|
750
|
+
names.push(elemName.text);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
493
755
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/injection/analyzeInjection.ts
|
|
759
|
+
function analyzeInjection(code, functionName) {
|
|
760
|
+
const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
|
|
761
|
+
const params = [];
|
|
762
|
+
ts5.forEachChild(sourceFile, (node) => {
|
|
763
|
+
if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
|
|
764
|
+
for (const param of node.parameters) {
|
|
765
|
+
const paramMeta = analyzeParam(param, sourceFile);
|
|
766
|
+
params.push(paramMeta);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return { params };
|
|
771
|
+
}
|
|
772
|
+
function analyzeParam(param, sourceFile) {
|
|
773
|
+
const name = param.name.getText(sourceFile);
|
|
774
|
+
const type = PARAM_TYPE_MAP[name] || "unknown";
|
|
775
|
+
const result = { name, type };
|
|
776
|
+
if (param.type) {
|
|
777
|
+
if (ts5.isTypeReferenceNode(param.type)) {
|
|
778
|
+
result.typeName = param.type.typeName.getText(sourceFile);
|
|
779
|
+
} else if (ts5.isTypeLiteralNode(param.type)) {
|
|
780
|
+
result.schema = extractSchema(param.type, sourceFile);
|
|
781
|
+
}
|
|
499
782
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
function extractSchema(typeNode, sourceFile) {
|
|
786
|
+
const schema = [];
|
|
787
|
+
for (const member of typeNode.members) {
|
|
788
|
+
if (ts5.isPropertySignature(member) && member.name && ts5.isIdentifier(member.name)) {
|
|
789
|
+
const propName = member.name.text;
|
|
790
|
+
const optional = !!member.questionToken;
|
|
791
|
+
const propType = member.type?.getText(sourceFile) || "unknown";
|
|
792
|
+
schema.push({
|
|
793
|
+
name: propName,
|
|
794
|
+
type: propType,
|
|
795
|
+
optional
|
|
796
|
+
});
|
|
797
|
+
}
|
|
505
798
|
}
|
|
506
|
-
|
|
507
|
-
var schemaRegistry = new SchemaRegistry();
|
|
508
|
-
|
|
509
|
-
// src/validator/schemaName.ts
|
|
510
|
-
function getSchemaName(method, inputType) {
|
|
511
|
-
return `${method.toUpperCase()}${inputType.charAt(0).toUpperCase() + inputType.slice(1)}`;
|
|
799
|
+
return schema;
|
|
512
800
|
}
|
|
513
801
|
|
|
514
|
-
// src/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
802
|
+
// src/cli/collectRouteSchemaSources.ts
|
|
803
|
+
import path from "path";
|
|
804
|
+
function collectRouteSchemaSources(routes, rootDir) {
|
|
805
|
+
const methodsByFile = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const route of routes) {
|
|
807
|
+
const filePath = rootDir ? path.resolve(rootDir, route.filePath) : route.filePath;
|
|
808
|
+
let entry = methodsByFile.get(filePath);
|
|
809
|
+
if (!entry) {
|
|
810
|
+
entry = { urlPath: route.urlPath, methods: /* @__PURE__ */ new Set() };
|
|
811
|
+
methodsByFile.set(filePath, entry);
|
|
812
|
+
}
|
|
813
|
+
entry.methods.add(route.method);
|
|
521
814
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return type.name;
|
|
555
|
-
case "any":
|
|
556
|
-
case "unknown":
|
|
557
|
-
return "unknown";
|
|
558
|
-
default:
|
|
559
|
-
return "unknown";
|
|
815
|
+
const programByFile = /* @__PURE__ */ new Map();
|
|
816
|
+
const allTypesByFile = /* @__PURE__ */ new Map();
|
|
817
|
+
const mergedAllTypes = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const filePath of methodsByFile.keys()) {
|
|
819
|
+
const program = createProgram(filePath);
|
|
820
|
+
programByFile.set(filePath, program);
|
|
821
|
+
const allTypes = extractAllTypes(program, filePath);
|
|
822
|
+
allTypesByFile.set(filePath, allTypes);
|
|
823
|
+
for (const [name, info] of allTypes) {
|
|
824
|
+
mergedAllTypes.set(name, info);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const sources = [];
|
|
828
|
+
for (const [filePath, entry] of methodsByFile) {
|
|
829
|
+
const program = programByFile.get(filePath);
|
|
830
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
831
|
+
const code = sourceFile?.text ?? "";
|
|
832
|
+
for (const method of entry.methods) {
|
|
833
|
+
const inputType = getInputTypeForMethod(method);
|
|
834
|
+
const schemaName = getSchemaName(method, inputType);
|
|
835
|
+
const meta = analyzeInjection(code, method);
|
|
836
|
+
const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
|
|
837
|
+
const isForm = param?.type === "form";
|
|
838
|
+
const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
|
|
839
|
+
sources.push({
|
|
840
|
+
urlPath: entry.urlPath,
|
|
841
|
+
filePath,
|
|
842
|
+
schemaName,
|
|
843
|
+
typeInfo,
|
|
844
|
+
coerce: isForm || void 0
|
|
845
|
+
});
|
|
846
|
+
}
|
|
560
847
|
}
|
|
848
|
+
return { sources, allTypesByFile, mergedAllTypes };
|
|
561
849
|
}
|
|
562
850
|
|
|
563
851
|
// src/middleware/cors.ts
|
|
@@ -627,103 +915,2475 @@ function cors(options = {}) {
|
|
|
627
915
|
|
|
628
916
|
// src/middleware/logger.ts
|
|
629
917
|
function logger(options = {}) {
|
|
630
|
-
const { log = console.log } = options;
|
|
631
918
|
return async (ctx, next) => {
|
|
919
|
+
const log = options.log ?? console.log;
|
|
632
920
|
const start = Date.now();
|
|
633
921
|
try {
|
|
634
922
|
const response = await next();
|
|
635
923
|
const duration = Date.now() - start;
|
|
636
|
-
|
|
924
|
+
const entry = {
|
|
925
|
+
method: ctx.method,
|
|
926
|
+
path: ctx.path,
|
|
927
|
+
status: response.status,
|
|
928
|
+
durationMs: duration
|
|
929
|
+
};
|
|
930
|
+
log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
|
|
637
931
|
return response;
|
|
638
932
|
} catch (err) {
|
|
639
933
|
const duration = Date.now() - start;
|
|
640
934
|
const message = err instanceof Error ? err.message : String(err);
|
|
641
935
|
const status = err?.statusCode ?? 500;
|
|
642
|
-
|
|
936
|
+
const entry = {
|
|
937
|
+
method: ctx.method,
|
|
938
|
+
path: ctx.path,
|
|
939
|
+
status,
|
|
940
|
+
durationMs: duration,
|
|
941
|
+
error: message
|
|
942
|
+
};
|
|
943
|
+
log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
|
|
643
944
|
throw err;
|
|
644
945
|
}
|
|
645
946
|
};
|
|
646
947
|
}
|
|
647
948
|
|
|
949
|
+
// src/middleware/helmet.ts
|
|
950
|
+
var DEFAULTS = {
|
|
951
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
952
|
+
xFrameOptions: "SAMEORIGIN",
|
|
953
|
+
xContentTypeOptions: true,
|
|
954
|
+
referrerPolicy: "no-referrer",
|
|
955
|
+
strictTransportSecurity: "max-age=31536000; includeSubDomains",
|
|
956
|
+
xDnsPrefetchControl: true,
|
|
957
|
+
xDownloadOptions: true,
|
|
958
|
+
xPermittedCrossDomainPolicies: "none",
|
|
959
|
+
crossOriginOpenerPolicy: "same-origin",
|
|
960
|
+
crossOriginResourcePolicy: "same-origin",
|
|
961
|
+
crossOriginEmbedderPolicy: false,
|
|
962
|
+
originAgentCluster: true,
|
|
963
|
+
xPoweredBy: true
|
|
964
|
+
};
|
|
965
|
+
function helmet(options = {}) {
|
|
966
|
+
const opts = { ...DEFAULTS, ...options };
|
|
967
|
+
return async (ctx, next) => {
|
|
968
|
+
if (opts.contentSecurityPolicy !== false) {
|
|
969
|
+
ctx.setHeader("Content-Security-Policy", opts.contentSecurityPolicy);
|
|
970
|
+
}
|
|
971
|
+
if (opts.xFrameOptions !== false) {
|
|
972
|
+
ctx.setHeader("X-Frame-Options", opts.xFrameOptions);
|
|
973
|
+
}
|
|
974
|
+
if (opts.xContentTypeOptions) {
|
|
975
|
+
ctx.setHeader("X-Content-Type-Options", "nosniff");
|
|
976
|
+
}
|
|
977
|
+
if (opts.referrerPolicy !== false) {
|
|
978
|
+
ctx.setHeader("Referrer-Policy", opts.referrerPolicy);
|
|
979
|
+
}
|
|
980
|
+
if (opts.strictTransportSecurity !== false) {
|
|
981
|
+
ctx.setHeader("Strict-Transport-Security", opts.strictTransportSecurity);
|
|
982
|
+
}
|
|
983
|
+
if (opts.xDnsPrefetchControl) {
|
|
984
|
+
ctx.setHeader("X-DNS-Prefetch-Control", "off");
|
|
985
|
+
}
|
|
986
|
+
if (opts.xDownloadOptions) {
|
|
987
|
+
ctx.setHeader("X-Download-Options", "noopen");
|
|
988
|
+
}
|
|
989
|
+
if (opts.xPermittedCrossDomainPolicies !== false) {
|
|
990
|
+
ctx.setHeader("X-Permitted-Cross-Domain-Policies", opts.xPermittedCrossDomainPolicies);
|
|
991
|
+
}
|
|
992
|
+
if (opts.crossOriginOpenerPolicy !== false) {
|
|
993
|
+
ctx.setHeader("Cross-Origin-Opener-Policy", opts.crossOriginOpenerPolicy);
|
|
994
|
+
}
|
|
995
|
+
if (opts.crossOriginResourcePolicy !== false) {
|
|
996
|
+
ctx.setHeader("Cross-Origin-Resource-Policy", opts.crossOriginResourcePolicy);
|
|
997
|
+
}
|
|
998
|
+
if (opts.crossOriginEmbedderPolicy !== false) {
|
|
999
|
+
ctx.setHeader("Cross-Origin-Embedder-Policy", opts.crossOriginEmbedderPolicy);
|
|
1000
|
+
}
|
|
1001
|
+
if (opts.originAgentCluster) {
|
|
1002
|
+
ctx.setHeader("Origin-Agent-Cluster", "?1");
|
|
1003
|
+
}
|
|
1004
|
+
if (opts.xPoweredBy) {
|
|
1005
|
+
ctx.setHeader("X-Powered-By", "faapi");
|
|
1006
|
+
}
|
|
1007
|
+
return await next();
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
648
1011
|
// src/config/loadConfig.ts
|
|
649
|
-
import
|
|
1012
|
+
import path2 from "path";
|
|
650
1013
|
import fs from "fs";
|
|
1014
|
+
|
|
1015
|
+
// src/utils/importWithCacheBust.ts
|
|
651
1016
|
import { pathToFileURL } from "url";
|
|
652
|
-
var
|
|
653
|
-
function
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
function deepMerge(base, override) {
|
|
657
|
-
const result = { ...base };
|
|
658
|
-
for (const key of Object.keys(override)) {
|
|
659
|
-
const baseVal = base[key];
|
|
660
|
-
const overVal = override[key];
|
|
661
|
-
if (baseVal instanceof Date || overVal instanceof Date || baseVal instanceof RegExp || overVal instanceof RegExp || baseVal instanceof Map || overVal instanceof Map || baseVal instanceof Set || overVal instanceof Set) {
|
|
662
|
-
result[key] = overVal;
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
if (baseVal !== null && overVal !== null && typeof baseVal === "object" && typeof overVal === "object" && !Array.isArray(baseVal) && !Array.isArray(overVal) && !(baseVal instanceof Function) && !(overVal instanceof Function)) {
|
|
666
|
-
result[key] = deepMerge(
|
|
667
|
-
baseVal,
|
|
668
|
-
overVal
|
|
669
|
-
);
|
|
670
|
-
} else {
|
|
671
|
-
result[key] = overVal;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return result;
|
|
1017
|
+
var loadTs;
|
|
1018
|
+
function setLoadTimestamp(ts6) {
|
|
1019
|
+
loadTs = ts6;
|
|
675
1020
|
}
|
|
676
|
-
async function
|
|
677
|
-
|
|
678
|
-
|
|
1021
|
+
async function importWithCacheBust(filePath) {
|
|
1022
|
+
let url = pathToFileURL(filePath).href;
|
|
1023
|
+
if (loadTs !== void 0) {
|
|
1024
|
+
url += `?t=${loadTs}`;
|
|
679
1025
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1026
|
+
return await import(url);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/config/loadConfig.ts
|
|
1030
|
+
var CONFIG_PRODUCT_FILE = "faapi-config.js";
|
|
1031
|
+
async function loadConfig(rootDir, dist) {
|
|
1032
|
+
const configProductPath = path2.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
|
|
1033
|
+
if (fs.existsSync(configProductPath)) {
|
|
1034
|
+
const module = await importWithCacheBust(configProductPath);
|
|
683
1035
|
return module.default ?? {};
|
|
684
|
-
}
|
|
1036
|
+
}
|
|
1037
|
+
const hasSourceConfig = fs.existsSync(path2.join(rootDir, "faapi.config.ts")) || fs.existsSync(path2.join(rootDir, "faapi.config.js"));
|
|
1038
|
+
if (hasSourceConfig) {
|
|
685
1039
|
throw new Error(
|
|
686
|
-
`
|
|
687
|
-
{ cause: err }
|
|
1040
|
+
`[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
688
1041
|
);
|
|
689
1042
|
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/errors/FaapiError.ts
|
|
1047
|
+
var FaapiError = class extends Error {
|
|
1048
|
+
constructor(code, message, statusCode) {
|
|
1049
|
+
super(message);
|
|
1050
|
+
this.code = code;
|
|
1051
|
+
this.statusCode = statusCode;
|
|
1052
|
+
this.name = "FaapiError";
|
|
1053
|
+
}
|
|
1054
|
+
code;
|
|
1055
|
+
statusCode;
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/errors/httpErrors.ts
|
|
1059
|
+
function deriveStatusCode(issues) {
|
|
1060
|
+
const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
|
|
1061
|
+
return has400 ? 400 : 422;
|
|
1062
|
+
}
|
|
1063
|
+
var ValidationError = class extends FaapiError {
|
|
1064
|
+
constructor(message, issues) {
|
|
1065
|
+
super("VALIDATION_ERROR", message, deriveStatusCode(issues));
|
|
1066
|
+
this.issues = issues;
|
|
1067
|
+
this.name = "ValidationError";
|
|
1068
|
+
}
|
|
1069
|
+
issues;
|
|
1070
|
+
};
|
|
1071
|
+
var RouteNotFoundError = class extends FaapiError {
|
|
1072
|
+
constructor(path9) {
|
|
1073
|
+
super("ROUTE_NOT_FOUND", `Route not found: ${path9}`, 404);
|
|
1074
|
+
this.name = "RouteNotFoundError";
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
var MethodNotAllowedError = class extends FaapiError {
|
|
1078
|
+
constructor(method, path9, allowedMethods) {
|
|
1079
|
+
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path9}`, 405);
|
|
1080
|
+
this.allowedMethods = allowedMethods;
|
|
1081
|
+
this.name = "MethodNotAllowedError";
|
|
1082
|
+
}
|
|
1083
|
+
allowedMethods;
|
|
1084
|
+
};
|
|
1085
|
+
var InternalError = class extends FaapiError {
|
|
1086
|
+
constructor(message) {
|
|
1087
|
+
super("INTERNAL_ERROR", message, 500);
|
|
1088
|
+
this.name = "InternalError";
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
var ModuleLoadError = class extends FaapiError {
|
|
1092
|
+
constructor(filePath, reason) {
|
|
1093
|
+
super("MODULE_LOAD_ERROR", `Failed to load module ${filePath}: ${reason}`, 500);
|
|
1094
|
+
this.name = "ModuleLoadError";
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// src/runtime/sse.ts
|
|
1099
|
+
function encodeSseEvent(event) {
|
|
1100
|
+
let out = "";
|
|
1101
|
+
if (event.comment !== void 0) {
|
|
1102
|
+
out += `: ${event.comment}
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
if (event.event !== void 0) {
|
|
1106
|
+
out += `event: ${event.event}
|
|
1107
|
+
`;
|
|
1108
|
+
}
|
|
1109
|
+
if (event.id !== void 0) {
|
|
1110
|
+
out += `id: ${event.id}
|
|
1111
|
+
`;
|
|
1112
|
+
}
|
|
1113
|
+
if (event.retry !== void 0) {
|
|
1114
|
+
out += `retry: ${event.retry}
|
|
1115
|
+
`;
|
|
1116
|
+
}
|
|
1117
|
+
if (event.data !== void 0) {
|
|
1118
|
+
let dataStr;
|
|
1119
|
+
if (typeof event.data === "string") {
|
|
1120
|
+
dataStr = event.data;
|
|
1121
|
+
} else if (event.data === null) {
|
|
1122
|
+
dataStr = "null";
|
|
1123
|
+
} else {
|
|
1124
|
+
dataStr = JSON.stringify(event.data);
|
|
1125
|
+
}
|
|
1126
|
+
const lines = dataStr.split("\n");
|
|
1127
|
+
for (const line of lines) {
|
|
1128
|
+
out += `data: ${line}
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
out += "\n";
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
function createSseWriter() {
|
|
1136
|
+
const encoder = new TextEncoder();
|
|
1137
|
+
let controller = null;
|
|
1138
|
+
let closed = false;
|
|
1139
|
+
let aborted = false;
|
|
1140
|
+
const stream = new ReadableStream({
|
|
1141
|
+
start(c) {
|
|
1142
|
+
controller = c;
|
|
1143
|
+
},
|
|
1144
|
+
cancel() {
|
|
1145
|
+
aborted = true;
|
|
1146
|
+
closed = true;
|
|
1147
|
+
controller = null;
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
const response = new Response(stream, {
|
|
1151
|
+
status: 200,
|
|
1152
|
+
headers: {
|
|
1153
|
+
"Content-Type": "text/event-stream",
|
|
1154
|
+
"Cache-Control": "no-cache",
|
|
1155
|
+
Connection: "keep-alive"
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
const writer = {
|
|
1159
|
+
send(event) {
|
|
1160
|
+
if (closed || !controller) return;
|
|
1161
|
+
const text = encodeSseEvent(event);
|
|
1162
|
+
controller.enqueue(encoder.encode(text));
|
|
1163
|
+
},
|
|
1164
|
+
sendError(error) {
|
|
1165
|
+
if (closed || !controller) return;
|
|
1166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1167
|
+
const text = encodeSseEvent({ event: "error", data: message });
|
|
1168
|
+
try {
|
|
1169
|
+
controller.enqueue(encoder.encode(text));
|
|
1170
|
+
} finally {
|
|
1171
|
+
writer.close();
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
close() {
|
|
1175
|
+
if (closed) return;
|
|
1176
|
+
closed = true;
|
|
1177
|
+
if (controller) {
|
|
1178
|
+
try {
|
|
1179
|
+
controller.close();
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
controller = null;
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
get closed() {
|
|
1186
|
+
return closed;
|
|
1187
|
+
},
|
|
1188
|
+
get aborted() {
|
|
1189
|
+
return aborted;
|
|
1190
|
+
},
|
|
1191
|
+
get response() {
|
|
1192
|
+
return response;
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
return writer;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/runtime/createContext.ts
|
|
1199
|
+
function parseCookies(cookieHeader) {
|
|
1200
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
1201
|
+
if (!cookieHeader) return cookies;
|
|
1202
|
+
for (const pair of cookieHeader.split(";")) {
|
|
1203
|
+
const [name, ...rest] = pair.split("=");
|
|
1204
|
+
const trimmed = name?.trim();
|
|
1205
|
+
if (trimmed) {
|
|
1206
|
+
cookies.set(trimmed, rest.join("=").trim());
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return cookies;
|
|
1210
|
+
}
|
|
1211
|
+
function formatSetCookie(name, value, options) {
|
|
1212
|
+
let cookie = `${name}=${value}`;
|
|
1213
|
+
if (options?.domain) cookie += `; Domain=${options.domain}`;
|
|
1214
|
+
if (options?.path) cookie += `; Path=${options.path}`;
|
|
1215
|
+
if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
|
|
1216
|
+
if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
1217
|
+
if (options?.httpOnly) cookie += `; HttpOnly`;
|
|
1218
|
+
if (options?.secure) cookie += `; Secure`;
|
|
1219
|
+
if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
1220
|
+
return cookie;
|
|
690
1221
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1222
|
+
function createContext(request, params, config = {}, ip = "") {
|
|
1223
|
+
const url = new URL(request.url);
|
|
1224
|
+
const meta = { headers: {}, setCookies: [] };
|
|
1225
|
+
const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
|
|
1226
|
+
const cookiesObj = {};
|
|
1227
|
+
for (const [key, val] of parsedCookies) {
|
|
1228
|
+
cookiesObj[key] = val;
|
|
1229
|
+
}
|
|
1230
|
+
const ctx = {
|
|
1231
|
+
request,
|
|
1232
|
+
params,
|
|
1233
|
+
query: url.searchParams,
|
|
1234
|
+
headers: request.headers,
|
|
1235
|
+
method: request.method,
|
|
1236
|
+
path: url.pathname,
|
|
1237
|
+
ip,
|
|
1238
|
+
cookies: cookiesObj,
|
|
1239
|
+
config,
|
|
1240
|
+
meta,
|
|
1241
|
+
setStatus(status) {
|
|
1242
|
+
meta.status = status;
|
|
1243
|
+
},
|
|
1244
|
+
setHeader(key, value) {
|
|
1245
|
+
meta.headers[key] = value;
|
|
1246
|
+
},
|
|
1247
|
+
setETag(value) {
|
|
1248
|
+
meta.headers["etag"] = value;
|
|
1249
|
+
},
|
|
1250
|
+
redirect(url2, status = 302) {
|
|
1251
|
+
return new Response(null, {
|
|
1252
|
+
status,
|
|
1253
|
+
headers: { Location: url2 }
|
|
1254
|
+
});
|
|
1255
|
+
},
|
|
1256
|
+
json(data, status) {
|
|
1257
|
+
const headers = { "Content-Type": "application/json" };
|
|
1258
|
+
return new Response(JSON.stringify(data), {
|
|
1259
|
+
status: status ?? 200,
|
|
1260
|
+
headers
|
|
1261
|
+
});
|
|
1262
|
+
},
|
|
1263
|
+
html(html, status) {
|
|
1264
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
1265
|
+
return new Response(html, {
|
|
1266
|
+
status: status ?? 200,
|
|
1267
|
+
headers
|
|
1268
|
+
});
|
|
1269
|
+
},
|
|
1270
|
+
getCookie(name) {
|
|
1271
|
+
return parsedCookies.get(name);
|
|
1272
|
+
},
|
|
1273
|
+
setCookie(name, value, options) {
|
|
1274
|
+
meta.setCookies.push(formatSetCookie(name, value, options));
|
|
1275
|
+
},
|
|
1276
|
+
deleteCookie(name) {
|
|
1277
|
+
meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
|
|
1278
|
+
},
|
|
1279
|
+
/**
|
|
1280
|
+
* 创建 SSE writer,用于流式推送事件
|
|
1281
|
+
*
|
|
1282
|
+
* handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
|
|
1283
|
+
* 作为 HTTP 响应(Content-Type: text/event-stream)。
|
|
1284
|
+
*
|
|
1285
|
+
* 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
|
|
1286
|
+
*/
|
|
1287
|
+
sse() {
|
|
1288
|
+
const writer = createSseWriter();
|
|
1289
|
+
const ctxWithSse = ctx;
|
|
1290
|
+
ctxWithSse.__sseResponse = writer.response;
|
|
1291
|
+
ctxWithSse.__sseWriter = writer;
|
|
1292
|
+
return writer;
|
|
696
1293
|
}
|
|
697
|
-
|
|
1294
|
+
};
|
|
1295
|
+
const extend = config?.extendContext;
|
|
1296
|
+
if (typeof extend === "function") {
|
|
1297
|
+
extend(ctx);
|
|
698
1298
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1299
|
+
return ctx;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/utils/isPlainObject.ts
|
|
1303
|
+
function isPlainObject(value) {
|
|
1304
|
+
if (value === null || typeof value !== "object") {
|
|
1305
|
+
return false;
|
|
704
1306
|
}
|
|
705
|
-
if (
|
|
706
|
-
return
|
|
1307
|
+
if (Array.isArray(value)) {
|
|
1308
|
+
return false;
|
|
707
1309
|
}
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1310
|
+
const proto = Object.getPrototypeOf(value);
|
|
1311
|
+
return proto === null || proto === Object.prototype;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/response/toResponse.ts
|
|
1315
|
+
async function toResponse(value, meta) {
|
|
1316
|
+
if (value instanceof Promise) {
|
|
1317
|
+
return toResponse(await value, meta);
|
|
1318
|
+
}
|
|
1319
|
+
const applyMeta = (headers2) => {
|
|
1320
|
+
if (!meta) return;
|
|
1321
|
+
for (const [key, val] of Object.entries(meta.headers)) {
|
|
1322
|
+
headers2.set(key, val);
|
|
1323
|
+
}
|
|
1324
|
+
for (const cookie of meta.setCookies ?? []) {
|
|
1325
|
+
headers2.append("set-cookie", cookie);
|
|
715
1326
|
}
|
|
1327
|
+
};
|
|
1328
|
+
if (value instanceof Response) {
|
|
1329
|
+
if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
|
|
1330
|
+
const headers2 = new Headers(value.headers);
|
|
1331
|
+
applyMeta(headers2);
|
|
1332
|
+
return new Response(value.body, {
|
|
1333
|
+
status: meta.status ?? value.status,
|
|
1334
|
+
headers: headers2
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
return value;
|
|
716
1338
|
}
|
|
717
|
-
|
|
1339
|
+
if (value === null || value === void 0) {
|
|
1340
|
+
const status = meta?.status ?? 204;
|
|
1341
|
+
const headers2 = new Headers();
|
|
1342
|
+
applyMeta(headers2);
|
|
1343
|
+
return new Response(null, { status, headers: headers2 });
|
|
1344
|
+
}
|
|
1345
|
+
if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
|
|
1346
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1347
|
+
applyMeta(headers2);
|
|
1348
|
+
return new Response(value, {
|
|
1349
|
+
status: meta?.status ?? 200,
|
|
1350
|
+
headers: headers2
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
|
|
1354
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1355
|
+
applyMeta(headers2);
|
|
1356
|
+
return new Response(value, {
|
|
1357
|
+
status: meta?.status ?? 200,
|
|
1358
|
+
headers: headers2
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
if (value instanceof Uint8Array) {
|
|
1362
|
+
const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
|
|
1363
|
+
applyMeta(headers2);
|
|
1364
|
+
return new Response(value, {
|
|
1365
|
+
status: meta?.status ?? 200,
|
|
1366
|
+
headers: headers2
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (isPlainObject(value) || Array.isArray(value)) {
|
|
1370
|
+
const body2 = JSON.stringify(value);
|
|
1371
|
+
const headers2 = new Headers({ "Content-Type": "application/json" });
|
|
1372
|
+
applyMeta(headers2);
|
|
1373
|
+
return new Response(body2, {
|
|
1374
|
+
status: meta?.status ?? 200,
|
|
1375
|
+
headers: headers2
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
if (typeof value === "string") {
|
|
1379
|
+
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
1380
|
+
applyMeta(headers2);
|
|
1381
|
+
return new Response(value, {
|
|
1382
|
+
status: meta?.status ?? 200,
|
|
1383
|
+
headers: headers2
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1387
|
+
const headers2 = new Headers({ "Content-Type": "text/plain" });
|
|
1388
|
+
applyMeta(headers2);
|
|
1389
|
+
return new Response(String(value), {
|
|
1390
|
+
status: meta?.status ?? 200,
|
|
1391
|
+
headers: headers2
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
const body = JSON.stringify(value);
|
|
1395
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1396
|
+
applyMeta(headers);
|
|
1397
|
+
return new Response(body, {
|
|
1398
|
+
status: meta?.status ?? 200,
|
|
1399
|
+
headers
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/utils/queryToObject.ts
|
|
1404
|
+
function queryToObject(params) {
|
|
1405
|
+
const result = {};
|
|
1406
|
+
for (const [key, value] of params) {
|
|
1407
|
+
result[key] = value;
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/injection/injectParams.ts
|
|
1413
|
+
function getBuiltinInjectionValue(type, ctx, body) {
|
|
1414
|
+
switch (type) {
|
|
1415
|
+
case "query":
|
|
1416
|
+
return queryToObject(ctx.query);
|
|
1417
|
+
case "params":
|
|
1418
|
+
return ctx.params;
|
|
1419
|
+
case "headers":
|
|
1420
|
+
return ctx.headers;
|
|
1421
|
+
case "context":
|
|
1422
|
+
return ctx;
|
|
1423
|
+
case "cookies":
|
|
1424
|
+
return ctx.cookies;
|
|
1425
|
+
case "ip":
|
|
1426
|
+
return ctx.ip;
|
|
1427
|
+
case "body":
|
|
1428
|
+
return body;
|
|
1429
|
+
// form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
|
|
1430
|
+
// 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
|
|
1431
|
+
case "form":
|
|
1432
|
+
return body;
|
|
1433
|
+
case "files":
|
|
1434
|
+
if (body && typeof body === "object" && "files" in body) {
|
|
1435
|
+
return body.files;
|
|
1436
|
+
}
|
|
1437
|
+
return [];
|
|
1438
|
+
case "fields":
|
|
1439
|
+
if (body && typeof body === "object" && "fields" in body) {
|
|
1440
|
+
return body.fields;
|
|
1441
|
+
}
|
|
1442
|
+
return {};
|
|
1443
|
+
default:
|
|
1444
|
+
return void 0;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function injectParamsAsync(handler, ctx, body, injectors) {
|
|
1448
|
+
const injections = resolveInjection(handler);
|
|
1449
|
+
if (injections.length === 0) {
|
|
1450
|
+
return await handler();
|
|
1451
|
+
}
|
|
1452
|
+
const args = await Promise.all(
|
|
1453
|
+
injections.map(async (injection) => {
|
|
1454
|
+
if (injection.type !== "unknown") {
|
|
1455
|
+
return getBuiltinInjectionValue(injection.type, ctx, body);
|
|
1456
|
+
}
|
|
1457
|
+
if (injectors && injection.name in injectors) {
|
|
1458
|
+
return await injectors[injection.name](ctx);
|
|
1459
|
+
}
|
|
1460
|
+
return void 0;
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
return await handler(...args);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/runtime/invokeHandler.ts
|
|
1467
|
+
function mergeMeta(response, meta) {
|
|
1468
|
+
const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
|
|
1469
|
+
if (!hasMeta) return response;
|
|
1470
|
+
const headers = new Headers(response.headers);
|
|
1471
|
+
for (const [key, value] of Object.entries(meta.headers)) {
|
|
1472
|
+
headers.set(key, value);
|
|
1473
|
+
}
|
|
1474
|
+
for (const cookie of meta.setCookies) {
|
|
1475
|
+
headers.append("set-cookie", cookie);
|
|
1476
|
+
}
|
|
1477
|
+
return new Response(response.body, {
|
|
1478
|
+
status: meta.status ?? response.status,
|
|
1479
|
+
headers
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
async function compose(middlewares, ctx, finalHandler) {
|
|
1483
|
+
const meta = ctx.meta;
|
|
1484
|
+
let index = -1;
|
|
1485
|
+
async function dispatch(i) {
|
|
1486
|
+
if (i <= index) {
|
|
1487
|
+
throw new Error("next() called multiple times");
|
|
1488
|
+
}
|
|
1489
|
+
index = i;
|
|
1490
|
+
if (i >= middlewares.length) {
|
|
1491
|
+
return await finalHandler();
|
|
1492
|
+
}
|
|
1493
|
+
const mw = middlewares[i];
|
|
1494
|
+
let innerResponse;
|
|
1495
|
+
const next = async () => {
|
|
1496
|
+
innerResponse = await dispatch(i + 1);
|
|
1497
|
+
return innerResponse;
|
|
1498
|
+
};
|
|
1499
|
+
const result = await mw(ctx, next);
|
|
1500
|
+
if (result instanceof Response) {
|
|
1501
|
+
return mergeMeta(result, meta);
|
|
1502
|
+
}
|
|
1503
|
+
if (innerResponse !== void 0) {
|
|
1504
|
+
return innerResponse;
|
|
1505
|
+
}
|
|
1506
|
+
throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
|
|
1507
|
+
}
|
|
1508
|
+
return await dispatch(0);
|
|
1509
|
+
}
|
|
1510
|
+
async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
1511
|
+
const meta = ctx.meta;
|
|
1512
|
+
const pickSseAndAutoClose = () => {
|
|
1513
|
+
const sseWriter = ctx.__sseWriter;
|
|
1514
|
+
if (!sseWriter) return null;
|
|
1515
|
+
if (!sseWriter.closed && !sseWriter.aborted) {
|
|
1516
|
+
sseWriter.close();
|
|
1517
|
+
}
|
|
1518
|
+
return mergeMeta(sseWriter.response, meta);
|
|
1519
|
+
};
|
|
1520
|
+
const autoCloseSseOnError = () => {
|
|
1521
|
+
const sseWriter = ctx.__sseWriter;
|
|
1522
|
+
if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
|
|
1523
|
+
sseWriter.close();
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
if (!middlewares || middlewares.length === 0) {
|
|
1527
|
+
try {
|
|
1528
|
+
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
1529
|
+
const sseResponse = pickSseAndAutoClose();
|
|
1530
|
+
if (sseResponse) return sseResponse;
|
|
1531
|
+
return toResponse(result, meta);
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
autoCloseSseOnError();
|
|
1534
|
+
throw err;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const finalHandler = async () => {
|
|
1538
|
+
try {
|
|
1539
|
+
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
1540
|
+
const sseResponse = pickSseAndAutoClose();
|
|
1541
|
+
if (sseResponse) return sseResponse;
|
|
1542
|
+
return toResponse(result, meta);
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
autoCloseSseOnError();
|
|
1545
|
+
throw err;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
return await compose(middlewares, ctx, finalHandler);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/cli/createAppCore.ts
|
|
1552
|
+
import fs4 from "fs";
|
|
1553
|
+
import path7 from "path";
|
|
1554
|
+
import { PassThrough } from "stream";
|
|
1555
|
+
|
|
1556
|
+
// src/router/sortRoutes.ts
|
|
1557
|
+
function sortRoutes(routes) {
|
|
1558
|
+
return [...routes].sort((a, b) => {
|
|
1559
|
+
if (a.isDynamic !== b.isDynamic) {
|
|
1560
|
+
return a.isDynamic ? 1 : -1;
|
|
1561
|
+
}
|
|
1562
|
+
if (a.isCatchAll !== b.isCatchAll) {
|
|
1563
|
+
return a.isCatchAll ? 1 : -1;
|
|
1564
|
+
}
|
|
1565
|
+
const aSegments = a.urlPath.split("/").filter(Boolean).length;
|
|
1566
|
+
const bSegments = b.urlPath.split("/").filter(Boolean).length;
|
|
1567
|
+
if (aSegments !== bSegments) {
|
|
1568
|
+
return aSegments - bSegments;
|
|
1569
|
+
}
|
|
1570
|
+
return a.urlPath.localeCompare(b.urlPath);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/router/detectRouteConflicts.ts
|
|
1575
|
+
function detectRouteConflicts(routes) {
|
|
1576
|
+
const map = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const route of routes) {
|
|
1578
|
+
const key = `${route.method} ${route.urlPath}`;
|
|
1579
|
+
const existing = map.get(key);
|
|
1580
|
+
if (existing) {
|
|
1581
|
+
existing.files.push(route.filePath);
|
|
1582
|
+
} else {
|
|
1583
|
+
map.set(key, {
|
|
1584
|
+
method: route.method,
|
|
1585
|
+
urlPath: route.urlPath,
|
|
1586
|
+
files: [route.filePath]
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const conflicts = [];
|
|
1591
|
+
for (const conflict of map.values()) {
|
|
1592
|
+
if (conflict.files.length > 1) {
|
|
1593
|
+
conflicts.push(conflict);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return conflicts;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/server/createServer.ts
|
|
1600
|
+
import {
|
|
1601
|
+
createServer as createHttpServer
|
|
1602
|
+
} from "http";
|
|
1603
|
+
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
1604
|
+
import { readFileSync } from "fs";
|
|
1605
|
+
import { Readable as Readable2 } from "stream";
|
|
1606
|
+
import path5 from "path";
|
|
1607
|
+
|
|
1608
|
+
// src/router/matchRoute.ts
|
|
1609
|
+
function matchRoute(routes, method, path9) {
|
|
1610
|
+
for (const route of routes) {
|
|
1611
|
+
if (route.method !== method) {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (!route.isDynamic) {
|
|
1615
|
+
if (route.urlPath === path9) {
|
|
1616
|
+
return { route, params: {} };
|
|
1617
|
+
}
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1621
|
+
if (params !== null) {
|
|
1622
|
+
return { route, params };
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function matchWsRoute(wsRoutes, path9) {
|
|
1628
|
+
for (const route of wsRoutes) {
|
|
1629
|
+
if (!route.isDynamic) {
|
|
1630
|
+
if (route.urlPath === path9) {
|
|
1631
|
+
return { route, params: {} };
|
|
1632
|
+
}
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1636
|
+
if (params !== null) {
|
|
1637
|
+
return { route, params };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
function matchDynamicPath(pattern, path9, paramNames, isCatchAll) {
|
|
1643
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1644
|
+
const pathSegments = path9.split("/").filter(Boolean);
|
|
1645
|
+
if (isCatchAll) {
|
|
1646
|
+
const nonCatchAllCount = patternSegments.length - 1;
|
|
1647
|
+
if (pathSegments.length <= nonCatchAllCount) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
const params2 = {};
|
|
1651
|
+
for (let i = 0; i < nonCatchAllCount; i++) {
|
|
1652
|
+
const patternSeg = patternSegments[i];
|
|
1653
|
+
const pathSeg = pathSegments[i];
|
|
1654
|
+
if (patternSeg.startsWith(":")) {
|
|
1655
|
+
const paramName = patternSeg.slice(1);
|
|
1656
|
+
params2[paramName] = pathSeg;
|
|
1657
|
+
} else if (patternSeg !== pathSeg) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
|
|
1662
|
+
const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
|
|
1663
|
+
params2[catchAllParamName] = catchAllValue;
|
|
1664
|
+
if (Object.keys(params2).length !== paramNames.length) {
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
return params2;
|
|
1668
|
+
}
|
|
1669
|
+
if (patternSegments.length !== pathSegments.length) {
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
const params = {};
|
|
1673
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
1674
|
+
const patternSeg = patternSegments[i];
|
|
1675
|
+
const pathSeg = pathSegments[i];
|
|
1676
|
+
if (patternSeg.startsWith(":")) {
|
|
1677
|
+
const paramName = patternSeg.slice(1);
|
|
1678
|
+
params[paramName] = pathSeg;
|
|
1679
|
+
} else if (patternSeg !== pathSeg) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (Object.keys(params).length !== paramNames.length) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
return params;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/loader/resolveExports.ts
|
|
1690
|
+
function resolveExport(module, exportName) {
|
|
1691
|
+
if (exportName in module && typeof module[exportName] !== "undefined") {
|
|
1692
|
+
return module[exportName];
|
|
1693
|
+
}
|
|
1694
|
+
const defaultExport = module.default;
|
|
1695
|
+
if (defaultExport !== null && typeof defaultExport === "object") {
|
|
1696
|
+
const value = defaultExport[exportName];
|
|
1697
|
+
if (value !== void 0) {
|
|
1698
|
+
return value;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/loader/validateRouteModule.ts
|
|
1705
|
+
function validateRouteModule(value, method, filePath) {
|
|
1706
|
+
if (typeof value !== "function") {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/loader/loadRouteModule.ts
|
|
1714
|
+
async function loadRouteModule(filePath, method) {
|
|
1715
|
+
let module;
|
|
1716
|
+
try {
|
|
1717
|
+
module = await importWithCacheBust(filePath);
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1720
|
+
throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
|
|
1721
|
+
}
|
|
1722
|
+
const handler = resolveExport(module, method);
|
|
1723
|
+
validateRouteModule(handler, method, filePath);
|
|
1724
|
+
return { handler, method };
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/utils/parseJsonBody.ts
|
|
1728
|
+
function parseJsonBody(text) {
|
|
1729
|
+
try {
|
|
1730
|
+
const data = JSON.parse(text);
|
|
1731
|
+
return { success: true, data };
|
|
1732
|
+
} catch {
|
|
1733
|
+
return { success: false, error: "Invalid JSON body" };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/utils/parseMultipart.ts
|
|
1738
|
+
async function parseMultipart(request) {
|
|
1739
|
+
const formData = await request.formData();
|
|
1740
|
+
const fields = {};
|
|
1741
|
+
const files = [];
|
|
1742
|
+
for (const [key, value] of formData.entries()) {
|
|
1743
|
+
if (value instanceof File) {
|
|
1744
|
+
files.push({
|
|
1745
|
+
name: key,
|
|
1746
|
+
filename: value.name,
|
|
1747
|
+
type: value.type,
|
|
1748
|
+
size: value.size,
|
|
1749
|
+
arrayBuffer: () => value.arrayBuffer()
|
|
1750
|
+
});
|
|
1751
|
+
} else {
|
|
1752
|
+
if (key in fields) {
|
|
1753
|
+
const existing = fields[key];
|
|
1754
|
+
if (Array.isArray(existing)) {
|
|
1755
|
+
existing.push(value);
|
|
1756
|
+
} else {
|
|
1757
|
+
fields[key] = [existing, value];
|
|
1758
|
+
}
|
|
1759
|
+
} else {
|
|
1760
|
+
fields[key] = value;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return { fields, files };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/runtime/resolveInput.ts
|
|
1768
|
+
async function resolveInput(method, request) {
|
|
1769
|
+
const inputType = getInputTypeForMethod(method);
|
|
1770
|
+
if (inputType === "body") {
|
|
1771
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
1772
|
+
if (contentType.includes("multipart/form-data")) {
|
|
1773
|
+
return parseMultipart(request);
|
|
1774
|
+
}
|
|
1775
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1776
|
+
const text2 = await request.text();
|
|
1777
|
+
if (text2.trim() === "") return null;
|
|
1778
|
+
const params = new URLSearchParams(text2);
|
|
1779
|
+
const obj = {};
|
|
1780
|
+
for (const [key, value] of params) {
|
|
1781
|
+
obj[key] = value;
|
|
1782
|
+
}
|
|
1783
|
+
return obj;
|
|
1784
|
+
}
|
|
1785
|
+
const text = await request.text();
|
|
1786
|
+
if (text.trim() === "") {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
const result = parseJsonBody(text);
|
|
1790
|
+
if (!result.success) {
|
|
1791
|
+
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
1792
|
+
{
|
|
1793
|
+
path: "body",
|
|
1794
|
+
code: "INVALID_FORMAT",
|
|
1795
|
+
expected: "JSON",
|
|
1796
|
+
received: "text",
|
|
1797
|
+
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
1798
|
+
}
|
|
1799
|
+
]);
|
|
1800
|
+
}
|
|
1801
|
+
return result.data;
|
|
1802
|
+
}
|
|
1803
|
+
const url = new URL(request.url);
|
|
1804
|
+
return queryToObject(url.searchParams);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/response/sendNodeResponse.ts
|
|
1808
|
+
import { Readable } from "stream";
|
|
1809
|
+
async function sendNodeResponse(response, res) {
|
|
1810
|
+
res.statusCode = response.status;
|
|
1811
|
+
for (const [key, value] of response.headers) {
|
|
1812
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
1813
|
+
res.appendHeader(key, value);
|
|
1814
|
+
} else {
|
|
1815
|
+
res.setHeader(key, value);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (response.body) {
|
|
1819
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
1820
|
+
await new Promise((resolve, reject) => {
|
|
1821
|
+
nodeStream.on("error", reject);
|
|
1822
|
+
res.on("error", reject);
|
|
1823
|
+
res.on("finish", resolve);
|
|
1824
|
+
nodeStream.pipe(res);
|
|
1825
|
+
});
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
res.end();
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/validator/validateInput.ts
|
|
1832
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
1833
|
+
function invalidateSchemaCache() {
|
|
1834
|
+
moduleCache.clear();
|
|
1835
|
+
}
|
|
1836
|
+
async function loadSchemaModule(schemaPath) {
|
|
1837
|
+
let mod = moduleCache.get(schemaPath);
|
|
1838
|
+
if (!mod) {
|
|
1839
|
+
mod = await importWithCacheBust(schemaPath);
|
|
1840
|
+
moduleCache.set(schemaPath, mod);
|
|
1841
|
+
}
|
|
1842
|
+
return mod;
|
|
1843
|
+
}
|
|
1844
|
+
async function validateInput(schemaPath, method, inputType, input) {
|
|
1845
|
+
const schemaName = getSchemaName(method, inputType);
|
|
1846
|
+
const schemaKey = `${schemaName}Schema`;
|
|
1847
|
+
let mod;
|
|
1848
|
+
try {
|
|
1849
|
+
mod = await loadSchemaModule(schemaPath);
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1852
|
+
throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
|
|
1853
|
+
}
|
|
1854
|
+
const schema = mod[schemaKey];
|
|
1855
|
+
if (schema === void 0 || schema === null) {
|
|
1856
|
+
const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
1857
|
+
return { valid: true, issues: [], data };
|
|
1858
|
+
}
|
|
1859
|
+
if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
|
|
1860
|
+
throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
|
|
1861
|
+
}
|
|
1862
|
+
const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
1863
|
+
const zodSchema = schema;
|
|
1864
|
+
const result = zodSchema.safeParse(inputObj);
|
|
1865
|
+
if (result.success) {
|
|
1866
|
+
const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
|
|
1867
|
+
return { valid: true, issues: [], data };
|
|
1868
|
+
}
|
|
1869
|
+
const issues = mapZodIssues(result.error);
|
|
1870
|
+
return { valid: false, issues, data: inputObj };
|
|
1871
|
+
}
|
|
1872
|
+
function mapZodIssues(error) {
|
|
1873
|
+
return error.issues.map((issue) => {
|
|
1874
|
+
const code = mapZodCode(issue.code, issue.message);
|
|
1875
|
+
const path9 = issue.path.map(String).join(".") || "";
|
|
1876
|
+
return {
|
|
1877
|
+
path: path9,
|
|
1878
|
+
code,
|
|
1879
|
+
expected: issue.expected ?? mapExpectedFromMessage(issue.message),
|
|
1880
|
+
received: issue.received ?? mapReceivedFromMessage(issue.message),
|
|
1881
|
+
message: issue.message
|
|
1882
|
+
};
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
function mapZodCode(zodCode, message) {
|
|
1886
|
+
switch (zodCode) {
|
|
1887
|
+
case "invalid_type":
|
|
1888
|
+
case "invalid_union":
|
|
1889
|
+
case "invalid_union_discriminator":
|
|
1890
|
+
return "TYPE_MISMATCH";
|
|
1891
|
+
case "unrecognized_keys":
|
|
1892
|
+
return "INVALID_FORMAT";
|
|
1893
|
+
case "invalid_value":
|
|
1894
|
+
case "invalid_string":
|
|
1895
|
+
case "too_small":
|
|
1896
|
+
case "too_big":
|
|
1897
|
+
case "invalid_intersection_types":
|
|
1898
|
+
case "not_multiple_of":
|
|
1899
|
+
return "INVALID_VALUE";
|
|
1900
|
+
case "custom":
|
|
1901
|
+
return "INVALID_VALUE";
|
|
1902
|
+
default:
|
|
1903
|
+
if (message.includes("Required") || message.includes("required")) {
|
|
1904
|
+
return "MISSING_FIELD";
|
|
1905
|
+
}
|
|
1906
|
+
return "INVALID_VALUE";
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function mapExpectedFromMessage(message) {
|
|
1910
|
+
const match = message.match(/Expected\s+(\w+)/i);
|
|
1911
|
+
return match ? match[1].toLowerCase() : "unknown";
|
|
1912
|
+
}
|
|
1913
|
+
function mapReceivedFromMessage(message) {
|
|
1914
|
+
const match = message.match(/received\s+(\w+)/i);
|
|
1915
|
+
return match ? match[1].toLowerCase() : "unknown";
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/utils/getClientIp.ts
|
|
1919
|
+
function getClientIp(req) {
|
|
1920
|
+
const xff = req.headers["x-forwarded-for"];
|
|
1921
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
1922
|
+
const first = xff.split(",")[0]?.trim();
|
|
1923
|
+
if (first) return first;
|
|
1924
|
+
}
|
|
1925
|
+
const remote = req.socket?.remoteAddress;
|
|
1926
|
+
if (remote) {
|
|
1927
|
+
if (remote.startsWith("::ffff:")) {
|
|
1928
|
+
return remote.slice(7);
|
|
1929
|
+
}
|
|
1930
|
+
return remote;
|
|
1931
|
+
}
|
|
1932
|
+
return "";
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/server/handleWsUpgrade.ts
|
|
1936
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1937
|
+
import path3 from "path";
|
|
1938
|
+
|
|
1939
|
+
// src/errors/formatErrorResponse.ts
|
|
1940
|
+
function formatErrorResponse(error) {
|
|
1941
|
+
if (error instanceof ValidationError) {
|
|
1942
|
+
const body2 = {
|
|
1943
|
+
code: error.code,
|
|
1944
|
+
message: error.message,
|
|
1945
|
+
issues: error.issues
|
|
1946
|
+
};
|
|
1947
|
+
return new Response(JSON.stringify({ error: body2 }), {
|
|
1948
|
+
status: error.statusCode,
|
|
1949
|
+
headers: { "Content-Type": "application/json" }
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
if (error instanceof MethodNotAllowedError) {
|
|
1953
|
+
const body2 = {
|
|
1954
|
+
code: error.code,
|
|
1955
|
+
message: error.message
|
|
1956
|
+
};
|
|
1957
|
+
return new Response(JSON.stringify({ error: body2 }), {
|
|
1958
|
+
status: error.statusCode,
|
|
1959
|
+
headers: {
|
|
1960
|
+
"Content-Type": "application/json",
|
|
1961
|
+
Allow: error.allowedMethods.join(", ")
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
if (error instanceof FaapiError) {
|
|
1966
|
+
const body2 = {
|
|
1967
|
+
code: error.code,
|
|
1968
|
+
message: error.message
|
|
1969
|
+
};
|
|
1970
|
+
return new Response(JSON.stringify({ error: body2 }), {
|
|
1971
|
+
status: error.statusCode,
|
|
1972
|
+
headers: { "Content-Type": "application/json" }
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
const body = {
|
|
1976
|
+
code: "INTERNAL_ERROR",
|
|
1977
|
+
message: error instanceof Error ? error.message : "An unknown error occurred"
|
|
1978
|
+
};
|
|
1979
|
+
return new Response(JSON.stringify({ error: body }), {
|
|
1980
|
+
status: 500,
|
|
1981
|
+
headers: { "Content-Type": "application/json" }
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/server/serverUtils.ts
|
|
1986
|
+
function nodeHttpToWebHeaders(req) {
|
|
1987
|
+
const headers = new Headers();
|
|
1988
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1989
|
+
if (value === void 0) continue;
|
|
1990
|
+
if (Array.isArray(value)) {
|
|
1991
|
+
for (const v of value) headers.append(key, v);
|
|
1992
|
+
} else {
|
|
1993
|
+
headers.set(key, value);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return headers;
|
|
1997
|
+
}
|
|
1998
|
+
function buildErrorResponse(err) {
|
|
1999
|
+
try {
|
|
2000
|
+
return formatErrorResponse(err);
|
|
2001
|
+
} catch {
|
|
2002
|
+
return new Response(
|
|
2003
|
+
JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
|
|
2004
|
+
{
|
|
2005
|
+
status: 500,
|
|
2006
|
+
headers: { "Content-Type": "application/json" }
|
|
2007
|
+
}
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// src/runtime/wsHandler.ts
|
|
2013
|
+
function wrapWsSocket(rawSocket) {
|
|
2014
|
+
return {
|
|
2015
|
+
send(data) {
|
|
2016
|
+
const payload = typeof data === "string" || Buffer.isBuffer(data) ? data : JSON.stringify(data);
|
|
2017
|
+
rawSocket.send(payload);
|
|
2018
|
+
},
|
|
2019
|
+
close(code, reason) {
|
|
2020
|
+
rawSocket.close(code, reason);
|
|
2021
|
+
},
|
|
2022
|
+
get readyState() {
|
|
2023
|
+
return rawSocket.readyState;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/server/handleWsUpgrade.ts
|
|
2029
|
+
function getPathname(req) {
|
|
2030
|
+
const url = req.url ?? "/";
|
|
2031
|
+
const idx = url.indexOf("?");
|
|
2032
|
+
return idx >= 0 ? url.slice(0, idx) : url;
|
|
2033
|
+
}
|
|
2034
|
+
async function loadWsHandler(filePath, ctx) {
|
|
2035
|
+
const module = await importWithCacheBust(filePath);
|
|
2036
|
+
const handler = module["WS"];
|
|
2037
|
+
if (typeof handler !== "function") {
|
|
2038
|
+
throw new Error(`WS export not found in ${filePath}`);
|
|
2039
|
+
}
|
|
2040
|
+
return handler(ctx);
|
|
2041
|
+
}
|
|
2042
|
+
function bindEvents(rawSocket, handlers) {
|
|
2043
|
+
if (!handlers) return;
|
|
2044
|
+
const ws = wrapWsSocket(rawSocket);
|
|
2045
|
+
if (handlers.onOpen) {
|
|
2046
|
+
if (rawSocket.readyState === WebSocket.OPEN) {
|
|
2047
|
+
handlers.onOpen(ws);
|
|
2048
|
+
} else {
|
|
2049
|
+
rawSocket.once("open", () => handlers.onOpen(ws));
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (handlers.onMessage) {
|
|
2053
|
+
rawSocket.on("message", (data) => {
|
|
2054
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2055
|
+
handlers.onMessage(ws, buf.toString("utf8"));
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
if (handlers.onClose) {
|
|
2059
|
+
rawSocket.on("close", (code, reason) => {
|
|
2060
|
+
handlers.onClose(ws, code, reason.toString("utf8"));
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
if (handlers.onError) {
|
|
2064
|
+
rawSocket.on("error", (err) => {
|
|
2065
|
+
handlers.onError(ws, err);
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
async function sendResponseToSocket(socket, response) {
|
|
2070
|
+
const body = await response.text().catch(() => "");
|
|
2071
|
+
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
|
|
2072
|
+
`;
|
|
2073
|
+
const headerLines = [];
|
|
2074
|
+
let hasContentLength = false;
|
|
2075
|
+
for (const [key, value] of response.headers) {
|
|
2076
|
+
if (key.toLowerCase() === "content-length") {
|
|
2077
|
+
hasContentLength = true;
|
|
2078
|
+
}
|
|
2079
|
+
headerLines.push(`${key}: ${value}`);
|
|
2080
|
+
}
|
|
2081
|
+
if (!hasContentLength) {
|
|
2082
|
+
headerLines.push(`Content-Length: ${Buffer.byteLength(body)}`);
|
|
2083
|
+
}
|
|
2084
|
+
socket.write(statusLine + headerLines.join("\r\n") + "\r\n\r\n" + body);
|
|
2085
|
+
socket.destroy();
|
|
2086
|
+
}
|
|
2087
|
+
function attachWebSocket(options) {
|
|
2088
|
+
const { server, routesRef, rootDir, config, globalMiddlewares } = options;
|
|
2089
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
2090
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
2091
|
+
const currentWsRoutes = routesRef.wsCurrent;
|
|
2092
|
+
const pathname = getPathname(req);
|
|
2093
|
+
const match = matchWsRoute(currentWsRoutes, pathname);
|
|
2094
|
+
if (!match) {
|
|
2095
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
2096
|
+
socket.destroy();
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const { route, params } = match;
|
|
2100
|
+
const headers = nodeHttpToWebHeaders(req);
|
|
2101
|
+
const host = req.headers.host ?? "localhost";
|
|
2102
|
+
const url = `http://${host}${req.url ?? "/"}`;
|
|
2103
|
+
const request = new Request(url, { method: "GET", headers });
|
|
2104
|
+
const ctx = createContext(request, params, config, getClientIp(req));
|
|
2105
|
+
const meta = ctx.meta;
|
|
2106
|
+
let upgraded = false;
|
|
2107
|
+
const finalHandler = async () => {
|
|
2108
|
+
let handlers;
|
|
2109
|
+
try {
|
|
2110
|
+
const absoluteFilePath = path3.resolve(rootDir, route.filePath);
|
|
2111
|
+
handlers = await loadWsHandler(absoluteFilePath, ctx);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2114
|
+
console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
|
|
2115
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
2116
|
+
}
|
|
2117
|
+
await new Promise((resolve, reject) => {
|
|
2118
|
+
wss.handleUpgrade(req, socket, head, (rawSocket) => {
|
|
2119
|
+
try {
|
|
2120
|
+
bindEvents(rawSocket, handlers);
|
|
2121
|
+
wss.emit("connection", rawSocket, req);
|
|
2122
|
+
upgraded = true;
|
|
2123
|
+
resolve();
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
reject(err);
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
});
|
|
2129
|
+
return new Response(null, { status: 200 });
|
|
2130
|
+
};
|
|
2131
|
+
let response;
|
|
2132
|
+
try {
|
|
2133
|
+
const dirMiddlewares = route.middlewares ?? [];
|
|
2134
|
+
const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
|
|
2135
|
+
if (allMiddlewares.length > 0) {
|
|
2136
|
+
response = await compose(allMiddlewares, ctx, finalHandler);
|
|
2137
|
+
} else {
|
|
2138
|
+
response = await finalHandler();
|
|
2139
|
+
}
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
if (upgraded) {
|
|
2142
|
+
console.error("[faapi] WS \u63E1\u624B\u540E\u4E2D\u95F4\u4EF6\u629B\u9519:", err);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
response = buildErrorResponse(err);
|
|
2146
|
+
}
|
|
2147
|
+
if (upgraded) {
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
await sendResponseToSocket(socket, mergeMeta(response, meta));
|
|
2151
|
+
});
|
|
2152
|
+
return wss;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/cli/generateSchemaFiles.ts
|
|
2156
|
+
import path4 from "path";
|
|
2157
|
+
import fs2 from "fs/promises";
|
|
2158
|
+
|
|
2159
|
+
// src/ast/generateZodSchema.ts
|
|
2160
|
+
var CodeGenContext = class {
|
|
2161
|
+
/** 命名类型集合:name → RuntimeType */
|
|
2162
|
+
namedTypes = /* @__PURE__ */ new Map();
|
|
2163
|
+
/** 类型解析器(用于解析 ref 的实际类型) */
|
|
2164
|
+
resolveType;
|
|
2165
|
+
/** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
|
|
2166
|
+
entryTypeName = "";
|
|
2167
|
+
/** 入口类型导出名(exportName,自引用时用此名生成变量名) */
|
|
2168
|
+
entryExportName = "";
|
|
2169
|
+
/**
|
|
2170
|
+
* 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
|
|
2171
|
+
*
|
|
2172
|
+
* true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
|
|
2173
|
+
* 嵌套类型(array/object/tuple/union 等)的元素递归处理。
|
|
2174
|
+
*/
|
|
2175
|
+
coerce = false;
|
|
2176
|
+
constructor(resolveType) {
|
|
2177
|
+
this.resolveType = resolveType;
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
function collectNamedTypes(type, ctx) {
|
|
2181
|
+
switch (type.kind) {
|
|
2182
|
+
case "string":
|
|
2183
|
+
case "number":
|
|
2184
|
+
case "boolean":
|
|
2185
|
+
case "bigint":
|
|
2186
|
+
case "null":
|
|
2187
|
+
case "undefined":
|
|
2188
|
+
case "any":
|
|
2189
|
+
case "unknown":
|
|
2190
|
+
case "literal":
|
|
2191
|
+
case "date":
|
|
2192
|
+
return;
|
|
2193
|
+
case "array":
|
|
2194
|
+
collectNamedTypes(type.element, ctx);
|
|
2195
|
+
return;
|
|
2196
|
+
case "tuple":
|
|
2197
|
+
for (const el of type.elements) {
|
|
2198
|
+
collectNamedTypes(el.type, ctx);
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
case "object":
|
|
2202
|
+
for (const prop of type.properties) {
|
|
2203
|
+
collectNamedTypes(prop.type, ctx);
|
|
2204
|
+
}
|
|
2205
|
+
return;
|
|
2206
|
+
case "union":
|
|
2207
|
+
for (const member of type.members) {
|
|
2208
|
+
collectNamedTypes(member, ctx);
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
case "record":
|
|
2212
|
+
collectNamedTypes(type.key, ctx);
|
|
2213
|
+
collectNamedTypes(type.value, ctx);
|
|
2214
|
+
return;
|
|
2215
|
+
case "map":
|
|
2216
|
+
collectNamedTypes(type.key, ctx);
|
|
2217
|
+
collectNamedTypes(type.value, ctx);
|
|
2218
|
+
return;
|
|
2219
|
+
case "set":
|
|
2220
|
+
collectNamedTypes(type.element, ctx);
|
|
2221
|
+
return;
|
|
2222
|
+
case "ref": {
|
|
2223
|
+
if (ctx.namedTypes.has(type.name)) return;
|
|
2224
|
+
ctx.namedTypes.set(type.name, { kind: "any" });
|
|
2225
|
+
const resolved = ctx.resolveType(type.name);
|
|
2226
|
+
if (resolved) {
|
|
2227
|
+
ctx.namedTypes.set(type.name, resolved);
|
|
2228
|
+
collectNamedTypes(resolved, ctx);
|
|
2229
|
+
}
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function runtimeTypeToZodExpression(type, ctx, constraints) {
|
|
2235
|
+
const expr = baseExpression(type, ctx);
|
|
2236
|
+
const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
|
|
2237
|
+
if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
|
|
2238
|
+
return wrapCoercePreprocess(type.kind, withConstraints);
|
|
2239
|
+
}
|
|
2240
|
+
return withConstraints;
|
|
2241
|
+
}
|
|
2242
|
+
function applyConstraints(baseExpr, constraints, typeKind) {
|
|
2243
|
+
const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
|
|
2244
|
+
return `${baseExpr}${suffix}`;
|
|
2245
|
+
}
|
|
2246
|
+
function constraintToZodChain(constraint, _typeKind) {
|
|
2247
|
+
switch (constraint.kind) {
|
|
2248
|
+
case "max":
|
|
2249
|
+
return `.max(${constraint.value})`;
|
|
2250
|
+
case "min":
|
|
2251
|
+
return `.min(${constraint.value})`;
|
|
2252
|
+
case "int":
|
|
2253
|
+
return ".int()";
|
|
2254
|
+
case "positive":
|
|
2255
|
+
return ".positive()";
|
|
2256
|
+
case "negative":
|
|
2257
|
+
return ".negative()";
|
|
2258
|
+
case "nonnegative":
|
|
2259
|
+
return ".nonnegative()";
|
|
2260
|
+
case "nonpositive":
|
|
2261
|
+
return ".nonpositive()";
|
|
2262
|
+
case "maxLength":
|
|
2263
|
+
return `.max(${constraint.value})`;
|
|
2264
|
+
case "minLength":
|
|
2265
|
+
return `.min(${constraint.value})`;
|
|
2266
|
+
case "length":
|
|
2267
|
+
return `.length(${constraint.value})`;
|
|
2268
|
+
case "regex": {
|
|
2269
|
+
const flags = constraint.flags ?? "";
|
|
2270
|
+
return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
|
|
2271
|
+
}
|
|
2272
|
+
case "email":
|
|
2273
|
+
return ".email()";
|
|
2274
|
+
case "url":
|
|
2275
|
+
return ".url()";
|
|
2276
|
+
case "uuid":
|
|
2277
|
+
return ".uuid()";
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function baseExpression(type, ctx) {
|
|
2281
|
+
switch (type.kind) {
|
|
2282
|
+
case "string":
|
|
2283
|
+
return "z.string()";
|
|
2284
|
+
case "number":
|
|
2285
|
+
return "z.number()";
|
|
2286
|
+
case "boolean":
|
|
2287
|
+
return "z.boolean()";
|
|
2288
|
+
case "bigint":
|
|
2289
|
+
return "z.never()";
|
|
2290
|
+
case "null":
|
|
2291
|
+
return "z.null()";
|
|
2292
|
+
case "undefined":
|
|
2293
|
+
return "z.undefined()";
|
|
2294
|
+
case "any":
|
|
2295
|
+
case "unknown":
|
|
2296
|
+
return "z.unknown()";
|
|
2297
|
+
case "literal":
|
|
2298
|
+
return `z.literal(${JSON.stringify(type.value)})`;
|
|
2299
|
+
case "array":
|
|
2300
|
+
return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
|
|
2301
|
+
case "tuple":
|
|
2302
|
+
return generateTupleExpression(type.elements, ctx);
|
|
2303
|
+
case "object":
|
|
2304
|
+
return generateObjectExpression(type.properties, ctx);
|
|
2305
|
+
case "union":
|
|
2306
|
+
return generateUnionExpression(type.members, ctx);
|
|
2307
|
+
case "date":
|
|
2308
|
+
return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
|
|
2309
|
+
case "record":
|
|
2310
|
+
return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
|
|
2311
|
+
case "map":
|
|
2312
|
+
return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
|
|
2313
|
+
case "set":
|
|
2314
|
+
return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
|
|
2315
|
+
case "ref":
|
|
2316
|
+
if (type.name === ctx.entryTypeName) {
|
|
2317
|
+
return `${ctx.entryExportName}Schema`;
|
|
2318
|
+
}
|
|
2319
|
+
return `${type.name}Schema`;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
|
|
2323
|
+
var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
|
|
2324
|
+
var COERCE_MAP_HELPER = 'export const coerceMap = (v) => Array.isArray(v) ? new Map(v) : v instanceof Map ? v : (v && typeof v === "object" ? new Map(Object.entries(v)) : v);';
|
|
2325
|
+
var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
|
|
2326
|
+
var HELPERS_FILENAME = "faapi-helpers.js";
|
|
2327
|
+
function generateHelpersFileSource() {
|
|
2328
|
+
return [
|
|
2329
|
+
"// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
|
|
2330
|
+
COERCE_NUMBER_HELPER,
|
|
2331
|
+
COERCE_BOOLEAN_HELPER,
|
|
2332
|
+
COERCE_MAP_HELPER,
|
|
2333
|
+
COERCE_SET_HELPER,
|
|
2334
|
+
""
|
|
2335
|
+
].join("\n");
|
|
2336
|
+
}
|
|
2337
|
+
function usesCoerceHelpers(code) {
|
|
2338
|
+
return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
|
|
2339
|
+
}
|
|
2340
|
+
function wrapCoercePreprocess(kind, inner) {
|
|
2341
|
+
if (kind === "number") {
|
|
2342
|
+
return `z.preprocess(coerceNumber, ${inner})`;
|
|
2343
|
+
}
|
|
2344
|
+
return `z.preprocess(coerceBoolean, ${inner})`;
|
|
2345
|
+
}
|
|
2346
|
+
function generateTupleExpression(elements, ctx) {
|
|
2347
|
+
const fixedExprs = [];
|
|
2348
|
+
const fixedOptional = [];
|
|
2349
|
+
let restExpression = "";
|
|
2350
|
+
let restStarted = false;
|
|
2351
|
+
for (const el of elements) {
|
|
2352
|
+
if (el.rest) {
|
|
2353
|
+
restExpression = runtimeTypeToZodExpression(el.type, ctx);
|
|
2354
|
+
restStarted = true;
|
|
2355
|
+
} else if (!restStarted) {
|
|
2356
|
+
fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
|
|
2357
|
+
fixedOptional.push(el.optional);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (restExpression) {
|
|
2361
|
+
return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
|
|
2362
|
+
}
|
|
2363
|
+
const hasOptional = fixedOptional.some((o) => o);
|
|
2364
|
+
if (!hasOptional) {
|
|
2365
|
+
return `z.tuple([${fixedExprs.join(", ")}])`;
|
|
2366
|
+
}
|
|
2367
|
+
const variants = [];
|
|
2368
|
+
for (let len = fixedExprs.length; len >= 0; len--) {
|
|
2369
|
+
const removed = fixedOptional.slice(len);
|
|
2370
|
+
if (removed.length > 0 && removed.some((o) => !o)) {
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
const subset = fixedExprs.slice(0, len);
|
|
2374
|
+
variants.push(`z.tuple([${subset.join(", ")}])`);
|
|
2375
|
+
}
|
|
2376
|
+
variants.reverse();
|
|
2377
|
+
if (variants.length === 1) {
|
|
2378
|
+
return variants[0];
|
|
2379
|
+
}
|
|
2380
|
+
return `z.union([${variants.join(", ")}])`;
|
|
2381
|
+
}
|
|
2382
|
+
function generateObjectExpression(properties, ctx) {
|
|
2383
|
+
const fields = properties.map((prop) => {
|
|
2384
|
+
const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
|
|
2385
|
+
const finalExpr = prop.optional ? `${expr}.optional()` : expr;
|
|
2386
|
+
return `${JSON.stringify(prop.name)}: ${finalExpr}`;
|
|
2387
|
+
});
|
|
2388
|
+
return `z.object({ ${fields.join(", ")} })`;
|
|
2389
|
+
}
|
|
2390
|
+
function generateUnionExpression(members, ctx) {
|
|
2391
|
+
const hasNull = members.some((m) => m.kind === "null");
|
|
2392
|
+
const nonNull = members.filter((m) => m.kind !== "null");
|
|
2393
|
+
if (hasNull && nonNull.length === 1) {
|
|
2394
|
+
return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
|
|
2395
|
+
}
|
|
2396
|
+
if (hasNull) {
|
|
2397
|
+
const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
|
|
2398
|
+
return `z.union([${unionInner2}]).nullable()`;
|
|
2399
|
+
}
|
|
2400
|
+
const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
|
|
2401
|
+
return `z.union([${unionInner}])`;
|
|
2402
|
+
}
|
|
2403
|
+
function generateNamedTypeDeclaration(name, type, ctx) {
|
|
2404
|
+
const expr = runtimeTypeToZodExpression(type, ctx);
|
|
2405
|
+
const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
|
|
2406
|
+
if (hasRef) {
|
|
2407
|
+
return `const ${name}Schema = z.lazy(() => ${expr});`;
|
|
2408
|
+
}
|
|
2409
|
+
return `const ${name}Schema = ${expr};`;
|
|
2410
|
+
}
|
|
2411
|
+
function containsRef(type, visited) {
|
|
2412
|
+
switch (type.kind) {
|
|
2413
|
+
case "ref":
|
|
2414
|
+
return visited.has(type.name);
|
|
2415
|
+
case "array":
|
|
2416
|
+
return containsRef(type.element, visited);
|
|
2417
|
+
case "tuple":
|
|
2418
|
+
return type.elements.some((el) => containsRef(el.type, visited));
|
|
2419
|
+
case "object":
|
|
2420
|
+
return type.properties.some((prop) => containsRef(prop.type, visited));
|
|
2421
|
+
case "union":
|
|
2422
|
+
return type.members.some((m) => containsRef(m, visited));
|
|
2423
|
+
case "record":
|
|
2424
|
+
return containsRef(type.key, visited) || containsRef(type.value, visited);
|
|
2425
|
+
case "map":
|
|
2426
|
+
return containsRef(type.key, visited) || containsRef(type.value, visited);
|
|
2427
|
+
case "set":
|
|
2428
|
+
return containsRef(type.element, visited);
|
|
2429
|
+
default:
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
|
|
2434
|
+
const ctx = new CodeGenContext(resolveType);
|
|
2435
|
+
const name = exportName ?? typeInfo.name;
|
|
2436
|
+
ctx.entryTypeName = typeInfo.name;
|
|
2437
|
+
ctx.entryExportName = name;
|
|
2438
|
+
ctx.coerce = coerce;
|
|
2439
|
+
collectNamedTypes(typeInfo.runtimeType, ctx);
|
|
2440
|
+
ctx.namedTypes.delete(typeInfo.name);
|
|
2441
|
+
const lines = [];
|
|
2442
|
+
lines.push("import { z } from 'zod';");
|
|
2443
|
+
lines.push("");
|
|
2444
|
+
for (const [n, type] of ctx.namedTypes) {
|
|
2445
|
+
lines.push(generateNamedTypeDeclaration(n, type, ctx));
|
|
2446
|
+
}
|
|
2447
|
+
if (ctx.namedTypes.size > 0) lines.push("");
|
|
2448
|
+
const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
|
|
2449
|
+
const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
|
|
2450
|
+
if (hasSelfRef) {
|
|
2451
|
+
lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
|
|
2452
|
+
} else {
|
|
2453
|
+
lines.push(`export const ${name}Schema = ${entryExpr};`);
|
|
2454
|
+
}
|
|
2455
|
+
return lines.join("\n");
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/cli/generateSchemaFiles.ts
|
|
2459
|
+
function getSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
2460
|
+
let rel = sourceFile.replace(/\\/g, "/");
|
|
2461
|
+
if (rel.startsWith("src/")) {
|
|
2462
|
+
rel = rel.slice(4);
|
|
2463
|
+
}
|
|
2464
|
+
const idx = rel.lastIndexOf("/");
|
|
2465
|
+
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2466
|
+
return path4.resolve(rootDir, dist, relDir, "zod.js");
|
|
2467
|
+
}
|
|
2468
|
+
function getRuntimeSchemaPath(filePath, dist, rootDir) {
|
|
2469
|
+
let rel = filePath.replace(/\\/g, "/");
|
|
2470
|
+
if (rel.startsWith("src/")) {
|
|
2471
|
+
rel = rel.slice(4);
|
|
2472
|
+
} else if (rel.startsWith(`${dist}/`)) {
|
|
2473
|
+
rel = rel.slice(dist.length + 1);
|
|
2474
|
+
}
|
|
2475
|
+
const idx = rel.lastIndexOf("/");
|
|
2476
|
+
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2477
|
+
return path4.resolve(rootDir, dist, relDir, "zod.js");
|
|
2478
|
+
}
|
|
2479
|
+
function getHelpersImportPath(relDir) {
|
|
2480
|
+
if (!relDir) return `./${HELPERS_FILENAME}`;
|
|
2481
|
+
const depth = relDir.split("/").filter(Boolean).length;
|
|
2482
|
+
return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
|
|
2483
|
+
}
|
|
2484
|
+
function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
2485
|
+
const resolveType = (name) => allTypes.get(name)?.runtimeType;
|
|
2486
|
+
const lines = ["import { z } from 'zod';"];
|
|
2487
|
+
const schemaBlocks = [];
|
|
2488
|
+
for (const source of sources) {
|
|
2489
|
+
const { schemaName, typeInfo } = source;
|
|
2490
|
+
if (!typeInfo) {
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
|
|
2494
|
+
const block = [`// ${schemaName}`];
|
|
2495
|
+
const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
|
|
2496
|
+
/^import \{ z \} from 'zod';\s*\n\s*\n/,
|
|
2497
|
+
""
|
|
2498
|
+
);
|
|
2499
|
+
block.push(schemaCode);
|
|
2500
|
+
block.push("");
|
|
2501
|
+
schemaBlocks.push(block.join("\n"));
|
|
2502
|
+
}
|
|
2503
|
+
const allSchemaCode = schemaBlocks.join("\n");
|
|
2504
|
+
if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
|
|
2505
|
+
lines.push(
|
|
2506
|
+
`import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
lines.push("");
|
|
2510
|
+
lines.push(...schemaBlocks);
|
|
2511
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
2512
|
+
}
|
|
2513
|
+
async function generateSchemaFiles(routes, rootDir, dist) {
|
|
2514
|
+
if (routes.length === 0) return;
|
|
2515
|
+
const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
|
|
2516
|
+
const sourcesByFile = /* @__PURE__ */ new Map();
|
|
2517
|
+
for (const source of sources) {
|
|
2518
|
+
let list = sourcesByFile.get(source.filePath);
|
|
2519
|
+
if (!list) {
|
|
2520
|
+
list = [];
|
|
2521
|
+
sourcesByFile.set(source.filePath, list);
|
|
2522
|
+
}
|
|
2523
|
+
list.push(source);
|
|
2524
|
+
}
|
|
2525
|
+
const fileEntries = [];
|
|
2526
|
+
for (const [filePath, fileSources] of sourcesByFile) {
|
|
2527
|
+
const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
2528
|
+
const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
|
|
2529
|
+
const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
|
|
2530
|
+
let relForDir = relFile;
|
|
2531
|
+
if (relForDir.startsWith("src/")) {
|
|
2532
|
+
relForDir = relForDir.slice(4);
|
|
2533
|
+
}
|
|
2534
|
+
const dirIdx = relForDir.lastIndexOf("/");
|
|
2535
|
+
const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
|
|
2536
|
+
const helpersImportPath = getHelpersImportPath(zodRelDir);
|
|
2537
|
+
const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
|
|
2538
|
+
fileEntries.push({ outputPath, source });
|
|
2539
|
+
}
|
|
2540
|
+
const allSourceCode = fileEntries.map((e) => e.source).join("\n");
|
|
2541
|
+
if (usesCoerceHelpers(allSourceCode)) {
|
|
2542
|
+
const helpersPath = path4.resolve(rootDir, dist, HELPERS_FILENAME);
|
|
2543
|
+
await writeSchemaFile(helpersPath, generateHelpersFileSource());
|
|
2544
|
+
}
|
|
2545
|
+
await Promise.all(
|
|
2546
|
+
fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
async function writeSchemaFile(outputPath, source) {
|
|
2550
|
+
await fs2.mkdir(path4.dirname(outputPath), { recursive: true });
|
|
2551
|
+
await fs2.writeFile(outputPath, source, "utf-8");
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/server/createServer.ts
|
|
2555
|
+
var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
|
|
2556
|
+
function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
|
|
2557
|
+
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
2558
|
+
const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
|
|
2559
|
+
const host = req.headers.host ?? "localhost";
|
|
2560
|
+
const url = new URL(req.url ?? "/", `${protocol}://${host}`);
|
|
2561
|
+
const headers = nodeHttpToWebHeaders(req);
|
|
2562
|
+
const method = req.method ?? "GET";
|
|
2563
|
+
if (method === "GET" || method === "HEAD") {
|
|
2564
|
+
return new Request(url.toString(), { method, headers });
|
|
2565
|
+
}
|
|
2566
|
+
const stream = Readable2.toWeb(req);
|
|
2567
|
+
const limitedStream = limitStreamSize(stream, bodyLimit);
|
|
2568
|
+
return new Request(url.toString(), {
|
|
2569
|
+
method,
|
|
2570
|
+
headers,
|
|
2571
|
+
body: limitedStream,
|
|
2572
|
+
duplex: "half"
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
function limitStreamSize(stream, maxSize) {
|
|
2576
|
+
let totalSize = 0;
|
|
2577
|
+
const reader = stream.getReader();
|
|
2578
|
+
return new ReadableStream({
|
|
2579
|
+
async pull(controller) {
|
|
2580
|
+
const { done, value } = await reader.read();
|
|
2581
|
+
if (done) {
|
|
2582
|
+
controller.close();
|
|
2583
|
+
reader.releaseLock();
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
totalSize += value.byteLength;
|
|
2587
|
+
if (totalSize > maxSize) {
|
|
2588
|
+
controller.error(new Error(`\u8BF7\u6C42\u4F53\u8D85\u8FC7\u5927\u5C0F\u9650\u5236 ${maxSize} \u5B57\u8282`));
|
|
2589
|
+
reader.releaseLock();
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
controller.enqueue(value);
|
|
2593
|
+
},
|
|
2594
|
+
cancel(reason) {
|
|
2595
|
+
reader.cancel(reason);
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
function findAllowedMethods(routes, path9) {
|
|
2600
|
+
const methods = /* @__PURE__ */ new Set();
|
|
2601
|
+
for (const route of routes) {
|
|
2602
|
+
if (route.urlPath === path9) {
|
|
2603
|
+
methods.add(route.method);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
if (route.isDynamic) {
|
|
2607
|
+
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
2608
|
+
if (params !== null) {
|
|
2609
|
+
methods.add(route.method);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
return Array.from(methods);
|
|
2614
|
+
}
|
|
2615
|
+
function createServer(options) {
|
|
2616
|
+
const {
|
|
2617
|
+
routes,
|
|
2618
|
+
rootDir,
|
|
2619
|
+
dist,
|
|
2620
|
+
cors: corsOption,
|
|
2621
|
+
onError,
|
|
2622
|
+
config,
|
|
2623
|
+
wsRoutes,
|
|
2624
|
+
middlewares: globalMiddlewares,
|
|
2625
|
+
injectors: globalInjectors,
|
|
2626
|
+
helmet: helmetOption,
|
|
2627
|
+
logger: loggerOption,
|
|
2628
|
+
bodyLimit = DEFAULT_BODY_LIMIT,
|
|
2629
|
+
http2: http2Option
|
|
2630
|
+
} = options;
|
|
2631
|
+
const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
|
|
2632
|
+
const configMiddlewares = [];
|
|
2633
|
+
const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
|
|
2634
|
+
if (corsMiddleware) configMiddlewares.push(corsMiddleware);
|
|
2635
|
+
if (helmetOption) {
|
|
2636
|
+
const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
|
|
2637
|
+
configMiddlewares.push(helmet(helmOpts));
|
|
2638
|
+
}
|
|
2639
|
+
const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
|
|
2640
|
+
if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
|
|
2641
|
+
const server = (() => {
|
|
2642
|
+
if (http2Option) {
|
|
2643
|
+
const h2Opts = typeof http2Option === "object" ? http2Option : {};
|
|
2644
|
+
return createHttp2SecureServer({
|
|
2645
|
+
key: h2Opts.key ? readFileSync(h2Opts.key) : void 0,
|
|
2646
|
+
cert: h2Opts.cert ? readFileSync(h2Opts.cert) : void 0,
|
|
2647
|
+
allowHTTP1: true
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
return createHttpServer();
|
|
2651
|
+
})();
|
|
2652
|
+
server.on("request", (req, res) => {
|
|
2653
|
+
const currentRoutes = routesRef.current;
|
|
2654
|
+
handleRequest(
|
|
2655
|
+
currentRoutes,
|
|
2656
|
+
rootDir,
|
|
2657
|
+
dist,
|
|
2658
|
+
req,
|
|
2659
|
+
res,
|
|
2660
|
+
configMiddlewares,
|
|
2661
|
+
onError,
|
|
2662
|
+
config,
|
|
2663
|
+
globalMiddlewares,
|
|
2664
|
+
globalInjectors,
|
|
2665
|
+
bodyLimit
|
|
2666
|
+
).catch(() => {
|
|
2667
|
+
res.statusCode = 500;
|
|
2668
|
+
res.end();
|
|
2669
|
+
});
|
|
2670
|
+
});
|
|
2671
|
+
if (routesRef.wsCurrent.length > 0) {
|
|
2672
|
+
attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares });
|
|
2673
|
+
}
|
|
2674
|
+
return { server, routesRef };
|
|
2675
|
+
}
|
|
2676
|
+
async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
|
|
2677
|
+
const request = toWebRequest(req, bodyLimit);
|
|
2678
|
+
const method = request.method.toUpperCase();
|
|
2679
|
+
const urlPath = new URL(request.url).pathname;
|
|
2680
|
+
const ctx = createContext(request, {}, config, getClientIp(req));
|
|
2681
|
+
const meta = ctx.meta;
|
|
2682
|
+
const routePipeline = async () => {
|
|
2683
|
+
const match = matchRoute(routes, method, urlPath);
|
|
2684
|
+
if (!match) {
|
|
2685
|
+
const allowedMethods = findAllowedMethods(routes, urlPath);
|
|
2686
|
+
if (allowedMethods.length > 0) {
|
|
2687
|
+
throw new MethodNotAllowedError(method, urlPath, allowedMethods);
|
|
2688
|
+
}
|
|
2689
|
+
throw new RouteNotFoundError(urlPath);
|
|
2690
|
+
}
|
|
2691
|
+
ctx.params = match.params;
|
|
2692
|
+
const { route } = match;
|
|
2693
|
+
const absoluteFilePath = path5.resolve(rootDir, route.filePath);
|
|
2694
|
+
const routeModule = await loadRouteModule(absoluteFilePath, route.method);
|
|
2695
|
+
const input = await resolveInput(route.method, request);
|
|
2696
|
+
const inputType = getInputTypeForMethod(route.method);
|
|
2697
|
+
const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
|
|
2698
|
+
const result = await validateInput(schemaPath, route.method, inputType, input);
|
|
2699
|
+
if (!result.valid) {
|
|
2700
|
+
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
|
|
2701
|
+
}
|
|
2702
|
+
const body = hasBody(route.method) ? result.data : void 0;
|
|
2703
|
+
const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
|
|
2704
|
+
const response = await invokeHandler(
|
|
2705
|
+
routeModule.handler,
|
|
2706
|
+
ctx,
|
|
2707
|
+
body,
|
|
2708
|
+
route.middlewares,
|
|
2709
|
+
mergedInjectors
|
|
2710
|
+
);
|
|
2711
|
+
return response;
|
|
2712
|
+
};
|
|
2713
|
+
try {
|
|
2714
|
+
let response;
|
|
2715
|
+
const outerMiddlewares = [];
|
|
2716
|
+
if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
|
|
2717
|
+
if (globalMiddlewares && globalMiddlewares.length > 0) {
|
|
2718
|
+
outerMiddlewares.push(...globalMiddlewares);
|
|
2719
|
+
}
|
|
2720
|
+
if (outerMiddlewares.length > 0) {
|
|
2721
|
+
response = await compose(outerMiddlewares, ctx, routePipeline);
|
|
2722
|
+
} else {
|
|
2723
|
+
response = await routePipeline();
|
|
2724
|
+
}
|
|
2725
|
+
await sendNodeResponse(response, res);
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
const errorResponse = buildErrorResponse(err);
|
|
2728
|
+
await sendNodeResponse(mergeMeta(errorResponse, meta), res);
|
|
2729
|
+
if (onError) {
|
|
2730
|
+
try {
|
|
2731
|
+
await onError(err, ctx);
|
|
2732
|
+
} catch {
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// src/server/startServer.ts
|
|
2739
|
+
function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
|
|
2740
|
+
if (handlerWrappers.length > 0) {
|
|
2741
|
+
const listeners = server.listeners("request");
|
|
2742
|
+
const original = listeners[0];
|
|
2743
|
+
if (original) {
|
|
2744
|
+
server.removeAllListeners("request");
|
|
2745
|
+
let handler = original;
|
|
2746
|
+
for (const wrap of handlerWrappers) {
|
|
2747
|
+
handler = wrap(handler);
|
|
2748
|
+
}
|
|
2749
|
+
server.on("request", handler);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
if (upgradeWrappers.length > 0) {
|
|
2753
|
+
const listeners = server.listeners("upgrade");
|
|
2754
|
+
const original = listeners[0];
|
|
2755
|
+
server.removeAllListeners("upgrade");
|
|
2756
|
+
let upgrade = original;
|
|
2757
|
+
for (const wrap of upgradeWrappers) {
|
|
2758
|
+
upgrade = wrap(upgrade);
|
|
2759
|
+
}
|
|
2760
|
+
if (upgrade) {
|
|
2761
|
+
server.on("upgrade", upgrade);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// src/cli/generateRoutes.ts
|
|
2767
|
+
import fs3 from "fs";
|
|
2768
|
+
import path6 from "path";
|
|
2769
|
+
|
|
2770
|
+
// src/middleware/loadMiddlewares.ts
|
|
2771
|
+
var middlewareCache = /* @__PURE__ */ new Map();
|
|
2772
|
+
function invalidateMiddlewareCache() {
|
|
2773
|
+
middlewareCache.clear();
|
|
2774
|
+
}
|
|
2775
|
+
function getCachedMiddlewares(absPath) {
|
|
2776
|
+
return middlewareCache.get(absPath);
|
|
2777
|
+
}
|
|
2778
|
+
function setCachedMiddlewares(absPath, bundle) {
|
|
2779
|
+
middlewareCache.set(absPath, bundle);
|
|
2780
|
+
}
|
|
2781
|
+
async function loadMiddlewaresFile(filePath) {
|
|
2782
|
+
try {
|
|
2783
|
+
const module = await importWithCacheBust(filePath);
|
|
2784
|
+
const middlewares = module.default ?? module.middlewares ?? [];
|
|
2785
|
+
if (!Array.isArray(middlewares)) {
|
|
2786
|
+
console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
|
|
2787
|
+
return { middlewares: [], injectors: {} };
|
|
2788
|
+
}
|
|
2789
|
+
const validMiddlewares = middlewares.filter((m) => {
|
|
2790
|
+
if (typeof m !== "function") {
|
|
2791
|
+
console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
|
|
2792
|
+
return false;
|
|
2793
|
+
}
|
|
2794
|
+
return true;
|
|
2795
|
+
});
|
|
2796
|
+
const injectors = module.injectors ?? {};
|
|
2797
|
+
if (typeof injectors !== "object" || injectors === null) {
|
|
2798
|
+
console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
|
|
2799
|
+
return { middlewares: validMiddlewares, injectors: {} };
|
|
2800
|
+
}
|
|
2801
|
+
const validInjectors = {};
|
|
2802
|
+
for (const [name, injector] of Object.entries(injectors)) {
|
|
2803
|
+
if (typeof injector !== "function") {
|
|
2804
|
+
console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
validInjectors[name] = injector;
|
|
2808
|
+
}
|
|
2809
|
+
return { middlewares: validMiddlewares, injectors: validInjectors };
|
|
2810
|
+
} catch {
|
|
2811
|
+
return { middlewares: [], injectors: {} };
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/cli/generateRoutes.ts
|
|
2816
|
+
async function hydrateRoutes(manifest) {
|
|
2817
|
+
const hydrateRoute = async (serialized) => {
|
|
2818
|
+
const bundle = await loadMiddlewarePaths(serialized.middlewarePaths);
|
|
2819
|
+
return {
|
|
2820
|
+
method: serialized.method,
|
|
2821
|
+
urlPath: serialized.urlPath,
|
|
2822
|
+
filePath: serialized.filePath,
|
|
2823
|
+
paramNames: serialized.paramNames,
|
|
2824
|
+
isDynamic: serialized.isDynamic,
|
|
2825
|
+
isCatchAll: serialized.isCatchAll,
|
|
2826
|
+
middlewares: bundle?.middlewares,
|
|
2827
|
+
injectors: bundle?.injectors
|
|
2828
|
+
};
|
|
2829
|
+
};
|
|
2830
|
+
const hydrateWsRoute = async (serialized) => {
|
|
2831
|
+
const bundle = await loadMiddlewarePaths(serialized.middlewarePaths);
|
|
2832
|
+
return {
|
|
2833
|
+
urlPath: serialized.urlPath,
|
|
2834
|
+
filePath: serialized.filePath,
|
|
2835
|
+
paramNames: serialized.paramNames,
|
|
2836
|
+
isDynamic: serialized.isDynamic,
|
|
2837
|
+
isCatchAll: serialized.isCatchAll,
|
|
2838
|
+
middlewares: bundle?.middlewares,
|
|
2839
|
+
injectors: bundle?.injectors
|
|
2840
|
+
};
|
|
2841
|
+
};
|
|
2842
|
+
const routes = await Promise.all(manifest.routes.map(hydrateRoute));
|
|
2843
|
+
const wsRoutes = await Promise.all(manifest.wsRoutes.map(hydrateWsRoute));
|
|
2844
|
+
return { routes, wsRoutes };
|
|
2845
|
+
}
|
|
2846
|
+
async function loadMiddlewarePaths(middlewarePaths) {
|
|
2847
|
+
if (middlewarePaths.length === 0) return void 0;
|
|
2848
|
+
const mergedMiddlewares = [];
|
|
2849
|
+
const mergedInjectors = {};
|
|
2850
|
+
for (const absMwPath of middlewarePaths) {
|
|
2851
|
+
let bundle = getCachedMiddlewares(absMwPath);
|
|
2852
|
+
if (bundle === void 0) {
|
|
2853
|
+
bundle = await loadMiddlewaresFile(absMwPath);
|
|
2854
|
+
setCachedMiddlewares(absMwPath, bundle);
|
|
2855
|
+
}
|
|
2856
|
+
mergedMiddlewares.push(...bundle.middlewares);
|
|
2857
|
+
for (const [name, injector] of Object.entries(bundle.injectors)) {
|
|
2858
|
+
mergedInjectors[name] = injector;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// src/cli/loadPlugins.ts
|
|
2865
|
+
async function loadPlugins(declarations, ctx) {
|
|
2866
|
+
const handlerWrappers = [];
|
|
2867
|
+
const upgradeWrappers = [];
|
|
2868
|
+
if (!declarations || declarations.length === 0) {
|
|
2869
|
+
return { handlerWrappers, upgradeWrappers };
|
|
2870
|
+
}
|
|
2871
|
+
const fullCtx = {
|
|
2872
|
+
...ctx,
|
|
2873
|
+
wrapHandler: (fn) => {
|
|
2874
|
+
handlerWrappers.push(fn);
|
|
2875
|
+
},
|
|
2876
|
+
wrapUpgradeHandler: (fn) => {
|
|
2877
|
+
upgradeWrappers.push(fn);
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
2881
|
+
for (const decl of declarations) {
|
|
2882
|
+
const { specifier, options, enable } = resolveDeclaration(decl);
|
|
2883
|
+
if (enable === false) continue;
|
|
2884
|
+
if (loaded.has(specifier)) {
|
|
2885
|
+
console.warn(`! Plugin already loaded: ${specifier}, skipping`);
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
loaded.add(specifier);
|
|
2889
|
+
try {
|
|
2890
|
+
const mod = await import(specifier);
|
|
2891
|
+
const plugin = mod.default ?? mod;
|
|
2892
|
+
if (typeof plugin.setup !== "function") {
|
|
2893
|
+
console.warn(`! Plugin ${specifier} has no setup function, skipping`);
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
await plugin.setup({ ...fullCtx, options });
|
|
2897
|
+
console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
|
|
2898
|
+
} catch (err) {
|
|
2899
|
+
console.warn(
|
|
2900
|
+
`! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return { handlerWrappers, upgradeWrappers };
|
|
2905
|
+
}
|
|
2906
|
+
function resolveDeclaration(decl) {
|
|
2907
|
+
if (typeof decl === "string") {
|
|
2908
|
+
return { specifier: decl };
|
|
2909
|
+
}
|
|
2910
|
+
if (Array.isArray(decl)) {
|
|
2911
|
+
const [specifier, options] = decl;
|
|
2912
|
+
return { specifier, options };
|
|
2913
|
+
}
|
|
2914
|
+
if ("package" in decl) {
|
|
2915
|
+
return { specifier: decl.package, options: decl.options, enable: decl.enable };
|
|
2916
|
+
}
|
|
2917
|
+
if ("path" in decl) {
|
|
2918
|
+
return { specifier: decl.path, options: decl.options, enable: decl.enable };
|
|
2919
|
+
}
|
|
2920
|
+
throw new Error(`Invalid plugin declaration: ${JSON.stringify(decl)}`);
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/cli/createAppCore.ts
|
|
2924
|
+
var DEFAULT_DIST = "dist";
|
|
2925
|
+
var DEFAULT_PORT = 3e3;
|
|
2926
|
+
var ROUTES_FILE = "faapi-routes.js";
|
|
2927
|
+
var PATTERNS = ["src/api/**/*.ts"];
|
|
2928
|
+
var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2929
|
+
"cors",
|
|
2930
|
+
"lifecycle",
|
|
2931
|
+
"middlewares",
|
|
2932
|
+
"injectors",
|
|
2933
|
+
"extendContext",
|
|
2934
|
+
"plugins",
|
|
2935
|
+
"helmet",
|
|
2936
|
+
"bodyLimit",
|
|
2937
|
+
"logger",
|
|
2938
|
+
"http2"
|
|
2939
|
+
]);
|
|
2940
|
+
function isFaapiConfigKey(key) {
|
|
2941
|
+
return FAAPI_CONFIG_KEYS.has(key);
|
|
2942
|
+
}
|
|
2943
|
+
async function createAppBase(options) {
|
|
2944
|
+
const rootDir = options?.rootDir ?? process.cwd();
|
|
2945
|
+
const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
|
|
2946
|
+
const routesPath = path7.resolve(rootDir, dist, ROUTES_FILE);
|
|
2947
|
+
if (!fs4.existsSync(routesPath)) {
|
|
2948
|
+
throw new Error(
|
|
2949
|
+
`[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const config = await loadConfig(rootDir, dist);
|
|
2953
|
+
const serialized = await importWithCacheBust(routesPath);
|
|
2954
|
+
const hydrated = await hydrateRoutes(serialized);
|
|
2955
|
+
let sorted = sortRoutes(hydrated.routes);
|
|
2956
|
+
let wsRoutes = hydrated.wsRoutes;
|
|
2957
|
+
const conflicts = detectRouteConflicts(sorted);
|
|
2958
|
+
if (conflicts.length > 0) {
|
|
2959
|
+
for (const conflict of conflicts) {
|
|
2960
|
+
console.warn(`! \u8DEF\u7531\u51B2\u7A81: ${conflict.method} ${conflict.urlPath}`);
|
|
2961
|
+
for (const file of conflict.files) {
|
|
2962
|
+
console.warn(` - ${file}`);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
const pluginConfig = config ? Object.fromEntries(Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))) : {};
|
|
2967
|
+
const { server, routesRef } = createServer({
|
|
2968
|
+
routes: sorted,
|
|
2969
|
+
rootDir,
|
|
2970
|
+
dist,
|
|
2971
|
+
cors: config?.cors ?? true,
|
|
2972
|
+
onError: config?.lifecycle?.onError,
|
|
2973
|
+
config: config ?? void 0,
|
|
2974
|
+
wsRoutes,
|
|
2975
|
+
middlewares: config?.middlewares,
|
|
2976
|
+
injectors: config?.injectors,
|
|
2977
|
+
helmet: config?.helmet,
|
|
2978
|
+
logger: config?.logger,
|
|
2979
|
+
bodyLimit: config?.bodyLimit,
|
|
2980
|
+
http2: config?.http2
|
|
2981
|
+
});
|
|
2982
|
+
const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
|
|
2983
|
+
rootDir,
|
|
2984
|
+
routes: sorted,
|
|
2985
|
+
getRoutes: () => sorted,
|
|
2986
|
+
server,
|
|
2987
|
+
config: pluginConfig
|
|
2988
|
+
});
|
|
2989
|
+
applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
|
|
2990
|
+
let closed = false;
|
|
2991
|
+
const app = {
|
|
2992
|
+
server: null,
|
|
2993
|
+
routes: sorted,
|
|
2994
|
+
wsRoutes,
|
|
2995
|
+
rootDir,
|
|
2996
|
+
async listen(listenPort) {
|
|
2997
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
|
|
2998
|
+
const actualPort = listenPort ?? options?.port ?? envPort ?? DEFAULT_PORT;
|
|
2999
|
+
return new Promise((resolve) => {
|
|
3000
|
+
server.listen(actualPort, async () => {
|
|
3001
|
+
const address = server.address();
|
|
3002
|
+
const p = typeof address === "object" && address !== null ? address.port : actualPort;
|
|
3003
|
+
console.log("faapi server started");
|
|
3004
|
+
console.log(`- Local: http://localhost:${p}`);
|
|
3005
|
+
console.log("- Loaded routes:");
|
|
3006
|
+
for (const route of sorted) {
|
|
3007
|
+
console.log(` ${route.method.padEnd(6)}${route.urlPath} ${route.filePath}`);
|
|
3008
|
+
}
|
|
3009
|
+
if (wsRoutes.length > 0) {
|
|
3010
|
+
console.log("- WebSocket routes:");
|
|
3011
|
+
for (const route of wsRoutes) {
|
|
3012
|
+
console.log(` WS ${route.urlPath} ${route.filePath}`);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
if (config?.lifecycle?.onClose) {
|
|
3016
|
+
const graceful = async (signal) => {
|
|
3017
|
+
console.log(`
|
|
3018
|
+
- Received ${signal}, shutting down...`);
|
|
3019
|
+
await app.close();
|
|
3020
|
+
process.exit(0);
|
|
3021
|
+
};
|
|
3022
|
+
process.on("SIGTERM", () => void graceful("SIGTERM"));
|
|
3023
|
+
process.on("SIGINT", () => void graceful("SIGINT"));
|
|
3024
|
+
}
|
|
3025
|
+
if (config?.lifecycle?.onReady) {
|
|
3026
|
+
await config.lifecycle.onReady({ rootDir, routes: sorted, server });
|
|
3027
|
+
console.log("- onReady hook executed");
|
|
3028
|
+
}
|
|
3029
|
+
app.server = server;
|
|
3030
|
+
resolve(server);
|
|
3031
|
+
});
|
|
3032
|
+
});
|
|
3033
|
+
},
|
|
3034
|
+
async inject(injectOpts) {
|
|
3035
|
+
const {
|
|
3036
|
+
method = "GET",
|
|
3037
|
+
path: reqPath = "/",
|
|
3038
|
+
headers: reqHeaders = {},
|
|
3039
|
+
query,
|
|
3040
|
+
body
|
|
3041
|
+
} = injectOpts ?? {};
|
|
3042
|
+
const queryStr = query ? "?" + new URLSearchParams(Object.entries(query).map(([k, v]) => [k, String(v)])).toString() : "";
|
|
3043
|
+
return new Promise((resolve, reject) => {
|
|
3044
|
+
const mockRes = {
|
|
3045
|
+
statusCode: 200,
|
|
3046
|
+
_headers: {},
|
|
3047
|
+
_body: Buffer.alloc(0),
|
|
3048
|
+
setHeader(name, value) {
|
|
3049
|
+
this._headers[name.toLowerCase()] = value;
|
|
3050
|
+
},
|
|
3051
|
+
appendHeader(name, value) {
|
|
3052
|
+
const key = name.toLowerCase();
|
|
3053
|
+
const existing = this._headers[key];
|
|
3054
|
+
this._headers[key] = existing ? `${existing}, ${value}` : value;
|
|
3055
|
+
},
|
|
3056
|
+
writeHead(status, headers) {
|
|
3057
|
+
this.statusCode = status;
|
|
3058
|
+
if (headers) {
|
|
3059
|
+
Object.assign(this._headers, headers);
|
|
3060
|
+
}
|
|
3061
|
+
},
|
|
3062
|
+
end(data) {
|
|
3063
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data ?? "");
|
|
3064
|
+
this._body = buf;
|
|
3065
|
+
resolve({
|
|
3066
|
+
status: this.statusCode,
|
|
3067
|
+
headers: new Headers(this._headers),
|
|
3068
|
+
body: this.parseBody()
|
|
3069
|
+
});
|
|
3070
|
+
},
|
|
3071
|
+
parseBody() {
|
|
3072
|
+
try {
|
|
3073
|
+
return JSON.parse(this._body.toString());
|
|
3074
|
+
} catch {
|
|
3075
|
+
return this._body.toString();
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
const listeners = server.listeners("request");
|
|
3080
|
+
const handler = listeners[listeners.length - 1];
|
|
3081
|
+
if (typeof handler !== "function") {
|
|
3082
|
+
reject(new Error("No request handler found"));
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const mockReq = new PassThrough({
|
|
3086
|
+
read() {
|
|
3087
|
+
this.push(null);
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
mockReq.method = method;
|
|
3091
|
+
mockReq.url = `${reqPath}${queryStr}`;
|
|
3092
|
+
mockReq.headers = {
|
|
3093
|
+
...reqHeaders,
|
|
3094
|
+
host: "localhost",
|
|
3095
|
+
"content-type": body !== void 0 ? "application/json" : void 0
|
|
3096
|
+
};
|
|
3097
|
+
mockReq.socket = { remoteAddress: "127.0.0.1" };
|
|
3098
|
+
if (body !== void 0) {
|
|
3099
|
+
mockReq.push(JSON.stringify(body));
|
|
3100
|
+
}
|
|
3101
|
+
handler(
|
|
3102
|
+
mockReq,
|
|
3103
|
+
mockRes
|
|
3104
|
+
);
|
|
3105
|
+
});
|
|
3106
|
+
},
|
|
3107
|
+
async close() {
|
|
3108
|
+
if (closed) return;
|
|
3109
|
+
closed = true;
|
|
3110
|
+
const s = server;
|
|
3111
|
+
if (typeof s.closeIdleConnections === "function") {
|
|
3112
|
+
s.closeIdleConnections();
|
|
3113
|
+
}
|
|
3114
|
+
if (typeof s.closeAllConnections === "function") {
|
|
3115
|
+
s.closeAllConnections();
|
|
3116
|
+
}
|
|
3117
|
+
if (config?.lifecycle?.onClose) {
|
|
3118
|
+
await config.lifecycle.onClose({ rootDir, routes: sorted, server });
|
|
3119
|
+
}
|
|
3120
|
+
return new Promise((resolve) => {
|
|
3121
|
+
server.close((err) => {
|
|
3122
|
+
if (err) console.error("Error closing server:", err);
|
|
3123
|
+
app.server = null;
|
|
3124
|
+
resolve();
|
|
3125
|
+
});
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
const ctx = {
|
|
3130
|
+
rootDir,
|
|
3131
|
+
dist,
|
|
3132
|
+
patterns: PATTERNS,
|
|
3133
|
+
server,
|
|
3134
|
+
routesRef,
|
|
3135
|
+
config,
|
|
3136
|
+
updateRoutes(newRoutes, newWsRoutes) {
|
|
3137
|
+
sorted = newRoutes;
|
|
3138
|
+
wsRoutes = newWsRoutes;
|
|
3139
|
+
app.routes = newRoutes;
|
|
3140
|
+
app.wsRoutes = newWsRoutes;
|
|
3141
|
+
routesRef.current = newRoutes;
|
|
3142
|
+
routesRef.wsCurrent = newWsRoutes;
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
return { app, ctx };
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/router/scanRoutes.ts
|
|
3149
|
+
import fg from "fast-glob";
|
|
3150
|
+
import path8 from "path";
|
|
3151
|
+
import fs5 from "fs";
|
|
3152
|
+
|
|
3153
|
+
// src/router/constants.ts
|
|
3154
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
3155
|
+
var HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
3156
|
+
function isHttpMethod(value) {
|
|
3157
|
+
return HTTP_METHOD_SET.has(value);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// src/utils/normalizePath.ts
|
|
3161
|
+
function normalizePath(path9) {
|
|
3162
|
+
if (!path9) return "";
|
|
3163
|
+
let result = path9.replace(/\\/g, "/");
|
|
3164
|
+
result = result.replace(/\/+/g, "/");
|
|
3165
|
+
result = result.replace(/\/+$/, "");
|
|
3166
|
+
if (result && !result.startsWith("/")) {
|
|
3167
|
+
result = "/" + result;
|
|
3168
|
+
}
|
|
3169
|
+
return result;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/router/parseRouteFile.ts
|
|
3173
|
+
function dynamicSegmentToParam(segment) {
|
|
3174
|
+
const match = segment.match(/^\[(.+)\]$/);
|
|
3175
|
+
if (match) {
|
|
3176
|
+
return ":" + match[1];
|
|
3177
|
+
}
|
|
3178
|
+
return segment;
|
|
3179
|
+
}
|
|
3180
|
+
function extractParamNames(urlPath) {
|
|
3181
|
+
const params = [];
|
|
3182
|
+
const segments = urlPath.split("/");
|
|
3183
|
+
for (const segment of segments) {
|
|
3184
|
+
if (segment.startsWith(":...")) {
|
|
3185
|
+
params.push(segment.slice(4));
|
|
3186
|
+
} else if (segment.startsWith(":")) {
|
|
3187
|
+
params.push(segment.slice(1));
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return params;
|
|
3191
|
+
}
|
|
3192
|
+
function isCatchAllSegment(segment) {
|
|
3193
|
+
return /^\[\.\.\..+\]$/.test(segment);
|
|
3194
|
+
}
|
|
3195
|
+
function isRouteGroup(segment) {
|
|
3196
|
+
return /^\(.+\)$/.test(segment);
|
|
3197
|
+
}
|
|
3198
|
+
function filePathToUrlPath(filePath) {
|
|
3199
|
+
const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
|
|
3200
|
+
const lastSlashIndex = withoutPrefix.lastIndexOf("/");
|
|
3201
|
+
const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
|
|
3202
|
+
if (!dirPath) {
|
|
3203
|
+
return "";
|
|
3204
|
+
}
|
|
3205
|
+
const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
|
|
3206
|
+
return normalizePath(segments.join("/"));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/router/scanRoutes.ts
|
|
3210
|
+
var APP_DIR = "src";
|
|
3211
|
+
function toProdAbsPath(sourceAbsPath, rootDir, dist) {
|
|
3212
|
+
let rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
3213
|
+
if (rel.startsWith(`${APP_DIR}/`)) {
|
|
3214
|
+
rel = rel.slice(APP_DIR.length + 1);
|
|
3215
|
+
}
|
|
3216
|
+
const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
|
|
3217
|
+
return path8.resolve(rootDir, prodRel);
|
|
3218
|
+
}
|
|
3219
|
+
async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
|
|
3220
|
+
const routeDir = path8.dirname(routeFilePath);
|
|
3221
|
+
const resolvedRoot = path8.resolve(rootDir);
|
|
3222
|
+
const mwPaths = [];
|
|
3223
|
+
let currentDir = path8.resolve(rootDir, routeDir);
|
|
3224
|
+
while (true) {
|
|
3225
|
+
if (dist) {
|
|
3226
|
+
const mwPath = path8.join(currentDir, "middlewares.js");
|
|
3227
|
+
const absMwPath = path8.resolve(rootDir, mwPath);
|
|
3228
|
+
const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
|
|
3229
|
+
if (fs5.existsSync(prodAbsMwPath)) {
|
|
3230
|
+
mwPaths.push(prodAbsMwPath);
|
|
3231
|
+
}
|
|
3232
|
+
} else {
|
|
3233
|
+
for (const ext of [".ts", ".js"]) {
|
|
3234
|
+
const mwPath = path8.join(currentDir, `middlewares${ext}`);
|
|
3235
|
+
const absMwPath = path8.resolve(rootDir, mwPath);
|
|
3236
|
+
if (fs5.existsSync(absMwPath)) {
|
|
3237
|
+
mwPaths.push(absMwPath);
|
|
3238
|
+
break;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
if (currentDir === resolvedRoot) break;
|
|
3243
|
+
const parentDir = path8.dirname(currentDir);
|
|
3244
|
+
if (parentDir === currentDir) break;
|
|
3245
|
+
currentDir = parentDir;
|
|
3246
|
+
}
|
|
3247
|
+
if (mwPaths.length === 0) return void 0;
|
|
3248
|
+
mwPaths.reverse();
|
|
3249
|
+
const mergedMiddlewares = [];
|
|
3250
|
+
const mergedInjectors = {};
|
|
3251
|
+
for (const absMwPath of mwPaths) {
|
|
3252
|
+
let bundle = getCachedMiddlewares(absMwPath);
|
|
3253
|
+
if (bundle === void 0) {
|
|
3254
|
+
bundle = await loadMiddlewaresFile(absMwPath);
|
|
3255
|
+
setCachedMiddlewares(absMwPath, bundle);
|
|
3256
|
+
}
|
|
3257
|
+
mergedMiddlewares.push(...bundle.middlewares);
|
|
3258
|
+
for (const [name, injector] of Object.entries(bundle.injectors)) {
|
|
3259
|
+
mergedInjectors[name] = injector;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
|
|
3263
|
+
return void 0;
|
|
3264
|
+
}
|
|
3265
|
+
return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
|
|
3266
|
+
}
|
|
3267
|
+
async function extractMethodsFromHandler(absPath) {
|
|
3268
|
+
try {
|
|
3269
|
+
const module = await importWithCacheBust(absPath);
|
|
3270
|
+
const methods = [];
|
|
3271
|
+
for (const key of Object.keys(module)) {
|
|
3272
|
+
if (isHttpMethod(key) && typeof module[key] === "function") {
|
|
3273
|
+
methods.push(key);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return methods;
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3279
|
+
console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25 ${absPath}: ${reason}`);
|
|
3280
|
+
return [];
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function hasWsExport(absPath) {
|
|
3284
|
+
try {
|
|
3285
|
+
const module = await importWithCacheBust(absPath);
|
|
3286
|
+
return typeof module["WS"] === "function";
|
|
3287
|
+
} catch (err) {
|
|
3288
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3289
|
+
console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25\uFF08WS \u68C0\u6D4B\uFF09${absPath}: ${reason}`);
|
|
3290
|
+
return false;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
async function scanRoutes(rootDir, patterns, dist) {
|
|
3294
|
+
const files = await fg(patterns, {
|
|
3295
|
+
cwd: rootDir,
|
|
3296
|
+
onlyFiles: true,
|
|
3297
|
+
absolute: false
|
|
3298
|
+
});
|
|
3299
|
+
const routes = [];
|
|
3300
|
+
const wsRoutes = [];
|
|
3301
|
+
for (const file of files) {
|
|
3302
|
+
const normalizedFile = file.replace(/\\/g, "/");
|
|
3303
|
+
const fileName = normalizedFile.split("/").pop();
|
|
3304
|
+
if (fileName === "handler.ts" || fileName === "handler.js") {
|
|
3305
|
+
const absPath = path8.resolve(rootDir, normalizedFile);
|
|
3306
|
+
const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
|
|
3307
|
+
const urlPath = filePathToUrlPath(normalizedFile);
|
|
3308
|
+
const paramNames = extractParamNames(urlPath);
|
|
3309
|
+
const isDynamic = paramNames.length > 0;
|
|
3310
|
+
const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
|
|
3311
|
+
const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
|
|
3312
|
+
const methods = await extractMethodsFromHandler(importPath);
|
|
3313
|
+
for (const method of methods) {
|
|
3314
|
+
routes.push({
|
|
3315
|
+
method,
|
|
3316
|
+
urlPath,
|
|
3317
|
+
filePath: normalizedFile,
|
|
3318
|
+
paramNames,
|
|
3319
|
+
isDynamic,
|
|
3320
|
+
isCatchAll: isCatchAll || void 0,
|
|
3321
|
+
middlewares: middlewareBundle?.middlewares,
|
|
3322
|
+
injectors: middlewareBundle?.injectors
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
const hasWs = await hasWsExport(importPath);
|
|
3326
|
+
if (hasWs) {
|
|
3327
|
+
wsRoutes.push({
|
|
3328
|
+
urlPath,
|
|
3329
|
+
filePath: normalizedFile,
|
|
3330
|
+
paramNames,
|
|
3331
|
+
isDynamic,
|
|
3332
|
+
isCatchAll: isCatchAll || void 0,
|
|
3333
|
+
middlewares: middlewareBundle?.middlewares,
|
|
3334
|
+
injectors: middlewareBundle?.injectors
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return { routes, wsRoutes };
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// src/cli/createDevApp.ts
|
|
3344
|
+
async function createDevApp(options) {
|
|
3345
|
+
const { app, ctx } = await createAppBase(options);
|
|
3346
|
+
const devApp = app;
|
|
3347
|
+
devApp.reloadRoutes = async () => {
|
|
3348
|
+
setLoadTimestamp(Date.now());
|
|
3349
|
+
invalidateMiddlewareCache();
|
|
3350
|
+
invalidateProgramCache();
|
|
3351
|
+
invalidateSchemaCache();
|
|
3352
|
+
const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
|
|
3353
|
+
const sorted = sortRoutes(reScanned.routes);
|
|
3354
|
+
await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
|
|
3355
|
+
ctx.updateRoutes(sorted, reScanned.wsRoutes);
|
|
3356
|
+
};
|
|
3357
|
+
return devApp;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// src/cli/createProdApp.ts
|
|
3361
|
+
async function createProdApp(options) {
|
|
3362
|
+
const { app } = await createAppBase(options);
|
|
3363
|
+
return app;
|
|
718
3364
|
}
|
|
719
3365
|
export {
|
|
3366
|
+
FaapiError,
|
|
3367
|
+
InternalError,
|
|
3368
|
+
MethodNotAllowedError,
|
|
3369
|
+
ModuleLoadError,
|
|
3370
|
+
RouteNotFoundError,
|
|
720
3371
|
SchemaExtractionError,
|
|
3372
|
+
ValidationError,
|
|
3373
|
+
collectRouteSchemaSources,
|
|
721
3374
|
cors,
|
|
3375
|
+
createProdApp as createApp,
|
|
3376
|
+
createContext,
|
|
3377
|
+
createDevApp,
|
|
3378
|
+
createProdApp,
|
|
722
3379
|
createProgram,
|
|
723
3380
|
extractTypeInfo,
|
|
724
3381
|
getInputTypeForMethod,
|
|
725
|
-
|
|
3382
|
+
helmet,
|
|
3383
|
+
invalidateProgramCache,
|
|
3384
|
+
invokeHandler,
|
|
726
3385
|
loadConfig,
|
|
727
|
-
logger
|
|
3386
|
+
logger,
|
|
3387
|
+
resolveTypeNode
|
|
728
3388
|
};
|
|
729
3389
|
//# sourceMappingURL=index.js.map
|