@faapi/faapi 3.2.1 → 4.0.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/testing.js CHANGED
@@ -129,14 +129,14 @@ var ValidationError = class extends FaapiError {
129
129
  issues;
130
130
  };
131
131
  var RouteNotFoundError = class extends FaapiError {
132
- constructor(path12) {
133
- super("ROUTE_NOT_FOUND", `Route not found: ${path12}`, 404);
132
+ constructor(path15) {
133
+ super("ROUTE_NOT_FOUND", `Route not found: ${path15}`, 404);
134
134
  this.name = "RouteNotFoundError";
135
135
  }
136
136
  };
137
137
  var MethodNotAllowedError = class extends FaapiError {
138
- constructor(method, path12, allowedMethods) {
139
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path12}`, 405);
138
+ constructor(method, path15, allowedMethods) {
139
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path15}`, 405);
140
140
  this.allowedMethods = allowedMethods;
141
141
  this.name = "MethodNotAllowedError";
142
142
  }
@@ -205,10 +205,14 @@ function formatErrorResponse(error, config) {
205
205
  code: error.code,
206
206
  message: error.message
207
207
  });
208
- const bodyObj = typeof body2 === "object" && body2 !== null ? body2 : { error: body2 };
209
- const errorObj = bodyObj.error ?? bodyObj;
210
- if (errorObj) {
211
- errorObj.issues = error.issues;
208
+ const bodyObj = typeof body2 === "object" && body2 !== null ? { ...body2 } : { error: body2 };
209
+ const existingError = bodyObj.error;
210
+ if (existingError && typeof existingError === "object") {
211
+ bodyObj.error = { ...existingError, issues: error.issues };
212
+ } else if (typeof body2 === "object" && body2 !== null) {
213
+ bodyObj.issues = error.issues;
214
+ } else {
215
+ bodyObj.error = { issues: error.issues };
212
216
  }
213
217
  return jsonOk(bodyObj, error.statusCode);
214
218
  }
@@ -265,8 +269,10 @@ function formatSetCookie(name, value, options) {
265
269
  if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
266
270
  return cookie;
267
271
  }
268
- function createContext(request, params, config = {}, ip = "") {
269
- const url = new URL(request.url);
272
+ function createContext(request, params, config = {}, ip = "", registries) {
273
+ return createContextFromUrl(request, new URL(request.url), params, config, ip, registries);
274
+ }
275
+ function createContextFromUrl(request, url, params, config = {}, ip = "", registries) {
270
276
  const meta = { headers: {}, setCookies: [] };
271
277
  const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
272
278
  const cookiesObj = {};
@@ -363,6 +369,9 @@ function createContext(request, params, config = {}, ip = "") {
363
369
  return formatFailResponse(options, config);
364
370
  }
365
371
  };
372
+ if (registries) {
373
+ ctx.registries = registries;
374
+ }
366
375
  const extend = config?.extendContext;
367
376
  if (typeof extend === "function") {
368
377
  extend(ctx);
@@ -370,8 +379,17 @@ function createContext(request, params, config = {}, ip = "") {
370
379
  return ctx;
371
380
  }
372
381
  function createTestContext(options) {
373
- const { method = "GET", path: path12, query, headers, params = {}, config = {}, ip = "" } = options;
374
- const url = new URL(`http://localhost${path12}`);
382
+ const {
383
+ method = "GET",
384
+ path: path15,
385
+ query,
386
+ headers,
387
+ params = {},
388
+ config = {},
389
+ ip = "",
390
+ registries
391
+ } = options;
392
+ const url = new URL(`http://localhost${path15}`);
375
393
  if (query) {
376
394
  for (const [key, value] of Object.entries(query)) {
377
395
  if (Array.isArray(value)) {
@@ -387,7 +405,7 @@ function createTestContext(options) {
387
405
  method,
388
406
  headers
389
407
  });
390
- return createContext(request, params, config, ip);
408
+ return createContext(request, params, config, ip, registries);
391
409
  }
392
410
 
393
411
  // src/utils/isPlainObject.ts
@@ -402,6 +420,25 @@ function isPlainObject(value) {
402
420
  return proto === null || proto === Object.prototype;
403
421
  }
404
422
 
423
+ // src/response/pendingMeta.ts
424
+ var pending = /* @__PURE__ */ new WeakMap();
425
+ function deferMetaHeaders(response, headers) {
426
+ const existing = pending.get(response);
427
+ if (existing) {
428
+ Object.assign(existing, headers);
429
+ return;
430
+ }
431
+ pending.set(response, { ...headers });
432
+ }
433
+ function consumePendingMetaHeaders(response) {
434
+ const headers = pending.get(response);
435
+ if (headers) pending.delete(response);
436
+ return headers;
437
+ }
438
+ function isHeadersOnlyMeta(meta) {
439
+ return meta.status === void 0 && meta.setCookies.length === 0;
440
+ }
441
+
405
442
  // src/response/toResponse.ts
406
443
  async function toResponse(value, meta) {
407
444
  if (value instanceof Promise) {
@@ -418,6 +455,10 @@ async function toResponse(value, meta) {
418
455
  };
419
456
  if (value instanceof Response) {
420
457
  if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
458
+ if (isHeadersOnlyMeta(meta)) {
459
+ deferMetaHeaders(value, meta.headers);
460
+ return value;
461
+ }
421
462
  const headers2 = new Headers(value.headers);
422
463
  applyMeta(headers2);
423
464
  return new Response(value.body, {
@@ -512,10 +553,15 @@ var PARAM_TYPE_MAP = {
512
553
  agents: "agents"
513
554
  // Phase 2.3
514
555
  };
556
+ var injectionCache = /* @__PURE__ */ new WeakMap();
515
557
  function resolveInjection(fn) {
558
+ const cached = injectionCache.get(fn);
559
+ if (cached) {
560
+ return cached;
561
+ }
516
562
  const fnStr = fn.toString();
517
563
  const params = extractParamsWithAst(fnStr);
518
- return params.map((param) => {
564
+ const items = params.map((param) => {
519
565
  const type = PARAM_TYPE_MAP[param.name] || "unknown";
520
566
  return {
521
567
  name: param.name,
@@ -524,6 +570,8 @@ function resolveInjection(fn) {
524
570
  // 运行时类型已擦除
525
571
  };
526
572
  });
573
+ injectionCache.set(fn, items);
574
+ return items;
527
575
  }
528
576
  function extractParamsWithAst(fnStr) {
529
577
  const sourceFile = ts.createSourceFile(
@@ -585,24 +633,157 @@ function extractParamName(param, names) {
585
633
  function queryToObject(params) {
586
634
  const result = {};
587
635
  for (const [key, value] of params) {
588
- result[key] = value;
636
+ const existing = result[key];
637
+ if (existing === void 0) {
638
+ result[key] = value;
639
+ } else if (Array.isArray(existing)) {
640
+ existing.push(value);
641
+ } else {
642
+ result[key] = [existing, value];
643
+ }
589
644
  }
590
645
  return result;
591
646
  }
592
647
 
648
+ // src/injection/registries.ts
649
+ function createToolRegistry() {
650
+ let registry = /* @__PURE__ */ new Map();
651
+ return {
652
+ hydrate(tools) {
653
+ const next = /* @__PURE__ */ new Map();
654
+ for (const tool of tools) {
655
+ next.set(tool.name, tool);
656
+ }
657
+ registry = next;
658
+ },
659
+ get(name) {
660
+ return registry.get(name);
661
+ },
662
+ list() {
663
+ return Array.from(registry.values());
664
+ },
665
+ clear() {
666
+ registry = /* @__PURE__ */ new Map();
667
+ }
668
+ };
669
+ }
670
+ function createAgentRegistry(tool) {
671
+ let registry = /* @__PURE__ */ new Map();
672
+ const getAgent = (name) => registry.get(name);
673
+ return {
674
+ hydrate(agents) {
675
+ const next = /* @__PURE__ */ new Map();
676
+ for (const agent of agents) {
677
+ next.set(agent.name, agent);
678
+ }
679
+ registry = next;
680
+ },
681
+ getAgent,
682
+ getAgentEntry(name) {
683
+ return registry.get(name);
684
+ },
685
+ listAgents() {
686
+ const merged = /* @__PURE__ */ new Map();
687
+ for (const agent of registry.values()) merged.set(agent.name, agent);
688
+ return Array.from(merged.values());
689
+ },
690
+ asTool(name) {
691
+ const agent = getAgent(name);
692
+ if (!agent) return void 0;
693
+ return {
694
+ kind: "agent",
695
+ name: `agent.${agent.name}`,
696
+ agentName: agent.name,
697
+ description: agent.description,
698
+ metadata: agent
699
+ };
700
+ },
701
+ resolveAgentTools(name) {
702
+ const agent = getAgent(name);
703
+ if (!agent) return [];
704
+ const result = /* @__PURE__ */ new Map();
705
+ if (agent.tools) {
706
+ for (const toolName of agent.tools) {
707
+ const resolved = tool.get(toolName);
708
+ if (resolved) result.set(resolved.name, resolved);
709
+ }
710
+ }
711
+ return Array.from(result.values());
712
+ },
713
+ resolveSubAgents(name) {
714
+ const agent = getAgent(name);
715
+ if (!agent || !agent.agents) return [];
716
+ const result = [];
717
+ for (const subName of agent.agents) {
718
+ const sub = getAgent(subName);
719
+ if (sub) result.push(sub);
720
+ }
721
+ return result;
722
+ },
723
+ clear() {
724
+ registry = /* @__PURE__ */ new Map();
725
+ }
726
+ };
727
+ }
728
+ function createSkillRegistry() {
729
+ let registry = /* @__PURE__ */ new Map();
730
+ return {
731
+ hydrate(skills) {
732
+ const next = /* @__PURE__ */ new Map();
733
+ for (const skill of skills) {
734
+ next.set(skill.name, skill);
735
+ }
736
+ registry = next;
737
+ },
738
+ upsert(skill) {
739
+ registry.set(skill.name, skill);
740
+ },
741
+ remove(name) {
742
+ registry.delete(name);
743
+ },
744
+ get(name) {
745
+ return registry.get(name);
746
+ },
747
+ list() {
748
+ return Array.from(registry.values());
749
+ },
750
+ clear() {
751
+ registry = /* @__PURE__ */ new Map();
752
+ }
753
+ };
754
+ }
755
+ function createAgentHandleStore() {
756
+ let currentFactory = null;
757
+ return {
758
+ register(factory) {
759
+ currentFactory = factory;
760
+ },
761
+ get(ctx) {
762
+ if (currentFactory === null) return void 0;
763
+ return currentFactory(ctx);
764
+ },
765
+ clear() {
766
+ currentFactory = null;
767
+ }
768
+ };
769
+ }
770
+ function createAppRegistries() {
771
+ const tool = createToolRegistry();
772
+ const agent = createAgentRegistry(tool);
773
+ const skill = createSkillRegistry();
774
+ const agentHandle = createAgentHandleStore();
775
+ return { tool, agent, skill, agentHandle };
776
+ }
777
+ var defaultRegistries = createAppRegistries();
778
+
593
779
  // src/injection/agentRegistry.ts
594
- var registry = /* @__PURE__ */ new Map();
595
780
  function listAgents() {
596
- const merged = /* @__PURE__ */ new Map();
597
- for (const agent of registry.values()) merged.set(agent.name, agent);
598
- return Array.from(merged.values());
781
+ return defaultRegistries.agent.listAgents();
599
782
  }
600
783
 
601
784
  // src/injection/agentHandle.ts
602
- var currentFactory = null;
603
785
  function getAgentHandle(ctx) {
604
- if (currentFactory === null) return void 0;
605
- return currentFactory(ctx);
786
+ return defaultRegistries.agentHandle.get(ctx);
606
787
  }
607
788
 
608
789
  // src/injection/injectParams.ts
@@ -639,11 +820,12 @@ function getBuiltinInjectionValue(type, ctx, body) {
639
820
  }
640
821
  return {};
641
822
  // Phase 2.3:注入所有已注册 agent 元数据列表
823
+ // 方案 A:优先读 app 实例注册表,无实例(编程式直调 ctx)回退默认全局实例
642
824
  case "agents":
643
- return listAgents();
825
+ return ctx.registries ? ctx.registries.agent.listAgents() : listAgents();
644
826
  // Phase 3.5:调 @faapi/agent 插件注册的工厂获取 AgentHandle
645
827
  case "agent":
646
- return getAgentHandle(ctx);
828
+ return ctx.registries ? ctx.registries.agentHandle.get(ctx) : getAgentHandle(ctx);
647
829
  default:
648
830
  return void 0;
649
831
  }
@@ -674,6 +856,10 @@ function wrapResult(result, ctx) {
674
856
  function mergeMeta(response, meta) {
675
857
  const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
676
858
  if (!hasMeta) return response;
859
+ if (isHeadersOnlyMeta(meta)) {
860
+ deferMetaHeaders(response, meta.headers);
861
+ return response;
862
+ }
677
863
  const headers = new Headers(response.headers);
678
864
  for (const [key, value] of Object.entries(meta.headers)) {
679
865
  headers.set(key, value);
@@ -756,23 +942,23 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
756
942
  }
757
943
 
758
944
  // src/testServer.ts
759
- import path11 from "path";
945
+ import path14 from "path";
760
946
  import os from "os";
761
- import fs10 from "fs/promises";
947
+ import fs11 from "fs/promises";
762
948
 
763
949
  // src/router/scanRoutes.ts
764
950
  import fg from "fast-glob";
765
- import path from "path";
766
- import fs from "fs";
951
+ import path2 from "path";
952
+ import fs2 from "fs";
767
953
 
768
954
  // src/router/constants.ts
769
955
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
770
956
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
771
957
 
772
958
  // src/utils/normalizePath.ts
773
- function normalizePath(path12) {
774
- if (!path12) return "";
775
- let result = path12.replace(/\\/g, "/");
959
+ function normalizePath(path15) {
960
+ if (!path15) return "";
961
+ let result = path15.replace(/\\/g, "/");
776
962
  result = result.replace(/\/+/g, "/");
777
963
  result = result.replace(/\/+$/, "");
778
964
  if (result && !result.startsWith("/")) {
@@ -845,6 +1031,7 @@ async function importWithCacheBust(filePath, bustViteCache = false) {
845
1031
 
846
1032
  // src/middleware/loadMiddlewares.ts
847
1033
  var middlewareCache = /* @__PURE__ */ new Map();
1034
+ var inFlight = /* @__PURE__ */ new Map();
848
1035
  function getCachedMiddlewares(absPath) {
849
1036
  return middlewareCache.get(absPath);
850
1037
  }
@@ -896,8 +1083,15 @@ async function loadMergedMiddlewares(middlewarePaths) {
896
1083
  for (const absMwPath of middlewarePaths) {
897
1084
  let bundle = getCachedMiddlewares(absMwPath);
898
1085
  if (bundle === void 0) {
899
- bundle = await loadMiddlewaresFile(absMwPath);
900
- setCachedMiddlewares(absMwPath, bundle);
1086
+ let loading = inFlight.get(absMwPath);
1087
+ if (!loading) {
1088
+ loading = loadMiddlewaresFile(absMwPath).then((result) => {
1089
+ setCachedMiddlewares(absMwPath, result);
1090
+ return result;
1091
+ });
1092
+ inFlight.set(absMwPath, loading);
1093
+ }
1094
+ bundle = await loading;
901
1095
  }
902
1096
  mergedMiddlewares.push(...bundle.middlewares);
903
1097
  for (const [name, injector] of Object.entries(bundle.injectors)) {
@@ -910,6 +1104,36 @@ async function loadMergedMiddlewares(middlewarePaths) {
910
1104
  return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
911
1105
  }
912
1106
 
1107
+ // src/utils/prodPaths.ts
1108
+ import path from "path";
1109
+ import fs from "fs";
1110
+ var APP_DIR = "src";
1111
+ function toProdFilePath(filePath, dist) {
1112
+ let rel = filePath.replace(/\\/g, "/");
1113
+ if (rel.startsWith("src/")) {
1114
+ rel = rel.slice(4);
1115
+ }
1116
+ const jsPath = rel.replace(/\.ts$/, ".js");
1117
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
1118
+ }
1119
+ function toProdExtension(filePath) {
1120
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
1121
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
1122
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
1123
+ return filePath;
1124
+ }
1125
+ function toRealPath(p) {
1126
+ try {
1127
+ return fs.realpathSync(p);
1128
+ } catch {
1129
+ return p;
1130
+ }
1131
+ }
1132
+ function isInsideDir(filePath, dir) {
1133
+ const rel = path.relative(dir, filePath);
1134
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
1135
+ }
1136
+
913
1137
  // src/router/scanRoutes.ts
914
1138
  var HTTP_OR_WS_EXPORT_RE = new RegExp(
915
1139
  String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
@@ -925,48 +1149,40 @@ function extractExportsFromSource(source) {
925
1149
  return names;
926
1150
  }
927
1151
  function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
928
- const routeDir = path.dirname(routeFilePath);
929
- const resolvedRoot = path.resolve(rootDir);
1152
+ const routeDir = path2.dirname(routeFilePath);
1153
+ const resolvedRoot = path2.resolve(rootDir);
930
1154
  const paths = [];
931
- let currentDir = path.resolve(rootDir, routeDir);
1155
+ let currentDir = path2.resolve(rootDir, routeDir);
932
1156
  while (true) {
933
1157
  if (dist) {
934
- const mwTsPath = path.join(currentDir, "middlewares.ts");
935
- const mwJsPath = path.join(currentDir, "middlewares.js");
936
- const absTsPath = path.resolve(rootDir, mwTsPath);
937
- const absJsPath = path.resolve(rootDir, mwJsPath);
938
- const absMwPath = fs.existsSync(absTsPath) ? absTsPath : fs.existsSync(absJsPath) ? absJsPath : null;
1158
+ const mwTsPath = path2.join(currentDir, "middlewares.ts");
1159
+ const mwJsPath = path2.join(currentDir, "middlewares.js");
1160
+ const absTsPath = path2.resolve(rootDir, mwTsPath);
1161
+ const absJsPath = path2.resolve(rootDir, mwJsPath);
1162
+ const absMwPath = fs2.existsSync(absTsPath) ? absTsPath : fs2.existsSync(absJsPath) ? absJsPath : null;
939
1163
  if (absMwPath) {
940
- const relMwPath = path.relative(rootDir, absMwPath);
941
- const prodAbsPath = path.resolve(rootDir, toProdFilePath(relMwPath, dist));
1164
+ const relMwPath = path2.relative(rootDir, absMwPath);
1165
+ const prodAbsPath = path2.resolve(rootDir, toProdFilePath(relMwPath, dist));
942
1166
  paths.push(prodAbsPath);
943
1167
  }
944
1168
  } else {
945
1169
  for (const ext of [".ts", ".js"]) {
946
- const mwPath = path.join(currentDir, `middlewares${ext}`);
947
- const absMwPath = path.resolve(rootDir, mwPath);
948
- if (fs.existsSync(absMwPath)) {
1170
+ const mwPath = path2.join(currentDir, `middlewares${ext}`);
1171
+ const absMwPath = path2.resolve(rootDir, mwPath);
1172
+ if (fs2.existsSync(absMwPath)) {
949
1173
  paths.push(absMwPath);
950
1174
  break;
951
1175
  }
952
1176
  }
953
1177
  }
954
1178
  if (currentDir === resolvedRoot) break;
955
- const parentDir = path.dirname(currentDir);
1179
+ const parentDir = path2.dirname(currentDir);
956
1180
  if (parentDir === currentDir) break;
957
1181
  currentDir = parentDir;
958
1182
  }
959
1183
  paths.reverse();
960
1184
  return paths;
961
1185
  }
962
- function toProdFilePath(filePath, dist) {
963
- let rel = filePath.replace(/\\/g, "/");
964
- if (rel.startsWith("src/")) {
965
- rel = rel.slice(4);
966
- }
967
- const jsPath = rel.replace(/\.ts$/, ".js");
968
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
969
- }
970
1186
  async function scanRoutes(rootDir, patterns, dist) {
971
1187
  const files = await fg(patterns, {
972
1188
  cwd: rootDir,
@@ -979,7 +1195,7 @@ async function scanRoutes(rootDir, patterns, dist) {
979
1195
  const normalizedFile = file.replace(/\\/g, "/");
980
1196
  const fileName = normalizedFile.split("/").pop();
981
1197
  if (fileName === "handler.ts" || fileName === "handler.js") {
982
- const absPath = path.resolve(rootDir, normalizedFile);
1198
+ const absPath = path2.resolve(rootDir, normalizedFile);
983
1199
  const urlPath = filePathToUrlPath(normalizedFile);
984
1200
  const paramNames = extractParamNames(urlPath);
985
1201
  const isDynamic = paramNames.length > 0;
@@ -992,7 +1208,7 @@ async function scanRoutes(rootDir, patterns, dist) {
992
1208
  const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
993
1209
  middlewareBundle = await loadMergedMiddlewares(mwPaths);
994
1210
  }
995
- const source = await fs.promises.readFile(absPath, "utf8").catch(() => "");
1211
+ const source = await fs2.promises.readFile(absPath, "utf8").catch(() => "");
996
1212
  const exportNames = extractExportsFromSource(source);
997
1213
  const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
998
1214
  for (const method of methods) {
@@ -1045,25 +1261,34 @@ function sortRoutes(routes) {
1045
1261
  }
1046
1262
 
1047
1263
  // src/cli/generateSchemaFiles.ts
1048
- import path4 from "path";
1049
- import fs3 from "fs/promises";
1264
+ import path6 from "path";
1265
+
1266
+ // src/utils/atomicWrite.ts
1267
+ import path3 from "path";
1268
+ import fs3 from "fs";
1269
+ async function atomicWriteFile(outputPath, content) {
1270
+ await fs3.promises.mkdir(path3.dirname(outputPath), { recursive: true });
1271
+ const tmp = `${outputPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1272
+ await fs3.promises.writeFile(tmp, content, "utf-8");
1273
+ await fs3.promises.rename(tmp, outputPath);
1274
+ }
1050
1275
 
1051
1276
  // src/ast/createProgram.ts
1052
1277
  import ts2 from "typescript";
1053
- import fs2 from "fs";
1054
- import path2 from "path";
1278
+ import fs4 from "fs";
1279
+ import path4 from "path";
1055
1280
  var programCache = /* @__PURE__ */ new Map();
1056
1281
  var tsConfigCache = /* @__PURE__ */ new Map();
1057
1282
  function findTsConfig(filePath) {
1058
- let dir = path2.dirname(filePath);
1059
- const root = path2.parse(dir).root;
1283
+ let dir = path4.dirname(filePath);
1284
+ const root = path4.parse(dir).root;
1060
1285
  while (true) {
1061
- const candidate = path2.join(dir, "tsconfig.json");
1062
- if (fs2.existsSync(candidate)) {
1286
+ const candidate = path4.join(dir, "tsconfig.json");
1287
+ if (fs4.existsSync(candidate)) {
1063
1288
  return candidate;
1064
1289
  }
1065
1290
  if (dir === root) return null;
1066
- const parent = path2.dirname(dir);
1291
+ const parent = path4.dirname(dir);
1067
1292
  if (parent === dir) return null;
1068
1293
  dir = parent;
1069
1294
  }
@@ -1073,13 +1298,13 @@ function parseTsConfig(tsconfigPath) {
1073
1298
  if (cached) return cached;
1074
1299
  const result = { fileNames: [] };
1075
1300
  try {
1076
- const configFile = ts2.readConfigFile(tsconfigPath, (p) => fs2.readFileSync(p, "utf-8"));
1301
+ const configFile = ts2.readConfigFile(tsconfigPath, (p) => fs4.readFileSync(p, "utf-8"));
1077
1302
  if (configFile.error) {
1078
1303
  tsConfigCache.set(tsconfigPath, result);
1079
1304
  return result;
1080
1305
  }
1081
1306
  const config = configFile.config ?? {};
1082
- const basePath = path2.dirname(tsconfigPath);
1307
+ const basePath = path4.dirname(tsconfigPath);
1083
1308
  const parsed = ts2.parseJsonConfigFileContent(
1084
1309
  config,
1085
1310
  ts2.sys,
@@ -1105,6 +1330,46 @@ function createProgram(filePath) {
1105
1330
  if (cached) {
1106
1331
  return cached;
1107
1332
  }
1333
+ const program = buildProgram([filePath], findTsConfig(filePath));
1334
+ programCache.set(filePath, program);
1335
+ return program;
1336
+ }
1337
+ function createPrograms(filePaths) {
1338
+ const unique = [...new Set(filePaths)];
1339
+ const result = /* @__PURE__ */ new Map();
1340
+ const groups = /* @__PURE__ */ new Map();
1341
+ const noTsconfigFiles = [];
1342
+ for (const filePath of unique) {
1343
+ const tsconfigPath = findTsConfig(filePath);
1344
+ if (!tsconfigPath) {
1345
+ noTsconfigFiles.push(filePath);
1346
+ continue;
1347
+ }
1348
+ const group = groups.get(tsconfigPath);
1349
+ if (group) {
1350
+ group.files.push(filePath);
1351
+ } else {
1352
+ groups.set(tsconfigPath, { tsconfigPath, files: [filePath] });
1353
+ }
1354
+ }
1355
+ for (const { tsconfigPath, files } of groups.values()) {
1356
+ const cacheKey = `shared::${tsconfigPath}`;
1357
+ let program = programCache.get(cacheKey);
1358
+ const coversAll = program !== void 0 && files.every((f) => program.getSourceFile(f) !== void 0);
1359
+ if (!program || !coversAll) {
1360
+ program = buildProgram(files, tsconfigPath);
1361
+ programCache.set(cacheKey, program);
1362
+ }
1363
+ for (const filePath of files) {
1364
+ result.set(filePath, program);
1365
+ }
1366
+ }
1367
+ for (const filePath of noTsconfigFiles) {
1368
+ result.set(filePath, createProgram(filePath));
1369
+ }
1370
+ return result;
1371
+ }
1372
+ function buildProgram(entryFiles, tsconfigPath) {
1108
1373
  const options = {
1109
1374
  strict: true,
1110
1375
  target: ts2.ScriptTarget.ES2022,
@@ -1113,8 +1378,7 @@ function createProgram(filePath) {
1113
1378
  skipLibCheck: true,
1114
1379
  noEmit: true
1115
1380
  };
1116
- let rootNames = [filePath];
1117
- const tsconfigPath = findTsConfig(filePath);
1381
+ const rootNames = [...entryFiles];
1118
1382
  if (tsconfigPath) {
1119
1383
  const tsOptions = parseTsConfig(tsconfigPath);
1120
1384
  if (tsOptions.module !== void 0) {
@@ -1124,16 +1388,14 @@ function createProgram(filePath) {
1124
1388
  options.moduleResolution = tsOptions.moduleResolution;
1125
1389
  }
1126
1390
  if (tsOptions.fileNames.length > 0) {
1127
- if (!tsOptions.fileNames.includes(filePath)) {
1128
- rootNames = [filePath, ...tsOptions.fileNames];
1129
- } else {
1130
- rootNames = tsOptions.fileNames;
1391
+ for (const fileName of tsOptions.fileNames) {
1392
+ if (!rootNames.includes(fileName)) {
1393
+ rootNames.push(fileName);
1394
+ }
1131
1395
  }
1132
1396
  }
1133
1397
  }
1134
- const program = ts2.createProgram(rootNames, options);
1135
- programCache.set(filePath, program);
1136
- return program;
1398
+ return ts2.createProgram(rootNames, options);
1137
1399
  }
1138
1400
 
1139
1401
  // src/ast/extractHandlerTypes.ts
@@ -1145,17 +1407,40 @@ var currentProgram = null;
1145
1407
  function setProgramContext(program) {
1146
1408
  currentProgram = program;
1147
1409
  }
1148
- var SchemaExtractionError = class extends Error {
1149
- constructor(typeText, reason, options) {
1150
- super(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}`, options);
1410
+ var SchemaExtractionError = class _SchemaExtractionError extends Error {
1411
+ constructor(typeText, reason, options, location) {
1412
+ super(
1413
+ `\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}` + (location ? ` (${location.file}:${location.line}:${location.column})` : ""),
1414
+ options
1415
+ );
1151
1416
  this.typeText = typeText;
1152
1417
  this.reason = reason;
1418
+ this.location = location;
1153
1419
  this.name = "SchemaExtractionError";
1154
1420
  }
1155
1421
  typeText;
1156
1422
  reason;
1423
+ location;
1424
+ /**
1425
+ * 从 AST 节点构造错误(自动携带 file:line:column)
1426
+ *
1427
+ * 所有抛错点应优先使用此工厂——错误无行号时,几百行的类型文件只能靠
1428
+ * 类型名肉眼定位;解析 lib.d.ts 类型别名时还会出现错误文本与文件上下文错位
1429
+ */
1430
+ static at(node, typeText, reason) {
1431
+ const sourceFile = node.getSourceFile();
1432
+ if (!sourceFile) {
1433
+ return new _SchemaExtractionError(typeText, reason);
1434
+ }
1435
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1436
+ return new _SchemaExtractionError(typeText, reason, void 0, {
1437
+ file: sourceFile.fileName,
1438
+ line: line + 1,
1439
+ column: character + 1
1440
+ });
1441
+ }
1157
1442
  };
1158
- function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1443
+ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1159
1444
  const kind = typeNode.kind;
1160
1445
  switch (kind) {
1161
1446
  case ts3.SyntaxKind.StringKeyword:
@@ -1211,13 +1496,13 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1211
1496
  if (ts3.isArrayTypeNode(typeNode)) {
1212
1497
  return {
1213
1498
  kind: "array",
1214
- element: resolveTypeNode(typeNode.elementType, checker, visited)
1499
+ element: resolveTypeNode(typeNode.elementType, checker, visited, bindings)
1215
1500
  };
1216
1501
  }
1217
1502
  if (ts3.isTupleTypeNode(typeNode)) {
1218
1503
  const elements = typeNode.elements.map((e) => {
1219
1504
  if (ts3.isRestTypeNode(e)) {
1220
- const inner = resolveTypeNode(e.type, checker, visited);
1505
+ const inner = resolveTypeNode(e.type, checker, visited, bindings);
1221
1506
  if (inner.kind === "array") {
1222
1507
  return { type: inner.element, optional: false, rest: true };
1223
1508
  }
@@ -1225,20 +1510,20 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1225
1510
  }
1226
1511
  if (ts3.isNamedTupleMember(e)) {
1227
1512
  return {
1228
- type: resolveTypeNode(e.type, checker, visited),
1513
+ type: resolveTypeNode(e.type, checker, visited, bindings),
1229
1514
  optional: !!e.questionToken,
1230
1515
  rest: false
1231
1516
  };
1232
1517
  }
1233
1518
  if (ts3.isOptionalTypeNode(e)) {
1234
1519
  return {
1235
- type: resolveTypeNode(e.type, checker, visited),
1520
+ type: resolveTypeNode(e.type, checker, visited, bindings),
1236
1521
  optional: true,
1237
1522
  rest: false
1238
1523
  };
1239
1524
  }
1240
1525
  return {
1241
- type: resolveTypeNode(e, checker, visited),
1526
+ type: resolveTypeNode(e, checker, visited, bindings),
1242
1527
  optional: false,
1243
1528
  rest: false
1244
1529
  };
@@ -1246,40 +1531,80 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1246
1531
  return { kind: "tuple", elements };
1247
1532
  }
1248
1533
  if (ts3.isUnionTypeNode(typeNode)) {
1249
- const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
1534
+ const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited, bindings));
1250
1535
  return { kind: "union", members };
1251
1536
  }
1252
1537
  if (ts3.isIntersectionTypeNode(typeNode)) {
1253
- const properties = [];
1538
+ const propMap = /* @__PURE__ */ new Map();
1254
1539
  for (const t of typeNode.types) {
1255
- const resolved = resolveTypeNode(t, checker, visited);
1256
- if (resolved.kind === "object") {
1257
- properties.push(...resolved.properties);
1540
+ const resolved = resolveTypeNode(t, checker, visited, bindings);
1541
+ if (resolved.kind !== "object") {
1542
+ throw new SchemaExtractionError(
1543
+ typeNode.getText(),
1544
+ `\u4EA4\u53C9\u7C7B\u578B\u5305\u542B\u975E object \u6210\u5458\uFF08${resolved.kind}\uFF09,\u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\u2014\u2014branded \u7C7B\u578B\u5EFA\u8BAE\u6539\u7528\u5177\u4F53\u7C7B\u578B\u6216 unknown`
1545
+ );
1546
+ }
1547
+ for (const prop of resolved.properties) {
1548
+ const existing = propMap.get(prop.name);
1549
+ if (!existing) {
1550
+ propMap.set(prop.name, prop);
1551
+ continue;
1552
+ }
1553
+ if (JSON.stringify(existing.type) !== JSON.stringify(prop.type) || existing.optional !== prop.optional) {
1554
+ throw SchemaExtractionError.at(
1555
+ typeNode,
1556
+ typeNode.getText(),
1557
+ `\u4EA4\u53C9\u7C7B\u578B\u6210\u5458\u7684\u540C\u540D\u5B57\u6BB5 "${prop.name}" \u7C7B\u578B\u51B2\u7A81\uFF08TS \u4E2D\u4E3A never\uFF09,\u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C`
1558
+ );
1559
+ }
1560
+ if (!existing.constraints?.length && prop.constraints?.length) {
1561
+ propMap.set(prop.name, prop);
1562
+ }
1258
1563
  }
1259
1564
  }
1260
- return { kind: "object", properties };
1565
+ return { kind: "object", properties: [...propMap.values()] };
1261
1566
  }
1262
1567
  if (ts3.isTypeLiteralNode(typeNode)) {
1263
- return resolveTypeLiteral(typeNode, checker, visited);
1568
+ return resolveTypeLiteral(typeNode, checker, visited, bindings);
1264
1569
  }
1265
1570
  if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.KeyOfKeyword) {
1266
1571
  return resolveKeyOf(typeNode, checker);
1267
1572
  }
1268
1573
  if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.ReadonlyKeyword) {
1269
- return resolveTypeNode(typeNode.type, checker, visited);
1574
+ return resolveTypeNode(typeNode.type, checker, visited, bindings);
1270
1575
  }
1271
1576
  if (ts3.isTypeReferenceNode(typeNode)) {
1272
- return resolveTypeReference(typeNode, checker, visited);
1577
+ return resolveTypeReference(typeNode, checker, visited, bindings);
1273
1578
  }
1274
- throw new SchemaExtractionError(typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
1579
+ if (ts3.isExpressionWithTypeArguments(typeNode)) {
1580
+ return resolveTypeReference(
1581
+ {
1582
+ getText: () => typeNode.getText(),
1583
+ typeName: typeNode.expression,
1584
+ typeArguments: typeNode.typeArguments
1585
+ },
1586
+ checker,
1587
+ visited,
1588
+ bindings
1589
+ );
1590
+ }
1591
+ throw SchemaExtractionError.at(typeNode, typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
1275
1592
  }
1276
- function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1593
+ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1277
1594
  const properties = [];
1595
+ let catchall;
1278
1596
  for (const member of typeNode.members) {
1597
+ if (ts3.isMethodSignature(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) {
1598
+ throw SchemaExtractionError.at(
1599
+ member,
1600
+ member.getText(),
1601
+ "\u5BF9\u8C61\u7C7B\u578B\u542B\u65B9\u6CD5\u7B7E\u540D\u6216\u5B58\u53D6\u5668,\u8FD0\u884C\u65F6 JSON \u6570\u636E\u65E0\u6CD5\u6821\u9A8C\u65B9\u6CD5\u2014\u2014\u8BF7\u6539\u7528\u5177\u4F53\u5C5E\u6027\u7C7B\u578B"
1602
+ );
1603
+ }
1279
1604
  if (ts3.isPropertySignature(member) && member.name) {
1280
1605
  const name = member.name.getText();
1281
1606
  const optional = !!member.questionToken;
1282
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1607
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1283
1608
  const constraints = extractConstraintsFromJsDoc(member, name);
1284
1609
  validateConstraints(constraints, type, name);
1285
1610
  properties.push(
@@ -1287,12 +1612,10 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
1287
1612
  );
1288
1613
  }
1289
1614
  if (ts3.isIndexSignatureDeclaration(member)) {
1290
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1291
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1292
- return { kind: "record", key: keyType, value: valueType };
1615
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1293
1616
  }
1294
1617
  }
1295
- return { kind: "object", properties };
1618
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
1296
1619
  }
1297
1620
  function extractLiteralKeys(type) {
1298
1621
  if (type.kind === "literal" && typeof type.value === "string") {
@@ -1361,36 +1684,48 @@ function resolveKeyOf(typeNode, checker) {
1361
1684
  }
1362
1685
  throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u8054\u5408");
1363
1686
  }
1364
- function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1687
+ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1365
1688
  const typeName = typeNode.typeName.getText();
1689
+ const bound = bindings.get(typeName);
1690
+ if (bound) {
1691
+ return bound;
1692
+ }
1366
1693
  if (typeName === "Date") {
1367
1694
  return { kind: "date" };
1368
1695
  }
1369
1696
  if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
1697
+ const [arg] = typeNode.typeArguments;
1370
1698
  return {
1371
1699
  kind: "array",
1372
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1700
+ element: resolveTypeNode(arg, checker, visited, bindings)
1373
1701
  };
1374
1702
  }
1375
1703
  if (typeName === "Record" && typeNode.typeArguments?.length === 2) {
1704
+ const [keyArg, valueArg] = typeNode.typeArguments;
1376
1705
  return {
1377
1706
  kind: "record",
1378
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1379
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1707
+ key: resolveTypeNode(keyArg, checker, visited, bindings),
1708
+ value: resolveTypeNode(valueArg, checker, visited, bindings)
1380
1709
  };
1381
1710
  }
1382
1711
  if ((typeName === "Partial" || typeName === "Required" || typeName === "Readonly") && typeNode.typeArguments?.length === 1) {
1383
- const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1712
+ const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
1384
1713
  if (inner.kind === "object" && typeName === "Partial") {
1385
1714
  return {
1386
1715
  kind: "object",
1387
1716
  properties: inner.properties.map((p) => ({ ...p, optional: true }))
1388
1717
  };
1389
1718
  }
1719
+ if (inner.kind === "object" && typeName === "Required") {
1720
+ return {
1721
+ kind: "object",
1722
+ properties: inner.properties.map((p) => ({ ...p, optional: false }))
1723
+ };
1724
+ }
1390
1725
  return inner;
1391
1726
  }
1392
1727
  if ((typeName === "Pick" || typeName === "Omit") && typeNode.typeArguments?.length === 2) {
1393
- const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1728
+ const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
1394
1729
  if (innerType.kind !== "object") {
1395
1730
  throw new SchemaExtractionError(
1396
1731
  typeNode.getText(),
@@ -1398,7 +1733,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1398
1733
  );
1399
1734
  }
1400
1735
  const keyTypeNode = typeNode.typeArguments[1];
1401
- let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited));
1736
+ let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited, bindings));
1402
1737
  if (keys === null) {
1403
1738
  keys = extractKeysFromChecker(keyTypeNode, checker);
1404
1739
  }
@@ -1416,10 +1751,11 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1416
1751
  "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
1417
1752
  );
1418
1753
  }
1754
+ const [mapKey, mapValue] = typeNode.typeArguments;
1419
1755
  return {
1420
1756
  kind: "map",
1421
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1422
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1757
+ key: resolveTypeNode(mapKey, checker, visited, bindings),
1758
+ value: resolveTypeNode(mapValue, checker, visited, bindings)
1423
1759
  };
1424
1760
  }
1425
1761
  if (typeName === "Set") {
@@ -1429,9 +1765,10 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1429
1765
  "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
1430
1766
  );
1431
1767
  }
1768
+ const [setArg] = typeNode.typeArguments;
1432
1769
  return {
1433
1770
  kind: "set",
1434
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1771
+ element: resolveTypeNode(setArg, checker, visited, bindings)
1435
1772
  };
1436
1773
  }
1437
1774
  if (typeName === "WeakMap" || typeName === "WeakSet") {
@@ -1454,21 +1791,42 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1454
1791
  }
1455
1792
  visited.add(typeName);
1456
1793
  if (checker) {
1457
- const symbol = typeNode.typeName.kind === ts3.SyntaxKind.Identifier ? checker.getSymbolAtLocation(typeNode.typeName) : void 0;
1794
+ const symbol = ts3.isIdentifier(typeNode.typeName) || ts3.isQualifiedName(typeNode.typeName) ? checker.getSymbolAtLocation(typeNode.typeName) : void 0;
1458
1795
  if (symbol) {
1459
1796
  const declaration = symbol.declarations?.[0];
1460
1797
  if (declaration) {
1461
1798
  if (ts3.isInterfaceDeclaration(declaration)) {
1462
- return resolveInterfaceDeclaration(declaration, checker, visited);
1799
+ return resolveInterfaceDeclaration(
1800
+ declaration,
1801
+ checker,
1802
+ visited,
1803
+ bindings,
1804
+ typeNode.typeArguments
1805
+ );
1463
1806
  }
1464
1807
  if (ts3.isTypeAliasDeclaration(declaration)) {
1465
- return resolveTypeNode(declaration.type, checker, visited);
1808
+ const declBindings = bindTypeParameters(
1809
+ declaration.typeParameters,
1810
+ typeNode.typeArguments,
1811
+ bindings,
1812
+ checker,
1813
+ visited,
1814
+ typeNode
1815
+ );
1816
+ return resolveTypeNode(declaration.type, checker, visited, declBindings);
1466
1817
  }
1467
1818
  if (ts3.isEnumDeclaration(declaration)) {
1468
1819
  return resolveEnumDeclaration(declaration);
1469
1820
  }
1470
1821
  if (ts3.isImportSpecifier(declaration) || ts3.isImportClause(declaration)) {
1471
- const resolved = resolveImportAlias(typeNode, symbol, checker, visited);
1822
+ const resolved = resolveImportAlias(
1823
+ typeNode,
1824
+ symbol,
1825
+ checker,
1826
+ visited,
1827
+ bindings,
1828
+ typeNode.typeArguments
1829
+ );
1472
1830
  if (resolved) return resolved;
1473
1831
  }
1474
1832
  }
@@ -1476,17 +1834,45 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1476
1834
  }
1477
1835
  throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
1478
1836
  }
1479
- function resolveImportAlias(typeNode, symbol, checker, visited) {
1837
+ function bindTypeParameters(typeParameters, typeArguments, outerBindings, checker, visited, errorNode) {
1838
+ if (!typeParameters || typeParameters.length === 0) return outerBindings;
1839
+ const bindings = new Map(outerBindings);
1840
+ for (let i = 0; i < typeParameters.length; i++) {
1841
+ const param = typeParameters[i];
1842
+ if (!param) continue;
1843
+ const arg = typeArguments?.[i];
1844
+ if (arg) {
1845
+ bindings.set(param.name.text, resolveTypeNode(arg, checker, visited, outerBindings));
1846
+ } else if (param.default) {
1847
+ bindings.set(param.name.text, resolveTypeNode(param.default, checker, visited, bindings));
1848
+ } else {
1849
+ throw new SchemaExtractionError(
1850
+ errorNode.getText(),
1851
+ `\u6CDB\u578B\u53C2\u6570 "${param.name.text}" \u7F3A\u5C11\u7C7B\u578B\u5B9E\u53C2\uFF08\u4E14\u65E0\u9ED8\u8BA4\u7C7B\u578B\uFF09`
1852
+ );
1853
+ }
1854
+ }
1855
+ return bindings;
1856
+ }
1857
+ function resolveImportAlias(typeNode, symbol, checker, visited, bindings = /* @__PURE__ */ new Map(), typeArguments) {
1480
1858
  const typeName = typeNode.typeName.getText();
1481
1859
  try {
1482
1860
  const aliased = checker.getAliasedSymbol(symbol);
1483
1861
  if (aliased && aliased.declarations && aliased.declarations.length > 0) {
1484
1862
  const decl = aliased.declarations[0];
1485
1863
  if (ts3.isInterfaceDeclaration(decl)) {
1486
- return resolveInterfaceDeclaration(decl, checker, visited);
1864
+ return resolveInterfaceDeclaration(decl, checker, visited, bindings, typeArguments);
1487
1865
  }
1488
1866
  if (ts3.isTypeAliasDeclaration(decl)) {
1489
- return resolveTypeNode(decl.type, checker, visited);
1867
+ const declBindings = bindTypeParameters(
1868
+ decl.typeParameters,
1869
+ typeArguments,
1870
+ bindings,
1871
+ checker,
1872
+ visited,
1873
+ typeNode
1874
+ );
1875
+ return resolveTypeNode(decl.type, checker, visited, declBindings);
1490
1876
  }
1491
1877
  if (ts3.isEnumDeclaration(decl)) {
1492
1878
  return resolveEnumDeclaration(decl);
@@ -1504,10 +1890,18 @@ function resolveImportAlias(typeNode, symbol, checker, visited) {
1504
1890
  const found = findTopLevelDecl(sourceFile, typeName);
1505
1891
  if (found) {
1506
1892
  if (found.kind === "interface") {
1507
- return resolveInterfaceDeclaration(found.node, checker, visited);
1893
+ return resolveInterfaceDeclaration(found.node, checker, visited, bindings, typeArguments);
1508
1894
  }
1509
1895
  if (found.kind === "typeAlias") {
1510
- return resolveTypeNode(found.node.type, checker, visited);
1896
+ const declBindings = bindTypeParameters(
1897
+ found.node.typeParameters,
1898
+ typeArguments,
1899
+ bindings,
1900
+ checker,
1901
+ visited,
1902
+ typeNode
1903
+ );
1904
+ return resolveTypeNode(found.node.type, checker, visited, declBindings);
1511
1905
  }
1512
1906
  if (found.kind === "enum") {
1513
1907
  return resolveEnumDeclaration(found.node);
@@ -1554,13 +1948,22 @@ function resolveEnumDeclaration(node) {
1554
1948
  }
1555
1949
  return { kind: "union", members };
1556
1950
  }
1557
- function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
1951
+ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set(), outerBindings = /* @__PURE__ */ new Map(), typeArguments) {
1558
1952
  const properties = [];
1559
1953
  const propMap = /* @__PURE__ */ new Map();
1954
+ let catchall;
1955
+ const bindings = bindTypeParameters(
1956
+ node.typeParameters,
1957
+ typeArguments,
1958
+ outerBindings,
1959
+ checker,
1960
+ visited,
1961
+ node
1962
+ );
1560
1963
  for (const heritageClause of node.heritageClauses ?? []) {
1561
1964
  if (heritageClause.token === ts3.SyntaxKind.ExtendsKeyword) {
1562
1965
  for (const expr of heritageClause.types) {
1563
- const parentType = resolveTypeNode(expr, checker, visited);
1966
+ const parentType = resolveTypeNode(expr, checker, visited, bindings);
1564
1967
  if (parentType.kind === "object") {
1565
1968
  for (const prop of parentType.properties) {
1566
1969
  propMap.set(prop.name, prop);
@@ -1570,10 +1973,17 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
1570
1973
  }
1571
1974
  }
1572
1975
  for (const member of node.members) {
1976
+ if (ts3.isMethodSignature(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) {
1977
+ throw SchemaExtractionError.at(
1978
+ member,
1979
+ member.getText(),
1980
+ "\u63A5\u53E3\u542B\u65B9\u6CD5\u7B7E\u540D\u6216\u5B58\u53D6\u5668,\u8FD0\u884C\u65F6 JSON \u6570\u636E\u65E0\u6CD5\u6821\u9A8C\u65B9\u6CD5\u2014\u2014\u8BF7\u6539\u7528\u5177\u4F53\u5C5E\u6027\u7C7B\u578B"
1981
+ );
1982
+ }
1573
1983
  if (ts3.isPropertySignature(member) && member.name) {
1574
1984
  const name = member.name.getText();
1575
1985
  const optional = !!member.questionToken;
1576
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1986
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1577
1987
  const constraints = extractConstraintsFromJsDoc(member, name);
1578
1988
  validateConstraints(constraints, type, name);
1579
1989
  propMap.set(
@@ -1582,15 +1992,13 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
1582
1992
  );
1583
1993
  }
1584
1994
  if (ts3.isIndexSignatureDeclaration(member)) {
1585
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1586
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1587
- return { kind: "record", key: keyType, value: valueType };
1995
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1588
1996
  }
1589
1997
  }
1590
1998
  for (const prop of propMap.values()) {
1591
1999
  properties.push(prop);
1592
2000
  }
1593
- return { kind: "object", properties };
2001
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
1594
2002
  }
1595
2003
  var NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
1596
2004
  "max",
@@ -1770,55 +2178,47 @@ function extractTypeInfo(program, filePath, typeName) {
1770
2178
  return;
1771
2179
  }
1772
2180
  });
1773
- return result;
1774
- } finally {
1775
- setProgramContext(null);
1776
- }
1777
- }
1778
- function extractAllTypes(program, filePath) {
1779
- const sourceFile = program.getSourceFile(filePath);
1780
- if (!sourceFile) return /* @__PURE__ */ new Map();
1781
- const checker = program.getTypeChecker();
1782
- setProgramContext(program);
1783
- try {
1784
- const result = /* @__PURE__ */ new Map();
1785
- ts4.forEachChild(sourceFile, (node) => {
1786
- if (ts4.isInterfaceDeclaration(node)) {
1787
- const visited = /* @__PURE__ */ new Set();
1788
- visited.add(node.name.text);
1789
- const runtimeType = withFileContext(
1790
- filePath,
1791
- node.name.text,
1792
- () => resolveInterfaceDeclaration(node, checker, visited)
1793
- );
1794
- result.set(node.name.text, {
1795
- name: node.name.text,
1796
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1797
- runtimeType
1798
- });
1799
- return;
1800
- }
1801
- if (ts4.isTypeAliasDeclaration(node)) {
1802
- const visited = /* @__PURE__ */ new Set();
1803
- visited.add(node.name.text);
1804
- const runtimeType = withFileContext(
1805
- filePath,
1806
- node.name.text,
1807
- () => resolveTypeNode(node.type, checker, visited)
1808
- );
1809
- result.set(node.name.text, {
1810
- name: node.name.text,
1811
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1812
- runtimeType
1813
- });
1814
- return;
2181
+ if (result) return result;
2182
+ for (const sf of program.getSourceFiles()) {
2183
+ if (sf === sourceFile) continue;
2184
+ if (sf.fileName.includes("/node_modules/") || sf.fileName.includes("typescript/lib/")) {
2185
+ continue;
1815
2186
  }
1816
- });
1817
- return result;
2187
+ const found = findTopLevelDecl(sf, typeName);
2188
+ if (!found) continue;
2189
+ const visited = /* @__PURE__ */ new Set();
2190
+ visited.add(typeName);
2191
+ const runtimeType = withFileContext(filePath, typeName, () => {
2192
+ if (found.kind === "interface") {
2193
+ return resolveInterfaceDeclaration(found.node, checker, visited);
2194
+ }
2195
+ if (found.kind === "typeAlias") {
2196
+ return resolveTypeNode(found.node.type, checker, visited);
2197
+ }
2198
+ return resolveEnumDeclaration(found.node);
2199
+ });
2200
+ return {
2201
+ name: typeName,
2202
+ properties: runtimeType.kind === "object" ? runtimeType.properties : [],
2203
+ runtimeType
2204
+ };
2205
+ }
2206
+ return null;
1818
2207
  } finally {
1819
2208
  setProgramContext(null);
1820
2209
  }
1821
2210
  }
2211
+ function createLazyTypeResolver(program, filePath) {
2212
+ const cache = /* @__PURE__ */ new Map();
2213
+ return {
2214
+ resolve(name) {
2215
+ if (cache.has(name)) return cache.get(name);
2216
+ const info = extractTypeInfo(program, filePath, name);
2217
+ cache.set(name, info);
2218
+ return info;
2219
+ }
2220
+ };
2221
+ }
1822
2222
  function withFileContext(filePath, typeName, fn) {
1823
2223
  try {
1824
2224
  return fn();
@@ -1828,7 +2228,8 @@ function withFileContext(filePath, typeName, fn) {
1828
2228
  const enriched = new SchemaExtractionError(
1829
2229
  err.typeText,
1830
2230
  `${err.reason}\uFF08\u6587\u4EF6: ${fileName}, \u7C7B\u578B: ${typeName}\uFF09`,
1831
- { cause: err }
2231
+ { cause: err },
2232
+ err.location
1832
2233
  );
1833
2234
  throw enriched;
1834
2235
  }
@@ -1856,8 +2257,7 @@ function getSchemaName(method, inputType) {
1856
2257
 
1857
2258
  // src/injection/analyzeInjection.ts
1858
2259
  import ts5 from "typescript";
1859
- function analyzeInjection(code, functionName) {
1860
- const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
2260
+ function analyzeInjectionInSourceFile(sourceFile, functionName) {
1861
2261
  const params = [];
1862
2262
  ts5.forEachChild(sourceFile, (node) => {
1863
2263
  if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
@@ -1900,11 +2300,11 @@ function extractSchema(typeNode, sourceFile) {
1900
2300
  }
1901
2301
 
1902
2302
  // src/cli/collectRouteSchemaSources.ts
1903
- import path3 from "path";
2303
+ import path5 from "path";
1904
2304
  function collectRouteSchemaSources(routes, rootDir) {
1905
2305
  const methodsByFile = /* @__PURE__ */ new Map();
1906
2306
  for (const route of routes) {
1907
- const filePath = rootDir ? path3.resolve(rootDir, route.filePath) : route.filePath;
2307
+ const filePath = rootDir ? path5.resolve(rootDir, route.filePath) : route.filePath;
1908
2308
  let entry = methodsByFile.get(filePath);
1909
2309
  if (!entry) {
1910
2310
  entry = { urlPath: route.urlPath, methods: /* @__PURE__ */ new Set() };
@@ -1912,30 +2312,24 @@ function collectRouteSchemaSources(routes, rootDir) {
1912
2312
  }
1913
2313
  entry.methods.add(route.method);
1914
2314
  }
1915
- const programByFile = /* @__PURE__ */ new Map();
1916
- const allTypesByFile = /* @__PURE__ */ new Map();
1917
- const mergedAllTypes = /* @__PURE__ */ new Map();
2315
+ const programByFile = createPrograms([...methodsByFile.keys()]);
2316
+ const resolversByFile = /* @__PURE__ */ new Map();
1918
2317
  for (const filePath of methodsByFile.keys()) {
1919
- const program = createProgram(filePath);
1920
- programByFile.set(filePath, program);
1921
- const allTypes = extractAllTypes(program, filePath);
1922
- allTypesByFile.set(filePath, allTypes);
1923
- for (const [name, info] of allTypes) {
1924
- mergedAllTypes.set(name, info);
1925
- }
2318
+ resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
1926
2319
  }
1927
2320
  const sources = [];
1928
2321
  for (const [filePath, entry] of methodsByFile) {
1929
2322
  const program = programByFile.get(filePath);
1930
2323
  const sourceFile = program.getSourceFile(filePath);
1931
- const code = sourceFile?.text ?? "";
2324
+ if (!sourceFile) continue;
2325
+ const resolver = resolversByFile.get(filePath);
1932
2326
  for (const method of entry.methods) {
1933
2327
  const inputType = getInputTypeForMethod(method);
1934
2328
  const schemaName = getSchemaName(method, inputType);
1935
- const meta = analyzeInjection(code, method);
2329
+ const meta = analyzeInjectionInSourceFile(sourceFile, method);
1936
2330
  const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1937
2331
  const isForm = param?.type === "form";
1938
- const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
2332
+ const typeInfo = param?.typeName ? resolver.resolve(param.typeName) : null;
1939
2333
  sources.push({
1940
2334
  urlPath: entry.urlPath,
1941
2335
  filePath,
@@ -1945,7 +2339,7 @@ function collectRouteSchemaSources(routes, rootDir) {
1945
2339
  });
1946
2340
  }
1947
2341
  }
1948
- return { sources, allTypesByFile, mergedAllTypes };
2342
+ return { sources, resolversByFile };
1949
2343
  }
1950
2344
 
1951
2345
  // src/ast/generateZodSchema.ts
@@ -2018,8 +2412,12 @@ function collectNamedTypes(type, ctx) {
2018
2412
  if (resolved) {
2019
2413
  ctx.namedTypes.set(type.name, resolved);
2020
2414
  collectNamedTypes(resolved, ctx);
2415
+ return;
2021
2416
  }
2022
- return;
2417
+ throw new SchemaExtractionError(
2418
+ type.name,
2419
+ `\u65E0\u6CD5\u89E3\u6790\u547D\u540D\u7C7B\u578B\u5F15\u7528 "${type.name}"\uFF08handler \u6587\u4EF6\u4E0E program \u6E90\u6587\u4EF6\u4E2D\u5747\u672A\u627E\u5230\u540C\u540D\u9876\u5C42\u58F0\u660E\uFF09`
2420
+ );
2023
2421
  }
2024
2422
  }
2025
2423
  }
@@ -2029,6 +2427,12 @@ function runtimeTypeToZodExpression(type, ctx, constraints) {
2029
2427
  if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
2030
2428
  return wrapCoercePreprocess(type.kind, withConstraints);
2031
2429
  }
2430
+ if (ctx.coerce && type.kind === "literal" && (typeof type.value === "number" || typeof type.value === "boolean")) {
2431
+ return wrapCoercePreprocess(
2432
+ typeof type.value === "number" ? "number" : "boolean",
2433
+ withConstraints
2434
+ );
2435
+ }
2032
2436
  return withConstraints;
2033
2437
  }
2034
2438
  function applyConstraints(baseExpr, constraints, typeKind) {
@@ -2093,7 +2497,7 @@ function baseExpression(type, ctx) {
2093
2497
  case "tuple":
2094
2498
  return generateTupleExpression(type.elements, ctx);
2095
2499
  case "object":
2096
- return generateObjectExpression(type.properties, ctx);
2500
+ return generateObjectExpression(type, ctx);
2097
2501
  case "union":
2098
2502
  return generateUnionExpression(type.members, ctx);
2099
2503
  case "date":
@@ -2112,7 +2516,7 @@ function baseExpression(type, ctx) {
2112
2516
  }
2113
2517
  }
2114
2518
  var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2115
- var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
2519
+ var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => {\n const lower = typeof v === "string" ? v.toLowerCase() : v;\n return lower === "true" || lower === "1" ? true : lower === "false" || lower === "0" ? false : v;\n};';
2116
2520
  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);';
2117
2521
  var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2118
2522
  var HELPERS_FILENAME = "faapi-helpers.js";
@@ -2150,7 +2554,10 @@ function generateTupleExpression(elements, ctx) {
2150
2554
  }
2151
2555
  }
2152
2556
  if (restExpression) {
2153
- return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
2557
+ const fixedWithOptional = fixedExprs.map(
2558
+ (expr, i) => fixedOptional[i] ? `${expr}.optional()` : expr
2559
+ );
2560
+ return `z.tuple([${fixedWithOptional.join(", ")}]).rest(${restExpression})`;
2154
2561
  }
2155
2562
  const hasOptional = fixedOptional.some((o) => o);
2156
2563
  if (!hasOptional) {
@@ -2171,13 +2578,14 @@ function generateTupleExpression(elements, ctx) {
2171
2578
  }
2172
2579
  return `z.union([${variants.join(", ")}])`;
2173
2580
  }
2174
- function generateObjectExpression(properties, ctx) {
2175
- const fields = properties.map((prop) => {
2581
+ function generateObjectExpression(type, ctx) {
2582
+ const fields = type.properties.map((prop) => {
2176
2583
  const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
2177
2584
  const finalExpr = prop.optional ? `${expr}.optional()` : expr;
2178
2585
  return `${JSON.stringify(prop.name)}: ${finalExpr}`;
2179
2586
  });
2180
- return `z.object({ ${fields.join(", ")} })`;
2587
+ const objectExpr = `z.object({ ${fields.join(", ")} })`;
2588
+ return type.catchall !== void 0 ? `${objectExpr}.catchall(${runtimeTypeToZodExpression(type.catchall, ctx)})` : objectExpr;
2181
2589
  }
2182
2590
  function generateUnionExpression(members, ctx) {
2183
2591
  const hasNull = members.some((m) => m.kind === "null");
@@ -2222,7 +2630,7 @@ function containsRef(type, visited) {
2222
2630
  return false;
2223
2631
  }
2224
2632
  }
2225
- function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
2633
+ function generateZodSchemaSourceParts(typeInfo, resolveType, exportName, coerce = false) {
2226
2634
  const ctx = new CodeGenContext(resolveType);
2227
2635
  const name = exportName ?? typeInfo.name;
2228
2636
  ctx.entryTypeName = typeInfo.name;
@@ -2230,21 +2638,14 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
2230
2638
  ctx.coerce = coerce;
2231
2639
  collectNamedTypes(typeInfo.runtimeType, ctx);
2232
2640
  ctx.namedTypes.delete(typeInfo.name);
2233
- const lines = [];
2234
- lines.push("import { z } from 'zod';");
2235
- lines.push("");
2236
- for (const [n, type] of ctx.namedTypes) {
2237
- lines.push(generateNamedTypeDeclaration(n, type, ctx));
2238
- }
2239
- if (ctx.namedTypes.size > 0) lines.push("");
2641
+ const namedTypeDeclarations = [...ctx.namedTypes].map(([n, type]) => ({
2642
+ name: n,
2643
+ declaration: generateNamedTypeDeclaration(n, type, ctx)
2644
+ }));
2240
2645
  const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
2241
2646
  const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
2242
- if (hasSelfRef) {
2243
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
2244
- } else {
2245
- lines.push(`export const ${name}Schema = ${entryExpr};`);
2246
- }
2247
- return lines.join("\n");
2647
+ const entryDeclaration = hasSelfRef ? `export const ${name}Schema = z.lazy(() => ${entryExpr});` : `export const ${name}Schema = ${entryExpr};`;
2648
+ return { namedTypeDeclarations, entryDeclaration };
2248
2649
  }
2249
2650
 
2250
2651
  // src/cli/generateSchemaFiles.ts
@@ -2255,7 +2656,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
2255
2656
  }
2256
2657
  const idx = rel.lastIndexOf("/");
2257
2658
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2258
- return path4.resolve(rootDir, dist, relDir, "zod.js");
2659
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
2259
2660
  }
2260
2661
  function getRuntimeSchemaPath(filePath, dist, rootDir) {
2261
2662
  let rel = filePath.replace(/\\/g, "/");
@@ -2266,16 +2667,17 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
2266
2667
  }
2267
2668
  const idx = rel.lastIndexOf("/");
2268
2669
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2269
- return path4.resolve(rootDir, dist, relDir, "zod.js");
2670
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
2270
2671
  }
2271
2672
  function getHelpersImportPath(relDir) {
2272
2673
  if (!relDir) return `./${HELPERS_FILENAME}`;
2273
2674
  const depth = relDir.split("/").filter(Boolean).length;
2274
2675
  return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2275
2676
  }
2276
- function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2277
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
2677
+ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
2278
2678
  const lines = ["import { z } from 'zod';"];
2679
+ const namedTypeDeclarations = [];
2680
+ const seenNamedTypes = /* @__PURE__ */ new Set();
2279
2681
  const schemaBlocks = [];
2280
2682
  for (const source of sources) {
2281
2683
  const { schemaName, typeInfo } = source;
@@ -2283,16 +2685,24 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2283
2685
  continue;
2284
2686
  }
2285
2687
  const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2286
- const block = [`// ${schemaName}`];
2287
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2288
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
2289
- ""
2688
+ const { namedTypeDeclarations: decls, entryDeclaration } = generateZodSchemaSourceParts(
2689
+ typeInfo,
2690
+ resolveType,
2691
+ schemaName,
2692
+ coerce
2290
2693
  );
2291
- block.push(schemaCode);
2292
- block.push("");
2293
- schemaBlocks.push(block.join("\n"));
2694
+ for (const { name, declaration } of decls) {
2695
+ if (seenNamedTypes.has(name)) continue;
2696
+ seenNamedTypes.add(name);
2697
+ namedTypeDeclarations.push(declaration);
2698
+ }
2699
+ schemaBlocks.push([`// ${schemaName}`, entryDeclaration, ""].join("\n"));
2700
+ }
2701
+ if (namedTypeDeclarations.length > 0) {
2702
+ lines.push(...namedTypeDeclarations);
2703
+ lines.push("");
2294
2704
  }
2295
- const allSchemaCode = schemaBlocks.join("\n");
2705
+ const allSchemaCode = [...namedTypeDeclarations, ...schemaBlocks].join("\n");
2296
2706
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2297
2707
  lines.push(
2298
2708
  `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
@@ -2304,7 +2714,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2304
2714
  }
2305
2715
  async function generateSchemaFiles(routes, rootDir, dist) {
2306
2716
  if (routes.length === 0) return;
2307
- const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2717
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
2308
2718
  const sourcesByFile = /* @__PURE__ */ new Map();
2309
2719
  for (const source of sources) {
2310
2720
  let list = sourcesByFile.get(source.filePath);
@@ -2316,9 +2726,8 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2316
2726
  }
2317
2727
  const fileEntries = [];
2318
2728
  for (const [filePath, fileSources] of sourcesByFile) {
2319
- const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
2729
+ const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
2320
2730
  const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2321
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2322
2731
  let relForDir = relFile;
2323
2732
  if (relForDir.startsWith("src/")) {
2324
2733
  relForDir = relForDir.slice(4);
@@ -2326,12 +2735,17 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2326
2735
  const dirIdx = relForDir.lastIndexOf("/");
2327
2736
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2328
2737
  const helpersImportPath = getHelpersImportPath(zodRelDir);
2329
- const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
2738
+ const resolver = resolversByFile.get(filePath);
2739
+ const source = generateSchemaFileSource(
2740
+ fileSources,
2741
+ (name) => resolver?.resolve(name)?.runtimeType,
2742
+ helpersImportPath
2743
+ );
2330
2744
  fileEntries.push({ outputPath, source });
2331
2745
  }
2332
2746
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2333
2747
  if (usesCoerceHelpers(allSourceCode)) {
2334
- const helpersPath = path4.resolve(rootDir, dist, HELPERS_FILENAME);
2748
+ const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
2335
2749
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
2336
2750
  }
2337
2751
  await Promise.all(
@@ -2339,22 +2753,21 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2339
2753
  );
2340
2754
  }
2341
2755
  async function writeSchemaFile(outputPath, source) {
2342
- await fs3.mkdir(path4.dirname(outputPath), { recursive: true });
2343
- await fs3.writeFile(outputPath, source, "utf-8");
2756
+ await atomicWriteFile(outputPath, source);
2344
2757
  }
2345
2758
 
2346
2759
  // src/cli/compileOnDemand.ts
2347
- import path8 from "path";
2348
- import fs7 from "fs";
2760
+ import path11 from "path";
2761
+ import fs9 from "fs";
2349
2762
 
2350
- // src/cli/compileDevRoutes.ts
2351
- import path7 from "path";
2352
- import fs6 from "fs";
2763
+ // src/cli/compileSourceFiles.ts
2764
+ import path9 from "path";
2765
+ import fs7 from "fs";
2353
2766
  import fg2 from "fast-glob";
2354
2767
 
2355
2768
  // src/cli/aliasPlugin.ts
2356
- import path6 from "path";
2357
- import fs5 from "fs";
2769
+ import path8 from "path";
2770
+ import fs6 from "fs";
2358
2771
 
2359
2772
  // src/utils/resolveAlias.ts
2360
2773
  function resolveAlias(specifier, config) {
@@ -2381,55 +2794,51 @@ function resolveAlias(specifier, config) {
2381
2794
 
2382
2795
  // src/utils/readTsconfig.ts
2383
2796
  import ts6 from "typescript";
2384
- import path5 from "path";
2385
- import fs4 from "fs";
2797
+ import path7 from "path";
2798
+ import fs5 from "fs";
2799
+ var tsconfigCache = /* @__PURE__ */ new Map();
2386
2800
  function readTsconfig(rootDir) {
2387
- const tsconfigPath = path5.resolve(rootDir, "tsconfig.json");
2388
- if (!fs4.existsSync(tsconfigPath)) return null;
2801
+ const tsconfigPath = path7.resolve(rootDir, "tsconfig.json");
2802
+ if (!fs5.existsSync(tsconfigPath)) return null;
2803
+ let mtimeMs;
2804
+ try {
2805
+ mtimeMs = fs5.statSync(tsconfigPath).mtimeMs;
2806
+ } catch {
2807
+ return null;
2808
+ }
2809
+ const cached = tsconfigCache.get(tsconfigPath);
2810
+ if (cached && cached.mtimeMs === mtimeMs) {
2811
+ return cached.config;
2812
+ }
2389
2813
  const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2390
2814
  if (configFile.error || !configFile.config) return null;
2391
2815
  const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2392
2816
  const baseUrl = parsed.options.baseUrl ?? rootDir;
2393
2817
  const rawPaths = parsed.options.paths;
2394
- if (!rawPaths) return null;
2395
- const paths = {};
2396
- for (const [pattern, targets] of Object.entries(rawPaths)) {
2397
- paths[pattern] = targets.map((t) => path5.resolve(baseUrl, t));
2398
- }
2399
- return { baseUrl, paths };
2818
+ const config = rawPaths ? (() => {
2819
+ const paths = {};
2820
+ for (const [pattern, targets] of Object.entries(rawPaths)) {
2821
+ paths[pattern] = targets.map((t) => path7.resolve(baseUrl, t));
2822
+ }
2823
+ return { baseUrl, paths };
2824
+ })() : null;
2825
+ tsconfigCache.set(tsconfigPath, { mtimeMs, config });
2826
+ return config;
2400
2827
  }
2401
2828
 
2402
2829
  // src/cli/aliasPlugin.ts
2403
- function toProdExtension(filePath) {
2404
- if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2405
- if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2406
- if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2407
- return filePath;
2408
- }
2409
2830
  function toProdImportPath(sourceFile, importer) {
2410
- const importerDir = path6.dirname(importer);
2411
- let rel = path6.relative(importerDir, sourceFile);
2412
- rel = rel.split(path6.sep).join("/");
2831
+ const importerDir = path8.dirname(importer);
2832
+ let rel = path8.relative(importerDir, sourceFile);
2833
+ rel = rel.split(path8.sep).join("/");
2413
2834
  if (!rel.startsWith(".")) rel = "./" + rel;
2414
2835
  return toProdExtension(rel);
2415
2836
  }
2416
- function toRealPath(p) {
2417
- try {
2418
- return fs5.realpathSync(p);
2419
- } catch {
2420
- return p;
2421
- }
2422
- }
2423
- function isInsideDir(filePath, dir) {
2424
- const rel = path6.relative(dir, filePath);
2425
- return rel !== "" && !rel.startsWith("..") && !path6.isAbsolute(rel);
2426
- }
2427
- var APP_DIR = "src";
2428
2837
  function toStrippedProdImportPath(sourceFile, rootDir) {
2429
- const appDirAbs = toRealPath(path6.resolve(rootDir, APP_DIR));
2838
+ const appDirAbs = toRealPath(path8.resolve(rootDir, APP_DIR));
2430
2839
  const sourceReal = toRealPath(sourceFile);
2431
- let rel = path6.relative(appDirAbs, sourceReal);
2432
- rel = rel.split(path6.sep).join("/");
2840
+ let rel = path8.relative(appDirAbs, sourceReal);
2841
+ rel = rel.split(path8.sep).join("/");
2433
2842
  if (!rel.startsWith(".")) rel = "./" + rel;
2434
2843
  return toProdExtension(rel);
2435
2844
  }
@@ -2444,41 +2853,41 @@ var INDEX_EXTS = [
2444
2853
  "/index.cjs"
2445
2854
  ];
2446
2855
  function resolveRelativeSpecifier(importer, specifier) {
2447
- const importerDir = path6.dirname(importer);
2448
- const base = path6.resolve(importerDir, specifier);
2856
+ const importerDir = path8.dirname(importer);
2857
+ const base = path8.resolve(importerDir, specifier);
2449
2858
  if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2450
- return fs5.existsSync(base) ? base : null;
2859
+ return fs6.existsSync(base) ? base : null;
2451
2860
  }
2452
2861
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2453
- return fs5.existsSync(base) ? base : null;
2862
+ return fs6.existsSync(base) ? base : null;
2454
2863
  }
2455
2864
  for (const ext of SOURCE_EXTS) {
2456
2865
  const file = base + ext;
2457
- if (fs5.existsSync(file)) return file;
2866
+ if (fs6.existsSync(file)) return file;
2458
2867
  }
2459
2868
  for (const indexExt of INDEX_EXTS) {
2460
2869
  const file = base + indexExt;
2461
- if (fs5.existsSync(file)) return file;
2870
+ if (fs6.existsSync(file)) return file;
2462
2871
  }
2463
2872
  return null;
2464
2873
  }
2465
2874
  function createAliasPlugin(config, options) {
2466
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2467
- const appDirAbs = options?.rootDir ? toRealPath(path6.resolve(options.rootDir, APP_DIR)) : null;
2875
+ const SPEC_RE2 = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2876
+ const appDirAbs = options?.rootDir ? toRealPath(path8.resolve(options.rootDir, APP_DIR)) : null;
2468
2877
  return {
2469
2878
  name: "faapi-alias",
2470
2879
  setup(build) {
2471
2880
  build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2472
2881
  let source;
2473
2882
  try {
2474
- source = fs5.readFileSync(args.path, "utf8");
2883
+ source = fs6.readFileSync(args.path, "utf8");
2475
2884
  } catch {
2476
2885
  return void 0;
2477
2886
  }
2478
2887
  const importer = args.path;
2479
2888
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2480
2889
  let modified = false;
2481
- const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2890
+ const newSource = source.replace(SPEC_RE2, (full, prefix, quote, specifier) => {
2482
2891
  if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2483
2892
  return full;
2484
2893
  }
@@ -2504,7 +2913,7 @@ function createAliasPlugin(config, options) {
2504
2913
  for (const candidate of candidates) {
2505
2914
  for (const ext of SOURCE_EXTS) {
2506
2915
  const file = candidate + ext;
2507
- if (fs5.existsSync(file)) {
2916
+ if (fs6.existsSync(file)) {
2508
2917
  modified = true;
2509
2918
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2510
2919
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2517,7 +2926,7 @@ function createAliasPlugin(config, options) {
2517
2926
  }
2518
2927
  for (const indexExt of INDEX_EXTS) {
2519
2928
  const file = candidate + indexExt;
2520
- if (fs5.existsSync(file)) {
2929
+ if (fs6.existsSync(file)) {
2521
2930
  modified = true;
2522
2931
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2523
2932
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2542,11 +2951,10 @@ function buildAliasPlugins(rootDir) {
2542
2951
  return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2543
2952
  }
2544
2953
 
2545
- // src/cli/compileDevRoutes.ts
2546
- var APP_DIR2 = "src";
2547
- async function compileDevRoutes(options) {
2548
- const { rootDir, dist, files, logLevel = "silent" } = options;
2549
- const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2954
+ // src/cli/compileSourceFiles.ts
2955
+ async function compileSourceFiles(options) {
2956
+ const { rootDir, dist, files, logLevel = "silent", production, atomicWrite } = options;
2957
+ const entryPoints = files ?? await fg2([`${APP_DIR}/**/*.ts`], {
2550
2958
  cwd: rootDir,
2551
2959
  onlyFiles: true,
2552
2960
  absolute: true,
@@ -2555,11 +2963,11 @@ async function compileDevRoutes(options) {
2555
2963
  if (entryPoints.length === 0) {
2556
2964
  return { compiledFiles: [] };
2557
2965
  }
2558
- const absDist = path7.resolve(rootDir, dist);
2559
- await fs6.promises.mkdir(absDist, { recursive: true });
2966
+ const absDist = path9.resolve(rootDir, dist);
2967
+ await fs7.promises.mkdir(absDist, { recursive: true });
2560
2968
  const plugins = buildAliasPlugins(rootDir);
2561
2969
  const esbuild = await import("esbuild");
2562
- const outbase = path7.resolve(rootDir, APP_DIR2);
2970
+ const outbase = path9.resolve(rootDir, APP_DIR);
2563
2971
  const result = await esbuild.build({
2564
2972
  entryPoints,
2565
2973
  outdir: absDist,
@@ -2570,27 +2978,102 @@ async function compileDevRoutes(options) {
2570
2978
  sourcemap: true,
2571
2979
  packages: "external",
2572
2980
  plugins,
2981
+ // build 语义:编译期 NODE_ENV 替换 + 死分支删除(见 AGENTS.md §5.3)
2982
+ ...production ? { define: { "process.env.NODE_ENV": '"production"' }, minifySyntax: true } : {},
2573
2983
  logLevel,
2574
- write: false
2984
+ // dev 语义:esbuild 返回内存内容,由下方原子写落盘
2985
+ ...atomicWrite ? { write: false } : {}
2575
2986
  });
2576
- if (result.outputFiles) {
2987
+ if (atomicWrite && result.outputFiles) {
2577
2988
  await Promise.all(
2578
2989
  result.outputFiles.map(async (file) => {
2579
- await fs6.promises.mkdir(path7.dirname(file.path), { recursive: true });
2990
+ await fs7.promises.mkdir(path9.dirname(file.path), { recursive: true });
2580
2991
  const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2581
- await fs6.promises.writeFile(tmp, file.contents);
2582
- await fs6.promises.rename(tmp, file.path);
2992
+ await fs7.promises.writeFile(tmp, file.contents);
2993
+ await fs7.promises.rename(tmp, file.path);
2583
2994
  })
2584
2995
  );
2585
2996
  }
2586
2997
  return { compiledFiles: entryPoints };
2587
2998
  }
2588
2999
 
3000
+ // src/cli/compileDevRoutes.ts
3001
+ async function compileDevRoutes(options) {
3002
+ return compileSourceFiles({ ...options, atomicWrite: true });
3003
+ }
3004
+
3005
+ // src/cli/collectImports.ts
3006
+ import path10 from "path";
3007
+ import fs8 from "fs";
3008
+ var SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
3009
+ function extractImportSpecifiers(source) {
3010
+ const specifiers = [];
3011
+ let match;
3012
+ SPEC_RE.lastIndex = 0;
3013
+ while ((match = SPEC_RE.exec(source)) !== null) {
3014
+ specifiers.push(match[3]);
3015
+ }
3016
+ return specifiers;
3017
+ }
3018
+ function resolveSpecifierFromDir(dir, specifier) {
3019
+ return resolveRelativeSpecifier(path10.join(dir, "__faapi_probe__.ts"), specifier);
3020
+ }
3021
+ async function collectRelativeImports(entryFiles, rootDir) {
3022
+ const appDirAbs = toRealPath(path10.resolve(rootDir, "src"));
3023
+ const tsconfig = readTsconfig(rootDir);
3024
+ const visited = /* @__PURE__ */ new Set();
3025
+ const insideFiles = /* @__PURE__ */ new Set();
3026
+ const outsideFiles = /* @__PURE__ */ new Set();
3027
+ async function collect(filePath) {
3028
+ if (visited.has(filePath)) return;
3029
+ visited.add(filePath);
3030
+ let source;
3031
+ try {
3032
+ source = await fs8.promises.readFile(filePath, "utf8");
3033
+ } catch {
3034
+ return;
3035
+ }
3036
+ for (const specifier of extractImportSpecifiers(source)) {
3037
+ if (/\.(js|mjs|cjs)$/.test(specifier)) continue;
3038
+ let resolved;
3039
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
3040
+ resolved = resolveRelativeSpecifier(filePath, specifier);
3041
+ } else if (tsconfig) {
3042
+ resolved = null;
3043
+ for (const candidate of resolveAlias(specifier, tsconfig)) {
3044
+ const probed = resolveSpecifierFromDir(rootDir, candidate);
3045
+ if (probed) {
3046
+ resolved = probed;
3047
+ break;
3048
+ }
3049
+ }
3050
+ } else {
3051
+ resolved = null;
3052
+ }
3053
+ if (!resolved) continue;
3054
+ if (!isInsideDir(toRealPath(resolved), toRealPath(path10.resolve(rootDir)))) continue;
3055
+ if (isInsideDir(toRealPath(resolved), appDirAbs)) {
3056
+ insideFiles.add(resolved);
3057
+ } else {
3058
+ outsideFiles.add(resolved);
3059
+ }
3060
+ await collect(resolved);
3061
+ }
3062
+ }
3063
+ for (const entry of entryFiles) {
3064
+ await collect(entry);
3065
+ }
3066
+ return {
3067
+ insideFiles: Array.from(insideFiles),
3068
+ outsideFiles: Array.from(outsideFiles)
3069
+ };
3070
+ }
3071
+
2589
3072
  // src/cli/compileOnDemand.ts
2590
3073
  function isProductFresh(sourceAbsPath, productAbsPath) {
2591
3074
  try {
2592
- const srcStat = fs7.statSync(sourceAbsPath);
2593
- const prodStat = fs7.statSync(productAbsPath);
3075
+ const srcStat = fs9.statSync(sourceAbsPath);
3076
+ const prodStat = fs9.statSync(productAbsPath);
2594
3077
  return prodStat.mtimeMs >= srcStat.mtimeMs;
2595
3078
  } catch {
2596
3079
  return false;
@@ -2608,16 +3091,16 @@ function createDevOnDemandState() {
2608
3091
  }
2609
3092
  var state = createDevOnDemandState();
2610
3093
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2611
- const inFlight = state.inFlightCompilations.get(sourceAbsPath);
2612
- if (inFlight) {
2613
- await inFlight.catch(() => {
3094
+ const inFlight2 = state.inFlightCompilations.get(sourceAbsPath);
3095
+ if (inFlight2) {
3096
+ await inFlight2.catch(() => {
2614
3097
  });
2615
3098
  return false;
2616
3099
  }
2617
3100
  if (state.compiledFiles.has(sourceAbsPath)) {
2618
3101
  return false;
2619
3102
  }
2620
- if (!fs7.existsSync(sourceAbsPath)) {
3103
+ if (!fs9.existsSync(sourceAbsPath)) {
2621
3104
  return false;
2622
3105
  }
2623
3106
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
@@ -2626,12 +3109,23 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2626
3109
  return false;
2627
3110
  }
2628
3111
  const compilePromise = (async () => {
3112
+ const { insideFiles } = await collectRelativeImports([sourceAbsPath], rootDir);
3113
+ const files = [sourceAbsPath];
3114
+ for (const dep of insideFiles) {
3115
+ if (state.compiledFiles.has(dep)) continue;
3116
+ const depProduct = prodSourcePathToProductPath(dep, rootDir, dist);
3117
+ if (depProduct && isProductFresh(dep, depProduct)) continue;
3118
+ files.push(dep);
3119
+ }
2629
3120
  await compileDevRoutes({
2630
3121
  rootDir,
2631
3122
  dist,
2632
- files: [sourceAbsPath],
3123
+ files,
2633
3124
  logLevel: "silent"
2634
3125
  });
3126
+ for (const file of files) {
3127
+ state.compiledFiles.add(file);
3128
+ }
2635
3129
  state.compiledFiles.add(sourceAbsPath);
2636
3130
  })();
2637
3131
  state.inFlightCompilations.set(sourceAbsPath, compilePromise);
@@ -2642,26 +3136,39 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2642
3136
  state.inFlightCompilations.delete(sourceAbsPath);
2643
3137
  }
2644
3138
  }
3139
+ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
3140
+ if (!isDevOnDemandEnabled() || middlewarePaths.length === 0) return;
3141
+ const dist = getDevDist();
3142
+ if (!dist) return;
3143
+ for (const mwPath of middlewarePaths) {
3144
+ const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
3145
+ try {
3146
+ await ensureCompiled(sourcePath, rootDir, dist);
3147
+ } catch (err) {
3148
+ console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
3149
+ }
3150
+ }
3151
+ }
2645
3152
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2646
- const rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3153
+ const rel = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2647
3154
  if (!rel.startsWith("src/")) return null;
2648
3155
  const relWithoutSrc = rel.slice(4);
2649
3156
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2650
- return path8.resolve(rootDir, dist, jsRel);
3157
+ return path11.resolve(rootDir, dist, jsRel);
2651
3158
  }
2652
3159
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2653
- const inFlight = state.inFlightSchemaGenerations.get(schemaPath);
2654
- if (inFlight) {
2655
- await inFlight.catch(() => {
3160
+ const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3161
+ if (inFlight2) {
3162
+ await inFlight2.catch(() => {
2656
3163
  });
2657
3164
  return false;
2658
3165
  }
2659
3166
  if (state.generatedSchemas.has(schemaPath)) {
2660
3167
  return false;
2661
3168
  }
2662
- const prodAbsPath = path8.resolve(rootDir, routeFilePath);
3169
+ const prodAbsPath = path11.resolve(rootDir, routeFilePath);
2663
3170
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2664
- if (!fs7.existsSync(sourceAbsPath)) {
3171
+ if (!fs9.existsSync(sourceAbsPath)) {
2665
3172
  return false;
2666
3173
  }
2667
3174
  if (isProductFresh(sourceAbsPath, schemaPath)) {
@@ -2672,7 +3179,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2672
3179
  if (fileRoutes.length === 0) {
2673
3180
  return false;
2674
3181
  }
2675
- const sourceRelPath = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3182
+ const sourceRelPath = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2676
3183
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2677
3184
  const generatePromise = (async () => {
2678
3185
  await generateSchemaFiles(sourceRoutes, rootDir, dist);
@@ -2686,17 +3193,26 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2686
3193
  state.inFlightSchemaGenerations.delete(schemaPath);
2687
3194
  }
2688
3195
  }
3196
+ var sourcePathCache = /* @__PURE__ */ new Map();
2689
3197
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2690
- const rel = path8.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
3198
+ const cached = sourcePathCache.get(prodAbsPath);
3199
+ if (cached) return cached;
3200
+ const rel = path11.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2691
3201
  let relWithoutDist = rel;
2692
3202
  if (relWithoutDist.startsWith(`${dist}/`)) {
2693
3203
  relWithoutDist = relWithoutDist.slice(dist.length + 1);
2694
3204
  }
2695
3205
  const srcRel = `src/${relWithoutDist}`;
2696
3206
  const tsRel = srcRel.replace(/\.js$/, ".ts");
2697
- const tsAbs = path8.resolve(rootDir, tsRel);
2698
- if (fs7.existsSync(tsAbs)) return tsAbs;
2699
- return path8.resolve(rootDir, srcRel);
3207
+ const tsAbs = path11.resolve(rootDir, tsRel);
3208
+ let result;
3209
+ if (fs9.existsSync(tsAbs)) {
3210
+ result = tsAbs;
3211
+ } else {
3212
+ result = path11.resolve(rootDir, srcRel);
3213
+ }
3214
+ sourcePathCache.set(prodAbsPath, result);
3215
+ return result;
2700
3216
  }
2701
3217
  function isDevOnDemandEnabled() {
2702
3218
  return state.enabled;
@@ -2730,28 +3246,25 @@ async function validateInput(schemaPath, method, inputType, input) {
2730
3246
  }
2731
3247
  const schema = mod[schemaKey];
2732
3248
  if (schema === void 0 || schema === null) {
2733
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2734
- return { valid: true, issues: [], data };
3249
+ return { valid: true, issues: [], data: input };
2735
3250
  }
2736
3251
  if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2737
3252
  throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2738
3253
  }
2739
- const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2740
3254
  const zodSchema = schema;
2741
- const result = zodSchema.safeParse(inputObj);
3255
+ const result = zodSchema.safeParse(input);
2742
3256
  if (result.success) {
2743
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2744
- return { valid: true, issues: [], data };
3257
+ return { valid: true, issues: [], data: result.data };
2745
3258
  }
2746
3259
  const issues = mapZodIssues(result.error);
2747
- return { valid: false, issues, data: inputObj };
3260
+ return { valid: false, issues, data: input };
2748
3261
  }
2749
3262
  function mapZodIssues(error) {
2750
3263
  return error.issues.map((issue) => {
2751
- const code = mapZodCode(issue.code, issue.message);
2752
- const path12 = issue.path.map(String).join(".") || "";
3264
+ const code = mapZodCode(issue);
3265
+ const path15 = issue.path.map(String).join(".") || "";
2753
3266
  return {
2754
- path: path12,
3267
+ path: path15,
2755
3268
  code,
2756
3269
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2757
3270
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -2759,27 +3272,29 @@ function mapZodIssues(error) {
2759
3272
  };
2760
3273
  });
2761
3274
  }
2762
- function mapZodCode(zodCode, message) {
2763
- switch (zodCode) {
3275
+ function mapZodCode(issue) {
3276
+ switch (issue.code) {
2764
3277
  case "invalid_type":
3278
+ if (issue.received === "undefined" || /received undefined/i.test(issue.message)) {
3279
+ return "MISSING_FIELD";
3280
+ }
3281
+ return "TYPE_MISMATCH";
2765
3282
  case "invalid_union":
2766
3283
  case "invalid_union_discriminator":
2767
3284
  return "TYPE_MISMATCH";
2768
3285
  case "unrecognized_keys":
2769
3286
  return "INVALID_FORMAT";
2770
3287
  case "invalid_value":
2771
- case "invalid_string":
3288
+ case "invalid_format":
3289
+ case "invalid_key":
3290
+ case "invalid_element":
2772
3291
  case "too_small":
2773
3292
  case "too_big":
2774
3293
  case "invalid_intersection_types":
2775
3294
  case "not_multiple_of":
2776
- return "INVALID_VALUE";
2777
3295
  case "custom":
2778
3296
  return "INVALID_VALUE";
2779
3297
  default:
2780
- if (message.includes("Required") || message.includes("required")) {
2781
- return "MISSING_FIELD";
2782
- }
2783
3298
  return "INVALID_VALUE";
2784
3299
  }
2785
3300
  }
@@ -2799,45 +3314,119 @@ import {
2799
3314
  import { createSecureServer as createHttp2SecureServer } from "http2";
2800
3315
  import { readFileSync } from "fs";
2801
3316
  import { Readable as Readable2 } from "stream";
2802
- import path10 from "path";
3317
+ import path13 from "path";
2803
3318
 
2804
3319
  // src/router/matchRoute.ts
2805
- function matchRoute(routes, method, path12) {
3320
+ var httpIndexCache = /* @__PURE__ */ new WeakMap();
3321
+ var wsIndexCache = /* @__PURE__ */ new WeakMap();
3322
+ function getHttpIndex(routes) {
3323
+ let index = httpIndexCache.get(routes);
3324
+ if (index) return index;
3325
+ index = { static: /* @__PURE__ */ new Map(), methodsByStaticPath: /* @__PURE__ */ new Map(), dynamics: [] };
2806
3326
  for (const route of routes) {
2807
- if (route.method !== method) {
2808
- continue;
2809
- }
2810
- if (!route.isDynamic) {
2811
- if (route.urlPath === path12) {
2812
- return { route, params: {} };
3327
+ if (route.isDynamic) {
3328
+ index.dynamics.push({
3329
+ route,
3330
+ segments: route.urlPath.split("/").filter(Boolean)
3331
+ });
3332
+ } else {
3333
+ index.static.set(`${route.method}|${route.urlPath}`, route);
3334
+ let methods = index.methodsByStaticPath.get(route.urlPath);
3335
+ if (!methods) {
3336
+ methods = /* @__PURE__ */ new Set();
3337
+ index.methodsByStaticPath.set(route.urlPath, methods);
2813
3338
  }
3339
+ methods.add(route.method);
3340
+ }
3341
+ }
3342
+ httpIndexCache.set(routes, index);
3343
+ return index;
3344
+ }
3345
+ function getWsIndex(routes) {
3346
+ let index = wsIndexCache.get(routes);
3347
+ if (index) return index;
3348
+ index = { static: /* @__PURE__ */ new Map(), dynamics: [] };
3349
+ for (const route of routes) {
3350
+ if (route.isDynamic) {
3351
+ index.dynamics.push(route);
3352
+ } else {
3353
+ index.static.set(route.urlPath, route);
3354
+ }
3355
+ }
3356
+ wsIndexCache.set(routes, index);
3357
+ return index;
3358
+ }
3359
+ function matchRoute(routes, method, path15) {
3360
+ const index = getHttpIndex(routes);
3361
+ const upper = method.toUpperCase();
3362
+ const hit = matchByMethod(index, upper, path15);
3363
+ if (hit) return hit;
3364
+ if (upper === "HEAD") {
3365
+ return matchByMethod(index, "GET", path15);
3366
+ }
3367
+ return null;
3368
+ }
3369
+ function matchByMethod(index, method, path15) {
3370
+ const staticHit = index.static.get(`${method}|${path15}`);
3371
+ if (staticHit) {
3372
+ return { route: staticHit, params: {} };
3373
+ }
3374
+ for (const entry of index.dynamics) {
3375
+ const route = entry.route;
3376
+ if (route.method !== method) {
2814
3377
  continue;
2815
3378
  }
2816
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
3379
+ const params = matchSegments(entry.segments, path15, route.paramNames, route.isCatchAll);
2817
3380
  if (params !== null) {
2818
3381
  return { route, params };
2819
3382
  }
2820
3383
  }
2821
3384
  return null;
2822
3385
  }
2823
- function matchWsRoute(wsRoutes, path12) {
2824
- for (const route of wsRoutes) {
2825
- if (!route.isDynamic) {
2826
- if (route.urlPath === path12) {
2827
- return { route, params: {} };
2828
- }
2829
- continue;
2830
- }
2831
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
3386
+ function matchWsRoute(wsRoutes, path15) {
3387
+ const index = getWsIndex(wsRoutes);
3388
+ const staticHit = index.static.get(path15);
3389
+ if (staticHit) {
3390
+ return { route: staticHit, params: {} };
3391
+ }
3392
+ for (const route of index.dynamics) {
3393
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2832
3394
  if (params !== null) {
2833
3395
  return { route, params };
2834
3396
  }
2835
3397
  }
2836
3398
  return null;
2837
3399
  }
2838
- function matchDynamicPath(pattern, path12, paramNames, isCatchAll) {
2839
- const patternSegments = pattern.split("/").filter(Boolean);
2840
- const pathSegments = path12.split("/").filter(Boolean);
3400
+ function findAllowedMethods(routes, path15) {
3401
+ const index = getHttpIndex(routes);
3402
+ const methods = /* @__PURE__ */ new Set();
3403
+ const staticMethods = index.methodsByStaticPath.get(path15);
3404
+ if (staticMethods) {
3405
+ for (const method of staticMethods) {
3406
+ methods.add(method);
3407
+ }
3408
+ }
3409
+ for (const entry of index.dynamics) {
3410
+ const params = matchSegments(
3411
+ entry.segments,
3412
+ path15,
3413
+ entry.route.paramNames,
3414
+ entry.route.isCatchAll
3415
+ );
3416
+ if (params !== null) {
3417
+ methods.add(entry.route.method);
3418
+ }
3419
+ }
3420
+ if (methods.has("GET")) {
3421
+ methods.add("HEAD");
3422
+ }
3423
+ return Array.from(methods);
3424
+ }
3425
+ function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
3426
+ return matchSegments(pattern.split("/").filter(Boolean), path15, paramNames, isCatchAll);
3427
+ }
3428
+ function matchSegments(patternSegments, path15, paramNames, isCatchAll) {
3429
+ const pathSegments = path15.split("/").filter(Boolean);
2841
3430
  if (isCatchAll) {
2842
3431
  const nonCatchAllCount = patternSegments.length - 1;
2843
3432
  if (pathSegments.length <= nonCatchAllCount) {
@@ -2882,9 +3471,6 @@ function matchDynamicPath(pattern, path12, paramNames, isCatchAll) {
2882
3471
  return params;
2883
3472
  }
2884
3473
 
2885
- // src/loader/loadRouteModule.ts
2886
- import fs8 from "fs";
2887
-
2888
3474
  // src/loader/resolveExports.ts
2889
3475
  function resolveExport(module, exportName) {
2890
3476
  if (exportName in module && typeof module[exportName] !== "undefined") {
@@ -2915,7 +3501,7 @@ async function loadRouteModule(filePath, method, rootDir) {
2915
3501
  const dist = getDevDist();
2916
3502
  if (dist) {
2917
3503
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2918
- if (sourcePath && fs8.existsSync(sourcePath)) {
3504
+ if (sourcePath) {
2919
3505
  try {
2920
3506
  await ensureCompiled(sourcePath, rootDir, dist);
2921
3507
  } catch (compileErr) {
@@ -2980,7 +3566,7 @@ async function parseMultipart(request) {
2980
3566
  }
2981
3567
 
2982
3568
  // src/runtime/resolveInput.ts
2983
- async function resolveInput(method, request) {
3569
+ async function resolveInputFromUrl(method, request, url) {
2984
3570
  const inputType = getInputTypeForMethod(method);
2985
3571
  if (inputType === "body") {
2986
3572
  const contentType = request.headers.get("content-type") ?? "";
@@ -2989,7 +3575,7 @@ async function resolveInput(method, request) {
2989
3575
  }
2990
3576
  if (contentType.includes("application/x-www-form-urlencoded")) {
2991
3577
  const text2 = await request.text();
2992
- if (text2.trim() === "") return null;
3578
+ if (isBlankText(text2)) return null;
2993
3579
  const params = new URLSearchParams(text2);
2994
3580
  const obj = {};
2995
3581
  for (const [key, value] of params) {
@@ -2998,7 +3584,7 @@ async function resolveInput(method, request) {
2998
3584
  return obj;
2999
3585
  }
3000
3586
  const text = await request.text();
3001
- if (text.trim() === "") {
3587
+ if (isBlankText(text)) {
3002
3588
  return null;
3003
3589
  }
3004
3590
  const result = parseJsonBody(text);
@@ -3015,9 +3601,28 @@ async function resolveInput(method, request) {
3015
3601
  }
3016
3602
  return result.data;
3017
3603
  }
3018
- const url = new URL(request.url);
3019
3604
  return queryToObject(url.searchParams);
3020
3605
  }
3606
+ function isBlankText(text) {
3607
+ return text.length === 0 || !/\S/.test(text);
3608
+ }
3609
+ async function resolveBodyForQueryMethod(request) {
3610
+ const text = await request.text();
3611
+ if (isBlankText(text)) return void 0;
3612
+ const result = parseJsonBody(text);
3613
+ if (!result.success) {
3614
+ throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
3615
+ {
3616
+ path: "body",
3617
+ code: "INVALID_FORMAT",
3618
+ expected: "JSON",
3619
+ received: "text",
3620
+ message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
3621
+ }
3622
+ ]);
3623
+ }
3624
+ return result.data;
3625
+ }
3021
3626
 
3022
3627
  // src/response/sendNodeResponse.ts
3023
3628
  import { Readable } from "stream";
@@ -3030,11 +3635,24 @@ async function sendNodeResponse(response, res) {
3030
3635
  res.setHeader(key, value);
3031
3636
  }
3032
3637
  }
3638
+ const deferred = consumePendingMetaHeaders(response);
3639
+ if (deferred) {
3640
+ for (const [key, value] of Object.entries(deferred)) {
3641
+ res.setHeader(key, value);
3642
+ }
3643
+ }
3033
3644
  if (response.body) {
3034
3645
  const nodeStream = Readable.fromWeb(response.body);
3035
3646
  await new Promise((resolve, reject) => {
3036
3647
  nodeStream.on("error", reject);
3037
- res.on("error", reject);
3648
+ const abort = () => {
3649
+ nodeStream.destroy();
3650
+ resolve();
3651
+ };
3652
+ res.on("error", abort);
3653
+ res.on("close", () => {
3654
+ if (!res.writableEnded) abort();
3655
+ });
3038
3656
  res.on("finish", resolve);
3039
3657
  nodeStream.pipe(res);
3040
3658
  });
@@ -3044,11 +3662,13 @@ async function sendNodeResponse(response, res) {
3044
3662
  }
3045
3663
 
3046
3664
  // src/utils/getClientIp.ts
3047
- function getClientIp(req) {
3048
- const xff = req.headers["x-forwarded-for"];
3049
- if (typeof xff === "string" && xff.length > 0) {
3050
- const first = xff.split(",")[0]?.trim();
3051
- if (first) return first;
3665
+ function getClientIp(req, trustedProxy = false) {
3666
+ if (trustedProxy) {
3667
+ const xff = req.headers["x-forwarded-for"];
3668
+ if (typeof xff === "string" && xff.length > 0) {
3669
+ const first = xff.split(",")[0]?.trim();
3670
+ if (first) return first;
3671
+ }
3052
3672
  }
3053
3673
  const remote = req.socket?.remoteAddress;
3054
3674
  if (remote) {
@@ -3085,11 +3705,6 @@ function cors(options = {}) {
3085
3705
  } else if (Array.isArray(origin)) {
3086
3706
  allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
3087
3707
  }
3088
- if (!allowOrigin) {
3089
- await next();
3090
- return;
3091
- }
3092
- ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
3093
3708
  if (origin === true || Array.isArray(origin)) {
3094
3709
  const existingVary = ctx.headers.get("vary");
3095
3710
  if (existingVary) {
@@ -3100,6 +3715,11 @@ function cors(options = {}) {
3100
3715
  ctx.setHeader("Vary", "Origin");
3101
3716
  }
3102
3717
  }
3718
+ if (!allowOrigin) {
3719
+ await next();
3720
+ return;
3721
+ }
3722
+ ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
3103
3723
  ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
3104
3724
  if (allowedHeaders) {
3105
3725
  ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
@@ -3187,6 +3807,172 @@ function helmet(options = {}) {
3187
3807
  };
3188
3808
  }
3189
3809
 
3810
+ // src/middleware/compression.ts
3811
+ import zlib from "zlib";
3812
+ import { promisify } from "util";
3813
+ var gzipAsync = promisify(zlib.gzip);
3814
+ var deflateAsync = promisify(zlib.deflate);
3815
+ var brotliAsync = promisify(zlib.brotliCompress);
3816
+ var DEFAULT_THRESHOLD = 1024;
3817
+ var COMPRESSIBLE_CHECKS = [
3818
+ (ct) => ct === "application/json",
3819
+ (ct) => ct === "application/javascript",
3820
+ (ct) => ct === "image/svg+xml",
3821
+ (ct) => ct.startsWith("text/") && ct !== "text/event-stream"
3822
+ ];
3823
+ function isCompressible(contentType) {
3824
+ const ct = contentType.split(";")[0].trim().toLowerCase();
3825
+ if (ct === "") return false;
3826
+ return COMPRESSIBLE_CHECKS.some((check) => check(ct));
3827
+ }
3828
+ function parseAcceptEncoding(header) {
3829
+ return header.split(",").map((part) => part.trim()).filter((part) => part !== "").map((part) => {
3830
+ const [encoding, ...params] = part.split(";");
3831
+ let quality = 1;
3832
+ for (const param of params) {
3833
+ const trimmed = param.trim();
3834
+ if (trimmed.startsWith("q=")) {
3835
+ const q = Number(trimmed.slice(2));
3836
+ if (Number.isFinite(q)) quality = q;
3837
+ }
3838
+ }
3839
+ return { encoding: (encoding ?? "").trim().toLowerCase(), quality };
3840
+ }).filter((e) => e.encoding !== "" && e.quality > 0);
3841
+ }
3842
+ function selectEncoding(accepted) {
3843
+ const byName = /* @__PURE__ */ new Map();
3844
+ for (const { encoding, quality } of accepted) {
3845
+ byName.set(encoding, Math.max(byName.get(encoding) ?? 0, quality));
3846
+ }
3847
+ for (const [encoding, quality] of byName) {
3848
+ if (quality === 0) byName.delete(encoding);
3849
+ }
3850
+ for (const candidate of ["br", "gzip", "deflate"]) {
3851
+ if ((byName.get(candidate) ?? 0) > 0) return candidate;
3852
+ }
3853
+ if ((byName.get("*") ?? 0) > 0) return "gzip";
3854
+ return null;
3855
+ }
3856
+ async function compressBody(encoding, body) {
3857
+ const buf = Buffer.from(body, "utf-8");
3858
+ switch (encoding) {
3859
+ case "br":
3860
+ return brotliAsync(buf);
3861
+ case "gzip":
3862
+ return gzipAsync(buf);
3863
+ case "deflate":
3864
+ return deflateAsync(buf);
3865
+ }
3866
+ }
3867
+ function mergeVary(meta, value) {
3868
+ const existing = meta.headers["Vary"] ?? meta.headers["vary"];
3869
+ if (!existing) {
3870
+ meta.headers["Vary"] = value;
3871
+ return;
3872
+ }
3873
+ if (!existing.toLowerCase().includes(value.toLowerCase())) {
3874
+ const key = meta.headers["Vary"] !== void 0 ? "Vary" : "vary";
3875
+ meta.headers[key] = `${existing}, ${value}`;
3876
+ }
3877
+ }
3878
+ function compression(options = {}) {
3879
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
3880
+ return async (ctx, next) => {
3881
+ const meta = ctx.meta;
3882
+ const response = await next();
3883
+ if (!response) return response;
3884
+ mergeVary(meta, "Accept-Encoding");
3885
+ const acceptEncoding = ctx.request.headers.get("accept-encoding") ?? "";
3886
+ const encoding = selectEncoding(parseAcceptEncoding(acceptEncoding));
3887
+ const contentType = response.headers.get("content-type") ?? "";
3888
+ if (!encoding || !isCompressible(contentType) || response.headers.has("content-encoding") || (response.headers.get("cache-control") ?? "").includes("no-transform") || response.status === 204 || response.status === 304 || response.body === null) {
3889
+ return response;
3890
+ }
3891
+ const deferredHeaders = consumePendingMetaHeaders(response);
3892
+ const bodyText = await response.text();
3893
+ if (bodyText.length < threshold) {
3894
+ const headers2 = new Headers(response.headers);
3895
+ for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
3896
+ headers2.set(key, value);
3897
+ }
3898
+ return new Response(bodyText, {
3899
+ status: response.status,
3900
+ statusText: response.statusText,
3901
+ headers: headers2
3902
+ });
3903
+ }
3904
+ const compressed = await compressBody(encoding, bodyText);
3905
+ const headers = new Headers(response.headers);
3906
+ for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
3907
+ headers.set(key, value);
3908
+ }
3909
+ headers.set("Content-Encoding", encoding);
3910
+ headers.delete("Content-Length");
3911
+ return new Response(compressed, {
3912
+ status: response.status,
3913
+ statusText: response.statusText,
3914
+ headers
3915
+ });
3916
+ };
3917
+ }
3918
+
3919
+ // src/middleware/etag.ts
3920
+ import { createHash } from "crypto";
3921
+ var DEFAULTS2 = { weak: true };
3922
+ function computeEtag(body, weak) {
3923
+ const hash = createHash("sha1").update(body).digest("base64");
3924
+ return weak ? `W/"${hash}"` : `"${hash}"`;
3925
+ }
3926
+ function ifNoneMatchMatches(ifNoneMatch, etag2, weak) {
3927
+ const normalize = (tag) => tag.trim().replace(/^W\//i, "");
3928
+ if (weak) {
3929
+ const target = normalize(etag2);
3930
+ return ifNoneMatch.split(",").some((tag) => {
3931
+ const trimmed = tag.trim();
3932
+ return trimmed === "*" || normalize(trimmed) === target;
3933
+ });
3934
+ }
3935
+ return ifNoneMatch.split(",").some((tag) => tag.trim() === etag2 || tag.trim() === "*");
3936
+ }
3937
+ function etag(options = {}) {
3938
+ const opts = { ...DEFAULTS2, ...options };
3939
+ return async (ctx, next) => {
3940
+ const meta = ctx.meta;
3941
+ const response = await next();
3942
+ if (!response) return response;
3943
+ const method = ctx.request.method.toUpperCase();
3944
+ if (method !== "GET" && method !== "HEAD") return response;
3945
+ if (response.status < 200 || response.status >= 300) return response;
3946
+ if (meta.headers["etag"] !== void 0 || response.headers.has("etag")) {
3947
+ return response;
3948
+ }
3949
+ if (response.body === null || (response.headers.get("content-type") ?? "").includes("text/event-stream")) {
3950
+ return response;
3951
+ }
3952
+ const bodyText = await response.text();
3953
+ const etagValue = computeEtag(bodyText, opts.weak);
3954
+ const ifNoneMatch = ctx.request.headers.get("if-none-match");
3955
+ if (ifNoneMatch && ifNoneMatchMatches(ifNoneMatch, etagValue, opts.weak)) {
3956
+ return new Response(null, {
3957
+ status: 304,
3958
+ statusText: response.statusText,
3959
+ headers: { ETag: etagValue }
3960
+ });
3961
+ }
3962
+ meta.headers["etag"] = etagValue;
3963
+ const headers = new Headers(response.headers);
3964
+ const deferredHeaders = consumePendingMetaHeaders(response);
3965
+ for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
3966
+ headers.set(key, value);
3967
+ }
3968
+ return new Response(bodyText, {
3969
+ status: response.status,
3970
+ statusText: response.statusText,
3971
+ headers
3972
+ });
3973
+ };
3974
+ }
3975
+
3190
3976
  // src/middleware/logger.ts
3191
3977
  function logger(options = {}) {
3192
3978
  return async (ctx, next) => {
@@ -3221,9 +4007,9 @@ function logger(options = {}) {
3221
4007
  }
3222
4008
 
3223
4009
  // src/server/handleWsUpgrade.ts
3224
- import fs9 from "fs";
4010
+ import fs10 from "fs";
3225
4011
  import { WebSocketServer, WebSocket } from "ws";
3226
- import path9 from "path";
4012
+ import path12 from "path";
3227
4013
 
3228
4014
  // src/server/serverUtils.ts
3229
4015
  function nodeHttpToWebHeaders(req) {
@@ -3279,7 +4065,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
3279
4065
  const dist = getDevDist();
3280
4066
  if (dist) {
3281
4067
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3282
- if (sourcePath && fs9.existsSync(sourcePath)) {
4068
+ if (sourcePath && fs10.existsSync(sourcePath)) {
3283
4069
  await ensureCompiled(sourcePath, rootDir, dist);
3284
4070
  }
3285
4071
  }
@@ -3302,9 +4088,9 @@ function bindEvents(rawSocket, handlers) {
3302
4088
  }
3303
4089
  }
3304
4090
  if (handlers.onMessage) {
3305
- rawSocket.on("message", (data) => {
3306
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
3307
- handlers.onMessage(ws, buf.toString("utf8"));
4091
+ rawSocket.on("message", (data, isBinary) => {
4092
+ const buf = toWsBuffer(data);
4093
+ handlers.onMessage(ws, isBinary ? buf : buf.toString("utf8"));
3308
4094
  });
3309
4095
  }
3310
4096
  if (handlers.onClose) {
@@ -3318,12 +4104,22 @@ function bindEvents(rawSocket, handlers) {
3318
4104
  });
3319
4105
  }
3320
4106
  }
4107
+ function toWsBuffer(data) {
4108
+ if (Buffer.isBuffer(data)) return data;
4109
+ if (Array.isArray(data)) return Buffer.concat(data);
4110
+ return Buffer.from(data);
4111
+ }
3321
4112
  async function sendResponseToSocket(socket, response) {
3322
4113
  const body = await response.text().catch(() => "");
3323
4114
  const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
3324
4115
  `;
3325
4116
  const headerLines = [];
3326
4117
  let hasContentLength = false;
4118
+ const deferredHeaders = consumePendingMetaHeaders(response);
4119
+ for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
4120
+ if (key.toLowerCase() === "content-length") hasContentLength = true;
4121
+ headerLines.push(`${key}: ${value}`);
4122
+ }
3327
4123
  for (const [key, value] of response.headers) {
3328
4124
  if (key.toLowerCase() === "content-length") {
3329
4125
  hasContentLength = true;
@@ -3337,9 +4133,33 @@ async function sendResponseToSocket(socket, response) {
3337
4133
  socket.destroy();
3338
4134
  }
3339
4135
  function attachWebSocket(options) {
3340
- const { server, routesRef, rootDir, config, globalMiddlewares } = options;
4136
+ const {
4137
+ server,
4138
+ routesRef,
4139
+ rootDir,
4140
+ config,
4141
+ globalMiddlewares,
4142
+ trustedProxy = false,
4143
+ registries
4144
+ } = options;
3341
4145
  const wss = new WebSocketServer({ noServer: true });
3342
4146
  server.on("upgrade", async (req, socket, head) => {
4147
+ try {
4148
+ await handleUpgradeRequest(req, socket, head);
4149
+ } catch (err) {
4150
+ console.error("[faapi] WS upgrade \u5904\u7406\u5931\u8D25:", err);
4151
+ if (!socket.destroyed) {
4152
+ try {
4153
+ if (!socket.writableEnded) {
4154
+ socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
4155
+ }
4156
+ } catch {
4157
+ }
4158
+ socket.destroy();
4159
+ }
4160
+ }
4161
+ });
4162
+ async function handleUpgradeRequest(req, socket, head) {
3343
4163
  const currentWsRoutes = routesRef.wsCurrent;
3344
4164
  const pathname = getPathname(req);
3345
4165
  const match = matchWsRoute(currentWsRoutes, pathname);
@@ -3353,13 +4173,13 @@ function attachWebSocket(options) {
3353
4173
  const host = req.headers.host ?? "localhost";
3354
4174
  const url = `http://${host}${req.url ?? "/"}`;
3355
4175
  const request = new Request(url, { method: "GET", headers });
3356
- const ctx = createContext(request, params, config, getClientIp(req));
4176
+ const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
3357
4177
  const meta = ctx.meta;
3358
4178
  let upgraded = false;
3359
4179
  const finalHandler = async () => {
3360
4180
  let handlers;
3361
4181
  try {
3362
- const absoluteFilePath = path9.resolve(rootDir, route.filePath);
4182
+ const absoluteFilePath = path12.resolve(rootDir, route.filePath);
3363
4183
  handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
3364
4184
  } catch (err) {
3365
4185
  const reason = err instanceof Error ? err.message : String(err);
@@ -3383,6 +4203,7 @@ function attachWebSocket(options) {
3383
4203
  let response;
3384
4204
  try {
3385
4205
  if (route.middlewares === void 0 && route.middlewarePaths) {
4206
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
3386
4207
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3387
4208
  if (bundle) {
3388
4209
  route.middlewares = bundle.middlewares;
@@ -3408,7 +4229,7 @@ function attachWebSocket(options) {
3408
4229
  return;
3409
4230
  }
3410
4231
  await sendResponseToSocket(socket, mergeMeta(response, meta));
3411
- });
4232
+ }
3412
4233
  return wss;
3413
4234
  }
3414
4235
 
@@ -3422,16 +4243,26 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3422
4243
  const headers = nodeHttpToWebHeaders(req);
3423
4244
  const method = req.method ?? "GET";
3424
4245
  if (method === "GET" || method === "HEAD") {
3425
- return new Request(url.toString(), { method, headers });
4246
+ return { request: new Request(url.toString(), { method, headers }), url };
4247
+ }
4248
+ const contentLength = req.headers["content-length"];
4249
+ if (contentLength !== void 0) {
4250
+ const declared = Number(Array.isArray(contentLength) ? contentLength[0] : contentLength);
4251
+ if (Number.isFinite(declared) && declared > bodyLimit) {
4252
+ throw new PayloadTooLargeError(bodyLimit);
4253
+ }
3426
4254
  }
3427
4255
  const stream = Readable2.toWeb(req);
3428
4256
  const limitedStream = limitStreamSize(stream, bodyLimit);
3429
- return new Request(url.toString(), {
3430
- method,
3431
- headers,
3432
- body: limitedStream,
3433
- duplex: "half"
3434
- });
4257
+ return {
4258
+ request: new Request(url.toString(), {
4259
+ method,
4260
+ headers,
4261
+ body: limitedStream,
4262
+ duplex: "half"
4263
+ }),
4264
+ url
4265
+ };
3435
4266
  }
3436
4267
  function limitStreamSize(stream, maxSize) {
3437
4268
  let totalSize = 0;
@@ -3483,22 +4314,6 @@ function limitStreamSize(stream, maxSize) {
3483
4314
  }
3484
4315
  });
3485
4316
  }
3486
- function findAllowedMethods(routes, path12) {
3487
- const methods = /* @__PURE__ */ new Set();
3488
- for (const route of routes) {
3489
- if (route.urlPath === path12) {
3490
- methods.add(route.method);
3491
- continue;
3492
- }
3493
- if (route.isDynamic) {
3494
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
3495
- if (params !== null) {
3496
- methods.add(route.method);
3497
- }
3498
- }
3499
- }
3500
- return Array.from(methods);
3501
- }
3502
4317
  function createServer(options) {
3503
4318
  const {
3504
4319
  routes,
@@ -3511,12 +4326,20 @@ function createServer(options) {
3511
4326
  middlewares: globalMiddlewares,
3512
4327
  injectors: globalInjectors,
3513
4328
  helmet: helmetOption,
4329
+ compression: compressionOption,
4330
+ etag: etagOption,
4331
+ registries,
3514
4332
  logger: loggerOption,
3515
4333
  bodyLimit = DEFAULT_BODY_LIMIT,
3516
- http2: http2Option
4334
+ http2: http2Option,
4335
+ trustedProxy = false
3517
4336
  } = options;
3518
4337
  const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
3519
4338
  const configMiddlewares = [];
4339
+ if (compressionOption) {
4340
+ const compOpts = typeof compressionOption === "object" ? compressionOption : {};
4341
+ configMiddlewares.push(compression(compOpts));
4342
+ }
3520
4343
  const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
3521
4344
  if (corsMiddleware) configMiddlewares.push(corsMiddleware);
3522
4345
  if (helmetOption) {
@@ -3525,6 +4348,14 @@ function createServer(options) {
3525
4348
  }
3526
4349
  const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
3527
4350
  if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
4351
+ if (etagOption) {
4352
+ const etagOpts = typeof etagOption === "object" ? etagOption : {};
4353
+ configMiddlewares.push(etag(etagOpts));
4354
+ }
4355
+ const outerMiddlewares = [...configMiddlewares];
4356
+ if (globalMiddlewares && globalMiddlewares.length > 0) {
4357
+ outerMiddlewares.push(...globalMiddlewares);
4358
+ }
3528
4359
  const server = (() => {
3529
4360
  if (http2Option) {
3530
4361
  const h2Opts = typeof http2Option === "object" ? http2Option : {};
@@ -3544,29 +4375,43 @@ function createServer(options) {
3544
4375
  dist,
3545
4376
  req,
3546
4377
  res,
3547
- configMiddlewares,
4378
+ outerMiddlewares,
3548
4379
  onError,
3549
4380
  config,
3550
- globalMiddlewares,
3551
4381
  globalInjectors,
3552
- bodyLimit
4382
+ bodyLimit,
4383
+ trustedProxy,
4384
+ registries
3553
4385
  ).catch(() => {
3554
4386
  res.statusCode = 500;
3555
4387
  res.end();
3556
4388
  });
3557
4389
  });
3558
- if (routesRef.wsCurrent.length > 0) {
3559
- attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares });
3560
- }
4390
+ attachWebSocket({
4391
+ server,
4392
+ routesRef,
4393
+ rootDir,
4394
+ config,
4395
+ globalMiddlewares,
4396
+ trustedProxy,
4397
+ registries
4398
+ });
3561
4399
  return { server, routesRef };
3562
4400
  }
3563
- function prepareRequest(req, config, bodyLimit) {
3564
- const request = toWebRequest(req, bodyLimit);
4401
+ function prepareRequest(req, config, bodyLimit, trustedProxy, registries) {
4402
+ const { request, url } = toWebRequest(req, bodyLimit);
3565
4403
  const method = request.method.toUpperCase();
3566
- const urlPath = new URL(request.url).pathname;
3567
- const ctx = createContext(request, {}, config, getClientIp(req));
4404
+ const urlPath = url.pathname;
4405
+ const ctx = createContextFromUrl(
4406
+ request,
4407
+ url,
4408
+ {},
4409
+ config,
4410
+ getClientIp(req, trustedProxy),
4411
+ registries
4412
+ );
3568
4413
  const meta = ctx.meta;
3569
- return { request, ctx, meta, method, urlPath };
4414
+ return { request, url, ctx, meta, method, urlPath };
3570
4415
  }
3571
4416
  function resolveRouteOrThrow(routes, method, urlPath) {
3572
4417
  const match = matchRoute(routes, method, urlPath);
@@ -3577,17 +4422,28 @@ function resolveRouteOrThrow(routes, method, urlPath) {
3577
4422
  }
3578
4423
  throw new RouteNotFoundError(urlPath);
3579
4424
  }
4425
+ var routePathCache = /* @__PURE__ */ new WeakMap();
4426
+ function getRoutePaths(route, rootDir, dist) {
4427
+ let cached = routePathCache.get(route);
4428
+ if (!cached) {
4429
+ cached = {
4430
+ absFilePath: path13.resolve(rootDir, route.filePath),
4431
+ schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
4432
+ };
4433
+ routePathCache.set(route, cached);
4434
+ }
4435
+ return cached;
4436
+ }
3580
4437
  function createRoutePipeline(opts) {
3581
- const { routes, method, urlPath, ctx, request, rootDir, dist, globalInjectors } = opts;
4438
+ const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
3582
4439
  return async () => {
3583
4440
  const match = resolveRouteOrThrow(routes, method, urlPath);
3584
4441
  ctx.params = match.params;
3585
4442
  const { route } = match;
3586
- const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3587
- const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
3588
- const input = await resolveInput(route.method, request);
4443
+ const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
4444
+ const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
4445
+ const input = await resolveInputFromUrl(route.method, request, url);
3589
4446
  const inputType = getInputTypeForMethod(route.method);
3590
- const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
3591
4447
  if (isDevOnDemandEnabled()) {
3592
4448
  const devDist = getDevDist();
3593
4449
  if (devDist) {
@@ -3598,8 +4454,9 @@ function createRoutePipeline(opts) {
3598
4454
  if (!result.valid) {
3599
4455
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
3600
4456
  }
3601
- const body = hasBody(route.method) ? result.data : void 0;
4457
+ const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
3602
4458
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
4459
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
3603
4460
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3604
4461
  if (bundle) {
3605
4462
  route.middlewares = bundle.middlewares;
@@ -3617,36 +4474,37 @@ async function sendSuccessResponse(response, res) {
3617
4474
  await sendNodeResponse(response, res);
3618
4475
  }
3619
4476
  async function sendErrorResponse(err, meta, res, onError, ctx) {
3620
- await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx.config), meta), res);
3621
- if (onError) {
4477
+ await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx?.config), meta), res);
4478
+ if (onError && ctx) {
3622
4479
  try {
3623
4480
  await onError(err, ctx);
3624
4481
  } catch {
3625
4482
  }
3626
4483
  }
3627
4484
  }
3628
- async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3629
- const { request, ctx, meta, method, urlPath } = prepareRequest(req, config, bodyLimit);
3630
- const routePipeline = createRoutePipeline({
3631
- routes,
3632
- method,
3633
- urlPath,
3634
- ctx,
3635
- request,
3636
- rootDir,
3637
- dist,
3638
- globalMiddlewares,
3639
- globalInjectors
3640
- });
3641
- const outerMiddlewares = [];
3642
- if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3643
- if (globalMiddlewares && globalMiddlewares.length > 0) {
3644
- outerMiddlewares.push(...globalMiddlewares);
3645
- }
4485
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
4486
+ let meta = { headers: {}, setCookies: [] };
4487
+ let ctx;
3646
4488
  try {
4489
+ const prepared = prepareRequest(req, config, bodyLimit, trustedProxy, registries);
4490
+ ctx = prepared.ctx;
4491
+ meta = prepared.meta;
4492
+ const { request, url, method, urlPath } = prepared;
4493
+ const routePipeline = createRoutePipeline({
4494
+ routes,
4495
+ method,
4496
+ urlPath,
4497
+ url,
4498
+ ctx,
4499
+ request,
4500
+ rootDir,
4501
+ dist,
4502
+ globalInjectors
4503
+ });
3647
4504
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
3648
4505
  await sendSuccessResponse(response, res);
3649
4506
  } catch (err) {
4507
+ if (res.destroyed || res.writableEnded) return;
3650
4508
  await sendErrorResponse(err, meta, res, onError, ctx);
3651
4509
  }
3652
4510
  }
@@ -3670,7 +4528,7 @@ async function createTestServer(options) {
3670
4528
  } = options;
3671
4529
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3672
4530
  const sorted = sortRoutes(routes);
3673
- const schemaDist = dist ? path11.isAbsolute(dist) ? dist : path11.resolve(rootDir, dist) : await fs10.mkdtemp(path11.join(os.tmpdir(), "faapi-test-schema-"));
4531
+ const schemaDist = dist ? path14.isAbsolute(dist) ? dist : path14.resolve(rootDir, dist) : await fs11.mkdtemp(path14.join(os.tmpdir(), "faapi-test-schema-"));
3674
4532
  await generateSchemaFiles(sorted, rootDir, schemaDist);
3675
4533
  const { server } = createServer({
3676
4534
  routes: sorted,
@@ -3703,7 +4561,7 @@ async function createTestServer(options) {
3703
4561
  await new Promise((resolve) => {
3704
4562
  server.close(() => resolve());
3705
4563
  });
3706
- await fs10.rm(schemaDist, { recursive: true, force: true }).catch(() => {
4564
+ await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3707
4565
  });
3708
4566
  invalidateSchemaCache();
3709
4567
  }