@faapi/faapi 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,15 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
1
11
  // src/ast/createProgram.ts
2
12
  import ts from "typescript";
3
- var programCache = /* @__PURE__ */ new Map();
4
13
  function invalidateProgramCache() {
5
14
  programCache.clear();
6
15
  }
@@ -20,22 +29,16 @@ function createProgram(filePath) {
20
29
  programCache.set(filePath, program);
21
30
  return program;
22
31
  }
23
-
24
- // src/ast/extractHandlerTypes.ts
25
- import ts3 from "typescript";
32
+ var programCache;
33
+ var init_createProgram = __esm({
34
+ "src/ast/createProgram.ts"() {
35
+ "use strict";
36
+ programCache = /* @__PURE__ */ new Map();
37
+ }
38
+ });
26
39
 
27
40
  // src/ast/resolveTypeNode.ts
28
41
  import ts2 from "typescript";
29
- var SchemaExtractionError = class extends Error {
30
- constructor(typeText, reason, options) {
31
- super(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}`, options);
32
- this.typeText = typeText;
33
- this.reason = reason;
34
- this.name = "SchemaExtractionError";
35
- }
36
- typeText;
37
- reason;
38
- };
39
42
  function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
40
43
  const kind = typeNode.kind;
41
44
  switch (kind) {
@@ -415,26 +418,6 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
415
418
  }
416
419
  return { kind: "object", properties };
417
420
  }
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
421
  function extractConstraintsFromJsDoc(node, fieldName) {
439
422
  const jsDocs = ts2.getJSDocCommentsAndTags(node).filter((entry) => ts2.isJSDoc(entry));
440
423
  if (jsDocs.length === 0) return [];
@@ -551,8 +534,45 @@ function validateConstraints(constraints, type, fieldName) {
551
534
  }
552
535
  }
553
536
  }
537
+ var SchemaExtractionError, NUMBER_CONSTRAINT_KINDS, LENGTH_CONSTRAINT_KINDS, STRING_FORMAT_CONSTRAINT_KINDS;
538
+ var init_resolveTypeNode = __esm({
539
+ "src/ast/resolveTypeNode.ts"() {
540
+ "use strict";
541
+ SchemaExtractionError = class extends Error {
542
+ constructor(typeText, reason, options) {
543
+ super(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}`, options);
544
+ this.typeText = typeText;
545
+ this.reason = reason;
546
+ this.name = "SchemaExtractionError";
547
+ }
548
+ typeText;
549
+ reason;
550
+ };
551
+ NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
552
+ "max",
553
+ "min",
554
+ "int",
555
+ "positive",
556
+ "negative",
557
+ "nonnegative",
558
+ "nonpositive"
559
+ ]);
560
+ LENGTH_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
561
+ "maxLength",
562
+ "minLength",
563
+ "length"
564
+ ]);
565
+ STRING_FORMAT_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
566
+ "regex",
567
+ "email",
568
+ "url",
569
+ "uuid"
570
+ ]);
571
+ }
572
+ });
554
573
 
555
574
  // src/ast/extractHandlerTypes.ts
575
+ import ts3 from "typescript";
556
576
  function extractTypeInfo(program, filePath, typeName) {
557
577
  const sourceFile = program.getSourceFile(filePath);
558
578
  if (!sourceFile) return null;
@@ -648,6 +668,12 @@ function withFileContext(filePath, typeName, fn) {
648
668
  throw err;
649
669
  }
650
670
  }
671
+ var init_extractHandlerTypes = __esm({
672
+ "src/ast/extractHandlerTypes.ts"() {
673
+ "use strict";
674
+ init_resolveTypeNode();
675
+ }
676
+ });
651
677
 
652
678
  // src/runtime/inputType.ts
653
679
  function getInputTypeForMethod(method) {
@@ -661,32 +687,24 @@ function hasBody(method) {
661
687
  const upper = method.toUpperCase();
662
688
  return upper === "POST" || upper === "PUT" || upper === "PATCH" || upper === "DELETE";
663
689
  }
690
+ var init_inputType = __esm({
691
+ "src/runtime/inputType.ts"() {
692
+ "use strict";
693
+ }
694
+ });
664
695
 
665
696
  // src/validator/schemaName.ts
666
697
  function getSchemaName(method, inputType) {
667
698
  return `${method.toUpperCase()}${inputType.charAt(0).toUpperCase() + inputType.slice(1)}`;
668
699
  }
669
-
670
- // src/injection/analyzeInjection.ts
671
- import ts5 from "typescript";
700
+ var init_schemaName = __esm({
701
+ "src/validator/schemaName.ts"() {
702
+ "use strict";
703
+ }
704
+ });
672
705
 
673
706
  // src/injection/resolveInjection.ts
674
707
  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
- ua: "ua",
687
- files: "files",
688
- fields: "fields"
689
- };
690
708
  function resolveInjection(fn) {
691
709
  const fnStr = fn.toString();
692
710
  const params = extractParamsWithAst(fnStr);
@@ -755,8 +773,30 @@ function extractParamName(param, names) {
755
773
  return;
756
774
  }
757
775
  }
776
+ var PARAM_TYPE_MAP;
777
+ var init_resolveInjection = __esm({
778
+ "src/injection/resolveInjection.ts"() {
779
+ "use strict";
780
+ PARAM_TYPE_MAP = {
781
+ query: "query",
782
+ body: "body",
783
+ form: "form",
784
+ headers: "headers",
785
+ params: "params",
786
+ context: "context",
787
+ ctx: "context",
788
+ // 别名
789
+ cookies: "cookies",
790
+ ip: "ip",
791
+ ua: "ua",
792
+ files: "files",
793
+ fields: "fields"
794
+ };
795
+ }
796
+ });
758
797
 
759
798
  // src/injection/analyzeInjection.ts
799
+ import ts5 from "typescript";
760
800
  function analyzeInjection(code, functionName) {
761
801
  const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
762
802
  const params = [];
@@ -799,6 +839,12 @@ function extractSchema(typeNode, sourceFile) {
799
839
  }
800
840
  return schema;
801
841
  }
842
+ var init_analyzeInjection = __esm({
843
+ "src/injection/analyzeInjection.ts"() {
844
+ "use strict";
845
+ init_resolveInjection();
846
+ }
847
+ });
802
848
 
803
849
  // src/cli/collectRouteSchemaSources.ts
804
850
  import path from "path";
@@ -848,192 +894,632 @@ function collectRouteSchemaSources(routes, rootDir) {
848
894
  }
849
895
  return { sources, allTypesByFile, mergedAllTypes };
850
896
  }
897
+ var init_collectRouteSchemaSources = __esm({
898
+ "src/cli/collectRouteSchemaSources.ts"() {
899
+ "use strict";
900
+ init_createProgram();
901
+ init_extractHandlerTypes();
902
+ init_inputType();
903
+ init_schemaName();
904
+ init_analyzeInjection();
905
+ }
906
+ });
851
907
 
852
- // src/middleware/cors.ts
853
- var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
854
- function cors(options = {}) {
855
- const {
856
- origin = true,
857
- methods = DEFAULT_METHODS,
858
- allowedHeaders,
859
- exposeHeaders,
860
- credentials = false,
861
- maxAge
862
- } = options;
863
- return async (ctx, next) => {
864
- const reqOrigin = ctx.headers.get("origin");
865
- if (!reqOrigin) {
866
- await next();
908
+ // src/ast/generateZodSchema.ts
909
+ function collectNamedTypes(type, ctx) {
910
+ switch (type.kind) {
911
+ case "string":
912
+ case "number":
913
+ case "boolean":
914
+ case "bigint":
915
+ case "null":
916
+ case "undefined":
917
+ case "any":
918
+ case "unknown":
919
+ case "literal":
920
+ case "date":
867
921
  return;
868
- }
869
- let allowOrigin = null;
870
- if (origin === true) {
871
- allowOrigin = reqOrigin;
872
- } else if (typeof origin === "string") {
873
- allowOrigin = reqOrigin === origin ? origin : null;
874
- } else if (Array.isArray(origin)) {
875
- allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
876
- }
877
- if (!allowOrigin) {
878
- await next();
922
+ case "array":
923
+ collectNamedTypes(type.element, ctx);
879
924
  return;
880
- }
881
- ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
882
- if (origin === true || Array.isArray(origin)) {
883
- const existingVary = ctx.headers.get("vary");
884
- if (existingVary) {
885
- if (!existingVary.toLowerCase().includes("origin")) {
886
- ctx.setHeader("Vary", `${existingVary}, Origin`);
887
- }
888
- } else {
889
- ctx.setHeader("Vary", "Origin");
925
+ case "tuple":
926
+ for (const el of type.elements) {
927
+ collectNamedTypes(el.type, ctx);
890
928
  }
891
- }
892
- ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
893
- if (allowedHeaders) {
894
- ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
895
- } else {
896
- const requestHeaders = ctx.headers.get("access-control-request-headers");
897
- if (requestHeaders) {
898
- ctx.setHeader("Access-Control-Allow-Headers", requestHeaders);
929
+ return;
930
+ case "object":
931
+ for (const prop of type.properties) {
932
+ collectNamedTypes(prop.type, ctx);
899
933
  }
934
+ return;
935
+ case "union":
936
+ for (const member of type.members) {
937
+ collectNamedTypes(member, ctx);
938
+ }
939
+ return;
940
+ case "record":
941
+ collectNamedTypes(type.key, ctx);
942
+ collectNamedTypes(type.value, ctx);
943
+ return;
944
+ case "map":
945
+ collectNamedTypes(type.key, ctx);
946
+ collectNamedTypes(type.value, ctx);
947
+ return;
948
+ case "set":
949
+ collectNamedTypes(type.element, ctx);
950
+ return;
951
+ case "ref": {
952
+ if (ctx.namedTypes.has(type.name)) return;
953
+ ctx.namedTypes.set(type.name, { kind: "any" });
954
+ const resolved = ctx.resolveType(type.name);
955
+ if (resolved) {
956
+ ctx.namedTypes.set(type.name, resolved);
957
+ collectNamedTypes(resolved, ctx);
958
+ }
959
+ return;
900
960
  }
901
- if (exposeHeaders && exposeHeaders.length > 0) {
902
- ctx.setHeader("Access-Control-Expose-Headers", exposeHeaders.join(", "));
903
- }
904
- if (credentials) {
905
- ctx.setHeader("Access-Control-Allow-Credentials", "true");
906
- }
907
- if (maxAge !== void 0) {
908
- ctx.setHeader("Access-Control-Max-Age", String(maxAge));
909
- }
910
- if (ctx.method === "OPTIONS") {
911
- return new Response(null, { status: 204 });
912
- }
913
- await next();
914
- };
915
- }
916
-
917
- // src/middleware/logger.ts
918
- function logger(options = {}) {
919
- return async (ctx, next) => {
920
- const log = options.log ?? console.log;
921
- const start = Date.now();
922
- try {
923
- const response = await next();
924
- const duration = Date.now() - start;
925
- const entry = {
926
- method: ctx.method,
927
- path: ctx.path,
928
- status: response.status,
929
- durationMs: duration
930
- };
931
- log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
932
- return response;
933
- } catch (err) {
934
- const duration = Date.now() - start;
935
- const message = err instanceof Error ? err.message : String(err);
936
- const status = err?.statusCode ?? 500;
937
- const entry = {
938
- method: ctx.method,
939
- path: ctx.path,
940
- status,
941
- durationMs: duration,
942
- error: message
943
- };
944
- log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
945
- throw err;
946
- }
947
- };
948
- }
949
-
950
- // src/middleware/helmet.ts
951
- var DEFAULTS = {
952
- contentSecurityPolicy: "default-src 'self'",
953
- xFrameOptions: "SAMEORIGIN",
954
- xContentTypeOptions: true,
955
- referrerPolicy: "no-referrer",
956
- strictTransportSecurity: "max-age=31536000; includeSubDomains",
957
- xDnsPrefetchControl: true,
958
- xDownloadOptions: true,
959
- xPermittedCrossDomainPolicies: "none",
960
- crossOriginOpenerPolicy: "same-origin",
961
- crossOriginResourcePolicy: "same-origin",
962
- crossOriginEmbedderPolicy: false,
963
- originAgentCluster: true,
964
- xPoweredBy: true
965
- };
966
- function helmet(options = {}) {
967
- const opts = { ...DEFAULTS, ...options };
968
- return async (ctx, next) => {
969
- if (opts.contentSecurityPolicy !== false) {
970
- ctx.setHeader("Content-Security-Policy", opts.contentSecurityPolicy);
971
- }
972
- if (opts.xFrameOptions !== false) {
973
- ctx.setHeader("X-Frame-Options", opts.xFrameOptions);
974
- }
975
- if (opts.xContentTypeOptions) {
976
- ctx.setHeader("X-Content-Type-Options", "nosniff");
977
- }
978
- if (opts.referrerPolicy !== false) {
979
- ctx.setHeader("Referrer-Policy", opts.referrerPolicy);
980
- }
981
- if (opts.strictTransportSecurity !== false) {
982
- ctx.setHeader("Strict-Transport-Security", opts.strictTransportSecurity);
983
- }
984
- if (opts.xDnsPrefetchControl) {
985
- ctx.setHeader("X-DNS-Prefetch-Control", "off");
986
- }
987
- if (opts.xDownloadOptions) {
988
- ctx.setHeader("X-Download-Options", "noopen");
989
- }
990
- if (opts.xPermittedCrossDomainPolicies !== false) {
991
- ctx.setHeader("X-Permitted-Cross-Domain-Policies", opts.xPermittedCrossDomainPolicies);
992
- }
993
- if (opts.crossOriginOpenerPolicy !== false) {
994
- ctx.setHeader("Cross-Origin-Opener-Policy", opts.crossOriginOpenerPolicy);
995
- }
996
- if (opts.crossOriginResourcePolicy !== false) {
997
- ctx.setHeader("Cross-Origin-Resource-Policy", opts.crossOriginResourcePolicy);
998
- }
999
- if (opts.crossOriginEmbedderPolicy !== false) {
1000
- ctx.setHeader("Cross-Origin-Embedder-Policy", opts.crossOriginEmbedderPolicy);
1001
- }
1002
- if (opts.originAgentCluster) {
1003
- ctx.setHeader("Origin-Agent-Cluster", "?1");
1004
- }
1005
- if (opts.xPoweredBy) {
1006
- ctx.setHeader("X-Powered-By", "faapi");
1007
- }
1008
- return await next();
1009
- };
1010
- }
1011
-
1012
- // src/config/loadConfig.ts
1013
- import path2 from "path";
1014
- import fs from "fs";
1015
-
1016
- // src/utils/importWithCacheBust.ts
1017
- import { pathToFileURL } from "url";
1018
- var loadTs;
1019
- function setLoadTimestamp(ts6) {
1020
- loadTs = ts6;
1021
- }
1022
- function getVitestImportActual() {
1023
- const vi = globalThis.vi;
1024
- if (typeof vi?.importActual !== "function") return void 0;
1025
- return vi.importActual.bind(vi);
1026
- }
1027
- async function importWithCacheBust(filePath) {
1028
- const importActual = getVitestImportActual();
1029
- if (importActual) {
1030
- return await importActual(filePath);
1031
961
  }
1032
- let url = pathToFileURL(filePath).href;
1033
- if (loadTs !== void 0) {
1034
- url += `?t=${loadTs}`;
962
+ }
963
+ function runtimeTypeToZodExpression(type, ctx, constraints) {
964
+ const expr = baseExpression(type, ctx);
965
+ const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
966
+ if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
967
+ return wrapCoercePreprocess(type.kind, withConstraints);
1035
968
  }
1036
- return await import(url);
969
+ return withConstraints;
970
+ }
971
+ function applyConstraints(baseExpr, constraints, typeKind) {
972
+ const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
973
+ return `${baseExpr}${suffix}`;
974
+ }
975
+ function constraintToZodChain(constraint, _typeKind) {
976
+ switch (constraint.kind) {
977
+ case "max":
978
+ return `.max(${constraint.value})`;
979
+ case "min":
980
+ return `.min(${constraint.value})`;
981
+ case "int":
982
+ return ".int()";
983
+ case "positive":
984
+ return ".positive()";
985
+ case "negative":
986
+ return ".negative()";
987
+ case "nonnegative":
988
+ return ".nonnegative()";
989
+ case "nonpositive":
990
+ return ".nonpositive()";
991
+ case "maxLength":
992
+ return `.max(${constraint.value})`;
993
+ case "minLength":
994
+ return `.min(${constraint.value})`;
995
+ case "length":
996
+ return `.length(${constraint.value})`;
997
+ case "regex": {
998
+ const flags = constraint.flags ?? "";
999
+ return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
1000
+ }
1001
+ case "email":
1002
+ return ".email()";
1003
+ case "url":
1004
+ return ".url()";
1005
+ case "uuid":
1006
+ return ".uuid()";
1007
+ }
1008
+ }
1009
+ function baseExpression(type, ctx) {
1010
+ switch (type.kind) {
1011
+ case "string":
1012
+ return "z.string()";
1013
+ case "number":
1014
+ return "z.number()";
1015
+ case "boolean":
1016
+ return "z.boolean()";
1017
+ case "bigint":
1018
+ return "z.never()";
1019
+ case "null":
1020
+ return "z.null()";
1021
+ case "undefined":
1022
+ return "z.undefined()";
1023
+ case "any":
1024
+ case "unknown":
1025
+ return "z.unknown()";
1026
+ case "literal":
1027
+ return `z.literal(${JSON.stringify(type.value)})`;
1028
+ case "array":
1029
+ return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
1030
+ case "tuple":
1031
+ return generateTupleExpression(type.elements, ctx);
1032
+ case "object":
1033
+ return generateObjectExpression(type.properties, ctx);
1034
+ case "union":
1035
+ return generateUnionExpression(type.members, ctx);
1036
+ case "date":
1037
+ return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
1038
+ case "record":
1039
+ return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
1040
+ case "map":
1041
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
1042
+ case "set":
1043
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
1044
+ case "ref":
1045
+ if (type.name === ctx.entryTypeName) {
1046
+ return `${ctx.entryExportName}Schema`;
1047
+ }
1048
+ return `${type.name}Schema`;
1049
+ }
1050
+ }
1051
+ function generateHelpersFileSource() {
1052
+ return [
1053
+ "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
1054
+ COERCE_NUMBER_HELPER,
1055
+ COERCE_BOOLEAN_HELPER,
1056
+ COERCE_MAP_HELPER,
1057
+ COERCE_SET_HELPER,
1058
+ ""
1059
+ ].join("\n");
1060
+ }
1061
+ function usesCoerceHelpers(code) {
1062
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
1063
+ }
1064
+ function wrapCoercePreprocess(kind, inner) {
1065
+ if (kind === "number") {
1066
+ return `z.preprocess(coerceNumber, ${inner})`;
1067
+ }
1068
+ return `z.preprocess(coerceBoolean, ${inner})`;
1069
+ }
1070
+ function generateTupleExpression(elements, ctx) {
1071
+ const fixedExprs = [];
1072
+ const fixedOptional = [];
1073
+ let restExpression = "";
1074
+ let restStarted = false;
1075
+ for (const el of elements) {
1076
+ if (el.rest) {
1077
+ restExpression = runtimeTypeToZodExpression(el.type, ctx);
1078
+ restStarted = true;
1079
+ } else if (!restStarted) {
1080
+ fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
1081
+ fixedOptional.push(el.optional);
1082
+ }
1083
+ }
1084
+ if (restExpression) {
1085
+ return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
1086
+ }
1087
+ const hasOptional = fixedOptional.some((o) => o);
1088
+ if (!hasOptional) {
1089
+ return `z.tuple([${fixedExprs.join(", ")}])`;
1090
+ }
1091
+ const variants = [];
1092
+ for (let len = fixedExprs.length; len >= 0; len--) {
1093
+ const removed = fixedOptional.slice(len);
1094
+ if (removed.length > 0 && removed.some((o) => !o)) {
1095
+ break;
1096
+ }
1097
+ const subset = fixedExprs.slice(0, len);
1098
+ variants.push(`z.tuple([${subset.join(", ")}])`);
1099
+ }
1100
+ variants.reverse();
1101
+ if (variants.length === 1) {
1102
+ return variants[0];
1103
+ }
1104
+ return `z.union([${variants.join(", ")}])`;
1105
+ }
1106
+ function generateObjectExpression(properties, ctx) {
1107
+ const fields = properties.map((prop) => {
1108
+ const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
1109
+ const finalExpr = prop.optional ? `${expr}.optional()` : expr;
1110
+ return `${JSON.stringify(prop.name)}: ${finalExpr}`;
1111
+ });
1112
+ return `z.object({ ${fields.join(", ")} })`;
1113
+ }
1114
+ function generateUnionExpression(members, ctx) {
1115
+ const hasNull = members.some((m) => m.kind === "null");
1116
+ const nonNull = members.filter((m) => m.kind !== "null");
1117
+ if (hasNull && nonNull.length === 1) {
1118
+ return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
1119
+ }
1120
+ if (hasNull) {
1121
+ const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1122
+ return `z.union([${unionInner2}]).nullable()`;
1123
+ }
1124
+ const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1125
+ return `z.union([${unionInner}])`;
1126
+ }
1127
+ function generateNamedTypeDeclaration(name, type, ctx) {
1128
+ const expr = runtimeTypeToZodExpression(type, ctx);
1129
+ const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
1130
+ if (hasRef) {
1131
+ return `const ${name}Schema = z.lazy(() => ${expr});`;
1132
+ }
1133
+ return `const ${name}Schema = ${expr};`;
1134
+ }
1135
+ function containsRef(type, visited) {
1136
+ switch (type.kind) {
1137
+ case "ref":
1138
+ return visited.has(type.name);
1139
+ case "array":
1140
+ return containsRef(type.element, visited);
1141
+ case "tuple":
1142
+ return type.elements.some((el) => containsRef(el.type, visited));
1143
+ case "object":
1144
+ return type.properties.some((prop) => containsRef(prop.type, visited));
1145
+ case "union":
1146
+ return type.members.some((m) => containsRef(m, visited));
1147
+ case "record":
1148
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1149
+ case "map":
1150
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1151
+ case "set":
1152
+ return containsRef(type.element, visited);
1153
+ default:
1154
+ return false;
1155
+ }
1156
+ }
1157
+ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
1158
+ const ctx = new CodeGenContext(resolveType);
1159
+ const name = exportName ?? typeInfo.name;
1160
+ ctx.entryTypeName = typeInfo.name;
1161
+ ctx.entryExportName = name;
1162
+ ctx.coerce = coerce;
1163
+ collectNamedTypes(typeInfo.runtimeType, ctx);
1164
+ ctx.namedTypes.delete(typeInfo.name);
1165
+ const lines = [];
1166
+ lines.push("import { z } from 'zod';");
1167
+ lines.push("");
1168
+ for (const [n, type] of ctx.namedTypes) {
1169
+ lines.push(generateNamedTypeDeclaration(n, type, ctx));
1170
+ }
1171
+ if (ctx.namedTypes.size > 0) lines.push("");
1172
+ const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
1173
+ const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
1174
+ if (hasSelfRef) {
1175
+ lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
1176
+ } else {
1177
+ lines.push(`export const ${name}Schema = ${entryExpr};`);
1178
+ }
1179
+ return lines.join("\n");
1180
+ }
1181
+ var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, COERCE_MAP_HELPER, COERCE_SET_HELPER, HELPERS_FILENAME;
1182
+ var init_generateZodSchema = __esm({
1183
+ "src/ast/generateZodSchema.ts"() {
1184
+ "use strict";
1185
+ CodeGenContext = class {
1186
+ /** 命名类型集合:name → RuntimeType */
1187
+ namedTypes = /* @__PURE__ */ new Map();
1188
+ /** 类型解析器(用于解析 ref 的实际类型) */
1189
+ resolveType;
1190
+ /** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
1191
+ entryTypeName = "";
1192
+ /** 入口类型导出名(exportName,自引用时用此名生成变量名) */
1193
+ entryExportName = "";
1194
+ /**
1195
+ * 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
1196
+ *
1197
+ * true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
1198
+ * 嵌套类型(array/object/tuple/union 等)的元素递归处理。
1199
+ */
1200
+ coerce = false;
1201
+ constructor(resolveType) {
1202
+ this.resolveType = resolveType;
1203
+ }
1204
+ };
1205
+ COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
1206
+ COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
1207
+ 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);';
1208
+ COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
1209
+ HELPERS_FILENAME = "faapi-helpers.js";
1210
+ }
1211
+ });
1212
+
1213
+ // src/cli/generateSchemaFiles.ts
1214
+ var generateSchemaFiles_exports = {};
1215
+ __export(generateSchemaFiles_exports, {
1216
+ generateSchemaFileSource: () => generateSchemaFileSource,
1217
+ generateSchemaFiles: () => generateSchemaFiles,
1218
+ getHelpersImportPath: () => getHelpersImportPath,
1219
+ getRuntimeSchemaPath: () => getRuntimeSchemaPath,
1220
+ getSchemaOutputPath: () => getSchemaOutputPath
1221
+ });
1222
+ import path5 from "path";
1223
+ import fs4 from "fs/promises";
1224
+ function getSchemaOutputPath(sourceFile, dist, rootDir) {
1225
+ let rel = sourceFile.replace(/\\/g, "/");
1226
+ if (rel.startsWith("src/")) {
1227
+ rel = rel.slice(4);
1228
+ }
1229
+ const idx = rel.lastIndexOf("/");
1230
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1231
+ return path5.resolve(rootDir, dist, relDir, "zod.js");
1232
+ }
1233
+ function getRuntimeSchemaPath(filePath, dist, rootDir) {
1234
+ let rel = filePath.replace(/\\/g, "/");
1235
+ if (rel.startsWith("src/")) {
1236
+ rel = rel.slice(4);
1237
+ } else if (rel.startsWith(`${dist}/`)) {
1238
+ rel = rel.slice(dist.length + 1);
1239
+ }
1240
+ const idx = rel.lastIndexOf("/");
1241
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1242
+ return path5.resolve(rootDir, dist, relDir, "zod.js");
1243
+ }
1244
+ function getHelpersImportPath(relDir) {
1245
+ if (!relDir) return `./${HELPERS_FILENAME}`;
1246
+ const depth = relDir.split("/").filter(Boolean).length;
1247
+ return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
1248
+ }
1249
+ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1250
+ const resolveType = (name) => allTypes.get(name)?.runtimeType;
1251
+ const lines = ["import { z } from 'zod';"];
1252
+ const schemaBlocks = [];
1253
+ for (const source of sources) {
1254
+ const { schemaName, typeInfo } = source;
1255
+ if (!typeInfo) {
1256
+ continue;
1257
+ }
1258
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
1259
+ const block = [`// ${schemaName}`];
1260
+ const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1261
+ /^import \{ z \} from 'zod';\s*\n\s*\n/,
1262
+ ""
1263
+ );
1264
+ block.push(schemaCode);
1265
+ block.push("");
1266
+ schemaBlocks.push(block.join("\n"));
1267
+ }
1268
+ const allSchemaCode = schemaBlocks.join("\n");
1269
+ if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
1270
+ lines.push(
1271
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
1272
+ );
1273
+ }
1274
+ lines.push("");
1275
+ lines.push(...schemaBlocks);
1276
+ return lines.join("\n").replace(/\n+$/, "\n");
1277
+ }
1278
+ async function generateSchemaFiles(routes, rootDir, dist) {
1279
+ if (routes.length === 0) return;
1280
+ const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
1281
+ const sourcesByFile = /* @__PURE__ */ new Map();
1282
+ for (const source of sources) {
1283
+ let list = sourcesByFile.get(source.filePath);
1284
+ if (!list) {
1285
+ list = [];
1286
+ sourcesByFile.set(source.filePath, list);
1287
+ }
1288
+ list.push(source);
1289
+ }
1290
+ const fileEntries = [];
1291
+ for (const [filePath, fileSources] of sourcesByFile) {
1292
+ const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
1293
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
1294
+ const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
1295
+ let relForDir = relFile;
1296
+ if (relForDir.startsWith("src/")) {
1297
+ relForDir = relForDir.slice(4);
1298
+ }
1299
+ const dirIdx = relForDir.lastIndexOf("/");
1300
+ const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
1301
+ const helpersImportPath = getHelpersImportPath(zodRelDir);
1302
+ const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
1303
+ fileEntries.push({ outputPath, source });
1304
+ }
1305
+ const allSourceCode = fileEntries.map((e) => e.source).join("\n");
1306
+ if (usesCoerceHelpers(allSourceCode)) {
1307
+ const helpersPath = path5.resolve(rootDir, dist, HELPERS_FILENAME);
1308
+ await writeSchemaFile(helpersPath, generateHelpersFileSource());
1309
+ }
1310
+ await Promise.all(
1311
+ fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
1312
+ );
1313
+ }
1314
+ async function writeSchemaFile(outputPath, source) {
1315
+ await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
1316
+ await fs4.writeFile(outputPath, source, "utf-8");
1317
+ }
1318
+ var init_generateSchemaFiles = __esm({
1319
+ "src/cli/generateSchemaFiles.ts"() {
1320
+ "use strict";
1321
+ init_collectRouteSchemaSources();
1322
+ init_generateZodSchema();
1323
+ }
1324
+ });
1325
+
1326
+ // src/index.ts
1327
+ init_createProgram();
1328
+ init_extractHandlerTypes();
1329
+ init_resolveTypeNode();
1330
+ init_inputType();
1331
+ init_collectRouteSchemaSources();
1332
+
1333
+ // src/middleware/cors.ts
1334
+ var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1335
+ function cors(options = {}) {
1336
+ const {
1337
+ origin = true,
1338
+ methods = DEFAULT_METHODS,
1339
+ allowedHeaders,
1340
+ exposeHeaders,
1341
+ credentials = false,
1342
+ maxAge
1343
+ } = options;
1344
+ return async (ctx, next) => {
1345
+ const reqOrigin = ctx.headers.get("origin");
1346
+ if (!reqOrigin) {
1347
+ await next();
1348
+ return;
1349
+ }
1350
+ let allowOrigin = null;
1351
+ if (origin === true) {
1352
+ allowOrigin = reqOrigin;
1353
+ } else if (typeof origin === "string") {
1354
+ allowOrigin = reqOrigin === origin ? origin : null;
1355
+ } else if (Array.isArray(origin)) {
1356
+ allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
1357
+ }
1358
+ if (!allowOrigin) {
1359
+ await next();
1360
+ return;
1361
+ }
1362
+ ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
1363
+ if (origin === true || Array.isArray(origin)) {
1364
+ const existingVary = ctx.headers.get("vary");
1365
+ if (existingVary) {
1366
+ if (!existingVary.toLowerCase().includes("origin")) {
1367
+ ctx.setHeader("Vary", `${existingVary}, Origin`);
1368
+ }
1369
+ } else {
1370
+ ctx.setHeader("Vary", "Origin");
1371
+ }
1372
+ }
1373
+ ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
1374
+ if (allowedHeaders) {
1375
+ ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
1376
+ } else {
1377
+ const requestHeaders = ctx.headers.get("access-control-request-headers");
1378
+ if (requestHeaders) {
1379
+ ctx.setHeader("Access-Control-Allow-Headers", requestHeaders);
1380
+ }
1381
+ }
1382
+ if (exposeHeaders && exposeHeaders.length > 0) {
1383
+ ctx.setHeader("Access-Control-Expose-Headers", exposeHeaders.join(", "));
1384
+ }
1385
+ if (credentials) {
1386
+ ctx.setHeader("Access-Control-Allow-Credentials", "true");
1387
+ }
1388
+ if (maxAge !== void 0) {
1389
+ ctx.setHeader("Access-Control-Max-Age", String(maxAge));
1390
+ }
1391
+ if (ctx.method === "OPTIONS") {
1392
+ return new Response(null, { status: 204 });
1393
+ }
1394
+ await next();
1395
+ };
1396
+ }
1397
+
1398
+ // src/middleware/logger.ts
1399
+ function logger(options = {}) {
1400
+ return async (ctx, next) => {
1401
+ const log = options.log ?? console.log;
1402
+ const start = Date.now();
1403
+ try {
1404
+ const response = await next();
1405
+ const duration = Date.now() - start;
1406
+ const entry = {
1407
+ method: ctx.method,
1408
+ path: ctx.path,
1409
+ status: response.status,
1410
+ durationMs: duration
1411
+ };
1412
+ log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
1413
+ return response;
1414
+ } catch (err) {
1415
+ const duration = Date.now() - start;
1416
+ const message = err instanceof Error ? err.message : String(err);
1417
+ const status = err?.statusCode ?? 500;
1418
+ const entry = {
1419
+ method: ctx.method,
1420
+ path: ctx.path,
1421
+ status,
1422
+ durationMs: duration,
1423
+ error: message
1424
+ };
1425
+ log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
1426
+ throw err;
1427
+ }
1428
+ };
1429
+ }
1430
+
1431
+ // src/middleware/helmet.ts
1432
+ var DEFAULTS = {
1433
+ contentSecurityPolicy: "default-src 'self'",
1434
+ xFrameOptions: "SAMEORIGIN",
1435
+ xContentTypeOptions: true,
1436
+ referrerPolicy: "no-referrer",
1437
+ strictTransportSecurity: "max-age=31536000; includeSubDomains",
1438
+ xDnsPrefetchControl: true,
1439
+ xDownloadOptions: true,
1440
+ xPermittedCrossDomainPolicies: "none",
1441
+ crossOriginOpenerPolicy: "same-origin",
1442
+ crossOriginResourcePolicy: "same-origin",
1443
+ crossOriginEmbedderPolicy: false,
1444
+ originAgentCluster: true,
1445
+ xPoweredBy: true
1446
+ };
1447
+ function helmet(options = {}) {
1448
+ const opts = { ...DEFAULTS, ...options };
1449
+ return async (ctx, next) => {
1450
+ if (opts.contentSecurityPolicy !== false) {
1451
+ ctx.setHeader("Content-Security-Policy", opts.contentSecurityPolicy);
1452
+ }
1453
+ if (opts.xFrameOptions !== false) {
1454
+ ctx.setHeader("X-Frame-Options", opts.xFrameOptions);
1455
+ }
1456
+ if (opts.xContentTypeOptions) {
1457
+ ctx.setHeader("X-Content-Type-Options", "nosniff");
1458
+ }
1459
+ if (opts.referrerPolicy !== false) {
1460
+ ctx.setHeader("Referrer-Policy", opts.referrerPolicy);
1461
+ }
1462
+ if (opts.strictTransportSecurity !== false) {
1463
+ ctx.setHeader("Strict-Transport-Security", opts.strictTransportSecurity);
1464
+ }
1465
+ if (opts.xDnsPrefetchControl) {
1466
+ ctx.setHeader("X-DNS-Prefetch-Control", "off");
1467
+ }
1468
+ if (opts.xDownloadOptions) {
1469
+ ctx.setHeader("X-Download-Options", "noopen");
1470
+ }
1471
+ if (opts.xPermittedCrossDomainPolicies !== false) {
1472
+ ctx.setHeader("X-Permitted-Cross-Domain-Policies", opts.xPermittedCrossDomainPolicies);
1473
+ }
1474
+ if (opts.crossOriginOpenerPolicy !== false) {
1475
+ ctx.setHeader("Cross-Origin-Opener-Policy", opts.crossOriginOpenerPolicy);
1476
+ }
1477
+ if (opts.crossOriginResourcePolicy !== false) {
1478
+ ctx.setHeader("Cross-Origin-Resource-Policy", opts.crossOriginResourcePolicy);
1479
+ }
1480
+ if (opts.crossOriginEmbedderPolicy !== false) {
1481
+ ctx.setHeader("Cross-Origin-Embedder-Policy", opts.crossOriginEmbedderPolicy);
1482
+ }
1483
+ if (opts.originAgentCluster) {
1484
+ ctx.setHeader("Origin-Agent-Cluster", "?1");
1485
+ }
1486
+ if (opts.xPoweredBy) {
1487
+ ctx.setHeader("X-Powered-By", "faapi");
1488
+ }
1489
+ return await next();
1490
+ };
1491
+ }
1492
+
1493
+ // src/config/loadConfig.ts
1494
+ import path2 from "path";
1495
+ import fs from "fs";
1496
+
1497
+ // src/utils/importWithCacheBust.ts
1498
+ import { pathToFileURL } from "url";
1499
+ var loadTs;
1500
+ function setLoadTimestamp(ts7) {
1501
+ loadTs = ts7;
1502
+ }
1503
+ function getVitestImportActual() {
1504
+ const vi = globalThis.vi;
1505
+ if (typeof vi?.importActual !== "function") return void 0;
1506
+ return vi.importActual.bind(vi);
1507
+ }
1508
+ async function importWithCacheBust(filePath, bustViteCache = false) {
1509
+ const importActual = getVitestImportActual();
1510
+ if (importActual) {
1511
+ if (bustViteCache) {
1512
+ let url2 = pathToFileURL(filePath).href;
1513
+ url2 += `?t=${Date.now()}`;
1514
+ return await import(url2);
1515
+ }
1516
+ return await importActual(filePath);
1517
+ }
1518
+ let url = pathToFileURL(filePath).href;
1519
+ if (loadTs !== void 0) {
1520
+ url += `?t=${loadTs}`;
1521
+ }
1522
+ return await import(url);
1037
1523
  }
1038
1524
 
1039
1525
  // src/config/loadConfig.ts
@@ -1160,14 +1646,14 @@ var ValidationError = class extends FaapiError {
1160
1646
  issues;
1161
1647
  };
1162
1648
  var RouteNotFoundError = class extends FaapiError {
1163
- constructor(path11) {
1164
- super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
1649
+ constructor(path15) {
1650
+ super("ROUTE_NOT_FOUND", `Route not found: ${path15}`, 404);
1165
1651
  this.name = "RouteNotFoundError";
1166
1652
  }
1167
1653
  };
1168
1654
  var MethodNotAllowedError = class extends FaapiError {
1169
- constructor(method, path11, allowedMethods) {
1170
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
1655
+ constructor(method, path15, allowedMethods) {
1656
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path15}`, 405);
1171
1657
  this.allowedMethods = allowedMethods;
1172
1658
  this.name = "MethodNotAllowedError";
1173
1659
  }
@@ -1497,6 +1983,9 @@ async function toResponse(value, meta) {
1497
1983
  });
1498
1984
  }
1499
1985
 
1986
+ // src/injection/injectParams.ts
1987
+ init_resolveInjection();
1988
+
1500
1989
  // src/utils/queryToObject.ts
1501
1990
  function queryToObject(params) {
1502
1991
  const result = {};
@@ -1648,9 +2137,9 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1648
2137
  }
1649
2138
 
1650
2139
  // src/testServer.ts
1651
- import path8 from "path";
2140
+ import path12 from "path";
1652
2141
  import os from "os";
1653
- import fs5 from "fs/promises";
2142
+ import fs11 from "fs/promises";
1654
2143
 
1655
2144
  // src/router/scanRoutes.ts
1656
2145
  import fg from "fast-glob";
@@ -1660,14 +2149,11 @@ import fs3 from "fs";
1660
2149
  // src/router/constants.ts
1661
2150
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1662
2151
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
1663
- function isHttpMethod(value) {
1664
- return HTTP_METHOD_SET.has(value);
1665
- }
1666
2152
 
1667
2153
  // src/utils/normalizePath.ts
1668
- function normalizePath(path11) {
1669
- if (!path11) return "";
1670
- let result = path11.replace(/\\/g, "/");
2154
+ function normalizePath(path15) {
2155
+ if (!path15) return "";
2156
+ let result = path15.replace(/\\/g, "/");
1671
2157
  result = result.replace(/\/+/g, "/");
1672
2158
  result = result.replace(/\/+$/, "");
1673
2159
  if (result && !result.startsWith("/")) {
@@ -1757,50 +2243,11 @@ async function loadMiddlewaresFile(filePath) {
1757
2243
  return { middlewares: [], injectors: {} };
1758
2244
  }
1759
2245
  }
1760
-
1761
- // src/router/scanRoutes.ts
1762
- var APP_DIR = "src";
1763
- function toProdAbsPath(sourceAbsPath, rootDir, dist) {
1764
- let rel = path4.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
1765
- if (rel.startsWith(`${APP_DIR}/`)) {
1766
- rel = rel.slice(APP_DIR.length + 1);
1767
- }
1768
- const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
1769
- return path4.resolve(rootDir, prodRel);
1770
- }
1771
- async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
1772
- const routeDir = path4.dirname(routeFilePath);
1773
- const resolvedRoot = path4.resolve(rootDir);
1774
- const mwPaths = [];
1775
- let currentDir = path4.resolve(rootDir, routeDir);
1776
- while (true) {
1777
- if (dist) {
1778
- const mwPath = path4.join(currentDir, "middlewares.js");
1779
- const absMwPath = path4.resolve(rootDir, mwPath);
1780
- const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
1781
- if (fs3.existsSync(prodAbsMwPath)) {
1782
- mwPaths.push(prodAbsMwPath);
1783
- }
1784
- } else {
1785
- for (const ext of [".ts", ".js"]) {
1786
- const mwPath = path4.join(currentDir, `middlewares${ext}`);
1787
- const absMwPath = path4.resolve(rootDir, mwPath);
1788
- if (fs3.existsSync(absMwPath)) {
1789
- mwPaths.push(absMwPath);
1790
- break;
1791
- }
1792
- }
1793
- }
1794
- if (currentDir === resolvedRoot) break;
1795
- const parentDir = path4.dirname(currentDir);
1796
- if (parentDir === currentDir) break;
1797
- currentDir = parentDir;
1798
- }
1799
- if (mwPaths.length === 0) return void 0;
1800
- mwPaths.reverse();
2246
+ async function loadMergedMiddlewares(middlewarePaths) {
2247
+ if (middlewarePaths.length === 0) return void 0;
1801
2248
  const mergedMiddlewares = [];
1802
2249
  const mergedInjectors = {};
1803
- for (const absMwPath of mwPaths) {
2250
+ for (const absMwPath of middlewarePaths) {
1804
2251
  let bundle = getCachedMiddlewares(absMwPath);
1805
2252
  if (bundle === void 0) {
1806
2253
  bundle = await loadMiddlewaresFile(absMwPath);
@@ -1816,31 +2263,63 @@ async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
1816
2263
  }
1817
2264
  return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
1818
2265
  }
1819
- async function extractMethodsFromHandler(absPath) {
1820
- try {
1821
- const module = await importWithCacheBust(absPath);
1822
- const methods = [];
1823
- for (const key of Object.keys(module)) {
1824
- if (isHttpMethod(key) && typeof module[key] === "function") {
1825
- methods.push(key);
2266
+
2267
+ // src/router/scanRoutes.ts
2268
+ var HTTP_OR_WS_EXPORT_RE = new RegExp(
2269
+ String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
2270
+ "g"
2271
+ );
2272
+ function extractExportsFromSource(source) {
2273
+ const names = /* @__PURE__ */ new Set();
2274
+ let match;
2275
+ HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
2276
+ while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
2277
+ names.add(match[1]);
2278
+ }
2279
+ return names;
2280
+ }
2281
+ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
2282
+ const routeDir = path4.dirname(routeFilePath);
2283
+ const resolvedRoot = path4.resolve(rootDir);
2284
+ const paths = [];
2285
+ let currentDir = path4.resolve(rootDir, routeDir);
2286
+ while (true) {
2287
+ if (dist) {
2288
+ const mwTsPath = path4.join(currentDir, "middlewares.ts");
2289
+ const mwJsPath = path4.join(currentDir, "middlewares.js");
2290
+ const absTsPath = path4.resolve(rootDir, mwTsPath);
2291
+ const absJsPath = path4.resolve(rootDir, mwJsPath);
2292
+ const absMwPath = fs3.existsSync(absTsPath) ? absTsPath : fs3.existsSync(absJsPath) ? absJsPath : null;
2293
+ if (absMwPath) {
2294
+ const relMwPath = path4.relative(rootDir, absMwPath);
2295
+ const prodAbsPath = path4.resolve(rootDir, toProdFilePath(relMwPath, dist));
2296
+ paths.push(prodAbsPath);
2297
+ }
2298
+ } else {
2299
+ for (const ext of [".ts", ".js"]) {
2300
+ const mwPath = path4.join(currentDir, `middlewares${ext}`);
2301
+ const absMwPath = path4.resolve(rootDir, mwPath);
2302
+ if (fs3.existsSync(absMwPath)) {
2303
+ paths.push(absMwPath);
2304
+ break;
2305
+ }
1826
2306
  }
1827
2307
  }
1828
- return methods;
1829
- } catch (err) {
1830
- const reason = err instanceof Error ? err.message : String(err);
1831
- console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25 ${absPath}: ${reason}`);
1832
- return [];
2308
+ if (currentDir === resolvedRoot) break;
2309
+ const parentDir = path4.dirname(currentDir);
2310
+ if (parentDir === currentDir) break;
2311
+ currentDir = parentDir;
1833
2312
  }
2313
+ paths.reverse();
2314
+ return paths;
1834
2315
  }
1835
- async function hasWsExport(absPath) {
1836
- try {
1837
- const module = await importWithCacheBust(absPath);
1838
- return typeof module["WS"] === "function";
1839
- } catch (err) {
1840
- const reason = err instanceof Error ? err.message : String(err);
1841
- console.warn(`[faapi] \u52A0\u8F7D\u8DEF\u7531\u6587\u4EF6\u5931\u8D25\uFF08WS \u68C0\u6D4B\uFF09${absPath}: ${reason}`);
1842
- return false;
2316
+ function toProdFilePath(filePath, dist) {
2317
+ let rel = filePath.replace(/\\/g, "/");
2318
+ if (rel.startsWith("src/")) {
2319
+ rel = rel.slice(4);
1843
2320
  }
2321
+ const jsPath = rel.replace(/\.ts$/, ".js");
2322
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
1844
2323
  }
1845
2324
  async function scanRoutes(rootDir, patterns, dist) {
1846
2325
  const files = await fg(patterns, {
@@ -1855,13 +2334,21 @@ async function scanRoutes(rootDir, patterns, dist) {
1855
2334
  const fileName = normalizedFile.split("/").pop();
1856
2335
  if (fileName === "handler.ts" || fileName === "handler.js") {
1857
2336
  const absPath = path4.resolve(rootDir, normalizedFile);
1858
- const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
1859
2337
  const urlPath = filePathToUrlPath(normalizedFile);
1860
2338
  const paramNames = extractParamNames(urlPath);
1861
2339
  const isDynamic = paramNames.length > 0;
1862
2340
  const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
1863
- const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
1864
- const methods = await extractMethodsFromHandler(importPath);
2341
+ let middlewarePaths;
2342
+ let middlewareBundle;
2343
+ if (dist) {
2344
+ middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
2345
+ } else {
2346
+ const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
2347
+ middlewareBundle = await loadMergedMiddlewares(mwPaths);
2348
+ }
2349
+ const source = await fs3.promises.readFile(absPath, "utf8").catch(() => "");
2350
+ const exportNames = extractExportsFromSource(source);
2351
+ const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
1865
2352
  for (const method of methods) {
1866
2353
  routes.push({
1867
2354
  method,
@@ -1870,18 +2357,19 @@ async function scanRoutes(rootDir, patterns, dist) {
1870
2357
  paramNames,
1871
2358
  isDynamic,
1872
2359
  isCatchAll: isCatchAll || void 0,
2360
+ middlewarePaths,
1873
2361
  middlewares: middlewareBundle?.middlewares,
1874
2362
  injectors: middlewareBundle?.injectors
1875
2363
  });
1876
2364
  }
1877
- const hasWs = await hasWsExport(importPath);
1878
- if (hasWs) {
2365
+ if (exportNames.has("WS")) {
1879
2366
  wsRoutes.push({
1880
2367
  urlPath,
1881
2368
  filePath: normalizedFile,
1882
2369
  paramNames,
1883
2370
  isDynamic,
1884
2371
  isCatchAll: isCatchAll || void 0,
2372
+ middlewarePaths,
1885
2373
  middlewares: middlewareBundle?.middlewares,
1886
2374
  injectors: middlewareBundle?.injectors
1887
2375
  });
@@ -1910,403 +2398,357 @@ function sortRoutes(routes) {
1910
2398
  });
1911
2399
  }
1912
2400
 
1913
- // src/cli/generateSchemaFiles.ts
1914
- import path5 from "path";
1915
- import fs4 from "fs/promises";
2401
+ // src/testServer.ts
2402
+ init_generateSchemaFiles();
1916
2403
 
1917
- // src/ast/generateZodSchema.ts
1918
- var CodeGenContext = class {
1919
- /** 命名类型集合:name → RuntimeType */
1920
- namedTypes = /* @__PURE__ */ new Map();
1921
- /** 类型解析器(用于解析 ref 的实际类型) */
1922
- resolveType;
1923
- /** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
1924
- entryTypeName = "";
1925
- /** 入口类型导出名(exportName,自引用时用此名生成变量名) */
1926
- entryExportName = "";
1927
- /**
1928
- * 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
1929
- *
1930
- * true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
1931
- * 嵌套类型(array/object/tuple/union 等)的元素递归处理。
1932
- */
1933
- coerce = false;
1934
- constructor(resolveType) {
1935
- this.resolveType = resolveType;
1936
- }
1937
- };
1938
- function collectNamedTypes(type, ctx) {
1939
- switch (type.kind) {
1940
- case "string":
1941
- case "number":
1942
- case "boolean":
1943
- case "bigint":
1944
- case "null":
1945
- case "undefined":
1946
- case "any":
1947
- case "unknown":
1948
- case "literal":
1949
- case "date":
1950
- return;
1951
- case "array":
1952
- collectNamedTypes(type.element, ctx);
1953
- return;
1954
- case "tuple":
1955
- for (const el of type.elements) {
1956
- collectNamedTypes(el.type, ctx);
1957
- }
1958
- return;
1959
- case "object":
1960
- for (const prop of type.properties) {
1961
- collectNamedTypes(prop.type, ctx);
1962
- }
1963
- return;
1964
- case "union":
1965
- for (const member of type.members) {
1966
- collectNamedTypes(member, ctx);
1967
- }
1968
- return;
1969
- case "record":
1970
- collectNamedTypes(type.key, ctx);
1971
- collectNamedTypes(type.value, ctx);
1972
- return;
1973
- case "map":
1974
- collectNamedTypes(type.key, ctx);
1975
- collectNamedTypes(type.value, ctx);
1976
- return;
1977
- case "set":
1978
- collectNamedTypes(type.element, ctx);
1979
- return;
1980
- case "ref": {
1981
- if (ctx.namedTypes.has(type.name)) return;
1982
- ctx.namedTypes.set(type.name, { kind: "any" });
1983
- const resolved = ctx.resolveType(type.name);
1984
- if (resolved) {
1985
- ctx.namedTypes.set(type.name, resolved);
1986
- collectNamedTypes(resolved, ctx);
1987
- }
1988
- return;
1989
- }
1990
- }
1991
- }
1992
- function runtimeTypeToZodExpression(type, ctx, constraints) {
1993
- const expr = baseExpression(type, ctx);
1994
- const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
1995
- if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
1996
- return wrapCoercePreprocess(type.kind, withConstraints);
1997
- }
1998
- return withConstraints;
1999
- }
2000
- function applyConstraints(baseExpr, constraints, typeKind) {
2001
- const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
2002
- return `${baseExpr}${suffix}`;
2003
- }
2004
- function constraintToZodChain(constraint, _typeKind) {
2005
- switch (constraint.kind) {
2006
- case "max":
2007
- return `.max(${constraint.value})`;
2008
- case "min":
2009
- return `.min(${constraint.value})`;
2010
- case "int":
2011
- return ".int()";
2012
- case "positive":
2013
- return ".positive()";
2014
- case "negative":
2015
- return ".negative()";
2016
- case "nonnegative":
2017
- return ".nonnegative()";
2018
- case "nonpositive":
2019
- return ".nonpositive()";
2020
- case "maxLength":
2021
- return `.max(${constraint.value})`;
2022
- case "minLength":
2023
- return `.min(${constraint.value})`;
2024
- case "length":
2025
- return `.length(${constraint.value})`;
2026
- case "regex": {
2027
- const flags = constraint.flags ?? "";
2028
- return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
2029
- }
2030
- case "email":
2031
- return ".email()";
2032
- case "url":
2033
- return ".url()";
2034
- case "uuid":
2035
- return ".uuid()";
2036
- }
2037
- }
2038
- function baseExpression(type, ctx) {
2039
- switch (type.kind) {
2040
- case "string":
2041
- return "z.string()";
2042
- case "number":
2043
- return "z.number()";
2044
- case "boolean":
2045
- return "z.boolean()";
2046
- case "bigint":
2047
- return "z.never()";
2048
- case "null":
2049
- return "z.null()";
2050
- case "undefined":
2051
- return "z.undefined()";
2052
- case "any":
2053
- case "unknown":
2054
- return "z.unknown()";
2055
- case "literal":
2056
- return `z.literal(${JSON.stringify(type.value)})`;
2057
- case "array":
2058
- return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
2059
- case "tuple":
2060
- return generateTupleExpression(type.elements, ctx);
2061
- case "object":
2062
- return generateObjectExpression(type.properties, ctx);
2063
- case "union":
2064
- return generateUnionExpression(type.members, ctx);
2065
- case "date":
2066
- return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
2067
- case "record":
2068
- return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
2069
- case "map":
2070
- return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
2071
- case "set":
2072
- return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
2073
- case "ref":
2074
- if (type.name === ctx.entryTypeName) {
2075
- return `${ctx.entryExportName}Schema`;
2076
- }
2077
- return `${type.name}Schema`;
2078
- }
2079
- }
2080
- var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2081
- var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
2082
- 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);';
2083
- var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2084
- var HELPERS_FILENAME = "faapi-helpers.js";
2085
- function generateHelpersFileSource() {
2086
- return [
2087
- "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
2088
- COERCE_NUMBER_HELPER,
2089
- COERCE_BOOLEAN_HELPER,
2090
- COERCE_MAP_HELPER,
2091
- COERCE_SET_HELPER,
2092
- ""
2093
- ].join("\n");
2094
- }
2095
- function usesCoerceHelpers(code) {
2096
- return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
2097
- }
2098
- function wrapCoercePreprocess(kind, inner) {
2099
- if (kind === "number") {
2100
- return `z.preprocess(coerceNumber, ${inner})`;
2101
- }
2102
- return `z.preprocess(coerceBoolean, ${inner})`;
2103
- }
2104
- function generateTupleExpression(elements, ctx) {
2105
- const fixedExprs = [];
2106
- const fixedOptional = [];
2107
- let restExpression = "";
2108
- let restStarted = false;
2109
- for (const el of elements) {
2110
- if (el.rest) {
2111
- restExpression = runtimeTypeToZodExpression(el.type, ctx);
2112
- restStarted = true;
2113
- } else if (!restStarted) {
2114
- fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
2115
- fixedOptional.push(el.optional);
2116
- }
2117
- }
2118
- if (restExpression) {
2119
- return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
2120
- }
2121
- const hasOptional = fixedOptional.some((o) => o);
2122
- if (!hasOptional) {
2123
- return `z.tuple([${fixedExprs.join(", ")}])`;
2124
- }
2125
- const variants = [];
2126
- for (let len = fixedExprs.length; len >= 0; len--) {
2127
- const removed = fixedOptional.slice(len);
2128
- if (removed.length > 0 && removed.some((o) => !o)) {
2129
- break;
2130
- }
2131
- const subset = fixedExprs.slice(0, len);
2132
- variants.push(`z.tuple([${subset.join(", ")}])`);
2133
- }
2134
- variants.reverse();
2135
- if (variants.length === 1) {
2136
- return variants[0];
2137
- }
2138
- return `z.union([${variants.join(", ")}])`;
2139
- }
2140
- function generateObjectExpression(properties, ctx) {
2141
- const fields = properties.map((prop) => {
2142
- const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
2143
- const finalExpr = prop.optional ? `${expr}.optional()` : expr;
2144
- return `${JSON.stringify(prop.name)}: ${finalExpr}`;
2145
- });
2146
- return `z.object({ ${fields.join(", ")} })`;
2147
- }
2148
- function generateUnionExpression(members, ctx) {
2149
- const hasNull = members.some((m) => m.kind === "null");
2150
- const nonNull = members.filter((m) => m.kind !== "null");
2151
- if (hasNull && nonNull.length === 1) {
2152
- return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
2153
- }
2154
- if (hasNull) {
2155
- const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2156
- return `z.union([${unionInner2}]).nullable()`;
2404
+ // src/validator/validateInput.ts
2405
+ init_schemaName();
2406
+
2407
+ // src/cli/compileOnDemand.ts
2408
+ import path9 from "path";
2409
+ import fs8 from "fs";
2410
+
2411
+ // src/cli/compileDevRoutes.ts
2412
+ import path8 from "path";
2413
+ import fs7 from "fs";
2414
+ import fg2 from "fast-glob";
2415
+
2416
+ // src/cli/aliasPlugin.ts
2417
+ import path7 from "path";
2418
+ import fs6 from "fs";
2419
+
2420
+ // src/utils/resolveAlias.ts
2421
+ function resolveAlias(specifier, config) {
2422
+ const candidates = [];
2423
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2424
+ const wildcardIndex = pattern.indexOf("*");
2425
+ if (wildcardIndex === -1) {
2426
+ if (specifier === pattern) {
2427
+ candidates.push(...targets);
2428
+ }
2429
+ continue;
2430
+ }
2431
+ const prefix = pattern.slice(0, wildcardIndex);
2432
+ const suffix = pattern.slice(wildcardIndex + 1);
2433
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
2434
+ const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
2435
+ for (const target of targets) {
2436
+ candidates.push(target.replace("*", captured));
2437
+ }
2438
+ }
2157
2439
  }
2158
- const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2159
- return `z.union([${unionInner}])`;
2440
+ return candidates;
2160
2441
  }
2161
- function generateNamedTypeDeclaration(name, type, ctx) {
2162
- const expr = runtimeTypeToZodExpression(type, ctx);
2163
- const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
2164
- if (hasRef) {
2165
- return `const ${name}Schema = z.lazy(() => ${expr});`;
2166
- }
2167
- return `const ${name}Schema = ${expr};`;
2442
+
2443
+ // src/utils/readTsconfig.ts
2444
+ import ts6 from "typescript";
2445
+ import path6 from "path";
2446
+ import fs5 from "fs";
2447
+ function readTsconfig(rootDir) {
2448
+ const tsconfigPath = path6.resolve(rootDir, "tsconfig.json");
2449
+ if (!fs5.existsSync(tsconfigPath)) return null;
2450
+ const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2451
+ if (configFile.error || !configFile.config) return null;
2452
+ const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2453
+ const baseUrl = parsed.options.baseUrl ?? rootDir;
2454
+ const rawPaths = parsed.options.paths;
2455
+ if (!rawPaths) return null;
2456
+ const paths = {};
2457
+ for (const [pattern, targets] of Object.entries(rawPaths)) {
2458
+ paths[pattern] = targets.map((t) => path6.resolve(baseUrl, t));
2459
+ }
2460
+ return { baseUrl, paths };
2168
2461
  }
2169
- function containsRef(type, visited) {
2170
- switch (type.kind) {
2171
- case "ref":
2172
- return visited.has(type.name);
2173
- case "array":
2174
- return containsRef(type.element, visited);
2175
- case "tuple":
2176
- return type.elements.some((el) => containsRef(el.type, visited));
2177
- case "object":
2178
- return type.properties.some((prop) => containsRef(prop.type, visited));
2179
- case "union":
2180
- return type.members.some((m) => containsRef(m, visited));
2181
- case "record":
2182
- return containsRef(type.key, visited) || containsRef(type.value, visited);
2183
- case "map":
2184
- return containsRef(type.key, visited) || containsRef(type.value, visited);
2185
- case "set":
2186
- return containsRef(type.element, visited);
2187
- default:
2188
- return false;
2462
+
2463
+ // src/cli/aliasPlugin.ts
2464
+ function toProdExtension(filePath) {
2465
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2466
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2467
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2468
+ return filePath;
2469
+ }
2470
+ function toProdImportPath(sourceFile, importer) {
2471
+ const importerDir = path7.dirname(importer);
2472
+ let rel = path7.relative(importerDir, sourceFile);
2473
+ rel = rel.split(path7.sep).join("/");
2474
+ if (!rel.startsWith(".")) rel = "./" + rel;
2475
+ return toProdExtension(rel);
2476
+ }
2477
+ function toRealPath(p) {
2478
+ try {
2479
+ return fs6.realpathSync(p);
2480
+ } catch {
2481
+ return p;
2189
2482
  }
2190
2483
  }
2191
- function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
2192
- const ctx = new CodeGenContext(resolveType);
2193
- const name = exportName ?? typeInfo.name;
2194
- ctx.entryTypeName = typeInfo.name;
2195
- ctx.entryExportName = name;
2196
- ctx.coerce = coerce;
2197
- collectNamedTypes(typeInfo.runtimeType, ctx);
2198
- ctx.namedTypes.delete(typeInfo.name);
2199
- const lines = [];
2200
- lines.push("import { z } from 'zod';");
2201
- lines.push("");
2202
- for (const [n, type] of ctx.namedTypes) {
2203
- lines.push(generateNamedTypeDeclaration(n, type, ctx));
2204
- }
2205
- if (ctx.namedTypes.size > 0) lines.push("");
2206
- const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
2207
- const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
2208
- if (hasSelfRef) {
2209
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
2210
- } else {
2211
- lines.push(`export const ${name}Schema = ${entryExpr};`);
2484
+ function isInsideDir(filePath, dir) {
2485
+ const rel = path7.relative(dir, filePath);
2486
+ return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
2487
+ }
2488
+ var APP_DIR = "src";
2489
+ function toStrippedProdImportPath(sourceFile, rootDir) {
2490
+ const appDirAbs = toRealPath(path7.resolve(rootDir, APP_DIR));
2491
+ const sourceReal = toRealPath(sourceFile);
2492
+ let rel = path7.relative(appDirAbs, sourceReal);
2493
+ rel = rel.split(path7.sep).join("/");
2494
+ if (!rel.startsWith(".")) rel = "./" + rel;
2495
+ return toProdExtension(rel);
2496
+ }
2497
+ var PROD_EXTS = [".js", ".mjs", ".cjs"];
2498
+ var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2499
+ var INDEX_EXTS = [
2500
+ "/index.ts",
2501
+ "/index.tsx",
2502
+ "/index.js",
2503
+ "/index.jsx",
2504
+ "/index.mjs",
2505
+ "/index.cjs"
2506
+ ];
2507
+ function resolveRelativeSpecifier(importer, specifier) {
2508
+ const importerDir = path7.dirname(importer);
2509
+ const base = path7.resolve(importerDir, specifier);
2510
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2511
+ return fs6.existsSync(base) ? base : null;
2512
+ }
2513
+ if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2514
+ return fs6.existsSync(base) ? base : null;
2515
+ }
2516
+ for (const ext of SOURCE_EXTS) {
2517
+ const file = base + ext;
2518
+ if (fs6.existsSync(file)) return file;
2519
+ }
2520
+ for (const indexExt of INDEX_EXTS) {
2521
+ const file = base + indexExt;
2522
+ if (fs6.existsSync(file)) return file;
2212
2523
  }
2213
- return lines.join("\n");
2524
+ return null;
2525
+ }
2526
+ function createAliasPlugin(config, options) {
2527
+ const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2528
+ const appDirAbs = options?.rootDir ? toRealPath(path7.resolve(options.rootDir, APP_DIR)) : null;
2529
+ return {
2530
+ name: "faapi-alias",
2531
+ setup(build) {
2532
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2533
+ let source;
2534
+ try {
2535
+ source = fs6.readFileSync(args.path, "utf8");
2536
+ } catch {
2537
+ return void 0;
2538
+ }
2539
+ const importer = args.path;
2540
+ const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2541
+ let modified = false;
2542
+ const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2543
+ if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2544
+ return full;
2545
+ }
2546
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2547
+ const resolved = resolveRelativeSpecifier(importer, specifier);
2548
+ if (resolved) {
2549
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2550
+ return full;
2551
+ }
2552
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
2553
+ modified = true;
2554
+ return `${prefix}${quote}${toStrippedProdImportPath(
2555
+ resolved,
2556
+ options.rootDir
2557
+ )}${quote}`;
2558
+ }
2559
+ modified = true;
2560
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
2561
+ }
2562
+ return full;
2563
+ }
2564
+ const candidates = resolveAlias(specifier, config);
2565
+ for (const candidate of candidates) {
2566
+ for (const ext of SOURCE_EXTS) {
2567
+ const file = candidate + ext;
2568
+ if (fs6.existsSync(file)) {
2569
+ modified = true;
2570
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2571
+ return `${prefix}${quote}${toStrippedProdImportPath(
2572
+ file,
2573
+ options.rootDir
2574
+ )}${quote}`;
2575
+ }
2576
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2577
+ }
2578
+ }
2579
+ for (const indexExt of INDEX_EXTS) {
2580
+ const file = candidate + indexExt;
2581
+ if (fs6.existsSync(file)) {
2582
+ modified = true;
2583
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2584
+ return `${prefix}${quote}${toStrippedProdImportPath(
2585
+ file,
2586
+ options.rootDir
2587
+ )}${quote}`;
2588
+ }
2589
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2590
+ }
2591
+ }
2592
+ }
2593
+ return full;
2594
+ });
2595
+ if (!modified) return void 0;
2596
+ return { contents: newSource, loader: "default" };
2597
+ });
2598
+ }
2599
+ };
2600
+ }
2601
+ function buildAliasPlugins(rootDir) {
2602
+ const tsconfig = readTsconfig(rootDir);
2603
+ return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2214
2604
  }
2215
2605
 
2216
- // src/cli/generateSchemaFiles.ts
2217
- function getSchemaOutputPath(sourceFile, dist, rootDir) {
2218
- let rel = sourceFile.replace(/\\/g, "/");
2219
- if (rel.startsWith("src/")) {
2220
- rel = rel.slice(4);
2606
+ // src/cli/compileDevRoutes.ts
2607
+ var APP_DIR2 = "src";
2608
+ async function compileDevRoutes(options) {
2609
+ const { rootDir, dist, files, logLevel = "silent" } = options;
2610
+ const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2611
+ cwd: rootDir,
2612
+ onlyFiles: true,
2613
+ absolute: true,
2614
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
2615
+ });
2616
+ if (entryPoints.length === 0) {
2617
+ return { compiledFiles: [] };
2618
+ }
2619
+ const absDist = path8.resolve(rootDir, dist);
2620
+ await fs7.promises.mkdir(absDist, { recursive: true });
2621
+ const plugins = buildAliasPlugins(rootDir);
2622
+ const esbuild = await import("esbuild");
2623
+ const outbase = path8.resolve(rootDir, APP_DIR2);
2624
+ const result = await esbuild.build({
2625
+ entryPoints,
2626
+ outdir: absDist,
2627
+ outbase,
2628
+ bundle: false,
2629
+ platform: "node",
2630
+ format: "esm",
2631
+ sourcemap: true,
2632
+ packages: "external",
2633
+ plugins,
2634
+ logLevel,
2635
+ write: false
2636
+ });
2637
+ if (result.outputFiles) {
2638
+ await Promise.all(
2639
+ result.outputFiles.map(async (file) => {
2640
+ await fs7.promises.mkdir(path8.dirname(file.path), { recursive: true });
2641
+ const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2642
+ await fs7.promises.writeFile(tmp, file.contents);
2643
+ await fs7.promises.rename(tmp, file.path);
2644
+ })
2645
+ );
2221
2646
  }
2222
- const idx = rel.lastIndexOf("/");
2223
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2224
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2647
+ return { compiledFiles: entryPoints };
2225
2648
  }
2226
- function getRuntimeSchemaPath(filePath, dist, rootDir) {
2227
- let rel = filePath.replace(/\\/g, "/");
2228
- if (rel.startsWith("src/")) {
2229
- rel = rel.slice(4);
2230
- } else if (rel.startsWith(`${dist}/`)) {
2231
- rel = rel.slice(dist.length + 1);
2649
+
2650
+ // src/cli/compileOnDemand.ts
2651
+ init_generateSchemaFiles();
2652
+ init_generateSchemaFiles();
2653
+ function isProductFresh(sourceAbsPath, productAbsPath) {
2654
+ try {
2655
+ const srcStat = fs8.statSync(sourceAbsPath);
2656
+ const prodStat = fs8.statSync(productAbsPath);
2657
+ return prodStat.mtimeMs >= srcStat.mtimeMs;
2658
+ } catch {
2659
+ return false;
2232
2660
  }
2233
- const idx = rel.lastIndexOf("/");
2234
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2235
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2236
2661
  }
2237
- function getHelpersImportPath(relDir) {
2238
- if (!relDir) return `./${HELPERS_FILENAME}`;
2239
- const depth = relDir.split("/").filter(Boolean).length;
2240
- return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2662
+ var compiledFiles = /* @__PURE__ */ new Set();
2663
+ function clearCompiledFiles() {
2664
+ compiledFiles.clear();
2241
2665
  }
2242
- function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2243
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
2244
- const lines = ["import { z } from 'zod';"];
2245
- const schemaBlocks = [];
2246
- for (const source of sources) {
2247
- const { schemaName, typeInfo } = source;
2248
- if (!typeInfo) {
2249
- continue;
2250
- }
2251
- const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2252
- const block = [`// ${schemaName}`];
2253
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2254
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
2255
- ""
2256
- );
2257
- block.push(schemaCode);
2258
- block.push("");
2259
- schemaBlocks.push(block.join("\n"));
2666
+ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2667
+ if (compiledFiles.has(sourceAbsPath)) {
2668
+ return false;
2260
2669
  }
2261
- const allSchemaCode = schemaBlocks.join("\n");
2262
- if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2263
- lines.push(
2264
- `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2265
- );
2670
+ if (!fs8.existsSync(sourceAbsPath)) {
2671
+ return false;
2266
2672
  }
2267
- lines.push("");
2268
- lines.push(...schemaBlocks);
2269
- return lines.join("\n").replace(/\n+$/, "\n");
2270
- }
2271
- async function generateSchemaFiles(routes, rootDir, dist) {
2272
- if (routes.length === 0) return;
2273
- const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2274
- const sourcesByFile = /* @__PURE__ */ new Map();
2275
- for (const source of sources) {
2276
- let list = sourcesByFile.get(source.filePath);
2277
- if (!list) {
2278
- list = [];
2279
- sourcesByFile.set(source.filePath, list);
2280
- }
2281
- list.push(source);
2673
+ const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2674
+ if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2675
+ compiledFiles.add(sourceAbsPath);
2676
+ return false;
2282
2677
  }
2283
- const fileEntries = [];
2284
- for (const [filePath, fileSources] of sourcesByFile) {
2285
- const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
2286
- const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2287
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2288
- let relForDir = relFile;
2289
- if (relForDir.startsWith("src/")) {
2290
- relForDir = relForDir.slice(4);
2678
+ await compileDevRoutes({
2679
+ rootDir,
2680
+ dist,
2681
+ files: [sourceAbsPath],
2682
+ logLevel: "silent"
2683
+ });
2684
+ compiledFiles.add(sourceAbsPath);
2685
+ return true;
2686
+ }
2687
+ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2688
+ const rel = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2689
+ if (!rel.startsWith("src/")) return null;
2690
+ const relWithoutSrc = rel.slice(4);
2691
+ const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2692
+ return path9.resolve(rootDir, dist, jsRel);
2693
+ }
2694
+ var generatedSchemas = /* @__PURE__ */ new Set();
2695
+ function clearGeneratedSchemas() {
2696
+ generatedSchemas.clear();
2697
+ }
2698
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2699
+ if (generatedSchemas.has(schemaPath)) {
2700
+ return false;
2701
+ }
2702
+ const prodAbsPath = path9.resolve(rootDir, routeFilePath);
2703
+ const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2704
+ if (!fs8.existsSync(sourceAbsPath)) {
2705
+ return false;
2706
+ }
2707
+ if (isProductFresh(sourceAbsPath, schemaPath)) {
2708
+ generatedSchemas.add(schemaPath);
2709
+ return false;
2710
+ }
2711
+ const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
2712
+ if (fileRoutes.length === 0) {
2713
+ return false;
2714
+ }
2715
+ const sourceRelPath = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2716
+ const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2717
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2718
+ generatedSchemas.add(schemaPath);
2719
+ return true;
2720
+ }
2721
+ async function deleteSchemaFiles(routes, rootDir, dist) {
2722
+ const deleted = /* @__PURE__ */ new Set();
2723
+ for (const route of routes) {
2724
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
2725
+ if (deleted.has(schemaPath)) continue;
2726
+ deleted.add(schemaPath);
2727
+ try {
2728
+ await fs8.promises.unlink(schemaPath);
2729
+ } catch {
2291
2730
  }
2292
- const dirIdx = relForDir.lastIndexOf("/");
2293
- const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2294
- const helpersImportPath = getHelpersImportPath(zodRelDir);
2295
- const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
2296
- fileEntries.push({ outputPath, source });
2297
2731
  }
2298
- const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2299
- if (usesCoerceHelpers(allSourceCode)) {
2300
- const helpersPath = path5.resolve(rootDir, dist, HELPERS_FILENAME);
2301
- await writeSchemaFile(helpersPath, generateHelpersFileSource());
2732
+ }
2733
+ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2734
+ const rel = path9.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2735
+ let relWithoutDist = rel;
2736
+ if (relWithoutDist.startsWith(`${dist}/`)) {
2737
+ relWithoutDist = relWithoutDist.slice(dist.length + 1);
2302
2738
  }
2303
- await Promise.all(
2304
- fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2305
- );
2739
+ const srcRel = `src/${relWithoutDist}`;
2740
+ const tsRel = srcRel.replace(/\.js$/, ".ts");
2741
+ const tsAbs = path9.resolve(rootDir, tsRel);
2742
+ if (fs8.existsSync(tsAbs)) return tsAbs;
2743
+ return path9.resolve(rootDir, srcRel);
2306
2744
  }
2307
- async function writeSchemaFile(outputPath, source) {
2308
- await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
2309
- await fs4.writeFile(outputPath, source, "utf-8");
2745
+ var devOnDemandEnabled = false;
2746
+ function isDevOnDemandEnabled() {
2747
+ return devOnDemandEnabled;
2748
+ }
2749
+ var devDistDir;
2750
+ function getDevDist() {
2751
+ return devDistDir;
2310
2752
  }
2311
2753
 
2312
2754
  // src/validator/validateInput.ts
@@ -2317,7 +2759,7 @@ function invalidateSchemaCache() {
2317
2759
  async function loadSchemaModule(schemaPath) {
2318
2760
  let mod = moduleCache.get(schemaPath);
2319
2761
  if (!mod) {
2320
- mod = await importWithCacheBust(schemaPath);
2762
+ mod = await importWithCacheBust(schemaPath, isDevOnDemandEnabled());
2321
2763
  moduleCache.set(schemaPath, mod);
2322
2764
  }
2323
2765
  return mod;
@@ -2353,9 +2795,9 @@ async function validateInput(schemaPath, method, inputType, input) {
2353
2795
  function mapZodIssues(error) {
2354
2796
  return error.issues.map((issue) => {
2355
2797
  const code = mapZodCode(issue.code, issue.message);
2356
- const path11 = issue.path.map(String).join(".") || "";
2798
+ const path15 = issue.path.map(String).join(".") || "";
2357
2799
  return {
2358
- path: path11,
2800
+ path: path15,
2359
2801
  code,
2360
2802
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2361
2803
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -2403,45 +2845,45 @@ import {
2403
2845
  import { createSecureServer as createHttp2SecureServer } from "http2";
2404
2846
  import { readFileSync } from "fs";
2405
2847
  import { Readable as Readable2 } from "stream";
2406
- import path7 from "path";
2848
+ import path11 from "path";
2407
2849
 
2408
2850
  // src/router/matchRoute.ts
2409
- function matchRoute(routes, method, path11) {
2851
+ function matchRoute(routes, method, path15) {
2410
2852
  for (const route of routes) {
2411
2853
  if (route.method !== method) {
2412
2854
  continue;
2413
2855
  }
2414
2856
  if (!route.isDynamic) {
2415
- if (route.urlPath === path11) {
2857
+ if (route.urlPath === path15) {
2416
2858
  return { route, params: {} };
2417
2859
  }
2418
2860
  continue;
2419
2861
  }
2420
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2862
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2421
2863
  if (params !== null) {
2422
2864
  return { route, params };
2423
2865
  }
2424
2866
  }
2425
2867
  return null;
2426
2868
  }
2427
- function matchWsRoute(wsRoutes, path11) {
2869
+ function matchWsRoute(wsRoutes, path15) {
2428
2870
  for (const route of wsRoutes) {
2429
2871
  if (!route.isDynamic) {
2430
- if (route.urlPath === path11) {
2872
+ if (route.urlPath === path15) {
2431
2873
  return { route, params: {} };
2432
2874
  }
2433
2875
  continue;
2434
2876
  }
2435
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2877
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2436
2878
  if (params !== null) {
2437
2879
  return { route, params };
2438
2880
  }
2439
2881
  }
2440
2882
  return null;
2441
2883
  }
2442
- function matchDynamicPath(pattern, path11, paramNames, isCatchAll) {
2884
+ function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
2443
2885
  const patternSegments = pattern.split("/").filter(Boolean);
2444
- const pathSegments = path11.split("/").filter(Boolean);
2886
+ const pathSegments = path15.split("/").filter(Boolean);
2445
2887
  if (isCatchAll) {
2446
2888
  const nonCatchAllCount = patternSegments.length - 1;
2447
2889
  if (pathSegments.length <= nonCatchAllCount) {
@@ -2486,6 +2928,9 @@ function matchDynamicPath(pattern, path11, paramNames, isCatchAll) {
2486
2928
  return params;
2487
2929
  }
2488
2930
 
2931
+ // src/loader/loadRouteModule.ts
2932
+ import fs9 from "fs";
2933
+
2489
2934
  // src/loader/resolveExports.ts
2490
2935
  function resolveExport(module, exportName) {
2491
2936
  if (exportName in module && typeof module[exportName] !== "undefined") {
@@ -2511,10 +2956,26 @@ function validateRouteModule(value, method, filePath) {
2511
2956
  }
2512
2957
 
2513
2958
  // src/loader/loadRouteModule.ts
2514
- async function loadRouteModule(filePath, method) {
2959
+ async function loadRouteModule(filePath, method, rootDir) {
2960
+ if (isDevOnDemandEnabled() && rootDir) {
2961
+ const dist = getDevDist();
2962
+ if (dist) {
2963
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2964
+ if (sourcePath && fs9.existsSync(sourcePath)) {
2965
+ try {
2966
+ await ensureCompiled(sourcePath, rootDir, dist);
2967
+ } catch (compileErr) {
2968
+ const reason = compileErr instanceof Error ? compileErr.message : String(compileErr);
2969
+ throw new Error(`Failed to compile route module "${sourcePath}": ${reason}`, {
2970
+ cause: compileErr
2971
+ });
2972
+ }
2973
+ }
2974
+ }
2975
+ }
2515
2976
  let module;
2516
2977
  try {
2517
- module = await importWithCacheBust(filePath);
2978
+ module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
2518
2979
  } catch (err) {
2519
2980
  const reason = err instanceof Error ? err.message : String(err);
2520
2981
  throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
@@ -2565,6 +3026,7 @@ async function parseMultipart(request) {
2565
3026
  }
2566
3027
 
2567
3028
  // src/runtime/resolveInput.ts
3029
+ init_inputType();
2568
3030
  async function resolveInput(method, request) {
2569
3031
  const inputType = getInputTypeForMethod(method);
2570
3032
  if (inputType === "body") {
@@ -2628,6 +3090,9 @@ async function sendNodeResponse(response, res) {
2628
3090
  res.end();
2629
3091
  }
2630
3092
 
3093
+ // src/server/createServer.ts
3094
+ init_inputType();
3095
+
2631
3096
  // src/utils/getClientIp.ts
2632
3097
  function getClientIp(req) {
2633
3098
  const xff = req.headers["x-forwarded-for"];
@@ -2646,8 +3111,9 @@ function getClientIp(req) {
2646
3111
  }
2647
3112
 
2648
3113
  // src/server/handleWsUpgrade.ts
3114
+ import fs10 from "fs";
2649
3115
  import { WebSocketServer, WebSocket } from "ws";
2650
- import path6 from "path";
3116
+ import path10 from "path";
2651
3117
 
2652
3118
  // src/errors/formatErrorResponse.ts
2653
3119
  function formatErrorResponse(error) {
@@ -2744,8 +3210,17 @@ function getPathname(req) {
2744
3210
  const idx = url.indexOf("?");
2745
3211
  return idx >= 0 ? url.slice(0, idx) : url;
2746
3212
  }
2747
- async function loadWsHandler(filePath, ctx) {
2748
- const module = await importWithCacheBust(filePath);
3213
+ async function loadWsHandler(filePath, ctx, rootDir) {
3214
+ if (isDevOnDemandEnabled() && rootDir) {
3215
+ const dist = getDevDist();
3216
+ if (dist) {
3217
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3218
+ if (sourcePath && fs10.existsSync(sourcePath)) {
3219
+ await ensureCompiled(sourcePath, rootDir, dist);
3220
+ }
3221
+ }
3222
+ }
3223
+ const module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
2749
3224
  const handler = module["WS"];
2750
3225
  if (typeof handler !== "function") {
2751
3226
  throw new Error(`WS export not found in ${filePath}`);
@@ -2820,8 +3295,8 @@ function attachWebSocket(options) {
2820
3295
  const finalHandler = async () => {
2821
3296
  let handlers;
2822
3297
  try {
2823
- const absoluteFilePath = path6.resolve(rootDir, route.filePath);
2824
- handlers = await loadWsHandler(absoluteFilePath, ctx);
3298
+ const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3299
+ handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
2825
3300
  } catch (err) {
2826
3301
  const reason = err instanceof Error ? err.message : String(err);
2827
3302
  console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
@@ -2843,6 +3318,14 @@ function attachWebSocket(options) {
2843
3318
  };
2844
3319
  let response;
2845
3320
  try {
3321
+ if (route.middlewares === void 0 && route.middlewarePaths) {
3322
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3323
+ if (bundle) {
3324
+ route.middlewares = bundle.middlewares;
3325
+ } else {
3326
+ route.middlewares = [];
3327
+ }
3328
+ }
2846
3329
  const dirMiddlewares = route.middlewares ?? [];
2847
3330
  const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
2848
3331
  if (allMiddlewares.length > 0) {
@@ -2866,6 +3349,7 @@ function attachWebSocket(options) {
2866
3349
  }
2867
3350
 
2868
3351
  // src/server/createServer.ts
3352
+ init_generateSchemaFiles();
2869
3353
  var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
2870
3354
  function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
2871
3355
  const forwardedProto = req.headers["x-forwarded-proto"];
@@ -2910,15 +3394,15 @@ function limitStreamSize(stream, maxSize) {
2910
3394
  }
2911
3395
  });
2912
3396
  }
2913
- function findAllowedMethods(routes, path11) {
3397
+ function findAllowedMethods(routes, path15) {
2914
3398
  const methods = /* @__PURE__ */ new Set();
2915
3399
  for (const route of routes) {
2916
- if (route.urlPath === path11) {
3400
+ if (route.urlPath === path15) {
2917
3401
  methods.add(route.method);
2918
3402
  continue;
2919
3403
  }
2920
3404
  if (route.isDynamic) {
2921
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
3405
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2922
3406
  if (params !== null) {
2923
3407
  methods.add(route.method);
2924
3408
  }
@@ -3004,16 +3488,32 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3004
3488
  }
3005
3489
  ctx.params = match.params;
3006
3490
  const { route } = match;
3007
- const absoluteFilePath = path7.resolve(rootDir, route.filePath);
3008
- const routeModule = await loadRouteModule(absoluteFilePath, route.method);
3491
+ const absoluteFilePath = path11.resolve(rootDir, route.filePath);
3492
+ const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
3009
3493
  const input = await resolveInput(route.method, request);
3010
3494
  const inputType = getInputTypeForMethod(route.method);
3011
3495
  const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
3496
+ if (isDevOnDemandEnabled()) {
3497
+ const devDist = getDevDist();
3498
+ if (devDist) {
3499
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
3500
+ }
3501
+ }
3012
3502
  const result = await validateInput(schemaPath, route.method, inputType, input);
3013
3503
  if (!result.valid) {
3014
3504
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
3015
3505
  }
3016
3506
  const body = hasBody(route.method) ? result.data : void 0;
3507
+ if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
3508
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3509
+ if (bundle) {
3510
+ route.middlewares = bundle.middlewares;
3511
+ route.injectors = bundle.injectors;
3512
+ } else {
3513
+ route.middlewares = [];
3514
+ route.injectors = {};
3515
+ }
3516
+ }
3017
3517
  const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
3018
3518
  const response = await invokeHandler(
3019
3519
  routeModule.handler,
@@ -3068,7 +3568,7 @@ async function createTestServer(options) {
3068
3568
  } = options;
3069
3569
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3070
3570
  const sorted = sortRoutes(routes);
3071
- const schemaDist = dist ? path8.isAbsolute(dist) ? dist : path8.resolve(rootDir, dist) : await fs5.mkdtemp(path8.join(os.tmpdir(), "faapi-test-schema-"));
3571
+ const schemaDist = dist ? path12.isAbsolute(dist) ? dist : path12.resolve(rootDir, dist) : await fs11.mkdtemp(path12.join(os.tmpdir(), "faapi-test-schema-"));
3072
3572
  await generateSchemaFiles(sorted, rootDir, schemaDist);
3073
3573
  const { server } = createServer({
3074
3574
  routes: sorted,
@@ -3101,7 +3601,7 @@ async function createTestServer(options) {
3101
3601
  await new Promise((resolve) => {
3102
3602
  server.close(() => resolve());
3103
3603
  });
3104
- await fs5.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3604
+ await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3105
3605
  });
3106
3606
  invalidateSchemaCache();
3107
3607
  }
@@ -3243,8 +3743,8 @@ async function connectWs(baseUrl, pathname, options = {}) {
3243
3743
  }
3244
3744
 
3245
3745
  // src/cli/createAppCore.ts
3246
- import fs7 from "fs";
3247
- import path10 from "path";
3746
+ import fs13 from "fs";
3747
+ import path14 from "path";
3248
3748
  import { PassThrough } from "stream";
3249
3749
 
3250
3750
  // src/router/detectRouteConflicts.ts
@@ -3301,55 +3801,30 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
3301
3801
  }
3302
3802
 
3303
3803
  // src/cli/generateRoutes.ts
3304
- import fs6 from "fs";
3305
- import path9 from "path";
3804
+ import fs12 from "fs";
3805
+ import path13 from "path";
3306
3806
  async function hydrateRoutes(manifest) {
3307
- const hydrateRoute = async (serialized) => {
3308
- const bundle = await loadMiddlewarePaths(serialized.middlewarePaths);
3309
- return {
3310
- method: serialized.method,
3311
- urlPath: serialized.urlPath,
3312
- filePath: serialized.filePath,
3313
- paramNames: serialized.paramNames,
3314
- isDynamic: serialized.isDynamic,
3315
- isCatchAll: serialized.isCatchAll,
3316
- middlewares: bundle?.middlewares,
3317
- injectors: bundle?.injectors
3318
- };
3319
- };
3320
- const hydrateWsRoute = async (serialized) => {
3321
- const bundle = await loadMiddlewarePaths(serialized.middlewarePaths);
3322
- return {
3323
- urlPath: serialized.urlPath,
3324
- filePath: serialized.filePath,
3325
- paramNames: serialized.paramNames,
3326
- isDynamic: serialized.isDynamic,
3327
- isCatchAll: serialized.isCatchAll,
3328
- middlewares: bundle?.middlewares,
3329
- injectors: bundle?.injectors
3330
- };
3331
- };
3332
- const routes = await Promise.all(manifest.routes.map(hydrateRoute));
3333
- const wsRoutes = await Promise.all(manifest.wsRoutes.map(hydrateWsRoute));
3807
+ const hydrateRoute = (serialized) => ({
3808
+ method: serialized.method,
3809
+ urlPath: serialized.urlPath,
3810
+ filePath: serialized.filePath,
3811
+ paramNames: serialized.paramNames,
3812
+ isDynamic: serialized.isDynamic,
3813
+ isCatchAll: serialized.isCatchAll,
3814
+ middlewarePaths: serialized.middlewarePaths
3815
+ });
3816
+ const hydrateWsRoute = (serialized) => ({
3817
+ urlPath: serialized.urlPath,
3818
+ filePath: serialized.filePath,
3819
+ paramNames: serialized.paramNames,
3820
+ isDynamic: serialized.isDynamic,
3821
+ isCatchAll: serialized.isCatchAll,
3822
+ middlewarePaths: serialized.middlewarePaths
3823
+ });
3824
+ const routes = manifest.routes.map(hydrateRoute);
3825
+ const wsRoutes = manifest.wsRoutes.map(hydrateWsRoute);
3334
3826
  return { routes, wsRoutes };
3335
3827
  }
3336
- async function loadMiddlewarePaths(middlewarePaths) {
3337
- if (middlewarePaths.length === 0) return void 0;
3338
- const mergedMiddlewares = [];
3339
- const mergedInjectors = {};
3340
- for (const absMwPath of middlewarePaths) {
3341
- let bundle = getCachedMiddlewares(absMwPath);
3342
- if (bundle === void 0) {
3343
- bundle = await loadMiddlewaresFile(absMwPath);
3344
- setCachedMiddlewares(absMwPath, bundle);
3345
- }
3346
- mergedMiddlewares.push(...bundle.middlewares);
3347
- for (const [name, injector] of Object.entries(bundle.injectors)) {
3348
- mergedInjectors[name] = injector;
3349
- }
3350
- }
3351
- return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
3352
- }
3353
3828
 
3354
3829
  // src/cli/loadPlugins.ts
3355
3830
  async function loadPlugins(declarations, ctx) {
@@ -3433,8 +3908,8 @@ function isFaapiConfigKey(key) {
3433
3908
  async function createAppBase(options) {
3434
3909
  const rootDir = options?.rootDir ?? process.cwd();
3435
3910
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
3436
- const routesPath = path10.resolve(rootDir, dist, ROUTES_FILE);
3437
- if (!fs7.existsSync(routesPath)) {
3911
+ const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE);
3912
+ if (!fs13.existsSync(routesPath)) {
3438
3913
  throw new Error(
3439
3914
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
3440
3915
  );
@@ -3607,6 +4082,10 @@ async function createAppBase(options) {
3607
4082
  if (config?.lifecycle?.onClose) {
3608
4083
  await config.lifecycle.onClose({ rootDir, routes: sorted, server });
3609
4084
  }
4085
+ if (!server.listening) {
4086
+ app.server = null;
4087
+ return;
4088
+ }
3610
4089
  return new Promise((resolve) => {
3611
4090
  server.close((err) => {
3612
4091
  if (err) console.error("Error closing server:", err);
@@ -3636,6 +4115,7 @@ async function createAppBase(options) {
3636
4115
  }
3637
4116
 
3638
4117
  // src/cli/createDevApp.ts
4118
+ init_createProgram();
3639
4119
  async function createDevApp(options) {
3640
4120
  const { app, ctx } = await createAppBase(options);
3641
4121
  const devApp = app;
@@ -3644,9 +4124,16 @@ async function createDevApp(options) {
3644
4124
  invalidateMiddlewareCache();
3645
4125
  invalidateProgramCache();
3646
4126
  invalidateSchemaCache();
4127
+ clearCompiledFiles();
3647
4128
  const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
3648
4129
  const sorted = sortRoutes(reScanned.routes);
3649
- await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
4130
+ if (isDevOnDemandEnabled()) {
4131
+ await deleteSchemaFiles(sorted, ctx.rootDir, ctx.dist);
4132
+ clearGeneratedSchemas();
4133
+ } else {
4134
+ const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
4135
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
4136
+ }
3650
4137
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
3651
4138
  };
3652
4139
  return devApp;