@faapi/faapi 1.1.1-canary.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,1468 +894,1510 @@ 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
- };
961
+ }
1010
962
  }
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;
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);
968
+ }
969
+ return withConstraints;
1021
970
  }
1022
- function getVitestImportActual() {
1023
- const vi = globalThis.vi;
1024
- if (typeof vi?.importActual !== "function") return void 0;
1025
- return vi.importActual.bind(vi);
971
+ function applyConstraints(baseExpr, constraints, typeKind) {
972
+ const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
973
+ return `${baseExpr}${suffix}`;
1026
974
  }
1027
- async function importWithCacheBust(filePath) {
1028
- const importActual = getVitestImportActual();
1029
- if (importActual) {
1030
- return await importActual(filePath);
1031
- }
1032
- let url = pathToFileURL(filePath).href;
1033
- if (loadTs !== void 0) {
1034
- url += `?t=${loadTs}`;
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()";
1035
1007
  }
1036
- return await import(url);
1037
1008
  }
1038
-
1039
- // src/config/loadConfig.ts
1040
- var CONFIG_PRODUCT_FILE = "faapi-config.js";
1041
- async function loadConfig(rootDir, dist) {
1042
- const configProductPath = path2.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
1043
- if (fs.existsSync(configProductPath)) {
1044
- const module = await importWithCacheBust(configProductPath);
1045
- return module.default ?? {};
1046
- }
1047
- const hasSourceConfig = fs.existsSync(path2.join(rootDir, "faapi.config.ts")) || fs.existsSync(path2.join(rootDir, "faapi.config.js"));
1048
- if (hasSourceConfig) {
1049
- throw new Error(
1050
- `[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
1051
- );
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`;
1052
1049
  }
1053
- return null;
1054
1050
  }
1055
-
1056
- // src/cli/loadEnv.ts
1057
- import fs2 from "fs";
1058
- import path3 from "path";
1059
- function resolveEnv() {
1060
- return process.env.NODE_ENV || "development";
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");
1061
1060
  }
1062
- function getEnvFiles(env) {
1063
- return [".env", ".env.local", `.env.${env}`, `.env.${env}.local`];
1061
+ function usesCoerceHelpers(code) {
1062
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
1064
1063
  }
1065
- function parseEnvFile(content, fileVars) {
1066
- const result = {};
1067
- const lines = content.replace(/\r\n/g, "\n").split("\n");
1068
- for (const line of lines) {
1069
- const trimmed = line.trim();
1070
- if (!trimmed || trimmed.startsWith("#")) continue;
1071
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(trimmed);
1072
- if (!match) continue;
1073
- const [, key, rawValue] = match;
1074
- const value = parseValue(rawValue, { ...fileVars, ...result });
1075
- result[key] = value;
1064
+ function wrapCoercePreprocess(kind, inner) {
1065
+ if (kind === "number") {
1066
+ return `z.preprocess(coerceNumber, ${inner})`;
1076
1067
  }
1077
- return result;
1068
+ return `z.preprocess(coerceBoolean, ${inner})`;
1078
1069
  }
1079
- function parseValue(raw, env) {
1080
- if (raw === "") return "";
1081
- if (raw[0] === "'") {
1082
- const end = raw.indexOf("'", 1);
1083
- return end === -1 ? raw.slice(1) : raw.slice(1, end);
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
+ }
1084
1083
  }
1085
- if (raw[0] === '"') {
1086
- const match = /^"((?:\\.|[^"\\])*)"/.exec(raw);
1087
- const inner = match ? match[1] : raw.slice(1);
1088
- return expandEscapesAndVars(inner, env);
1084
+ if (restExpression) {
1085
+ return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
1089
1086
  }
1090
- const commentMatch = /^(.*?)(\s+#.*)$/.exec(raw);
1091
- const value = commentMatch ? commentMatch[1] : raw;
1092
- return value.trim();
1093
- }
1094
- function expandEscapesAndVars(str, env) {
1095
- const escaped = str.replace(/\\(.)/g, (_, ch) => {
1096
- switch (ch) {
1097
- case "n":
1098
- return "\n";
1099
- case "r":
1100
- return "\r";
1101
- case "t":
1102
- return " ";
1103
- case "\\":
1104
- return "\\";
1105
- case '"':
1106
- return '"';
1107
- default:
1108
- return ch;
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;
1109
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}`;
1110
1111
  });
1111
- return escaped.replace(
1112
- /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
1113
- (_, braced, plain) => {
1114
- const varName = braced || plain;
1115
- return env[varName] ?? process.env[varName] ?? "";
1116
- }
1117
- );
1112
+ return `z.object({ ${fields.join(", ")} })`;
1118
1113
  }
1119
- function loadEnv(rootDir) {
1120
- const env = resolveEnv();
1121
- const files = getEnvFiles(env);
1122
- const merged = {};
1123
- for (const file of files) {
1124
- const filePath = path3.join(rootDir, file);
1125
- if (!fs2.existsSync(filePath)) continue;
1126
- const content = fs2.readFileSync(filePath, "utf-8");
1127
- const parsed = parseEnvFile(content, merged);
1128
- Object.assign(merged, parsed);
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()`;
1129
1119
  }
1130
- for (const [key, value] of Object.entries(merged)) {
1131
- if (process.env[key] === void 0) {
1132
- process.env[key] = value;
1133
- }
1120
+ if (hasNull) {
1121
+ const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1122
+ return `z.union([${unionInner2}]).nullable()`;
1134
1123
  }
1124
+ const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1125
+ return `z.union([${unionInner}])`;
1135
1126
  }
1136
-
1137
- // src/errors/FaapiError.ts
1138
- var FaapiError = class extends Error {
1139
- constructor(code, message, statusCode) {
1140
- super(message);
1141
- this.code = code;
1142
- this.statusCode = statusCode;
1143
- this.name = "FaapiError";
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});`;
1144
1132
  }
1145
- code;
1146
- statusCode;
1147
- };
1148
-
1149
- // src/errors/httpErrors.ts
1150
- function deriveStatusCode(issues) {
1151
- const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
1152
- return has400 ? 400 : 422;
1133
+ return `const ${name}Schema = ${expr};`;
1153
1134
  }
1154
- var ValidationError = class extends FaapiError {
1155
- constructor(message, issues) {
1156
- super("VALIDATION_ERROR", message, deriveStatusCode(issues));
1157
- this.issues = issues;
1158
- this.name = "ValidationError";
1159
- }
1160
- issues;
1161
- };
1162
- var RouteNotFoundError = class extends FaapiError {
1163
- constructor(path11) {
1164
- super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
1165
- this.name = "RouteNotFoundError";
1166
- }
1167
- };
1168
- var MethodNotAllowedError = class extends FaapiError {
1169
- constructor(method, path11, allowedMethods) {
1170
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
1171
- this.allowedMethods = allowedMethods;
1172
- this.name = "MethodNotAllowedError";
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;
1173
1155
  }
1174
- allowedMethods;
1175
- };
1176
- var InternalError = class extends FaapiError {
1177
- constructor(message) {
1178
- super("INTERNAL_ERROR", message, 500);
1179
- this.name = "InternalError";
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));
1180
1170
  }
1181
- };
1182
- var ModuleLoadError = class extends FaapiError {
1183
- constructor(filePath, reason) {
1184
- super("MODULE_LOAD_ERROR", `Failed to load module ${filePath}: ${reason}`, 500);
1185
- this.name = "ModuleLoadError";
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};`);
1186
1178
  }
1187
- };
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
+ });
1188
1212
 
1189
- // src/runtime/sse.ts
1190
- function encodeSseEvent(event) {
1191
- let out = "";
1192
- if (event.comment !== void 0) {
1193
- out += `: ${event.comment}
1194
- `;
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);
1195
1228
  }
1196
- if (event.event !== void 0) {
1197
- out += `event: ${event.event}
1198
- `;
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);
1199
1239
  }
1200
- if (event.id !== void 0) {
1201
- out += `id: ${event.id}
1202
- `;
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"));
1203
1267
  }
1204
- if (event.retry !== void 0) {
1205
- out += `retry: ${event.retry}
1206
- `;
1268
+ const allSchemaCode = schemaBlocks.join("\n");
1269
+ if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
1270
+ lines.push(
1271
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
1272
+ );
1207
1273
  }
1208
- if (event.data !== void 0) {
1209
- let dataStr;
1210
- if (typeof event.data === "string") {
1211
- dataStr = event.data;
1212
- } else if (event.data === null) {
1213
- dataStr = "null";
1214
- } else {
1215
- dataStr = JSON.stringify(event.data);
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);
1216
1287
  }
1217
- const lines = dataStr.split("\n");
1218
- for (const line of lines) {
1219
- out += `data: ${line}
1220
- `;
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);
1221
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 });
1222
1304
  }
1223
- out += "\n";
1224
- return out;
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
+ );
1225
1313
  }
1226
- function createSseWriter() {
1227
- const encoder = new TextEncoder();
1228
- let controller = null;
1229
- let closed = false;
1230
- let aborted = false;
1231
- const stream = new ReadableStream({
1232
- start(c) {
1233
- controller = c;
1234
- },
1235
- cancel() {
1236
- aborted = true;
1237
- closed = true;
1238
- controller = null;
1239
- }
1240
- });
1241
- const response = new Response(stream, {
1242
- status: 200,
1243
- headers: {
1244
- "Content-Type": "text/event-stream",
1245
- "Cache-Control": "no-cache",
1246
- Connection: "keep-alive"
1247
- }
1248
- });
1249
- const writer = {
1250
- send(event) {
1251
- if (closed || !controller) return;
1252
- const text = encodeSseEvent(event);
1253
- controller.enqueue(encoder.encode(text));
1254
- },
1255
- sendRaw(chunk) {
1256
- if (closed || !controller) return;
1257
- const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
1258
- controller.enqueue(bytes);
1259
- },
1260
- sendError(error) {
1261
- if (closed || !controller) return;
1262
- const message = error instanceof Error ? error.message : String(error);
1263
- const text = encodeSseEvent({ event: "error", data: message });
1264
- try {
1265
- controller.enqueue(encoder.encode(text));
1266
- } finally {
1267
- writer.close();
1268
- }
1269
- },
1270
- close() {
1271
- if (closed) return;
1272
- closed = true;
1273
- if (controller) {
1274
- try {
1275
- controller.close();
1276
- } catch {
1277
- }
1278
- controller = null;
1279
- }
1280
- },
1281
- get closed() {
1282
- return closed;
1283
- },
1284
- get aborted() {
1285
- return aborted;
1286
- },
1287
- get response() {
1288
- return response;
1289
- }
1290
- };
1291
- return writer;
1314
+ async function writeSchemaFile(outputPath, source) {
1315
+ await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
1316
+ await fs4.writeFile(outputPath, source, "utf-8");
1292
1317
  }
1293
-
1294
- // src/runtime/createContext.ts
1295
- function parseCookies(cookieHeader) {
1296
- const cookies = /* @__PURE__ */ new Map();
1297
- if (!cookieHeader) return cookies;
1298
- for (const pair of cookieHeader.split(";")) {
1299
- const [name, ...rest] = pair.split("=");
1300
- const trimmed = name?.trim();
1301
- if (trimmed) {
1302
- cookies.set(trimmed, rest.join("=").trim());
1303
- }
1318
+ var init_generateSchemaFiles = __esm({
1319
+ "src/cli/generateSchemaFiles.ts"() {
1320
+ "use strict";
1321
+ init_collectRouteSchemaSources();
1322
+ init_generateZodSchema();
1304
1323
  }
1305
- return cookies;
1306
- }
1307
- function formatSetCookie(name, value, options) {
1308
- let cookie = `${name}=${value}`;
1309
- if (options?.domain) cookie += `; Domain=${options.domain}`;
1310
- if (options?.path) cookie += `; Path=${options.path}`;
1311
- if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
1312
- if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
1313
- if (options?.httpOnly) cookie += `; HttpOnly`;
1314
- if (options?.secure) cookie += `; Secure`;
1315
- if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
1316
- return cookie;
1317
- }
1318
- function createContext(request, params, config = {}, ip = "") {
1319
- const url = new URL(request.url);
1320
- const meta = { headers: {}, setCookies: [] };
1321
- const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
1322
- const cookiesObj = {};
1323
- for (const [key, val] of parsedCookies) {
1324
- cookiesObj[key] = val;
1325
- }
1326
- const ctx = {
1327
- request,
1328
- params,
1329
- query: url.searchParams,
1330
- headers: request.headers,
1331
- method: request.method,
1332
- path: url.pathname,
1333
- ip,
1334
- ua: request.headers.get("user-agent") ?? "",
1335
- cookies: cookiesObj,
1336
- config,
1337
- meta,
1338
- setStatus(status) {
1339
- meta.status = status;
1340
- },
1341
- setHeader(key, value) {
1342
- meta.headers[key] = value;
1343
- },
1344
- setETag(value) {
1345
- meta.headers["etag"] = value;
1346
- },
1347
- redirect(url2, status = 302) {
1348
- return new Response(null, {
1349
- status,
1350
- headers: { Location: url2 }
1351
- });
1352
- },
1353
- json(data, status) {
1354
- const headers = { "Content-Type": "application/json" };
1355
- return new Response(JSON.stringify(data), {
1356
- status: status ?? 200,
1357
- headers
1358
- });
1359
- },
1360
- html(html, status) {
1361
- const headers = { "Content-Type": "text/html; charset=utf-8" };
1362
- return new Response(html, {
1363
- status: status ?? 200,
1364
- headers
1365
- });
1366
- },
1367
- getCookie(name) {
1368
- return parsedCookies.get(name);
1369
- },
1370
- setCookie(name, value, options) {
1371
- meta.setCookies.push(formatSetCookie(name, value, options));
1372
- },
1373
- deleteCookie(name) {
1374
- meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
1375
- },
1376
- /**
1377
- * 创建 SSE writer,用于流式推送事件
1378
- *
1379
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
1380
- * 作为 HTTP 响应(Content-Type: text/event-stream)。
1381
- *
1382
- * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
1383
- */
1384
- sse() {
1385
- const writer = createSseWriter();
1386
- const ctxWithSse = ctx;
1387
- ctxWithSse.__sseResponse = writer.response;
1388
- ctxWithSse.__sseWriter = writer;
1389
- return writer;
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");
1390
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();
1391
1395
  };
1392
- const extend = config?.extendContext;
1393
- if (typeof extend === "function") {
1394
- extend(ctx);
1395
- }
1396
- return ctx;
1397
1396
  }
1398
1397
 
1399
- // src/utils/isPlainObject.ts
1400
- function isPlainObject(value) {
1401
- if (value === null || typeof value !== "object") {
1402
- return false;
1403
- }
1404
- if (Array.isArray(value)) {
1405
- return false;
1406
- }
1407
- const proto = Object.getPrototypeOf(value);
1408
- return proto === null || proto === Object.prototype;
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
+ };
1409
1429
  }
1410
1430
 
1411
- // src/response/toResponse.ts
1412
- async function toResponse(value, meta) {
1413
- if (value instanceof Promise) {
1414
- return toResponse(await value, meta);
1415
- }
1416
- const applyMeta = (headers2) => {
1417
- if (!meta) return;
1418
- for (const [key, val] of Object.entries(meta.headers)) {
1419
- headers2.set(key, val);
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);
1420
1452
  }
1421
- for (const cookie of meta.setCookies ?? []) {
1422
- headers2.append("set-cookie", cookie);
1453
+ if (opts.xFrameOptions !== false) {
1454
+ ctx.setHeader("X-Frame-Options", opts.xFrameOptions);
1423
1455
  }
1424
- };
1425
- if (value instanceof Response) {
1426
- if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
1427
- const headers2 = new Headers(value.headers);
1428
- applyMeta(headers2);
1429
- return new Response(value.body, {
1430
- status: meta.status ?? value.status,
1431
- headers: headers2
1432
- });
1456
+ if (opts.xContentTypeOptions) {
1457
+ ctx.setHeader("X-Content-Type-Options", "nosniff");
1433
1458
  }
1434
- return value;
1435
- }
1436
- if (value === null || value === void 0) {
1437
- const status = meta?.status ?? 204;
1438
- const headers2 = new Headers();
1439
- applyMeta(headers2);
1440
- return new Response(null, { status, headers: headers2 });
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) {
1509
+ const importActual = getVitestImportActual();
1510
+ if (importActual) {
1511
+ return await importActual(filePath);
1441
1512
  }
1442
- if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
1443
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1444
- applyMeta(headers2);
1445
- return new Response(value, {
1446
- status: meta?.status ?? 200,
1447
- headers: headers2
1448
- });
1513
+ let url = pathToFileURL(filePath).href;
1514
+ if (loadTs !== void 0) {
1515
+ url += `?t=${loadTs}`;
1449
1516
  }
1450
- if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
1451
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1452
- applyMeta(headers2);
1453
- return new Response(value, {
1454
- status: meta?.status ?? 200,
1455
- headers: headers2
1456
- });
1517
+ return await import(url);
1518
+ }
1519
+
1520
+ // src/config/loadConfig.ts
1521
+ var CONFIG_PRODUCT_FILE = "faapi-config.js";
1522
+ async function loadConfig(rootDir, dist) {
1523
+ const configProductPath = path2.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
1524
+ if (fs.existsSync(configProductPath)) {
1525
+ const module = await importWithCacheBust(configProductPath);
1526
+ return module.default ?? {};
1457
1527
  }
1458
- if (value instanceof Uint8Array) {
1459
- const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1460
- applyMeta(headers2);
1461
- return new Response(value, {
1462
- status: meta?.status ?? 200,
1463
- headers: headers2
1464
- });
1528
+ const hasSourceConfig = fs.existsSync(path2.join(rootDir, "faapi.config.ts")) || fs.existsSync(path2.join(rootDir, "faapi.config.js"));
1529
+ if (hasSourceConfig) {
1530
+ throw new Error(
1531
+ `[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
1532
+ );
1465
1533
  }
1466
- if (isPlainObject(value) || Array.isArray(value)) {
1467
- const body2 = JSON.stringify(value);
1468
- const headers2 = new Headers({ "Content-Type": "application/json" });
1469
- applyMeta(headers2);
1470
- return new Response(body2, {
1471
- status: meta?.status ?? 200,
1472
- headers: headers2
1473
- });
1534
+ return null;
1535
+ }
1536
+
1537
+ // src/cli/loadEnv.ts
1538
+ import fs2 from "fs";
1539
+ import path3 from "path";
1540
+ function resolveEnv() {
1541
+ return process.env.NODE_ENV || "development";
1542
+ }
1543
+ function getEnvFiles(env) {
1544
+ return [".env", ".env.local", `.env.${env}`, `.env.${env}.local`];
1545
+ }
1546
+ function parseEnvFile(content, fileVars) {
1547
+ const result = {};
1548
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
1549
+ for (const line of lines) {
1550
+ const trimmed = line.trim();
1551
+ if (!trimmed || trimmed.startsWith("#")) continue;
1552
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(trimmed);
1553
+ if (!match) continue;
1554
+ const [, key, rawValue] = match;
1555
+ const value = parseValue(rawValue, { ...fileVars, ...result });
1556
+ result[key] = value;
1474
1557
  }
1475
- if (typeof value === "string") {
1476
- const headers2 = new Headers({ "Content-Type": "text/plain" });
1477
- applyMeta(headers2);
1478
- return new Response(value, {
1479
- status: meta?.status ?? 200,
1480
- headers: headers2
1481
- });
1558
+ return result;
1559
+ }
1560
+ function parseValue(raw, env) {
1561
+ if (raw === "") return "";
1562
+ if (raw[0] === "'") {
1563
+ const end = raw.indexOf("'", 1);
1564
+ return end === -1 ? raw.slice(1) : raw.slice(1, end);
1482
1565
  }
1483
- if (typeof value === "number" || typeof value === "boolean") {
1484
- const headers2 = new Headers({ "Content-Type": "text/plain" });
1485
- applyMeta(headers2);
1486
- return new Response(String(value), {
1487
- status: meta?.status ?? 200,
1488
- headers: headers2
1489
- });
1566
+ if (raw[0] === '"') {
1567
+ const match = /^"((?:\\.|[^"\\])*)"/.exec(raw);
1568
+ const inner = match ? match[1] : raw.slice(1);
1569
+ return expandEscapesAndVars(inner, env);
1490
1570
  }
1491
- const body = JSON.stringify(value);
1492
- const headers = new Headers({ "Content-Type": "application/json" });
1493
- applyMeta(headers);
1494
- return new Response(body, {
1495
- status: meta?.status ?? 200,
1496
- headers
1571
+ const commentMatch = /^(.*?)(\s+#.*)$/.exec(raw);
1572
+ const value = commentMatch ? commentMatch[1] : raw;
1573
+ return value.trim();
1574
+ }
1575
+ function expandEscapesAndVars(str, env) {
1576
+ const escaped = str.replace(/\\(.)/g, (_, ch) => {
1577
+ switch (ch) {
1578
+ case "n":
1579
+ return "\n";
1580
+ case "r":
1581
+ return "\r";
1582
+ case "t":
1583
+ return " ";
1584
+ case "\\":
1585
+ return "\\";
1586
+ case '"':
1587
+ return '"';
1588
+ default:
1589
+ return ch;
1590
+ }
1497
1591
  });
1592
+ return escaped.replace(
1593
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
1594
+ (_, braced, plain) => {
1595
+ const varName = braced || plain;
1596
+ return env[varName] ?? process.env[varName] ?? "";
1597
+ }
1598
+ );
1498
1599
  }
1499
-
1500
- // src/utils/queryToObject.ts
1501
- function queryToObject(params) {
1502
- const result = {};
1503
- for (const [key, value] of params) {
1504
- result[key] = value;
1600
+ function loadEnv(rootDir) {
1601
+ const env = resolveEnv();
1602
+ const files = getEnvFiles(env);
1603
+ const merged = {};
1604
+ for (const file of files) {
1605
+ const filePath = path3.join(rootDir, file);
1606
+ if (!fs2.existsSync(filePath)) continue;
1607
+ const content = fs2.readFileSync(filePath, "utf-8");
1608
+ const parsed = parseEnvFile(content, merged);
1609
+ Object.assign(merged, parsed);
1610
+ }
1611
+ for (const [key, value] of Object.entries(merged)) {
1612
+ if (process.env[key] === void 0) {
1613
+ process.env[key] = value;
1614
+ }
1505
1615
  }
1506
- return result;
1507
1616
  }
1508
1617
 
1509
- // src/injection/injectParams.ts
1510
- function getBuiltinInjectionValue(type, ctx, body) {
1511
- switch (type) {
1512
- case "query":
1513
- return queryToObject(ctx.query);
1514
- case "params":
1515
- return ctx.params;
1516
- case "headers":
1517
- return ctx.headers;
1518
- case "context":
1519
- return ctx;
1520
- case "cookies":
1521
- return ctx.cookies;
1522
- case "ip":
1523
- return ctx.ip;
1524
- case "ua":
1525
- return ctx.ua;
1526
- case "body":
1527
- return body;
1528
- // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
1529
- // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
1530
- case "form":
1531
- return body;
1532
- case "files":
1533
- if (body && typeof body === "object" && "files" in body) {
1534
- return body.files;
1535
- }
1536
- return [];
1537
- case "fields":
1538
- if (body && typeof body === "object" && "fields" in body) {
1539
- return body.fields;
1540
- }
1541
- return {};
1542
- default:
1543
- return void 0;
1618
+ // src/errors/FaapiError.ts
1619
+ var FaapiError = class extends Error {
1620
+ constructor(code, message, statusCode) {
1621
+ super(message);
1622
+ this.code = code;
1623
+ this.statusCode = statusCode;
1624
+ this.name = "FaapiError";
1544
1625
  }
1626
+ code;
1627
+ statusCode;
1628
+ };
1629
+
1630
+ // src/errors/httpErrors.ts
1631
+ function deriveStatusCode(issues) {
1632
+ const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
1633
+ return has400 ? 400 : 422;
1545
1634
  }
1546
- async function injectParamsAsync(handler, ctx, body, injectors) {
1547
- const injections = resolveInjection(handler);
1548
- if (injections.length === 0) {
1549
- return await handler();
1635
+ var ValidationError = class extends FaapiError {
1636
+ constructor(message, issues) {
1637
+ super("VALIDATION_ERROR", message, deriveStatusCode(issues));
1638
+ this.issues = issues;
1639
+ this.name = "ValidationError";
1550
1640
  }
1551
- const args = await Promise.all(
1552
- injections.map(async (injection) => {
1553
- if (injection.type !== "unknown") {
1554
- return getBuiltinInjectionValue(injection.type, ctx, body);
1555
- }
1556
- if (injectors && injection.name in injectors) {
1557
- return await injectors[injection.name](ctx);
1558
- }
1559
- return void 0;
1560
- })
1561
- );
1562
- return await handler(...args);
1563
- }
1641
+ issues;
1642
+ };
1643
+ var RouteNotFoundError = class extends FaapiError {
1644
+ constructor(path15) {
1645
+ super("ROUTE_NOT_FOUND", `Route not found: ${path15}`, 404);
1646
+ this.name = "RouteNotFoundError";
1647
+ }
1648
+ };
1649
+ var MethodNotAllowedError = class extends FaapiError {
1650
+ constructor(method, path15, allowedMethods) {
1651
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path15}`, 405);
1652
+ this.allowedMethods = allowedMethods;
1653
+ this.name = "MethodNotAllowedError";
1654
+ }
1655
+ allowedMethods;
1656
+ };
1657
+ var InternalError = class extends FaapiError {
1658
+ constructor(message) {
1659
+ super("INTERNAL_ERROR", message, 500);
1660
+ this.name = "InternalError";
1661
+ }
1662
+ };
1663
+ var ModuleLoadError = class extends FaapiError {
1664
+ constructor(filePath, reason) {
1665
+ super("MODULE_LOAD_ERROR", `Failed to load module ${filePath}: ${reason}`, 500);
1666
+ this.name = "ModuleLoadError";
1667
+ }
1668
+ };
1564
1669
 
1565
- // src/runtime/invokeHandler.ts
1566
- function mergeMeta(response, meta) {
1567
- const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
1568
- if (!hasMeta) return response;
1569
- const headers = new Headers(response.headers);
1570
- for (const [key, value] of Object.entries(meta.headers)) {
1571
- headers.set(key, value);
1670
+ // src/runtime/sse.ts
1671
+ function encodeSseEvent(event) {
1672
+ let out = "";
1673
+ if (event.comment !== void 0) {
1674
+ out += `: ${event.comment}
1675
+ `;
1572
1676
  }
1573
- for (const cookie of meta.setCookies) {
1574
- headers.append("set-cookie", cookie);
1677
+ if (event.event !== void 0) {
1678
+ out += `event: ${event.event}
1679
+ `;
1575
1680
  }
1576
- return new Response(response.body, {
1577
- status: meta.status ?? response.status,
1578
- headers
1579
- });
1580
- }
1581
- async function compose(middlewares, ctx, finalHandler) {
1582
- const meta = ctx.meta;
1583
- let index = -1;
1584
- async function dispatch(i) {
1585
- if (i <= index) {
1586
- throw new Error("next() called multiple times");
1587
- }
1588
- index = i;
1589
- if (i >= middlewares.length) {
1590
- return await finalHandler();
1591
- }
1592
- const mw = middlewares[i];
1593
- let innerResponse;
1594
- const next = async () => {
1595
- innerResponse = await dispatch(i + 1);
1596
- return innerResponse;
1597
- };
1598
- const result = await mw(ctx, next);
1599
- if (result instanceof Response) {
1600
- return mergeMeta(result, meta);
1681
+ if (event.id !== void 0) {
1682
+ out += `id: ${event.id}
1683
+ `;
1684
+ }
1685
+ if (event.retry !== void 0) {
1686
+ out += `retry: ${event.retry}
1687
+ `;
1688
+ }
1689
+ if (event.data !== void 0) {
1690
+ let dataStr;
1691
+ if (typeof event.data === "string") {
1692
+ dataStr = event.data;
1693
+ } else if (event.data === null) {
1694
+ dataStr = "null";
1695
+ } else {
1696
+ dataStr = JSON.stringify(event.data);
1601
1697
  }
1602
- if (innerResponse !== void 0) {
1603
- return innerResponse;
1698
+ const lines = dataStr.split("\n");
1699
+ for (const line of lines) {
1700
+ out += `data: ${line}
1701
+ `;
1604
1702
  }
1605
- throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
1606
1703
  }
1607
- return await dispatch(0);
1704
+ out += "\n";
1705
+ return out;
1608
1706
  }
1609
- async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1610
- const meta = ctx.meta;
1611
- const pickSseAndAutoClose = () => {
1612
- const sseWriter = ctx.__sseWriter;
1613
- if (!sseWriter) return null;
1614
- if (!sseWriter.closed && !sseWriter.aborted) {
1615
- sseWriter.close();
1616
- }
1617
- return mergeMeta(sseWriter.response, meta);
1618
- };
1619
- const autoCloseSseOnError = () => {
1620
- const sseWriter = ctx.__sseWriter;
1621
- if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
1622
- sseWriter.close();
1707
+ function createSseWriter() {
1708
+ const encoder = new TextEncoder();
1709
+ let controller = null;
1710
+ let closed = false;
1711
+ let aborted = false;
1712
+ const stream = new ReadableStream({
1713
+ start(c) {
1714
+ controller = c;
1715
+ },
1716
+ cancel() {
1717
+ aborted = true;
1718
+ closed = true;
1719
+ controller = null;
1623
1720
  }
1624
- };
1625
- if (!middlewares || middlewares.length === 0) {
1626
- try {
1627
- const result = await injectParamsAsync(handler, ctx, body, injectors);
1628
- const sseResponse = pickSseAndAutoClose();
1629
- if (sseResponse) return sseResponse;
1630
- return toResponse(result, meta);
1631
- } catch (err) {
1632
- autoCloseSseOnError();
1633
- throw err;
1721
+ });
1722
+ const response = new Response(stream, {
1723
+ status: 200,
1724
+ headers: {
1725
+ "Content-Type": "text/event-stream",
1726
+ "Cache-Control": "no-cache",
1727
+ Connection: "keep-alive"
1634
1728
  }
1635
- }
1636
- const finalHandler = async () => {
1637
- try {
1638
- const result = await injectParamsAsync(handler, ctx, body, injectors);
1639
- const sseResponse = pickSseAndAutoClose();
1640
- if (sseResponse) return sseResponse;
1641
- return toResponse(result, meta);
1642
- } catch (err) {
1643
- autoCloseSseOnError();
1644
- throw err;
1729
+ });
1730
+ const writer = {
1731
+ send(event) {
1732
+ if (closed || !controller) return;
1733
+ const text = encodeSseEvent(event);
1734
+ controller.enqueue(encoder.encode(text));
1735
+ },
1736
+ sendRaw(chunk) {
1737
+ if (closed || !controller) return;
1738
+ const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
1739
+ controller.enqueue(bytes);
1740
+ },
1741
+ sendError(error) {
1742
+ if (closed || !controller) return;
1743
+ const message = error instanceof Error ? error.message : String(error);
1744
+ const text = encodeSseEvent({ event: "error", data: message });
1745
+ try {
1746
+ controller.enqueue(encoder.encode(text));
1747
+ } finally {
1748
+ writer.close();
1749
+ }
1750
+ },
1751
+ close() {
1752
+ if (closed) return;
1753
+ closed = true;
1754
+ if (controller) {
1755
+ try {
1756
+ controller.close();
1757
+ } catch {
1758
+ }
1759
+ controller = null;
1760
+ }
1761
+ },
1762
+ get closed() {
1763
+ return closed;
1764
+ },
1765
+ get aborted() {
1766
+ return aborted;
1767
+ },
1768
+ get response() {
1769
+ return response;
1645
1770
  }
1646
1771
  };
1647
- return await compose(middlewares, ctx, finalHandler);
1648
- }
1649
-
1650
- // src/testServer.ts
1651
- import path8 from "path";
1652
- import os from "os";
1653
- import fs5 from "fs/promises";
1654
-
1655
- // src/router/scanRoutes.ts
1656
- import fg from "fast-glob";
1657
- import path4 from "path";
1658
- import fs3 from "fs";
1659
-
1660
- // src/router/constants.ts
1661
- var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1662
- var HTTP_METHOD_SET = new Set(HTTP_METHODS);
1663
- function isHttpMethod(value) {
1664
- return HTTP_METHOD_SET.has(value);
1665
- }
1666
-
1667
- // src/utils/normalizePath.ts
1668
- function normalizePath(path11) {
1669
- if (!path11) return "";
1670
- let result = path11.replace(/\\/g, "/");
1671
- result = result.replace(/\/+/g, "/");
1672
- result = result.replace(/\/+$/, "");
1673
- if (result && !result.startsWith("/")) {
1674
- result = "/" + result;
1675
- }
1676
- return result;
1772
+ return writer;
1677
1773
  }
1678
1774
 
1679
- // src/router/parseRouteFile.ts
1680
- function dynamicSegmentToParam(segment) {
1681
- const match = segment.match(/^\[(.+)\]$/);
1682
- if (match) {
1683
- return ":" + match[1];
1684
- }
1685
- return segment;
1686
- }
1687
- function extractParamNames(urlPath) {
1688
- const params = [];
1689
- const segments = urlPath.split("/");
1690
- for (const segment of segments) {
1691
- if (segment.startsWith(":...")) {
1692
- params.push(segment.slice(4));
1693
- } else if (segment.startsWith(":")) {
1694
- params.push(segment.slice(1));
1775
+ // src/runtime/createContext.ts
1776
+ function parseCookies(cookieHeader) {
1777
+ const cookies = /* @__PURE__ */ new Map();
1778
+ if (!cookieHeader) return cookies;
1779
+ for (const pair of cookieHeader.split(";")) {
1780
+ const [name, ...rest] = pair.split("=");
1781
+ const trimmed = name?.trim();
1782
+ if (trimmed) {
1783
+ cookies.set(trimmed, rest.join("=").trim());
1695
1784
  }
1696
1785
  }
1697
- return params;
1698
- }
1699
- function isCatchAllSegment(segment) {
1700
- return /^\[\.\.\..+\]$/.test(segment);
1786
+ return cookies;
1701
1787
  }
1702
- function isRouteGroup(segment) {
1703
- return /^\(.+\)$/.test(segment);
1788
+ function formatSetCookie(name, value, options) {
1789
+ let cookie = `${name}=${value}`;
1790
+ if (options?.domain) cookie += `; Domain=${options.domain}`;
1791
+ if (options?.path) cookie += `; Path=${options.path}`;
1792
+ if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
1793
+ if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
1794
+ if (options?.httpOnly) cookie += `; HttpOnly`;
1795
+ if (options?.secure) cookie += `; Secure`;
1796
+ if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
1797
+ return cookie;
1704
1798
  }
1705
- function filePathToUrlPath(filePath) {
1706
- const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
1707
- const lastSlashIndex = withoutPrefix.lastIndexOf("/");
1708
- const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
1709
- if (!dirPath) {
1710
- return "";
1799
+ function createContext(request, params, config = {}, ip = "") {
1800
+ const url = new URL(request.url);
1801
+ const meta = { headers: {}, setCookies: [] };
1802
+ const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
1803
+ const cookiesObj = {};
1804
+ for (const [key, val] of parsedCookies) {
1805
+ cookiesObj[key] = val;
1711
1806
  }
1712
- const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
1713
- return normalizePath(segments.join("/"));
1714
- }
1715
-
1716
- // src/middleware/loadMiddlewares.ts
1717
- var middlewareCache = /* @__PURE__ */ new Map();
1718
- function invalidateMiddlewareCache() {
1719
- middlewareCache.clear();
1720
- }
1721
- function getCachedMiddlewares(absPath) {
1722
- return middlewareCache.get(absPath);
1723
- }
1724
- function setCachedMiddlewares(absPath, bundle) {
1725
- middlewareCache.set(absPath, bundle);
1726
- }
1727
- async function loadMiddlewaresFile(filePath) {
1728
- try {
1729
- const module = await importWithCacheBust(filePath);
1730
- const middlewares = module.default ?? module.middlewares ?? [];
1731
- if (!Array.isArray(middlewares)) {
1732
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
1733
- return { middlewares: [], injectors: {} };
1734
- }
1735
- const validMiddlewares = middlewares.filter((m) => {
1736
- if (typeof m !== "function") {
1737
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
1738
- return false;
1739
- }
1740
- return true;
1741
- });
1742
- const injectors = module.injectors ?? {};
1743
- if (typeof injectors !== "object" || injectors === null) {
1744
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
1745
- return { middlewares: validMiddlewares, injectors: {} };
1746
- }
1747
- const validInjectors = {};
1748
- for (const [name, injector] of Object.entries(injectors)) {
1749
- if (typeof injector !== "function") {
1750
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
1751
- continue;
1752
- }
1753
- validInjectors[name] = injector;
1807
+ const ctx = {
1808
+ request,
1809
+ params,
1810
+ query: url.searchParams,
1811
+ headers: request.headers,
1812
+ method: request.method,
1813
+ path: url.pathname,
1814
+ ip,
1815
+ ua: request.headers.get("user-agent") ?? "",
1816
+ cookies: cookiesObj,
1817
+ config,
1818
+ meta,
1819
+ setStatus(status) {
1820
+ meta.status = status;
1821
+ },
1822
+ setHeader(key, value) {
1823
+ meta.headers[key] = value;
1824
+ },
1825
+ setETag(value) {
1826
+ meta.headers["etag"] = value;
1827
+ },
1828
+ redirect(url2, status = 302) {
1829
+ return new Response(null, {
1830
+ status,
1831
+ headers: { Location: url2 }
1832
+ });
1833
+ },
1834
+ json(data, status) {
1835
+ const headers = { "Content-Type": "application/json" };
1836
+ return new Response(JSON.stringify(data), {
1837
+ status: status ?? 200,
1838
+ headers
1839
+ });
1840
+ },
1841
+ html(html, status) {
1842
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
1843
+ return new Response(html, {
1844
+ status: status ?? 200,
1845
+ headers
1846
+ });
1847
+ },
1848
+ getCookie(name) {
1849
+ return parsedCookies.get(name);
1850
+ },
1851
+ setCookie(name, value, options) {
1852
+ meta.setCookies.push(formatSetCookie(name, value, options));
1853
+ },
1854
+ deleteCookie(name) {
1855
+ meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
1856
+ },
1857
+ /**
1858
+ * 创建 SSE writer,用于流式推送事件
1859
+ *
1860
+ * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
1861
+ * 作为 HTTP 响应(Content-Type: text/event-stream)。
1862
+ *
1863
+ * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
1864
+ */
1865
+ sse() {
1866
+ const writer = createSseWriter();
1867
+ const ctxWithSse = ctx;
1868
+ ctxWithSse.__sseResponse = writer.response;
1869
+ ctxWithSse.__sseWriter = writer;
1870
+ return writer;
1754
1871
  }
1755
- return { middlewares: validMiddlewares, injectors: validInjectors };
1756
- } catch {
1757
- return { middlewares: [], injectors: {} };
1872
+ };
1873
+ const extend = config?.extendContext;
1874
+ if (typeof extend === "function") {
1875
+ extend(ctx);
1758
1876
  }
1877
+ return ctx;
1759
1878
  }
1760
1879
 
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);
1880
+ // src/utils/isPlainObject.ts
1881
+ function isPlainObject(value) {
1882
+ if (value === null || typeof value !== "object") {
1883
+ return false;
1884
+ }
1885
+ if (Array.isArray(value)) {
1886
+ return false;
1767
1887
  }
1768
- const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
1769
- return path4.resolve(rootDir, prodRel);
1888
+ const proto = Object.getPrototypeOf(value);
1889
+ return proto === null || proto === Object.prototype;
1770
1890
  }
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;
1891
+
1892
+ // src/response/toResponse.ts
1893
+ async function toResponse(value, meta) {
1894
+ if (value instanceof Promise) {
1895
+ return toResponse(await value, meta);
1798
1896
  }
1799
- if (mwPaths.length === 0) return void 0;
1800
- mwPaths.reverse();
1801
- const mergedMiddlewares = [];
1802
- const mergedInjectors = {};
1803
- for (const absMwPath of mwPaths) {
1804
- let bundle = getCachedMiddlewares(absMwPath);
1805
- if (bundle === void 0) {
1806
- bundle = await loadMiddlewaresFile(absMwPath);
1807
- setCachedMiddlewares(absMwPath, bundle);
1897
+ const applyMeta = (headers2) => {
1898
+ if (!meta) return;
1899
+ for (const [key, val] of Object.entries(meta.headers)) {
1900
+ headers2.set(key, val);
1808
1901
  }
1809
- mergedMiddlewares.push(...bundle.middlewares);
1810
- for (const [name, injector] of Object.entries(bundle.injectors)) {
1811
- mergedInjectors[name] = injector;
1902
+ for (const cookie of meta.setCookies ?? []) {
1903
+ headers2.append("set-cookie", cookie);
1812
1904
  }
1813
- }
1814
- if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
1815
- return void 0;
1816
- }
1817
- return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
1818
- }
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);
1826
- }
1905
+ };
1906
+ if (value instanceof Response) {
1907
+ if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
1908
+ const headers2 = new Headers(value.headers);
1909
+ applyMeta(headers2);
1910
+ return new Response(value.body, {
1911
+ status: meta.status ?? value.status,
1912
+ headers: headers2
1913
+ });
1827
1914
  }
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 [];
1915
+ return value;
1833
1916
  }
1834
- }
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;
1917
+ if (value === null || value === void 0) {
1918
+ const status = meta?.status ?? 204;
1919
+ const headers2 = new Headers();
1920
+ applyMeta(headers2);
1921
+ return new Response(null, { status, headers: headers2 });
1843
1922
  }
1844
- }
1845
- async function scanRoutes(rootDir, patterns, dist) {
1846
- const files = await fg(patterns, {
1847
- cwd: rootDir,
1848
- onlyFiles: true,
1849
- absolute: false
1850
- });
1851
- const routes = [];
1852
- const wsRoutes = [];
1853
- for (const file of files) {
1854
- const normalizedFile = file.replace(/\\/g, "/");
1855
- const fileName = normalizedFile.split("/").pop();
1856
- if (fileName === "handler.ts" || fileName === "handler.js") {
1857
- const absPath = path4.resolve(rootDir, normalizedFile);
1858
- const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
1859
- const urlPath = filePathToUrlPath(normalizedFile);
1860
- const paramNames = extractParamNames(urlPath);
1861
- const isDynamic = paramNames.length > 0;
1862
- const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
1863
- const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
1864
- const methods = await extractMethodsFromHandler(importPath);
1865
- for (const method of methods) {
1866
- routes.push({
1867
- method,
1868
- urlPath,
1869
- filePath: normalizedFile,
1870
- paramNames,
1871
- isDynamic,
1872
- isCatchAll: isCatchAll || void 0,
1873
- middlewares: middlewareBundle?.middlewares,
1874
- injectors: middlewareBundle?.injectors
1875
- });
1876
- }
1877
- const hasWs = await hasWsExport(importPath);
1878
- if (hasWs) {
1879
- wsRoutes.push({
1880
- urlPath,
1881
- filePath: normalizedFile,
1882
- paramNames,
1883
- isDynamic,
1884
- isCatchAll: isCatchAll || void 0,
1885
- middlewares: middlewareBundle?.middlewares,
1886
- injectors: middlewareBundle?.injectors
1887
- });
1888
- }
1889
- continue;
1890
- }
1923
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
1924
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1925
+ applyMeta(headers2);
1926
+ return new Response(value, {
1927
+ status: meta?.status ?? 200,
1928
+ headers: headers2
1929
+ });
1891
1930
  }
1892
- return { routes, wsRoutes };
1893
- }
1894
-
1895
- // src/router/sortRoutes.ts
1896
- function sortRoutes(routes) {
1897
- return [...routes].sort((a, b) => {
1898
- if (a.isDynamic !== b.isDynamic) {
1899
- return a.isDynamic ? 1 : -1;
1900
- }
1901
- if (a.isCatchAll !== b.isCatchAll) {
1902
- return a.isCatchAll ? 1 : -1;
1903
- }
1904
- const aSegments = a.urlPath.split("/").filter(Boolean).length;
1905
- const bSegments = b.urlPath.split("/").filter(Boolean).length;
1906
- if (aSegments !== bSegments) {
1907
- return aSegments - bSegments;
1908
- }
1909
- return a.urlPath.localeCompare(b.urlPath);
1910
- });
1911
- }
1912
-
1913
- // src/cli/generateSchemaFiles.ts
1914
- import path5 from "path";
1915
- import fs4 from "fs/promises";
1916
-
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;
1931
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
1932
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1933
+ applyMeta(headers2);
1934
+ return new Response(value, {
1935
+ status: meta?.status ?? 200,
1936
+ headers: headers2
1937
+ });
1936
1938
  }
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
- }
1939
+ if (value instanceof Uint8Array) {
1940
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
1941
+ applyMeta(headers2);
1942
+ return new Response(value, {
1943
+ status: meta?.status ?? 200,
1944
+ headers: headers2
1945
+ });
1990
1946
  }
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);
1947
+ if (isPlainObject(value) || Array.isArray(value)) {
1948
+ const body2 = JSON.stringify(value);
1949
+ const headers2 = new Headers({ "Content-Type": "application/json" });
1950
+ applyMeta(headers2);
1951
+ return new Response(body2, {
1952
+ status: meta?.status ?? 200,
1953
+ headers: headers2
1954
+ });
1955
+ }
1956
+ if (typeof value === "string") {
1957
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
1958
+ applyMeta(headers2);
1959
+ return new Response(value, {
1960
+ status: meta?.status ?? 200,
1961
+ headers: headers2
1962
+ });
1997
1963
  }
1998
- return withConstraints;
1999
- }
2000
- function applyConstraints(baseExpr, constraints, typeKind) {
2001
- const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
2002
- return `${baseExpr}${suffix}`;
1964
+ if (typeof value === "number" || typeof value === "boolean") {
1965
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
1966
+ applyMeta(headers2);
1967
+ return new Response(String(value), {
1968
+ status: meta?.status ?? 200,
1969
+ headers: headers2
1970
+ });
1971
+ }
1972
+ const body = JSON.stringify(value);
1973
+ const headers = new Headers({ "Content-Type": "application/json" });
1974
+ applyMeta(headers);
1975
+ return new Response(body, {
1976
+ status: meta?.status ?? 200,
1977
+ headers
1978
+ });
2003
1979
  }
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()";
1980
+
1981
+ // src/injection/injectParams.ts
1982
+ init_resolveInjection();
1983
+
1984
+ // src/utils/queryToObject.ts
1985
+ function queryToObject(params) {
1986
+ const result = {};
1987
+ for (const [key, value] of params) {
1988
+ result[key] = value;
2036
1989
  }
1990
+ return result;
2037
1991
  }
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`;
1992
+
1993
+ // src/injection/injectParams.ts
1994
+ function getBuiltinInjectionValue(type, ctx, body) {
1995
+ switch (type) {
1996
+ case "query":
1997
+ return queryToObject(ctx.query);
1998
+ case "params":
1999
+ return ctx.params;
2000
+ case "headers":
2001
+ return ctx.headers;
2002
+ case "context":
2003
+ return ctx;
2004
+ case "cookies":
2005
+ return ctx.cookies;
2006
+ case "ip":
2007
+ return ctx.ip;
2008
+ case "ua":
2009
+ return ctx.ua;
2010
+ case "body":
2011
+ return body;
2012
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
2013
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
2014
+ case "form":
2015
+ return body;
2016
+ case "files":
2017
+ if (body && typeof body === "object" && "files" in body) {
2018
+ return body.files;
2076
2019
  }
2077
- return `${type.name}Schema`;
2020
+ return [];
2021
+ case "fields":
2022
+ if (body && typeof body === "object" && "fields" in body) {
2023
+ return body.fields;
2024
+ }
2025
+ return {};
2026
+ default:
2027
+ return void 0;
2078
2028
  }
2079
2029
  }
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})`;
2030
+ async function injectParamsAsync(handler, ctx, body, injectors) {
2031
+ const injections = resolveInjection(handler);
2032
+ if (injections.length === 0) {
2033
+ return await handler();
2101
2034
  }
2102
- return `z.preprocess(coerceBoolean, ${inner})`;
2035
+ const args = await Promise.all(
2036
+ injections.map(async (injection) => {
2037
+ if (injection.type !== "unknown") {
2038
+ return getBuiltinInjectionValue(injection.type, ctx, body);
2039
+ }
2040
+ if (injectors && injection.name in injectors) {
2041
+ return await injectors[injection.name](ctx);
2042
+ }
2043
+ return void 0;
2044
+ })
2045
+ );
2046
+ return await handler(...args);
2103
2047
  }
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
- }
2048
+
2049
+ // src/runtime/invokeHandler.ts
2050
+ function mergeMeta(response, meta) {
2051
+ const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
2052
+ if (!hasMeta) return response;
2053
+ const headers = new Headers(response.headers);
2054
+ for (const [key, value] of Object.entries(meta.headers)) {
2055
+ headers.set(key, value);
2117
2056
  }
2118
- if (restExpression) {
2119
- return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
2057
+ for (const cookie of meta.setCookies) {
2058
+ headers.append("set-cookie", cookie);
2120
2059
  }
2121
- const hasOptional = fixedOptional.some((o) => o);
2122
- if (!hasOptional) {
2123
- return `z.tuple([${fixedExprs.join(", ")}])`;
2060
+ return new Response(response.body, {
2061
+ status: meta.status ?? response.status,
2062
+ headers
2063
+ });
2064
+ }
2065
+ async function compose(middlewares, ctx, finalHandler) {
2066
+ const meta = ctx.meta;
2067
+ let index = -1;
2068
+ async function dispatch(i) {
2069
+ if (i <= index) {
2070
+ throw new Error("next() called multiple times");
2071
+ }
2072
+ index = i;
2073
+ if (i >= middlewares.length) {
2074
+ return await finalHandler();
2075
+ }
2076
+ const mw = middlewares[i];
2077
+ let innerResponse;
2078
+ const next = async () => {
2079
+ innerResponse = await dispatch(i + 1);
2080
+ return innerResponse;
2081
+ };
2082
+ const result = await mw(ctx, next);
2083
+ if (result instanceof Response) {
2084
+ return mergeMeta(result, meta);
2085
+ }
2086
+ if (innerResponse !== void 0) {
2087
+ return innerResponse;
2088
+ }
2089
+ throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
2124
2090
  }
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;
2091
+ return await dispatch(0);
2092
+ }
2093
+ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
2094
+ const meta = ctx.meta;
2095
+ const pickSseAndAutoClose = () => {
2096
+ const sseWriter = ctx.__sseWriter;
2097
+ if (!sseWriter) return null;
2098
+ if (!sseWriter.closed && !sseWriter.aborted) {
2099
+ sseWriter.close();
2100
+ }
2101
+ return mergeMeta(sseWriter.response, meta);
2102
+ };
2103
+ const autoCloseSseOnError = () => {
2104
+ const sseWriter = ctx.__sseWriter;
2105
+ if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
2106
+ sseWriter.close();
2107
+ }
2108
+ };
2109
+ if (!middlewares || middlewares.length === 0) {
2110
+ try {
2111
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
2112
+ const sseResponse = pickSseAndAutoClose();
2113
+ if (sseResponse) return sseResponse;
2114
+ return toResponse(result, meta);
2115
+ } catch (err) {
2116
+ autoCloseSseOnError();
2117
+ throw err;
2130
2118
  }
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
2119
  }
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(", ")} })`;
2120
+ const finalHandler = async () => {
2121
+ try {
2122
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
2123
+ const sseResponse = pickSseAndAutoClose();
2124
+ if (sseResponse) return sseResponse;
2125
+ return toResponse(result, meta);
2126
+ } catch (err) {
2127
+ autoCloseSseOnError();
2128
+ throw err;
2129
+ }
2130
+ };
2131
+ return await compose(middlewares, ctx, finalHandler);
2147
2132
  }
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()`;
2133
+
2134
+ // src/testServer.ts
2135
+ import path12 from "path";
2136
+ import os from "os";
2137
+ import fs9 from "fs/promises";
2138
+
2139
+ // src/router/scanRoutes.ts
2140
+ import fg from "fast-glob";
2141
+ import path4 from "path";
2142
+ import fs3 from "fs";
2143
+
2144
+ // src/router/constants.ts
2145
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
2146
+ var HTTP_METHOD_SET = new Set(HTTP_METHODS);
2147
+
2148
+ // src/utils/normalizePath.ts
2149
+ function normalizePath(path15) {
2150
+ if (!path15) return "";
2151
+ let result = path15.replace(/\\/g, "/");
2152
+ result = result.replace(/\/+/g, "/");
2153
+ result = result.replace(/\/+$/, "");
2154
+ if (result && !result.startsWith("/")) {
2155
+ result = "/" + result;
2157
2156
  }
2158
- const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
2159
- return `z.union([${unionInner}])`;
2157
+ return result;
2160
2158
  }
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});`;
2159
+
2160
+ // src/router/parseRouteFile.ts
2161
+ function dynamicSegmentToParam(segment) {
2162
+ const match = segment.match(/^\[(.+)\]$/);
2163
+ if (match) {
2164
+ return ":" + match[1];
2166
2165
  }
2167
- return `const ${name}Schema = ${expr};`;
2166
+ return segment;
2168
2167
  }
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;
2168
+ function extractParamNames(urlPath) {
2169
+ const params = [];
2170
+ const segments = urlPath.split("/");
2171
+ for (const segment of segments) {
2172
+ if (segment.startsWith(":...")) {
2173
+ params.push(segment.slice(4));
2174
+ } else if (segment.startsWith(":")) {
2175
+ params.push(segment.slice(1));
2176
+ }
2189
2177
  }
2178
+ return params;
2190
2179
  }
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};`);
2180
+ function isCatchAllSegment(segment) {
2181
+ return /^\[\.\.\..+\]$/.test(segment);
2182
+ }
2183
+ function isRouteGroup(segment) {
2184
+ return /^\(.+\)$/.test(segment);
2185
+ }
2186
+ function filePathToUrlPath(filePath) {
2187
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
2188
+ const lastSlashIndex = withoutPrefix.lastIndexOf("/");
2189
+ const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
2190
+ if (!dirPath) {
2191
+ return "";
2212
2192
  }
2213
- return lines.join("\n");
2193
+ const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
2194
+ return normalizePath(segments.join("/"));
2214
2195
  }
2215
2196
 
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);
2221
- }
2222
- const idx = rel.lastIndexOf("/");
2223
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2224
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2197
+ // src/middleware/loadMiddlewares.ts
2198
+ var middlewareCache = /* @__PURE__ */ new Map();
2199
+ function invalidateMiddlewareCache() {
2200
+ middlewareCache.clear();
2225
2201
  }
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);
2232
- }
2233
- const idx = rel.lastIndexOf("/");
2234
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2235
- return path5.resolve(rootDir, dist, relDir, "zod.js");
2202
+ function getCachedMiddlewares(absPath) {
2203
+ return middlewareCache.get(absPath);
2236
2204
  }
2237
- function getHelpersImportPath(relDir) {
2238
- if (!relDir) return `./${HELPERS_FILENAME}`;
2239
- const depth = relDir.split("/").filter(Boolean).length;
2240
- return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2205
+ function setCachedMiddlewares(absPath, bundle) {
2206
+ middlewareCache.set(absPath, bundle);
2241
2207
  }
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;
2208
+ async function loadMiddlewaresFile(filePath) {
2209
+ try {
2210
+ const module = await importWithCacheBust(filePath);
2211
+ const middlewares = module.default ?? module.middlewares ?? [];
2212
+ if (!Array.isArray(middlewares)) {
2213
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2214
+ return { middlewares: [], injectors: {} };
2250
2215
  }
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"));
2216
+ const validMiddlewares = middlewares.filter((m) => {
2217
+ if (typeof m !== "function") {
2218
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
2219
+ return false;
2220
+ }
2221
+ return true;
2222
+ });
2223
+ const injectors = module.injectors ?? {};
2224
+ if (typeof injectors !== "object" || injectors === null) {
2225
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
2226
+ return { middlewares: validMiddlewares, injectors: {} };
2227
+ }
2228
+ const validInjectors = {};
2229
+ for (const [name, injector] of Object.entries(injectors)) {
2230
+ if (typeof injector !== "function") {
2231
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
2232
+ continue;
2233
+ }
2234
+ validInjectors[name] = injector;
2235
+ }
2236
+ return { middlewares: validMiddlewares, injectors: validInjectors };
2237
+ } catch {
2238
+ return { middlewares: [], injectors: {} };
2260
2239
  }
2261
- const allSchemaCode = schemaBlocks.join("\n");
2262
- if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2263
- lines.push(
2264
- `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2265
- );
2240
+ }
2241
+ async function loadMergedMiddlewares(middlewarePaths) {
2242
+ if (middlewarePaths.length === 0) return void 0;
2243
+ const mergedMiddlewares = [];
2244
+ const mergedInjectors = {};
2245
+ for (const absMwPath of middlewarePaths) {
2246
+ let bundle = getCachedMiddlewares(absMwPath);
2247
+ if (bundle === void 0) {
2248
+ bundle = await loadMiddlewaresFile(absMwPath);
2249
+ setCachedMiddlewares(absMwPath, bundle);
2250
+ }
2251
+ mergedMiddlewares.push(...bundle.middlewares);
2252
+ for (const [name, injector] of Object.entries(bundle.injectors)) {
2253
+ mergedInjectors[name] = injector;
2254
+ }
2255
+ }
2256
+ if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
2257
+ return void 0;
2258
+ }
2259
+ return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
2260
+ }
2261
+
2262
+ // src/router/scanRoutes.ts
2263
+ var HTTP_OR_WS_EXPORT_RE = new RegExp(
2264
+ String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
2265
+ "g"
2266
+ );
2267
+ function extractExportsFromSource(source) {
2268
+ const names = /* @__PURE__ */ new Set();
2269
+ let match;
2270
+ HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
2271
+ while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
2272
+ names.add(match[1]);
2273
+ }
2274
+ return names;
2275
+ }
2276
+ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
2277
+ const routeDir = path4.dirname(routeFilePath);
2278
+ const resolvedRoot = path4.resolve(rootDir);
2279
+ const paths = [];
2280
+ let currentDir = path4.resolve(rootDir, routeDir);
2281
+ while (true) {
2282
+ if (dist) {
2283
+ const mwTsPath = path4.join(currentDir, "middlewares.ts");
2284
+ const mwJsPath = path4.join(currentDir, "middlewares.js");
2285
+ const absTsPath = path4.resolve(rootDir, mwTsPath);
2286
+ const absJsPath = path4.resolve(rootDir, mwJsPath);
2287
+ const absMwPath = fs3.existsSync(absTsPath) ? absTsPath : fs3.existsSync(absJsPath) ? absJsPath : null;
2288
+ if (absMwPath) {
2289
+ const relMwPath = path4.relative(rootDir, absMwPath);
2290
+ const prodAbsPath = path4.resolve(rootDir, toProdFilePath(relMwPath, dist));
2291
+ paths.push(prodAbsPath);
2292
+ }
2293
+ } else {
2294
+ for (const ext of [".ts", ".js"]) {
2295
+ const mwPath = path4.join(currentDir, `middlewares${ext}`);
2296
+ const absMwPath = path4.resolve(rootDir, mwPath);
2297
+ if (fs3.existsSync(absMwPath)) {
2298
+ paths.push(absMwPath);
2299
+ break;
2300
+ }
2301
+ }
2302
+ }
2303
+ if (currentDir === resolvedRoot) break;
2304
+ const parentDir = path4.dirname(currentDir);
2305
+ if (parentDir === currentDir) break;
2306
+ currentDir = parentDir;
2266
2307
  }
2267
- lines.push("");
2268
- lines.push(...schemaBlocks);
2269
- return lines.join("\n").replace(/\n+$/, "\n");
2308
+ paths.reverse();
2309
+ return paths;
2270
2310
  }
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);
2311
+ function toProdFilePath(filePath, dist) {
2312
+ let rel = filePath.replace(/\\/g, "/");
2313
+ if (rel.startsWith("src/")) {
2314
+ rel = rel.slice(4);
2282
2315
  }
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);
2316
+ const jsPath = rel.replace(/\.ts$/, ".js");
2317
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
2318
+ }
2319
+ async function scanRoutes(rootDir, patterns, dist) {
2320
+ const files = await fg(patterns, {
2321
+ cwd: rootDir,
2322
+ onlyFiles: true,
2323
+ absolute: false
2324
+ });
2325
+ const routes = [];
2326
+ const wsRoutes = [];
2327
+ for (const file of files) {
2328
+ const normalizedFile = file.replace(/\\/g, "/");
2329
+ const fileName = normalizedFile.split("/").pop();
2330
+ if (fileName === "handler.ts" || fileName === "handler.js") {
2331
+ const absPath = path4.resolve(rootDir, normalizedFile);
2332
+ const urlPath = filePathToUrlPath(normalizedFile);
2333
+ const paramNames = extractParamNames(urlPath);
2334
+ const isDynamic = paramNames.length > 0;
2335
+ const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
2336
+ let middlewarePaths;
2337
+ let middlewareBundle;
2338
+ if (dist) {
2339
+ middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
2340
+ } else {
2341
+ const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
2342
+ middlewareBundle = await loadMergedMiddlewares(mwPaths);
2343
+ }
2344
+ const source = await fs3.promises.readFile(absPath, "utf8").catch(() => "");
2345
+ const exportNames = extractExportsFromSource(source);
2346
+ const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
2347
+ for (const method of methods) {
2348
+ routes.push({
2349
+ method,
2350
+ urlPath,
2351
+ filePath: normalizedFile,
2352
+ paramNames,
2353
+ isDynamic,
2354
+ isCatchAll: isCatchAll || void 0,
2355
+ middlewarePaths,
2356
+ middlewares: middlewareBundle?.middlewares,
2357
+ injectors: middlewareBundle?.injectors
2358
+ });
2359
+ }
2360
+ if (exportNames.has("WS")) {
2361
+ wsRoutes.push({
2362
+ urlPath,
2363
+ filePath: normalizedFile,
2364
+ paramNames,
2365
+ isDynamic,
2366
+ isCatchAll: isCatchAll || void 0,
2367
+ middlewarePaths,
2368
+ middlewares: middlewareBundle?.middlewares,
2369
+ injectors: middlewareBundle?.injectors
2370
+ });
2371
+ }
2372
+ continue;
2291
2373
  }
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
- }
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());
2302
2374
  }
2303
- await Promise.all(
2304
- fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2305
- );
2375
+ return { routes, wsRoutes };
2306
2376
  }
2307
- async function writeSchemaFile(outputPath, source) {
2308
- await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
2309
- await fs4.writeFile(outputPath, source, "utf-8");
2377
+
2378
+ // src/router/sortRoutes.ts
2379
+ function sortRoutes(routes) {
2380
+ return [...routes].sort((a, b) => {
2381
+ if (a.isDynamic !== b.isDynamic) {
2382
+ return a.isDynamic ? 1 : -1;
2383
+ }
2384
+ if (a.isCatchAll !== b.isCatchAll) {
2385
+ return a.isCatchAll ? 1 : -1;
2386
+ }
2387
+ const aSegments = a.urlPath.split("/").filter(Boolean).length;
2388
+ const bSegments = b.urlPath.split("/").filter(Boolean).length;
2389
+ if (aSegments !== bSegments) {
2390
+ return aSegments - bSegments;
2391
+ }
2392
+ return a.urlPath.localeCompare(b.urlPath);
2393
+ });
2310
2394
  }
2311
2395
 
2396
+ // src/testServer.ts
2397
+ init_generateSchemaFiles();
2398
+
2312
2399
  // src/validator/validateInput.ts
2400
+ init_schemaName();
2313
2401
  var moduleCache = /* @__PURE__ */ new Map();
2314
2402
  function invalidateSchemaCache() {
2315
2403
  moduleCache.clear();
@@ -2353,9 +2441,9 @@ async function validateInput(schemaPath, method, inputType, input) {
2353
2441
  function mapZodIssues(error) {
2354
2442
  return error.issues.map((issue) => {
2355
2443
  const code = mapZodCode(issue.code, issue.message);
2356
- const path11 = issue.path.map(String).join(".") || "";
2444
+ const path15 = issue.path.map(String).join(".") || "";
2357
2445
  return {
2358
- path: path11,
2446
+ path: path15,
2359
2447
  code,
2360
2448
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2361
2449
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -2403,45 +2491,45 @@ import {
2403
2491
  import { createSecureServer as createHttp2SecureServer } from "http2";
2404
2492
  import { readFileSync } from "fs";
2405
2493
  import { Readable as Readable2 } from "stream";
2406
- import path7 from "path";
2494
+ import path11 from "path";
2407
2495
 
2408
2496
  // src/router/matchRoute.ts
2409
- function matchRoute(routes, method, path11) {
2497
+ function matchRoute(routes, method, path15) {
2410
2498
  for (const route of routes) {
2411
2499
  if (route.method !== method) {
2412
2500
  continue;
2413
2501
  }
2414
2502
  if (!route.isDynamic) {
2415
- if (route.urlPath === path11) {
2503
+ if (route.urlPath === path15) {
2416
2504
  return { route, params: {} };
2417
2505
  }
2418
2506
  continue;
2419
2507
  }
2420
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2508
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2421
2509
  if (params !== null) {
2422
2510
  return { route, params };
2423
2511
  }
2424
2512
  }
2425
2513
  return null;
2426
2514
  }
2427
- function matchWsRoute(wsRoutes, path11) {
2515
+ function matchWsRoute(wsRoutes, path15) {
2428
2516
  for (const route of wsRoutes) {
2429
2517
  if (!route.isDynamic) {
2430
- if (route.urlPath === path11) {
2518
+ if (route.urlPath === path15) {
2431
2519
  return { route, params: {} };
2432
2520
  }
2433
2521
  continue;
2434
2522
  }
2435
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2523
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2436
2524
  if (params !== null) {
2437
2525
  return { route, params };
2438
2526
  }
2439
2527
  }
2440
2528
  return null;
2441
2529
  }
2442
- function matchDynamicPath(pattern, path11, paramNames, isCatchAll) {
2530
+ function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
2443
2531
  const patternSegments = pattern.split("/").filter(Boolean);
2444
- const pathSegments = path11.split("/").filter(Boolean);
2532
+ const pathSegments = path15.split("/").filter(Boolean);
2445
2533
  if (isCatchAll) {
2446
2534
  const nonCatchAllCount = patternSegments.length - 1;
2447
2535
  if (pathSegments.length <= nonCatchAllCount) {
@@ -2510,12 +2598,379 @@ function validateRouteModule(value, method, filePath) {
2510
2598
  }
2511
2599
  }
2512
2600
 
2601
+ // src/cli/compileOnDemand.ts
2602
+ import path9 from "path";
2603
+ import fs8 from "fs";
2604
+
2605
+ // src/cli/compileDevRoutes.ts
2606
+ import path8 from "path";
2607
+ import fs7 from "fs";
2608
+ import fg2 from "fast-glob";
2609
+
2610
+ // src/cli/aliasPlugin.ts
2611
+ import path7 from "path";
2612
+ import fs6 from "fs";
2613
+
2614
+ // src/utils/resolveAlias.ts
2615
+ function resolveAlias(specifier, config) {
2616
+ const candidates = [];
2617
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2618
+ const wildcardIndex = pattern.indexOf("*");
2619
+ if (wildcardIndex === -1) {
2620
+ if (specifier === pattern) {
2621
+ candidates.push(...targets);
2622
+ }
2623
+ continue;
2624
+ }
2625
+ const prefix = pattern.slice(0, wildcardIndex);
2626
+ const suffix = pattern.slice(wildcardIndex + 1);
2627
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
2628
+ const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
2629
+ for (const target of targets) {
2630
+ candidates.push(target.replace("*", captured));
2631
+ }
2632
+ }
2633
+ }
2634
+ return candidates;
2635
+ }
2636
+
2637
+ // src/utils/readTsconfig.ts
2638
+ import ts6 from "typescript";
2639
+ import path6 from "path";
2640
+ import fs5 from "fs";
2641
+ function readTsconfig(rootDir) {
2642
+ const tsconfigPath = path6.resolve(rootDir, "tsconfig.json");
2643
+ if (!fs5.existsSync(tsconfigPath)) return null;
2644
+ const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2645
+ if (configFile.error || !configFile.config) return null;
2646
+ const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2647
+ const baseUrl = parsed.options.baseUrl ?? rootDir;
2648
+ const rawPaths = parsed.options.paths;
2649
+ if (!rawPaths) return null;
2650
+ const paths = {};
2651
+ for (const [pattern, targets] of Object.entries(rawPaths)) {
2652
+ paths[pattern] = targets.map((t) => path6.resolve(baseUrl, t));
2653
+ }
2654
+ return { baseUrl, paths };
2655
+ }
2656
+
2657
+ // src/cli/aliasPlugin.ts
2658
+ function toProdExtension(filePath) {
2659
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2660
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2661
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2662
+ return filePath;
2663
+ }
2664
+ function toProdImportPath(sourceFile, importer) {
2665
+ const importerDir = path7.dirname(importer);
2666
+ let rel = path7.relative(importerDir, sourceFile);
2667
+ rel = rel.split(path7.sep).join("/");
2668
+ if (!rel.startsWith(".")) rel = "./" + rel;
2669
+ return toProdExtension(rel);
2670
+ }
2671
+ function toRealPath(p) {
2672
+ try {
2673
+ return fs6.realpathSync(p);
2674
+ } catch {
2675
+ return p;
2676
+ }
2677
+ }
2678
+ function isInsideDir(filePath, dir) {
2679
+ const rel = path7.relative(dir, filePath);
2680
+ return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
2681
+ }
2682
+ var APP_DIR = "src";
2683
+ function toStrippedProdImportPath(sourceFile, rootDir) {
2684
+ const appDirAbs = toRealPath(path7.resolve(rootDir, APP_DIR));
2685
+ const sourceReal = toRealPath(sourceFile);
2686
+ let rel = path7.relative(appDirAbs, sourceReal);
2687
+ rel = rel.split(path7.sep).join("/");
2688
+ if (!rel.startsWith(".")) rel = "./" + rel;
2689
+ return toProdExtension(rel);
2690
+ }
2691
+ var PROD_EXTS = [".js", ".mjs", ".cjs"];
2692
+ var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2693
+ var INDEX_EXTS = [
2694
+ "/index.ts",
2695
+ "/index.tsx",
2696
+ "/index.js",
2697
+ "/index.jsx",
2698
+ "/index.mjs",
2699
+ "/index.cjs"
2700
+ ];
2701
+ function resolveRelativeSpecifier(importer, specifier) {
2702
+ const importerDir = path7.dirname(importer);
2703
+ const base = path7.resolve(importerDir, specifier);
2704
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2705
+ return fs6.existsSync(base) ? base : null;
2706
+ }
2707
+ if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2708
+ return fs6.existsSync(base) ? base : null;
2709
+ }
2710
+ for (const ext of SOURCE_EXTS) {
2711
+ const file = base + ext;
2712
+ if (fs6.existsSync(file)) return file;
2713
+ }
2714
+ for (const indexExt of INDEX_EXTS) {
2715
+ const file = base + indexExt;
2716
+ if (fs6.existsSync(file)) return file;
2717
+ }
2718
+ return null;
2719
+ }
2720
+ function createAliasPlugin(config, options) {
2721
+ const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2722
+ const appDirAbs = options?.rootDir ? toRealPath(path7.resolve(options.rootDir, APP_DIR)) : null;
2723
+ return {
2724
+ name: "faapi-alias",
2725
+ setup(build) {
2726
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2727
+ let source;
2728
+ try {
2729
+ source = fs6.readFileSync(args.path, "utf8");
2730
+ } catch {
2731
+ return void 0;
2732
+ }
2733
+ const importer = args.path;
2734
+ const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2735
+ let modified = false;
2736
+ const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2737
+ if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2738
+ return full;
2739
+ }
2740
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2741
+ const resolved = resolveRelativeSpecifier(importer, specifier);
2742
+ if (resolved) {
2743
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2744
+ return full;
2745
+ }
2746
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
2747
+ modified = true;
2748
+ return `${prefix}${quote}${toStrippedProdImportPath(
2749
+ resolved,
2750
+ options.rootDir
2751
+ )}${quote}`;
2752
+ }
2753
+ modified = true;
2754
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
2755
+ }
2756
+ return full;
2757
+ }
2758
+ const candidates = resolveAlias(specifier, config);
2759
+ for (const candidate of candidates) {
2760
+ for (const ext of SOURCE_EXTS) {
2761
+ const file = candidate + ext;
2762
+ if (fs6.existsSync(file)) {
2763
+ modified = true;
2764
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2765
+ return `${prefix}${quote}${toStrippedProdImportPath(
2766
+ file,
2767
+ options.rootDir
2768
+ )}${quote}`;
2769
+ }
2770
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2771
+ }
2772
+ }
2773
+ for (const indexExt of INDEX_EXTS) {
2774
+ const file = candidate + indexExt;
2775
+ if (fs6.existsSync(file)) {
2776
+ modified = true;
2777
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2778
+ return `${prefix}${quote}${toStrippedProdImportPath(
2779
+ file,
2780
+ options.rootDir
2781
+ )}${quote}`;
2782
+ }
2783
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2784
+ }
2785
+ }
2786
+ }
2787
+ return full;
2788
+ });
2789
+ if (!modified) return void 0;
2790
+ return { contents: newSource, loader: "default" };
2791
+ });
2792
+ }
2793
+ };
2794
+ }
2795
+ function buildAliasPlugins(rootDir) {
2796
+ const tsconfig = readTsconfig(rootDir);
2797
+ return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2798
+ }
2799
+
2800
+ // src/cli/compileDevRoutes.ts
2801
+ var APP_DIR2 = "src";
2802
+ async function compileDevRoutes(options) {
2803
+ const { rootDir, dist, files, logLevel = "silent" } = options;
2804
+ const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2805
+ cwd: rootDir,
2806
+ onlyFiles: true,
2807
+ absolute: true,
2808
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
2809
+ });
2810
+ if (entryPoints.length === 0) {
2811
+ return { compiledFiles: [] };
2812
+ }
2813
+ const absDist = path8.resolve(rootDir, dist);
2814
+ await fs7.promises.mkdir(absDist, { recursive: true });
2815
+ const plugins = buildAliasPlugins(rootDir);
2816
+ const esbuild = await import("esbuild");
2817
+ const outbase = path8.resolve(rootDir, APP_DIR2);
2818
+ const result = await esbuild.build({
2819
+ entryPoints,
2820
+ outdir: absDist,
2821
+ outbase,
2822
+ bundle: false,
2823
+ platform: "node",
2824
+ format: "esm",
2825
+ sourcemap: true,
2826
+ packages: "external",
2827
+ plugins,
2828
+ logLevel,
2829
+ write: false
2830
+ });
2831
+ if (result.outputFiles) {
2832
+ await Promise.all(
2833
+ result.outputFiles.map(async (file) => {
2834
+ await fs7.promises.mkdir(path8.dirname(file.path), { recursive: true });
2835
+ const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2836
+ await fs7.promises.writeFile(tmp, file.contents);
2837
+ await fs7.promises.rename(tmp, file.path);
2838
+ })
2839
+ );
2840
+ }
2841
+ return { compiledFiles: entryPoints };
2842
+ }
2843
+
2844
+ // src/cli/compileOnDemand.ts
2845
+ init_generateSchemaFiles();
2846
+ init_generateSchemaFiles();
2847
+ function isProductFresh(sourceAbsPath, productAbsPath) {
2848
+ try {
2849
+ const srcStat = fs8.statSync(sourceAbsPath);
2850
+ const prodStat = fs8.statSync(productAbsPath);
2851
+ return prodStat.mtimeMs >= srcStat.mtimeMs;
2852
+ } catch {
2853
+ return false;
2854
+ }
2855
+ }
2856
+ var compiledFiles = /* @__PURE__ */ new Set();
2857
+ function clearCompiledFiles() {
2858
+ compiledFiles.clear();
2859
+ }
2860
+ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2861
+ if (compiledFiles.has(sourceAbsPath)) {
2862
+ return false;
2863
+ }
2864
+ if (!fs8.existsSync(sourceAbsPath)) {
2865
+ return false;
2866
+ }
2867
+ const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2868
+ if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2869
+ compiledFiles.add(sourceAbsPath);
2870
+ return false;
2871
+ }
2872
+ await compileDevRoutes({
2873
+ rootDir,
2874
+ dist,
2875
+ files: [sourceAbsPath],
2876
+ logLevel: "silent"
2877
+ });
2878
+ compiledFiles.add(sourceAbsPath);
2879
+ return true;
2880
+ }
2881
+ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2882
+ const rel = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2883
+ if (!rel.startsWith("src/")) return null;
2884
+ const relWithoutSrc = rel.slice(4);
2885
+ const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2886
+ return path9.resolve(rootDir, dist, jsRel);
2887
+ }
2888
+ var generatedSchemas = /* @__PURE__ */ new Set();
2889
+ function clearGeneratedSchemas() {
2890
+ generatedSchemas.clear();
2891
+ }
2892
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2893
+ if (generatedSchemas.has(schemaPath)) {
2894
+ return false;
2895
+ }
2896
+ const prodAbsPath = path9.resolve(rootDir, routeFilePath);
2897
+ const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2898
+ if (!fs8.existsSync(sourceAbsPath)) {
2899
+ return false;
2900
+ }
2901
+ if (isProductFresh(sourceAbsPath, schemaPath)) {
2902
+ generatedSchemas.add(schemaPath);
2903
+ return false;
2904
+ }
2905
+ const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
2906
+ if (fileRoutes.length === 0) {
2907
+ return false;
2908
+ }
2909
+ const sourceRelPath = path9.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2910
+ const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2911
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2912
+ generatedSchemas.add(schemaPath);
2913
+ return true;
2914
+ }
2915
+ async function deleteSchemaFiles(routes, rootDir, dist) {
2916
+ const deleted = /* @__PURE__ */ new Set();
2917
+ for (const route of routes) {
2918
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
2919
+ if (deleted.has(schemaPath)) continue;
2920
+ deleted.add(schemaPath);
2921
+ try {
2922
+ await fs8.promises.unlink(schemaPath);
2923
+ } catch {
2924
+ }
2925
+ }
2926
+ }
2927
+ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2928
+ const rel = path9.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2929
+ let relWithoutDist = rel;
2930
+ if (relWithoutDist.startsWith(`${dist}/`)) {
2931
+ relWithoutDist = relWithoutDist.slice(dist.length + 1);
2932
+ }
2933
+ const srcRel = `src/${relWithoutDist}`;
2934
+ const tsRel = srcRel.replace(/\.js$/, ".ts");
2935
+ const tsAbs = path9.resolve(rootDir, tsRel);
2936
+ if (fs8.existsSync(tsAbs)) return tsAbs;
2937
+ return path9.resolve(rootDir, srcRel);
2938
+ }
2939
+ var devOnDemandEnabled = false;
2940
+ function isDevOnDemandEnabled() {
2941
+ return devOnDemandEnabled;
2942
+ }
2943
+ var devDistDir;
2944
+ function getDevDist() {
2945
+ return devDistDir;
2946
+ }
2947
+
2513
2948
  // src/loader/loadRouteModule.ts
2514
- async function loadRouteModule(filePath, method) {
2949
+ async function loadRouteModule(filePath, method, rootDir) {
2515
2950
  let module;
2516
2951
  try {
2517
2952
  module = await importWithCacheBust(filePath);
2518
2953
  } catch (err) {
2954
+ if (isDevOnDemandEnabled() && rootDir) {
2955
+ const dist = getDevDist();
2956
+ if (dist) {
2957
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2958
+ try {
2959
+ const compiled = await ensureCompiled(sourcePath, rootDir, dist);
2960
+ if (compiled) {
2961
+ module = await importWithCacheBust(filePath);
2962
+ const handler2 = resolveExport(module, method);
2963
+ validateRouteModule(handler2, method, filePath);
2964
+ return { handler: handler2, method };
2965
+ }
2966
+ } catch (compileErr) {
2967
+ const compileReason = compileErr instanceof Error ? compileErr.message : String(compileErr);
2968
+ throw new Error(`Failed to compile route module "${sourcePath}": ${compileReason}`, {
2969
+ cause: compileErr
2970
+ });
2971
+ }
2972
+ }
2973
+ }
2519
2974
  const reason = err instanceof Error ? err.message : String(err);
2520
2975
  throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
2521
2976
  }
@@ -2565,6 +3020,7 @@ async function parseMultipart(request) {
2565
3020
  }
2566
3021
 
2567
3022
  // src/runtime/resolveInput.ts
3023
+ init_inputType();
2568
3024
  async function resolveInput(method, request) {
2569
3025
  const inputType = getInputTypeForMethod(method);
2570
3026
  if (inputType === "body") {
@@ -2628,6 +3084,9 @@ async function sendNodeResponse(response, res) {
2628
3084
  res.end();
2629
3085
  }
2630
3086
 
3087
+ // src/server/createServer.ts
3088
+ init_inputType();
3089
+
2631
3090
  // src/utils/getClientIp.ts
2632
3091
  function getClientIp(req) {
2633
3092
  const xff = req.headers["x-forwarded-for"];
@@ -2647,7 +3106,7 @@ function getClientIp(req) {
2647
3106
 
2648
3107
  // src/server/handleWsUpgrade.ts
2649
3108
  import { WebSocketServer, WebSocket } from "ws";
2650
- import path6 from "path";
3109
+ import path10 from "path";
2651
3110
 
2652
3111
  // src/errors/formatErrorResponse.ts
2653
3112
  function formatErrorResponse(error) {
@@ -2744,8 +3203,25 @@ function getPathname(req) {
2744
3203
  const idx = url.indexOf("?");
2745
3204
  return idx >= 0 ? url.slice(0, idx) : url;
2746
3205
  }
2747
- async function loadWsHandler(filePath, ctx) {
2748
- const module = await importWithCacheBust(filePath);
3206
+ async function loadWsHandler(filePath, ctx, rootDir) {
3207
+ let module;
3208
+ try {
3209
+ module = await importWithCacheBust(filePath);
3210
+ } catch (err) {
3211
+ if (isDevOnDemandEnabled() && rootDir) {
3212
+ const dist = getDevDist();
3213
+ if (dist) {
3214
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3215
+ const compiled = await ensureCompiled(sourcePath, rootDir, dist);
3216
+ if (compiled) {
3217
+ module = await importWithCacheBust(filePath);
3218
+ }
3219
+ }
3220
+ }
3221
+ if (!module) {
3222
+ throw err;
3223
+ }
3224
+ }
2749
3225
  const handler = module["WS"];
2750
3226
  if (typeof handler !== "function") {
2751
3227
  throw new Error(`WS export not found in ${filePath}`);
@@ -2820,8 +3296,8 @@ function attachWebSocket(options) {
2820
3296
  const finalHandler = async () => {
2821
3297
  let handlers;
2822
3298
  try {
2823
- const absoluteFilePath = path6.resolve(rootDir, route.filePath);
2824
- handlers = await loadWsHandler(absoluteFilePath, ctx);
3299
+ const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3300
+ handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
2825
3301
  } catch (err) {
2826
3302
  const reason = err instanceof Error ? err.message : String(err);
2827
3303
  console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
@@ -2843,6 +3319,14 @@ function attachWebSocket(options) {
2843
3319
  };
2844
3320
  let response;
2845
3321
  try {
3322
+ if (route.middlewares === void 0 && route.middlewarePaths) {
3323
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3324
+ if (bundle) {
3325
+ route.middlewares = bundle.middlewares;
3326
+ } else {
3327
+ route.middlewares = [];
3328
+ }
3329
+ }
2846
3330
  const dirMiddlewares = route.middlewares ?? [];
2847
3331
  const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
2848
3332
  if (allMiddlewares.length > 0) {
@@ -2866,6 +3350,7 @@ function attachWebSocket(options) {
2866
3350
  }
2867
3351
 
2868
3352
  // src/server/createServer.ts
3353
+ init_generateSchemaFiles();
2869
3354
  var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
2870
3355
  function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
2871
3356
  const forwardedProto = req.headers["x-forwarded-proto"];
@@ -2910,15 +3395,15 @@ function limitStreamSize(stream, maxSize) {
2910
3395
  }
2911
3396
  });
2912
3397
  }
2913
- function findAllowedMethods(routes, path11) {
3398
+ function findAllowedMethods(routes, path15) {
2914
3399
  const methods = /* @__PURE__ */ new Set();
2915
3400
  for (const route of routes) {
2916
- if (route.urlPath === path11) {
3401
+ if (route.urlPath === path15) {
2917
3402
  methods.add(route.method);
2918
3403
  continue;
2919
3404
  }
2920
3405
  if (route.isDynamic) {
2921
- const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
3406
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2922
3407
  if (params !== null) {
2923
3408
  methods.add(route.method);
2924
3409
  }
@@ -3004,16 +3489,32 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3004
3489
  }
3005
3490
  ctx.params = match.params;
3006
3491
  const { route } = match;
3007
- const absoluteFilePath = path7.resolve(rootDir, route.filePath);
3008
- const routeModule = await loadRouteModule(absoluteFilePath, route.method);
3492
+ const absoluteFilePath = path11.resolve(rootDir, route.filePath);
3493
+ const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
3009
3494
  const input = await resolveInput(route.method, request);
3010
3495
  const inputType = getInputTypeForMethod(route.method);
3011
3496
  const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
3497
+ if (isDevOnDemandEnabled()) {
3498
+ const devDist = getDevDist();
3499
+ if (devDist) {
3500
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
3501
+ }
3502
+ }
3012
3503
  const result = await validateInput(schemaPath, route.method, inputType, input);
3013
3504
  if (!result.valid) {
3014
3505
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
3015
3506
  }
3016
3507
  const body = hasBody(route.method) ? result.data : void 0;
3508
+ if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
3509
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3510
+ if (bundle) {
3511
+ route.middlewares = bundle.middlewares;
3512
+ route.injectors = bundle.injectors;
3513
+ } else {
3514
+ route.middlewares = [];
3515
+ route.injectors = {};
3516
+ }
3517
+ }
3017
3518
  const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
3018
3519
  const response = await invokeHandler(
3019
3520
  routeModule.handler,
@@ -3068,7 +3569,7 @@ async function createTestServer(options) {
3068
3569
  } = options;
3069
3570
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3070
3571
  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-"));
3572
+ const schemaDist = dist ? path12.isAbsolute(dist) ? dist : path12.resolve(rootDir, dist) : await fs9.mkdtemp(path12.join(os.tmpdir(), "faapi-test-schema-"));
3072
3573
  await generateSchemaFiles(sorted, rootDir, schemaDist);
3073
3574
  const { server } = createServer({
3074
3575
  routes: sorted,
@@ -3101,7 +3602,7 @@ async function createTestServer(options) {
3101
3602
  await new Promise((resolve) => {
3102
3603
  server.close(() => resolve());
3103
3604
  });
3104
- await fs5.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3605
+ await fs9.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3105
3606
  });
3106
3607
  invalidateSchemaCache();
3107
3608
  }
@@ -3243,8 +3744,8 @@ async function connectWs(baseUrl, pathname, options = {}) {
3243
3744
  }
3244
3745
 
3245
3746
  // src/cli/createAppCore.ts
3246
- import fs7 from "fs";
3247
- import path10 from "path";
3747
+ import fs11 from "fs";
3748
+ import path14 from "path";
3248
3749
  import { PassThrough } from "stream";
3249
3750
 
3250
3751
  // src/router/detectRouteConflicts.ts
@@ -3301,55 +3802,30 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
3301
3802
  }
3302
3803
 
3303
3804
  // src/cli/generateRoutes.ts
3304
- import fs6 from "fs";
3305
- import path9 from "path";
3805
+ import fs10 from "fs";
3806
+ import path13 from "path";
3306
3807
  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));
3808
+ const hydrateRoute = (serialized) => ({
3809
+ method: serialized.method,
3810
+ urlPath: serialized.urlPath,
3811
+ filePath: serialized.filePath,
3812
+ paramNames: serialized.paramNames,
3813
+ isDynamic: serialized.isDynamic,
3814
+ isCatchAll: serialized.isCatchAll,
3815
+ middlewarePaths: serialized.middlewarePaths
3816
+ });
3817
+ const hydrateWsRoute = (serialized) => ({
3818
+ urlPath: serialized.urlPath,
3819
+ filePath: serialized.filePath,
3820
+ paramNames: serialized.paramNames,
3821
+ isDynamic: serialized.isDynamic,
3822
+ isCatchAll: serialized.isCatchAll,
3823
+ middlewarePaths: serialized.middlewarePaths
3824
+ });
3825
+ const routes = manifest.routes.map(hydrateRoute);
3826
+ const wsRoutes = manifest.wsRoutes.map(hydrateWsRoute);
3334
3827
  return { routes, wsRoutes };
3335
3828
  }
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
3829
 
3354
3830
  // src/cli/loadPlugins.ts
3355
3831
  async function loadPlugins(declarations, ctx) {
@@ -3433,8 +3909,8 @@ function isFaapiConfigKey(key) {
3433
3909
  async function createAppBase(options) {
3434
3910
  const rootDir = options?.rootDir ?? process.cwd();
3435
3911
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
3436
- const routesPath = path10.resolve(rootDir, dist, ROUTES_FILE);
3437
- if (!fs7.existsSync(routesPath)) {
3912
+ const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE);
3913
+ if (!fs11.existsSync(routesPath)) {
3438
3914
  throw new Error(
3439
3915
  `[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
3916
  );
@@ -3636,6 +4112,7 @@ async function createAppBase(options) {
3636
4112
  }
3637
4113
 
3638
4114
  // src/cli/createDevApp.ts
4115
+ init_createProgram();
3639
4116
  async function createDevApp(options) {
3640
4117
  const { app, ctx } = await createAppBase(options);
3641
4118
  const devApp = app;
@@ -3644,9 +4121,16 @@ async function createDevApp(options) {
3644
4121
  invalidateMiddlewareCache();
3645
4122
  invalidateProgramCache();
3646
4123
  invalidateSchemaCache();
4124
+ clearCompiledFiles();
3647
4125
  const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
3648
4126
  const sorted = sortRoutes(reScanned.routes);
3649
- await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
4127
+ if (isDevOnDemandEnabled()) {
4128
+ await deleteSchemaFiles(sorted, ctx.rootDir, ctx.dist);
4129
+ clearGeneratedSchemas();
4130
+ } else {
4131
+ const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
4132
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
4133
+ }
3650
4134
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
3651
4135
  };
3652
4136
  return devApp;