@faapi/faapi 3.3.0 → 4.1.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,10 +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
- return createContextFromUrl(request, new URL(request.url), params, config, ip);
272
+ function createContext(request, params, config = {}, ip = "", registries) {
273
+ return createContextFromUrl(request, new URL(request.url), params, config, ip, registries);
270
274
  }
271
- function createContextFromUrl(request, url, params, config = {}, ip = "") {
275
+ function createContextFromUrl(request, url, params, config = {}, ip = "", registries) {
272
276
  const meta = { headers: {}, setCookies: [] };
273
277
  const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
274
278
  const cookiesObj = {};
@@ -365,6 +369,9 @@ function createContextFromUrl(request, url, params, config = {}, ip = "") {
365
369
  return formatFailResponse(options, config);
366
370
  }
367
371
  };
372
+ if (registries) {
373
+ ctx.registries = registries;
374
+ }
368
375
  const extend = config?.extendContext;
369
376
  if (typeof extend === "function") {
370
377
  extend(ctx);
@@ -372,8 +379,17 @@ function createContextFromUrl(request, url, params, config = {}, ip = "") {
372
379
  return ctx;
373
380
  }
374
381
  function createTestContext(options) {
375
- const { method = "GET", path: path12, query, headers, params = {}, config = {}, ip = "" } = options;
376
- 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}`);
377
393
  if (query) {
378
394
  for (const [key, value] of Object.entries(query)) {
379
395
  if (Array.isArray(value)) {
@@ -389,7 +405,7 @@ function createTestContext(options) {
389
405
  method,
390
406
  headers
391
407
  });
392
- return createContext(request, params, config, ip);
408
+ return createContext(request, params, config, ip, registries);
393
409
  }
394
410
 
395
411
  // src/utils/isPlainObject.ts
@@ -404,6 +420,25 @@ function isPlainObject(value) {
404
420
  return proto === null || proto === Object.prototype;
405
421
  }
406
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
+
407
442
  // src/response/toResponse.ts
408
443
  async function toResponse(value, meta) {
409
444
  if (value instanceof Promise) {
@@ -420,6 +455,10 @@ async function toResponse(value, meta) {
420
455
  };
421
456
  if (value instanceof Response) {
422
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
+ }
423
462
  const headers2 = new Headers(value.headers);
424
463
  applyMeta(headers2);
425
464
  return new Response(value.body, {
@@ -514,10 +553,15 @@ var PARAM_TYPE_MAP = {
514
553
  agents: "agents"
515
554
  // Phase 2.3
516
555
  };
556
+ var injectionCache = /* @__PURE__ */ new WeakMap();
517
557
  function resolveInjection(fn) {
558
+ const cached = injectionCache.get(fn);
559
+ if (cached) {
560
+ return cached;
561
+ }
518
562
  const fnStr = fn.toString();
519
563
  const params = extractParamsWithAst(fnStr);
520
- return params.map((param) => {
564
+ const items = params.map((param) => {
521
565
  const type = PARAM_TYPE_MAP[param.name] || "unknown";
522
566
  return {
523
567
  name: param.name,
@@ -526,6 +570,8 @@ function resolveInjection(fn) {
526
570
  // 运行时类型已擦除
527
571
  };
528
572
  });
573
+ injectionCache.set(fn, items);
574
+ return items;
529
575
  }
530
576
  function extractParamsWithAst(fnStr) {
531
577
  const sourceFile = ts.createSourceFile(
@@ -587,24 +633,157 @@ function extractParamName(param, names) {
587
633
  function queryToObject(params) {
588
634
  const result = {};
589
635
  for (const [key, value] of params) {
590
- 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
+ }
591
644
  }
592
645
  return result;
593
646
  }
594
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
+
595
779
  // src/injection/agentRegistry.ts
596
- var registry = /* @__PURE__ */ new Map();
597
780
  function listAgents() {
598
- const merged = /* @__PURE__ */ new Map();
599
- for (const agent of registry.values()) merged.set(agent.name, agent);
600
- return Array.from(merged.values());
781
+ return defaultRegistries.agent.listAgents();
601
782
  }
602
783
 
603
784
  // src/injection/agentHandle.ts
604
- var currentFactory = null;
605
785
  function getAgentHandle(ctx) {
606
- if (currentFactory === null) return void 0;
607
- return currentFactory(ctx);
786
+ return defaultRegistries.agentHandle.get(ctx);
608
787
  }
609
788
 
610
789
  // src/injection/injectParams.ts
@@ -641,11 +820,12 @@ function getBuiltinInjectionValue(type, ctx, body) {
641
820
  }
642
821
  return {};
643
822
  // Phase 2.3:注入所有已注册 agent 元数据列表
823
+ // 方案 A:优先读 app 实例注册表,无实例(编程式直调 ctx)回退默认全局实例
644
824
  case "agents":
645
- return listAgents();
825
+ return ctx.registries ? ctx.registries.agent.listAgents() : listAgents();
646
826
  // Phase 3.5:调 @faapi/agent 插件注册的工厂获取 AgentHandle
647
827
  case "agent":
648
- return getAgentHandle(ctx);
828
+ return ctx.registries ? ctx.registries.agentHandle.get(ctx) : getAgentHandle(ctx);
649
829
  default:
650
830
  return void 0;
651
831
  }
@@ -676,6 +856,10 @@ function wrapResult(result, ctx) {
676
856
  function mergeMeta(response, meta) {
677
857
  const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
678
858
  if (!hasMeta) return response;
859
+ if (isHeadersOnlyMeta(meta)) {
860
+ deferMetaHeaders(response, meta.headers);
861
+ return response;
862
+ }
679
863
  const headers = new Headers(response.headers);
680
864
  for (const [key, value] of Object.entries(meta.headers)) {
681
865
  headers.set(key, value);
@@ -758,23 +942,23 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
758
942
  }
759
943
 
760
944
  // src/testServer.ts
761
- import path11 from "path";
945
+ import path14 from "path";
762
946
  import os from "os";
763
- import fs9 from "fs/promises";
947
+ import fs11 from "fs/promises";
764
948
 
765
949
  // src/router/scanRoutes.ts
766
950
  import fg from "fast-glob";
767
- import path from "path";
768
- import fs from "fs";
951
+ import path2 from "path";
952
+ import fs2 from "fs";
769
953
 
770
954
  // src/router/constants.ts
771
955
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
772
956
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
773
957
 
774
958
  // src/utils/normalizePath.ts
775
- function normalizePath(path12) {
776
- if (!path12) return "";
777
- let result = path12.replace(/\\/g, "/");
959
+ function normalizePath(path15) {
960
+ if (!path15) return "";
961
+ let result = path15.replace(/\\/g, "/");
778
962
  result = result.replace(/\/+/g, "/");
779
963
  result = result.replace(/\/+$/, "");
780
964
  if (result && !result.startsWith("/")) {
@@ -847,6 +1031,7 @@ async function importWithCacheBust(filePath, bustViteCache = false) {
847
1031
 
848
1032
  // src/middleware/loadMiddlewares.ts
849
1033
  var middlewareCache = /* @__PURE__ */ new Map();
1034
+ var inFlight = /* @__PURE__ */ new Map();
850
1035
  function getCachedMiddlewares(absPath) {
851
1036
  return middlewareCache.get(absPath);
852
1037
  }
@@ -898,8 +1083,15 @@ async function loadMergedMiddlewares(middlewarePaths) {
898
1083
  for (const absMwPath of middlewarePaths) {
899
1084
  let bundle = getCachedMiddlewares(absMwPath);
900
1085
  if (bundle === void 0) {
901
- bundle = await loadMiddlewaresFile(absMwPath);
902
- 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;
903
1095
  }
904
1096
  mergedMiddlewares.push(...bundle.middlewares);
905
1097
  for (const [name, injector] of Object.entries(bundle.injectors)) {
@@ -912,6 +1104,36 @@ async function loadMergedMiddlewares(middlewarePaths) {
912
1104
  return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
913
1105
  }
914
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
+
915
1137
  // src/router/scanRoutes.ts
916
1138
  var HTTP_OR_WS_EXPORT_RE = new RegExp(
917
1139
  String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
@@ -927,48 +1149,40 @@ function extractExportsFromSource(source) {
927
1149
  return names;
928
1150
  }
929
1151
  function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
930
- const routeDir = path.dirname(routeFilePath);
931
- const resolvedRoot = path.resolve(rootDir);
1152
+ const routeDir = path2.dirname(routeFilePath);
1153
+ const resolvedRoot = path2.resolve(rootDir);
932
1154
  const paths = [];
933
- let currentDir = path.resolve(rootDir, routeDir);
1155
+ let currentDir = path2.resolve(rootDir, routeDir);
934
1156
  while (true) {
935
1157
  if (dist) {
936
- const mwTsPath = path.join(currentDir, "middlewares.ts");
937
- const mwJsPath = path.join(currentDir, "middlewares.js");
938
- const absTsPath = path.resolve(rootDir, mwTsPath);
939
- const absJsPath = path.resolve(rootDir, mwJsPath);
940
- 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;
941
1163
  if (absMwPath) {
942
- const relMwPath = path.relative(rootDir, absMwPath);
943
- const prodAbsPath = path.resolve(rootDir, toProdFilePath(relMwPath, dist));
1164
+ const relMwPath = path2.relative(rootDir, absMwPath);
1165
+ const prodAbsPath = path2.resolve(rootDir, toProdFilePath(relMwPath, dist));
944
1166
  paths.push(prodAbsPath);
945
1167
  }
946
1168
  } else {
947
1169
  for (const ext of [".ts", ".js"]) {
948
- const mwPath = path.join(currentDir, `middlewares${ext}`);
949
- const absMwPath = path.resolve(rootDir, mwPath);
950
- if (fs.existsSync(absMwPath)) {
1170
+ const mwPath = path2.join(currentDir, `middlewares${ext}`);
1171
+ const absMwPath = path2.resolve(rootDir, mwPath);
1172
+ if (fs2.existsSync(absMwPath)) {
951
1173
  paths.push(absMwPath);
952
1174
  break;
953
1175
  }
954
1176
  }
955
1177
  }
956
1178
  if (currentDir === resolvedRoot) break;
957
- const parentDir = path.dirname(currentDir);
1179
+ const parentDir = path2.dirname(currentDir);
958
1180
  if (parentDir === currentDir) break;
959
1181
  currentDir = parentDir;
960
1182
  }
961
1183
  paths.reverse();
962
1184
  return paths;
963
1185
  }
964
- function toProdFilePath(filePath, dist) {
965
- let rel = filePath.replace(/\\/g, "/");
966
- if (rel.startsWith("src/")) {
967
- rel = rel.slice(4);
968
- }
969
- const jsPath = rel.replace(/\.ts$/, ".js");
970
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
971
- }
972
1186
  async function scanRoutes(rootDir, patterns, dist) {
973
1187
  const files = await fg(patterns, {
974
1188
  cwd: rootDir,
@@ -981,7 +1195,7 @@ async function scanRoutes(rootDir, patterns, dist) {
981
1195
  const normalizedFile = file.replace(/\\/g, "/");
982
1196
  const fileName = normalizedFile.split("/").pop();
983
1197
  if (fileName === "handler.ts" || fileName === "handler.js") {
984
- const absPath = path.resolve(rootDir, normalizedFile);
1198
+ const absPath = path2.resolve(rootDir, normalizedFile);
985
1199
  const urlPath = filePathToUrlPath(normalizedFile);
986
1200
  const paramNames = extractParamNames(urlPath);
987
1201
  const isDynamic = paramNames.length > 0;
@@ -994,7 +1208,7 @@ async function scanRoutes(rootDir, patterns, dist) {
994
1208
  const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
995
1209
  middlewareBundle = await loadMergedMiddlewares(mwPaths);
996
1210
  }
997
- const source = await fs.promises.readFile(absPath, "utf8").catch(() => "");
1211
+ const source = await fs2.promises.readFile(absPath, "utf8").catch(() => "");
998
1212
  const exportNames = extractExportsFromSource(source);
999
1213
  const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
1000
1214
  for (const method of methods) {
@@ -1047,25 +1261,34 @@ function sortRoutes(routes) {
1047
1261
  }
1048
1262
 
1049
1263
  // src/cli/generateSchemaFiles.ts
1050
- import path4 from "path";
1051
- 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
+ }
1052
1275
 
1053
1276
  // src/ast/createProgram.ts
1054
1277
  import ts2 from "typescript";
1055
- import fs2 from "fs";
1056
- import path2 from "path";
1278
+ import fs4 from "fs";
1279
+ import path4 from "path";
1057
1280
  var programCache = /* @__PURE__ */ new Map();
1058
1281
  var tsConfigCache = /* @__PURE__ */ new Map();
1059
1282
  function findTsConfig(filePath) {
1060
- let dir = path2.dirname(filePath);
1061
- const root = path2.parse(dir).root;
1283
+ let dir = path4.dirname(filePath);
1284
+ const root = path4.parse(dir).root;
1062
1285
  while (true) {
1063
- const candidate = path2.join(dir, "tsconfig.json");
1064
- if (fs2.existsSync(candidate)) {
1286
+ const candidate = path4.join(dir, "tsconfig.json");
1287
+ if (fs4.existsSync(candidate)) {
1065
1288
  return candidate;
1066
1289
  }
1067
1290
  if (dir === root) return null;
1068
- const parent = path2.dirname(dir);
1291
+ const parent = path4.dirname(dir);
1069
1292
  if (parent === dir) return null;
1070
1293
  dir = parent;
1071
1294
  }
@@ -1075,13 +1298,13 @@ function parseTsConfig(tsconfigPath) {
1075
1298
  if (cached) return cached;
1076
1299
  const result = { fileNames: [] };
1077
1300
  try {
1078
- const configFile = ts2.readConfigFile(tsconfigPath, (p) => fs2.readFileSync(p, "utf-8"));
1301
+ const configFile = ts2.readConfigFile(tsconfigPath, (p) => fs4.readFileSync(p, "utf-8"));
1079
1302
  if (configFile.error) {
1080
1303
  tsConfigCache.set(tsconfigPath, result);
1081
1304
  return result;
1082
1305
  }
1083
1306
  const config = configFile.config ?? {};
1084
- const basePath = path2.dirname(tsconfigPath);
1307
+ const basePath = path4.dirname(tsconfigPath);
1085
1308
  const parsed = ts2.parseJsonConfigFileContent(
1086
1309
  config,
1087
1310
  ts2.sys,
@@ -1130,9 +1353,10 @@ function createPrograms(filePaths) {
1130
1353
  }
1131
1354
  }
1132
1355
  for (const { tsconfigPath, files } of groups.values()) {
1133
- const cacheKey = `shared::${tsconfigPath}::${[...files].sort().join("|")}`;
1356
+ const cacheKey = `shared::${tsconfigPath}`;
1134
1357
  let program = programCache.get(cacheKey);
1135
- if (!program) {
1358
+ const coversAll = program !== void 0 && files.every((f) => program.getSourceFile(f) !== void 0);
1359
+ if (!program || !coversAll) {
1136
1360
  program = buildProgram(files, tsconfigPath);
1137
1361
  programCache.set(cacheKey, program);
1138
1362
  }
@@ -1183,17 +1407,40 @@ var currentProgram = null;
1183
1407
  function setProgramContext(program) {
1184
1408
  currentProgram = program;
1185
1409
  }
1186
- var SchemaExtractionError = class extends Error {
1187
- constructor(typeText, reason, options) {
1188
- 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
+ );
1189
1416
  this.typeText = typeText;
1190
1417
  this.reason = reason;
1418
+ this.location = location;
1191
1419
  this.name = "SchemaExtractionError";
1192
1420
  }
1193
1421
  typeText;
1194
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
+ }
1195
1442
  };
1196
- function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1443
+ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1197
1444
  const kind = typeNode.kind;
1198
1445
  switch (kind) {
1199
1446
  case ts3.SyntaxKind.StringKeyword:
@@ -1249,13 +1496,13 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1249
1496
  if (ts3.isArrayTypeNode(typeNode)) {
1250
1497
  return {
1251
1498
  kind: "array",
1252
- element: resolveTypeNode(typeNode.elementType, checker, visited)
1499
+ element: resolveTypeNode(typeNode.elementType, checker, visited, bindings)
1253
1500
  };
1254
1501
  }
1255
1502
  if (ts3.isTupleTypeNode(typeNode)) {
1256
1503
  const elements = typeNode.elements.map((e) => {
1257
1504
  if (ts3.isRestTypeNode(e)) {
1258
- const inner = resolveTypeNode(e.type, checker, visited);
1505
+ const inner = resolveTypeNode(e.type, checker, visited, bindings);
1259
1506
  if (inner.kind === "array") {
1260
1507
  return { type: inner.element, optional: false, rest: true };
1261
1508
  }
@@ -1263,20 +1510,20 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1263
1510
  }
1264
1511
  if (ts3.isNamedTupleMember(e)) {
1265
1512
  return {
1266
- type: resolveTypeNode(e.type, checker, visited),
1513
+ type: resolveTypeNode(e.type, checker, visited, bindings),
1267
1514
  optional: !!e.questionToken,
1268
1515
  rest: false
1269
1516
  };
1270
1517
  }
1271
1518
  if (ts3.isOptionalTypeNode(e)) {
1272
1519
  return {
1273
- type: resolveTypeNode(e.type, checker, visited),
1520
+ type: resolveTypeNode(e.type, checker, visited, bindings),
1274
1521
  optional: true,
1275
1522
  rest: false
1276
1523
  };
1277
1524
  }
1278
1525
  return {
1279
- type: resolveTypeNode(e, checker, visited),
1526
+ type: resolveTypeNode(e, checker, visited, bindings),
1280
1527
  optional: false,
1281
1528
  rest: false
1282
1529
  };
@@ -1284,40 +1531,80 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
1284
1531
  return { kind: "tuple", elements };
1285
1532
  }
1286
1533
  if (ts3.isUnionTypeNode(typeNode)) {
1287
- const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
1534
+ const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited, bindings));
1288
1535
  return { kind: "union", members };
1289
1536
  }
1290
1537
  if (ts3.isIntersectionTypeNode(typeNode)) {
1291
- const properties = [];
1538
+ const propMap = /* @__PURE__ */ new Map();
1292
1539
  for (const t of typeNode.types) {
1293
- const resolved = resolveTypeNode(t, checker, visited);
1294
- if (resolved.kind === "object") {
1295
- 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
+ }
1296
1563
  }
1297
1564
  }
1298
- return { kind: "object", properties };
1565
+ return { kind: "object", properties: [...propMap.values()] };
1299
1566
  }
1300
1567
  if (ts3.isTypeLiteralNode(typeNode)) {
1301
- return resolveTypeLiteral(typeNode, checker, visited);
1568
+ return resolveTypeLiteral(typeNode, checker, visited, bindings);
1302
1569
  }
1303
1570
  if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.KeyOfKeyword) {
1304
1571
  return resolveKeyOf(typeNode, checker);
1305
1572
  }
1306
1573
  if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.ReadonlyKeyword) {
1307
- return resolveTypeNode(typeNode.type, checker, visited);
1574
+ return resolveTypeNode(typeNode.type, checker, visited, bindings);
1308
1575
  }
1309
1576
  if (ts3.isTypeReferenceNode(typeNode)) {
1310
- return resolveTypeReference(typeNode, checker, visited);
1577
+ return resolveTypeReference(typeNode, checker, visited, bindings);
1578
+ }
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
+ );
1311
1590
  }
1312
- throw new SchemaExtractionError(typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
1591
+ throw SchemaExtractionError.at(typeNode, typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
1313
1592
  }
1314
- function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1593
+ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1315
1594
  const properties = [];
1595
+ let catchall;
1316
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
+ }
1317
1604
  if (ts3.isPropertySignature(member) && member.name) {
1318
1605
  const name = member.name.getText();
1319
1606
  const optional = !!member.questionToken;
1320
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1607
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1321
1608
  const constraints = extractConstraintsFromJsDoc(member, name);
1322
1609
  validateConstraints(constraints, type, name);
1323
1610
  properties.push(
@@ -1325,12 +1612,10 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
1325
1612
  );
1326
1613
  }
1327
1614
  if (ts3.isIndexSignatureDeclaration(member)) {
1328
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1329
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1330
- return { kind: "record", key: keyType, value: valueType };
1615
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1331
1616
  }
1332
1617
  }
1333
- return { kind: "object", properties };
1618
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
1334
1619
  }
1335
1620
  function extractLiteralKeys(type) {
1336
1621
  if (type.kind === "literal" && typeof type.value === "string") {
@@ -1399,36 +1684,48 @@ function resolveKeyOf(typeNode, checker) {
1399
1684
  }
1400
1685
  throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u8054\u5408");
1401
1686
  }
1402
- function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1687
+ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
1403
1688
  const typeName = typeNode.typeName.getText();
1689
+ const bound = bindings.get(typeName);
1690
+ if (bound) {
1691
+ return bound;
1692
+ }
1404
1693
  if (typeName === "Date") {
1405
1694
  return { kind: "date" };
1406
1695
  }
1407
1696
  if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
1697
+ const [arg] = typeNode.typeArguments;
1408
1698
  return {
1409
1699
  kind: "array",
1410
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1700
+ element: resolveTypeNode(arg, checker, visited, bindings)
1411
1701
  };
1412
1702
  }
1413
1703
  if (typeName === "Record" && typeNode.typeArguments?.length === 2) {
1704
+ const [keyArg, valueArg] = typeNode.typeArguments;
1414
1705
  return {
1415
1706
  kind: "record",
1416
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1417
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1707
+ key: resolveTypeNode(keyArg, checker, visited, bindings),
1708
+ value: resolveTypeNode(valueArg, checker, visited, bindings)
1418
1709
  };
1419
1710
  }
1420
1711
  if ((typeName === "Partial" || typeName === "Required" || typeName === "Readonly") && typeNode.typeArguments?.length === 1) {
1421
- const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1712
+ const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
1422
1713
  if (inner.kind === "object" && typeName === "Partial") {
1423
1714
  return {
1424
1715
  kind: "object",
1425
1716
  properties: inner.properties.map((p) => ({ ...p, optional: true }))
1426
1717
  };
1427
1718
  }
1719
+ if (inner.kind === "object" && typeName === "Required") {
1720
+ return {
1721
+ kind: "object",
1722
+ properties: inner.properties.map((p) => ({ ...p, optional: false }))
1723
+ };
1724
+ }
1428
1725
  return inner;
1429
1726
  }
1430
1727
  if ((typeName === "Pick" || typeName === "Omit") && typeNode.typeArguments?.length === 2) {
1431
- const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1728
+ const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
1432
1729
  if (innerType.kind !== "object") {
1433
1730
  throw new SchemaExtractionError(
1434
1731
  typeNode.getText(),
@@ -1436,7 +1733,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1436
1733
  );
1437
1734
  }
1438
1735
  const keyTypeNode = typeNode.typeArguments[1];
1439
- let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited));
1736
+ let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited, bindings));
1440
1737
  if (keys === null) {
1441
1738
  keys = extractKeysFromChecker(keyTypeNode, checker);
1442
1739
  }
@@ -1454,10 +1751,11 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1454
1751
  "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
1455
1752
  );
1456
1753
  }
1754
+ const [mapKey, mapValue] = typeNode.typeArguments;
1457
1755
  return {
1458
1756
  kind: "map",
1459
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1460
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1757
+ key: resolveTypeNode(mapKey, checker, visited, bindings),
1758
+ value: resolveTypeNode(mapValue, checker, visited, bindings)
1461
1759
  };
1462
1760
  }
1463
1761
  if (typeName === "Set") {
@@ -1467,9 +1765,10 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1467
1765
  "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
1468
1766
  );
1469
1767
  }
1768
+ const [setArg] = typeNode.typeArguments;
1470
1769
  return {
1471
1770
  kind: "set",
1472
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1771
+ element: resolveTypeNode(setArg, checker, visited, bindings)
1473
1772
  };
1474
1773
  }
1475
1774
  if (typeName === "WeakMap" || typeName === "WeakSet") {
@@ -1492,21 +1791,42 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1492
1791
  }
1493
1792
  visited.add(typeName);
1494
1793
  if (checker) {
1495
- 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;
1496
1795
  if (symbol) {
1497
1796
  const declaration = symbol.declarations?.[0];
1498
1797
  if (declaration) {
1499
1798
  if (ts3.isInterfaceDeclaration(declaration)) {
1500
- return resolveInterfaceDeclaration(declaration, checker, visited);
1799
+ return resolveInterfaceDeclaration(
1800
+ declaration,
1801
+ checker,
1802
+ visited,
1803
+ bindings,
1804
+ typeNode.typeArguments
1805
+ );
1501
1806
  }
1502
1807
  if (ts3.isTypeAliasDeclaration(declaration)) {
1503
- 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);
1504
1817
  }
1505
1818
  if (ts3.isEnumDeclaration(declaration)) {
1506
1819
  return resolveEnumDeclaration(declaration);
1507
1820
  }
1508
1821
  if (ts3.isImportSpecifier(declaration) || ts3.isImportClause(declaration)) {
1509
- 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
+ );
1510
1830
  if (resolved) return resolved;
1511
1831
  }
1512
1832
  }
@@ -1514,17 +1834,45 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
1514
1834
  }
1515
1835
  throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
1516
1836
  }
1517
- 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) {
1518
1858
  const typeName = typeNode.typeName.getText();
1519
1859
  try {
1520
1860
  const aliased = checker.getAliasedSymbol(symbol);
1521
1861
  if (aliased && aliased.declarations && aliased.declarations.length > 0) {
1522
1862
  const decl = aliased.declarations[0];
1523
1863
  if (ts3.isInterfaceDeclaration(decl)) {
1524
- return resolveInterfaceDeclaration(decl, checker, visited);
1864
+ return resolveInterfaceDeclaration(decl, checker, visited, bindings, typeArguments);
1525
1865
  }
1526
1866
  if (ts3.isTypeAliasDeclaration(decl)) {
1527
- 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);
1528
1876
  }
1529
1877
  if (ts3.isEnumDeclaration(decl)) {
1530
1878
  return resolveEnumDeclaration(decl);
@@ -1542,10 +1890,18 @@ function resolveImportAlias(typeNode, symbol, checker, visited) {
1542
1890
  const found = findTopLevelDecl(sourceFile, typeName);
1543
1891
  if (found) {
1544
1892
  if (found.kind === "interface") {
1545
- return resolveInterfaceDeclaration(found.node, checker, visited);
1893
+ return resolveInterfaceDeclaration(found.node, checker, visited, bindings, typeArguments);
1546
1894
  }
1547
1895
  if (found.kind === "typeAlias") {
1548
- 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);
1549
1905
  }
1550
1906
  if (found.kind === "enum") {
1551
1907
  return resolveEnumDeclaration(found.node);
@@ -1592,13 +1948,22 @@ function resolveEnumDeclaration(node) {
1592
1948
  }
1593
1949
  return { kind: "union", members };
1594
1950
  }
1595
- function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
1951
+ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set(), outerBindings = /* @__PURE__ */ new Map(), typeArguments) {
1596
1952
  const properties = [];
1597
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
+ );
1598
1963
  for (const heritageClause of node.heritageClauses ?? []) {
1599
1964
  if (heritageClause.token === ts3.SyntaxKind.ExtendsKeyword) {
1600
1965
  for (const expr of heritageClause.types) {
1601
- const parentType = resolveTypeNode(expr, checker, visited);
1966
+ const parentType = resolveTypeNode(expr, checker, visited, bindings);
1602
1967
  if (parentType.kind === "object") {
1603
1968
  for (const prop of parentType.properties) {
1604
1969
  propMap.set(prop.name, prop);
@@ -1608,10 +1973,17 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
1608
1973
  }
1609
1974
  }
1610
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
+ }
1611
1983
  if (ts3.isPropertySignature(member) && member.name) {
1612
1984
  const name = member.name.getText();
1613
1985
  const optional = !!member.questionToken;
1614
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1986
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1615
1987
  const constraints = extractConstraintsFromJsDoc(member, name);
1616
1988
  validateConstraints(constraints, type, name);
1617
1989
  propMap.set(
@@ -1620,15 +1992,13 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
1620
1992
  );
1621
1993
  }
1622
1994
  if (ts3.isIndexSignatureDeclaration(member)) {
1623
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1624
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1625
- return { kind: "record", key: keyType, value: valueType };
1995
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
1626
1996
  }
1627
1997
  }
1628
1998
  for (const prop of propMap.values()) {
1629
1999
  properties.push(prop);
1630
2000
  }
1631
- return { kind: "object", properties };
2001
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
1632
2002
  }
1633
2003
  var NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
1634
2004
  "max",
@@ -1808,55 +2178,47 @@ function extractTypeInfo(program, filePath, typeName) {
1808
2178
  return;
1809
2179
  }
1810
2180
  });
1811
- return result;
1812
- } finally {
1813
- setProgramContext(null);
1814
- }
1815
- }
1816
- function extractAllTypes(program, filePath) {
1817
- const sourceFile = program.getSourceFile(filePath);
1818
- if (!sourceFile) return /* @__PURE__ */ new Map();
1819
- const checker = program.getTypeChecker();
1820
- setProgramContext(program);
1821
- try {
1822
- const result = /* @__PURE__ */ new Map();
1823
- ts4.forEachChild(sourceFile, (node) => {
1824
- if (ts4.isInterfaceDeclaration(node)) {
1825
- const visited = /* @__PURE__ */ new Set();
1826
- visited.add(node.name.text);
1827
- const runtimeType = withFileContext(
1828
- filePath,
1829
- node.name.text,
1830
- () => resolveInterfaceDeclaration(node, checker, visited)
1831
- );
1832
- result.set(node.name.text, {
1833
- name: node.name.text,
1834
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1835
- runtimeType
1836
- });
1837
- return;
1838
- }
1839
- if (ts4.isTypeAliasDeclaration(node)) {
1840
- const visited = /* @__PURE__ */ new Set();
1841
- visited.add(node.name.text);
1842
- const runtimeType = withFileContext(
1843
- filePath,
1844
- node.name.text,
1845
- () => resolveTypeNode(node.type, checker, visited)
1846
- );
1847
- result.set(node.name.text, {
1848
- name: node.name.text,
1849
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1850
- runtimeType
1851
- });
1852
- 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;
1853
2186
  }
1854
- });
1855
- 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;
1856
2207
  } finally {
1857
2208
  setProgramContext(null);
1858
2209
  }
1859
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
+ }
1860
2222
  function withFileContext(filePath, typeName, fn) {
1861
2223
  try {
1862
2224
  return fn();
@@ -1866,7 +2228,8 @@ function withFileContext(filePath, typeName, fn) {
1866
2228
  const enriched = new SchemaExtractionError(
1867
2229
  err.typeText,
1868
2230
  `${err.reason}\uFF08\u6587\u4EF6: ${fileName}, \u7C7B\u578B: ${typeName}\uFF09`,
1869
- { cause: err }
2231
+ { cause: err },
2232
+ err.location
1870
2233
  );
1871
2234
  throw enriched;
1872
2235
  }
@@ -1894,8 +2257,7 @@ function getSchemaName(method, inputType) {
1894
2257
 
1895
2258
  // src/injection/analyzeInjection.ts
1896
2259
  import ts5 from "typescript";
1897
- function analyzeInjection(code, functionName) {
1898
- const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
2260
+ function analyzeInjectionInSourceFile(sourceFile, functionName) {
1899
2261
  const params = [];
1900
2262
  ts5.forEachChild(sourceFile, (node) => {
1901
2263
  if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
@@ -1938,11 +2300,11 @@ function extractSchema(typeNode, sourceFile) {
1938
2300
  }
1939
2301
 
1940
2302
  // src/cli/collectRouteSchemaSources.ts
1941
- import path3 from "path";
2303
+ import path5 from "path";
1942
2304
  function collectRouteSchemaSources(routes, rootDir) {
1943
2305
  const methodsByFile = /* @__PURE__ */ new Map();
1944
2306
  for (const route of routes) {
1945
- const filePath = rootDir ? path3.resolve(rootDir, route.filePath) : route.filePath;
2307
+ const filePath = rootDir ? path5.resolve(rootDir, route.filePath) : route.filePath;
1946
2308
  let entry = methodsByFile.get(filePath);
1947
2309
  if (!entry) {
1948
2310
  entry = { urlPath: route.urlPath, methods: /* @__PURE__ */ new Set() };
@@ -1951,28 +2313,23 @@ function collectRouteSchemaSources(routes, rootDir) {
1951
2313
  entry.methods.add(route.method);
1952
2314
  }
1953
2315
  const programByFile = createPrograms([...methodsByFile.keys()]);
1954
- const allTypesByFile = /* @__PURE__ */ new Map();
1955
- const mergedAllTypes = /* @__PURE__ */ new Map();
2316
+ const resolversByFile = /* @__PURE__ */ new Map();
1956
2317
  for (const filePath of methodsByFile.keys()) {
1957
- const program = programByFile.get(filePath);
1958
- const allTypes = extractAllTypes(program, filePath);
1959
- allTypesByFile.set(filePath, allTypes);
1960
- for (const [name, info] of allTypes) {
1961
- mergedAllTypes.set(name, info);
1962
- }
2318
+ resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
1963
2319
  }
1964
2320
  const sources = [];
1965
2321
  for (const [filePath, entry] of methodsByFile) {
1966
2322
  const program = programByFile.get(filePath);
1967
2323
  const sourceFile = program.getSourceFile(filePath);
1968
- const code = sourceFile?.text ?? "";
2324
+ if (!sourceFile) continue;
2325
+ const resolver = resolversByFile.get(filePath);
1969
2326
  for (const method of entry.methods) {
1970
2327
  const inputType = getInputTypeForMethod(method);
1971
2328
  const schemaName = getSchemaName(method, inputType);
1972
- const meta = analyzeInjection(code, method);
2329
+ const meta = analyzeInjectionInSourceFile(sourceFile, method);
1973
2330
  const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1974
2331
  const isForm = param?.type === "form";
1975
- const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
2332
+ const typeInfo = param?.typeName ? resolver.resolve(param.typeName) : null;
1976
2333
  sources.push({
1977
2334
  urlPath: entry.urlPath,
1978
2335
  filePath,
@@ -1982,7 +2339,7 @@ function collectRouteSchemaSources(routes, rootDir) {
1982
2339
  });
1983
2340
  }
1984
2341
  }
1985
- return { sources, allTypesByFile, mergedAllTypes };
2342
+ return { sources, resolversByFile };
1986
2343
  }
1987
2344
 
1988
2345
  // src/ast/generateZodSchema.ts
@@ -2055,8 +2412,12 @@ function collectNamedTypes(type, ctx) {
2055
2412
  if (resolved) {
2056
2413
  ctx.namedTypes.set(type.name, resolved);
2057
2414
  collectNamedTypes(resolved, ctx);
2415
+ return;
2058
2416
  }
2059
- 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
+ );
2060
2421
  }
2061
2422
  }
2062
2423
  }
@@ -2066,6 +2427,12 @@ function runtimeTypeToZodExpression(type, ctx, constraints) {
2066
2427
  if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
2067
2428
  return wrapCoercePreprocess(type.kind, withConstraints);
2068
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
+ }
2069
2436
  return withConstraints;
2070
2437
  }
2071
2438
  function applyConstraints(baseExpr, constraints, typeKind) {
@@ -2130,7 +2497,7 @@ function baseExpression(type, ctx) {
2130
2497
  case "tuple":
2131
2498
  return generateTupleExpression(type.elements, ctx);
2132
2499
  case "object":
2133
- return generateObjectExpression(type.properties, ctx);
2500
+ return generateObjectExpression(type, ctx);
2134
2501
  case "union":
2135
2502
  return generateUnionExpression(type.members, ctx);
2136
2503
  case "date":
@@ -2149,7 +2516,7 @@ function baseExpression(type, ctx) {
2149
2516
  }
2150
2517
  }
2151
2518
  var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2152
- 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};';
2153
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);';
2154
2521
  var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2155
2522
  var HELPERS_FILENAME = "faapi-helpers.js";
@@ -2187,7 +2554,10 @@ function generateTupleExpression(elements, ctx) {
2187
2554
  }
2188
2555
  }
2189
2556
  if (restExpression) {
2190
- 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})`;
2191
2561
  }
2192
2562
  const hasOptional = fixedOptional.some((o) => o);
2193
2563
  if (!hasOptional) {
@@ -2208,13 +2578,14 @@ function generateTupleExpression(elements, ctx) {
2208
2578
  }
2209
2579
  return `z.union([${variants.join(", ")}])`;
2210
2580
  }
2211
- function generateObjectExpression(properties, ctx) {
2212
- const fields = properties.map((prop) => {
2581
+ function generateObjectExpression(type, ctx) {
2582
+ const fields = type.properties.map((prop) => {
2213
2583
  const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
2214
2584
  const finalExpr = prop.optional ? `${expr}.optional()` : expr;
2215
2585
  return `${JSON.stringify(prop.name)}: ${finalExpr}`;
2216
2586
  });
2217
- 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;
2218
2589
  }
2219
2590
  function generateUnionExpression(members, ctx) {
2220
2591
  const hasNull = members.some((m) => m.kind === "null");
@@ -2259,7 +2630,7 @@ function containsRef(type, visited) {
2259
2630
  return false;
2260
2631
  }
2261
2632
  }
2262
- function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
2633
+ function generateZodSchemaSourceParts(typeInfo, resolveType, exportName, coerce = false) {
2263
2634
  const ctx = new CodeGenContext(resolveType);
2264
2635
  const name = exportName ?? typeInfo.name;
2265
2636
  ctx.entryTypeName = typeInfo.name;
@@ -2267,21 +2638,14 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
2267
2638
  ctx.coerce = coerce;
2268
2639
  collectNamedTypes(typeInfo.runtimeType, ctx);
2269
2640
  ctx.namedTypes.delete(typeInfo.name);
2270
- const lines = [];
2271
- lines.push("import { z } from 'zod';");
2272
- lines.push("");
2273
- for (const [n, type] of ctx.namedTypes) {
2274
- lines.push(generateNamedTypeDeclaration(n, type, ctx));
2275
- }
2276
- 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
+ }));
2277
2645
  const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
2278
2646
  const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
2279
- if (hasSelfRef) {
2280
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
2281
- } else {
2282
- lines.push(`export const ${name}Schema = ${entryExpr};`);
2283
- }
2284
- return lines.join("\n");
2647
+ const entryDeclaration = hasSelfRef ? `export const ${name}Schema = z.lazy(() => ${entryExpr});` : `export const ${name}Schema = ${entryExpr};`;
2648
+ return { namedTypeDeclarations, entryDeclaration };
2285
2649
  }
2286
2650
 
2287
2651
  // src/cli/generateSchemaFiles.ts
@@ -2292,7 +2656,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
2292
2656
  }
2293
2657
  const idx = rel.lastIndexOf("/");
2294
2658
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2295
- return path4.resolve(rootDir, dist, relDir, "zod.js");
2659
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
2296
2660
  }
2297
2661
  function getRuntimeSchemaPath(filePath, dist, rootDir) {
2298
2662
  let rel = filePath.replace(/\\/g, "/");
@@ -2303,16 +2667,17 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
2303
2667
  }
2304
2668
  const idx = rel.lastIndexOf("/");
2305
2669
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2306
- return path4.resolve(rootDir, dist, relDir, "zod.js");
2670
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
2307
2671
  }
2308
2672
  function getHelpersImportPath(relDir) {
2309
2673
  if (!relDir) return `./${HELPERS_FILENAME}`;
2310
2674
  const depth = relDir.split("/").filter(Boolean).length;
2311
2675
  return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
2312
2676
  }
2313
- function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2314
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
2677
+ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
2315
2678
  const lines = ["import { z } from 'zod';"];
2679
+ const namedTypeDeclarations = [];
2680
+ const seenNamedTypes = /* @__PURE__ */ new Set();
2316
2681
  const schemaBlocks = [];
2317
2682
  for (const source of sources) {
2318
2683
  const { schemaName, typeInfo } = source;
@@ -2320,16 +2685,24 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2320
2685
  continue;
2321
2686
  }
2322
2687
  const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2323
- const block = [`// ${schemaName}`];
2324
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2325
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
2326
- ""
2688
+ const { namedTypeDeclarations: decls, entryDeclaration } = generateZodSchemaSourceParts(
2689
+ typeInfo,
2690
+ resolveType,
2691
+ schemaName,
2692
+ coerce
2327
2693
  );
2328
- block.push(schemaCode);
2329
- block.push("");
2330
- 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"));
2331
2700
  }
2332
- const allSchemaCode = schemaBlocks.join("\n");
2701
+ if (namedTypeDeclarations.length > 0) {
2702
+ lines.push(...namedTypeDeclarations);
2703
+ lines.push("");
2704
+ }
2705
+ const allSchemaCode = [...namedTypeDeclarations, ...schemaBlocks].join("\n");
2333
2706
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2334
2707
  lines.push(
2335
2708
  `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
@@ -2341,7 +2714,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2341
2714
  }
2342
2715
  async function generateSchemaFiles(routes, rootDir, dist) {
2343
2716
  if (routes.length === 0) return;
2344
- const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2717
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
2345
2718
  const sourcesByFile = /* @__PURE__ */ new Map();
2346
2719
  for (const source of sources) {
2347
2720
  let list = sourcesByFile.get(source.filePath);
@@ -2353,9 +2726,8 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2353
2726
  }
2354
2727
  const fileEntries = [];
2355
2728
  for (const [filePath, fileSources] of sourcesByFile) {
2356
- const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
2729
+ const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
2357
2730
  const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2358
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2359
2731
  let relForDir = relFile;
2360
2732
  if (relForDir.startsWith("src/")) {
2361
2733
  relForDir = relForDir.slice(4);
@@ -2363,12 +2735,17 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2363
2735
  const dirIdx = relForDir.lastIndexOf("/");
2364
2736
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2365
2737
  const helpersImportPath = getHelpersImportPath(zodRelDir);
2366
- 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
+ );
2367
2744
  fileEntries.push({ outputPath, source });
2368
2745
  }
2369
2746
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2370
2747
  if (usesCoerceHelpers(allSourceCode)) {
2371
- const helpersPath = path4.resolve(rootDir, dist, HELPERS_FILENAME);
2748
+ const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
2372
2749
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
2373
2750
  }
2374
2751
  await Promise.all(
@@ -2376,22 +2753,21 @@ async function generateSchemaFiles(routes, rootDir, dist) {
2376
2753
  );
2377
2754
  }
2378
2755
  async function writeSchemaFile(outputPath, source) {
2379
- await fs3.mkdir(path4.dirname(outputPath), { recursive: true });
2380
- await fs3.writeFile(outputPath, source, "utf-8");
2756
+ await atomicWriteFile(outputPath, source);
2381
2757
  }
2382
2758
 
2383
2759
  // src/cli/compileOnDemand.ts
2384
- import path8 from "path";
2385
- import fs7 from "fs";
2760
+ import path11 from "path";
2761
+ import fs9 from "fs";
2386
2762
 
2387
- // src/cli/compileDevRoutes.ts
2388
- import path7 from "path";
2389
- import fs6 from "fs";
2763
+ // src/cli/compileSourceFiles.ts
2764
+ import path9 from "path";
2765
+ import fs7 from "fs";
2390
2766
  import fg2 from "fast-glob";
2391
2767
 
2392
2768
  // src/cli/aliasPlugin.ts
2393
- import path6 from "path";
2394
- import fs5 from "fs";
2769
+ import path8 from "path";
2770
+ import fs6 from "fs";
2395
2771
 
2396
2772
  // src/utils/resolveAlias.ts
2397
2773
  function resolveAlias(specifier, config) {
@@ -2418,55 +2794,51 @@ function resolveAlias(specifier, config) {
2418
2794
 
2419
2795
  // src/utils/readTsconfig.ts
2420
2796
  import ts6 from "typescript";
2421
- import path5 from "path";
2422
- import fs4 from "fs";
2797
+ import path7 from "path";
2798
+ import fs5 from "fs";
2799
+ var tsconfigCache = /* @__PURE__ */ new Map();
2423
2800
  function readTsconfig(rootDir) {
2424
- const tsconfigPath = path5.resolve(rootDir, "tsconfig.json");
2425
- 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
+ }
2426
2813
  const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2427
2814
  if (configFile.error || !configFile.config) return null;
2428
2815
  const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2429
2816
  const baseUrl = parsed.options.baseUrl ?? rootDir;
2430
2817
  const rawPaths = parsed.options.paths;
2431
- if (!rawPaths) return null;
2432
- const paths = {};
2433
- for (const [pattern, targets] of Object.entries(rawPaths)) {
2434
- paths[pattern] = targets.map((t) => path5.resolve(baseUrl, t));
2435
- }
2436
- 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;
2437
2827
  }
2438
2828
 
2439
2829
  // src/cli/aliasPlugin.ts
2440
- function toProdExtension(filePath) {
2441
- if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2442
- if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2443
- if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2444
- return filePath;
2445
- }
2446
2830
  function toProdImportPath(sourceFile, importer) {
2447
- const importerDir = path6.dirname(importer);
2448
- let rel = path6.relative(importerDir, sourceFile);
2449
- 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("/");
2450
2834
  if (!rel.startsWith(".")) rel = "./" + rel;
2451
2835
  return toProdExtension(rel);
2452
2836
  }
2453
- function toRealPath(p) {
2454
- try {
2455
- return fs5.realpathSync(p);
2456
- } catch {
2457
- return p;
2458
- }
2459
- }
2460
- function isInsideDir(filePath, dir) {
2461
- const rel = path6.relative(dir, filePath);
2462
- return rel !== "" && !rel.startsWith("..") && !path6.isAbsolute(rel);
2463
- }
2464
- var APP_DIR = "src";
2465
2837
  function toStrippedProdImportPath(sourceFile, rootDir) {
2466
- const appDirAbs = toRealPath(path6.resolve(rootDir, APP_DIR));
2838
+ const appDirAbs = toRealPath(path8.resolve(rootDir, APP_DIR));
2467
2839
  const sourceReal = toRealPath(sourceFile);
2468
- let rel = path6.relative(appDirAbs, sourceReal);
2469
- rel = rel.split(path6.sep).join("/");
2840
+ let rel = path8.relative(appDirAbs, sourceReal);
2841
+ rel = rel.split(path8.sep).join("/");
2470
2842
  if (!rel.startsWith(".")) rel = "./" + rel;
2471
2843
  return toProdExtension(rel);
2472
2844
  }
@@ -2481,41 +2853,41 @@ var INDEX_EXTS = [
2481
2853
  "/index.cjs"
2482
2854
  ];
2483
2855
  function resolveRelativeSpecifier(importer, specifier) {
2484
- const importerDir = path6.dirname(importer);
2485
- const base = path6.resolve(importerDir, specifier);
2856
+ const importerDir = path8.dirname(importer);
2857
+ const base = path8.resolve(importerDir, specifier);
2486
2858
  if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2487
- return fs5.existsSync(base) ? base : null;
2859
+ return fs6.existsSync(base) ? base : null;
2488
2860
  }
2489
2861
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2490
- return fs5.existsSync(base) ? base : null;
2862
+ return fs6.existsSync(base) ? base : null;
2491
2863
  }
2492
2864
  for (const ext of SOURCE_EXTS) {
2493
2865
  const file = base + ext;
2494
- if (fs5.existsSync(file)) return file;
2866
+ if (fs6.existsSync(file)) return file;
2495
2867
  }
2496
2868
  for (const indexExt of INDEX_EXTS) {
2497
2869
  const file = base + indexExt;
2498
- if (fs5.existsSync(file)) return file;
2870
+ if (fs6.existsSync(file)) return file;
2499
2871
  }
2500
2872
  return null;
2501
2873
  }
2502
2874
  function createAliasPlugin(config, options) {
2503
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2504
- 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;
2505
2877
  return {
2506
2878
  name: "faapi-alias",
2507
2879
  setup(build) {
2508
2880
  build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2509
2881
  let source;
2510
2882
  try {
2511
- source = fs5.readFileSync(args.path, "utf8");
2883
+ source = fs6.readFileSync(args.path, "utf8");
2512
2884
  } catch {
2513
2885
  return void 0;
2514
2886
  }
2515
2887
  const importer = args.path;
2516
2888
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2517
2889
  let modified = false;
2518
- const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2890
+ const newSource = source.replace(SPEC_RE2, (full, prefix, quote, specifier) => {
2519
2891
  if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2520
2892
  return full;
2521
2893
  }
@@ -2541,7 +2913,7 @@ function createAliasPlugin(config, options) {
2541
2913
  for (const candidate of candidates) {
2542
2914
  for (const ext of SOURCE_EXTS) {
2543
2915
  const file = candidate + ext;
2544
- if (fs5.existsSync(file)) {
2916
+ if (fs6.existsSync(file)) {
2545
2917
  modified = true;
2546
2918
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2547
2919
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2554,7 +2926,7 @@ function createAliasPlugin(config, options) {
2554
2926
  }
2555
2927
  for (const indexExt of INDEX_EXTS) {
2556
2928
  const file = candidate + indexExt;
2557
- if (fs5.existsSync(file)) {
2929
+ if (fs6.existsSync(file)) {
2558
2930
  modified = true;
2559
2931
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2560
2932
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -2579,11 +2951,10 @@ function buildAliasPlugins(rootDir) {
2579
2951
  return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2580
2952
  }
2581
2953
 
2582
- // src/cli/compileDevRoutes.ts
2583
- var APP_DIR2 = "src";
2584
- async function compileDevRoutes(options) {
2585
- const { rootDir, dist, files, logLevel = "silent" } = options;
2586
- 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`], {
2587
2958
  cwd: rootDir,
2588
2959
  onlyFiles: true,
2589
2960
  absolute: true,
@@ -2592,11 +2963,11 @@ async function compileDevRoutes(options) {
2592
2963
  if (entryPoints.length === 0) {
2593
2964
  return { compiledFiles: [] };
2594
2965
  }
2595
- const absDist = path7.resolve(rootDir, dist);
2596
- await fs6.promises.mkdir(absDist, { recursive: true });
2966
+ const absDist = path9.resolve(rootDir, dist);
2967
+ await fs7.promises.mkdir(absDist, { recursive: true });
2597
2968
  const plugins = buildAliasPlugins(rootDir);
2598
2969
  const esbuild = await import("esbuild");
2599
- const outbase = path7.resolve(rootDir, APP_DIR2);
2970
+ const outbase = path9.resolve(rootDir, APP_DIR);
2600
2971
  const result = await esbuild.build({
2601
2972
  entryPoints,
2602
2973
  outdir: absDist,
@@ -2607,27 +2978,102 @@ async function compileDevRoutes(options) {
2607
2978
  sourcemap: true,
2608
2979
  packages: "external",
2609
2980
  plugins,
2981
+ // build 语义:编译期 NODE_ENV 替换 + 死分支删除(见 AGENTS.md §5.3)
2982
+ ...production ? { define: { "process.env.NODE_ENV": '"production"' }, minifySyntax: true } : {},
2610
2983
  logLevel,
2611
- write: false
2984
+ // dev 语义:esbuild 返回内存内容,由下方原子写落盘
2985
+ ...atomicWrite ? { write: false } : {}
2612
2986
  });
2613
- if (result.outputFiles) {
2987
+ if (atomicWrite && result.outputFiles) {
2614
2988
  await Promise.all(
2615
2989
  result.outputFiles.map(async (file) => {
2616
- await fs6.promises.mkdir(path7.dirname(file.path), { recursive: true });
2990
+ await fs7.promises.mkdir(path9.dirname(file.path), { recursive: true });
2617
2991
  const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2618
- await fs6.promises.writeFile(tmp, file.contents);
2619
- await fs6.promises.rename(tmp, file.path);
2992
+ await fs7.promises.writeFile(tmp, file.contents);
2993
+ await fs7.promises.rename(tmp, file.path);
2620
2994
  })
2621
2995
  );
2622
2996
  }
2623
2997
  return { compiledFiles: entryPoints };
2624
2998
  }
2625
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
+
2626
3072
  // src/cli/compileOnDemand.ts
2627
3073
  function isProductFresh(sourceAbsPath, productAbsPath) {
2628
3074
  try {
2629
- const srcStat = fs7.statSync(sourceAbsPath);
2630
- const prodStat = fs7.statSync(productAbsPath);
3075
+ const srcStat = fs9.statSync(sourceAbsPath);
3076
+ const prodStat = fs9.statSync(productAbsPath);
2631
3077
  return prodStat.mtimeMs >= srcStat.mtimeMs;
2632
3078
  } catch {
2633
3079
  return false;
@@ -2645,16 +3091,16 @@ function createDevOnDemandState() {
2645
3091
  }
2646
3092
  var state = createDevOnDemandState();
2647
3093
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2648
- const inFlight = state.inFlightCompilations.get(sourceAbsPath);
2649
- if (inFlight) {
2650
- await inFlight.catch(() => {
3094
+ const inFlight2 = state.inFlightCompilations.get(sourceAbsPath);
3095
+ if (inFlight2) {
3096
+ await inFlight2.catch(() => {
2651
3097
  });
2652
3098
  return false;
2653
3099
  }
2654
3100
  if (state.compiledFiles.has(sourceAbsPath)) {
2655
3101
  return false;
2656
3102
  }
2657
- if (!fs7.existsSync(sourceAbsPath)) {
3103
+ if (!fs9.existsSync(sourceAbsPath)) {
2658
3104
  return false;
2659
3105
  }
2660
3106
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
@@ -2663,12 +3109,23 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2663
3109
  return false;
2664
3110
  }
2665
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
+ }
2666
3120
  await compileDevRoutes({
2667
3121
  rootDir,
2668
3122
  dist,
2669
- files: [sourceAbsPath],
3123
+ files,
2670
3124
  logLevel: "silent"
2671
3125
  });
3126
+ for (const file of files) {
3127
+ state.compiledFiles.add(file);
3128
+ }
2672
3129
  state.compiledFiles.add(sourceAbsPath);
2673
3130
  })();
2674
3131
  state.inFlightCompilations.set(sourceAbsPath, compilePromise);
@@ -2679,26 +3136,39 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2679
3136
  state.inFlightCompilations.delete(sourceAbsPath);
2680
3137
  }
2681
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
+ }
2682
3152
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2683
- const rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3153
+ const rel = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2684
3154
  if (!rel.startsWith("src/")) return null;
2685
3155
  const relWithoutSrc = rel.slice(4);
2686
3156
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2687
- return path8.resolve(rootDir, dist, jsRel);
3157
+ return path11.resolve(rootDir, dist, jsRel);
2688
3158
  }
2689
3159
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2690
- const inFlight = state.inFlightSchemaGenerations.get(schemaPath);
2691
- if (inFlight) {
2692
- await inFlight.catch(() => {
3160
+ const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3161
+ if (inFlight2) {
3162
+ await inFlight2.catch(() => {
2693
3163
  });
2694
3164
  return false;
2695
3165
  }
2696
3166
  if (state.generatedSchemas.has(schemaPath)) {
2697
3167
  return false;
2698
3168
  }
2699
- const prodAbsPath = path8.resolve(rootDir, routeFilePath);
3169
+ const prodAbsPath = path11.resolve(rootDir, routeFilePath);
2700
3170
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2701
- if (!fs7.existsSync(sourceAbsPath)) {
3171
+ if (!fs9.existsSync(sourceAbsPath)) {
2702
3172
  return false;
2703
3173
  }
2704
3174
  if (isProductFresh(sourceAbsPath, schemaPath)) {
@@ -2709,7 +3179,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2709
3179
  if (fileRoutes.length === 0) {
2710
3180
  return false;
2711
3181
  }
2712
- const sourceRelPath = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3182
+ const sourceRelPath = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2713
3183
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2714
3184
  const generatePromise = (async () => {
2715
3185
  await generateSchemaFiles(sourceRoutes, rootDir, dist);
@@ -2727,19 +3197,19 @@ var sourcePathCache = /* @__PURE__ */ new Map();
2727
3197
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2728
3198
  const cached = sourcePathCache.get(prodAbsPath);
2729
3199
  if (cached) return cached;
2730
- const rel = path8.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
3200
+ const rel = path11.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2731
3201
  let relWithoutDist = rel;
2732
3202
  if (relWithoutDist.startsWith(`${dist}/`)) {
2733
3203
  relWithoutDist = relWithoutDist.slice(dist.length + 1);
2734
3204
  }
2735
3205
  const srcRel = `src/${relWithoutDist}`;
2736
3206
  const tsRel = srcRel.replace(/\.js$/, ".ts");
2737
- const tsAbs = path8.resolve(rootDir, tsRel);
3207
+ const tsAbs = path11.resolve(rootDir, tsRel);
2738
3208
  let result;
2739
- if (fs7.existsSync(tsAbs)) {
3209
+ if (fs9.existsSync(tsAbs)) {
2740
3210
  result = tsAbs;
2741
3211
  } else {
2742
- result = path8.resolve(rootDir, srcRel);
3212
+ result = path11.resolve(rootDir, srcRel);
2743
3213
  }
2744
3214
  sourcePathCache.set(prodAbsPath, result);
2745
3215
  return result;
@@ -2776,28 +3246,25 @@ async function validateInput(schemaPath, method, inputType, input) {
2776
3246
  }
2777
3247
  const schema = mod[schemaKey];
2778
3248
  if (schema === void 0 || schema === null) {
2779
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2780
- return { valid: true, issues: [], data };
3249
+ return { valid: true, issues: [], data: input };
2781
3250
  }
2782
3251
  if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2783
3252
  throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2784
3253
  }
2785
- const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2786
3254
  const zodSchema = schema;
2787
- const result = zodSchema.safeParse(inputObj);
3255
+ const result = zodSchema.safeParse(input);
2788
3256
  if (result.success) {
2789
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2790
- return { valid: true, issues: [], data };
3257
+ return { valid: true, issues: [], data: result.data };
2791
3258
  }
2792
3259
  const issues = mapZodIssues(result.error);
2793
- return { valid: false, issues, data: inputObj };
3260
+ return { valid: false, issues, data: input };
2794
3261
  }
2795
3262
  function mapZodIssues(error) {
2796
3263
  return error.issues.map((issue) => {
2797
- const code = mapZodCode(issue.code, issue.message);
2798
- const path12 = issue.path.map(String).join(".") || "";
3264
+ const code = mapZodCode(issue);
3265
+ const path15 = issue.path.map(String).join(".") || "";
2799
3266
  return {
2800
- path: path12,
3267
+ path: path15,
2801
3268
  code,
2802
3269
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2803
3270
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -2805,27 +3272,29 @@ function mapZodIssues(error) {
2805
3272
  };
2806
3273
  });
2807
3274
  }
2808
- function mapZodCode(zodCode, message) {
2809
- switch (zodCode) {
3275
+ function mapZodCode(issue) {
3276
+ switch (issue.code) {
2810
3277
  case "invalid_type":
3278
+ if (issue.received === "undefined" || /received undefined/i.test(issue.message)) {
3279
+ return "MISSING_FIELD";
3280
+ }
3281
+ return "TYPE_MISMATCH";
2811
3282
  case "invalid_union":
2812
3283
  case "invalid_union_discriminator":
2813
3284
  return "TYPE_MISMATCH";
2814
3285
  case "unrecognized_keys":
2815
3286
  return "INVALID_FORMAT";
2816
3287
  case "invalid_value":
2817
- case "invalid_string":
3288
+ case "invalid_format":
3289
+ case "invalid_key":
3290
+ case "invalid_element":
2818
3291
  case "too_small":
2819
3292
  case "too_big":
2820
3293
  case "invalid_intersection_types":
2821
3294
  case "not_multiple_of":
2822
- return "INVALID_VALUE";
2823
3295
  case "custom":
2824
3296
  return "INVALID_VALUE";
2825
3297
  default:
2826
- if (message.includes("Required") || message.includes("required")) {
2827
- return "MISSING_FIELD";
2828
- }
2829
3298
  return "INVALID_VALUE";
2830
3299
  }
2831
3300
  }
@@ -2845,7 +3314,7 @@ import {
2845
3314
  import { createSecureServer as createHttp2SecureServer } from "http2";
2846
3315
  import { readFileSync } from "fs";
2847
3316
  import { Readable as Readable2 } from "stream";
2848
- import path10 from "path";
3317
+ import path13 from "path";
2849
3318
 
2850
3319
  // src/router/matchRoute.ts
2851
3320
  var httpIndexCache = /* @__PURE__ */ new WeakMap();
@@ -2856,7 +3325,10 @@ function getHttpIndex(routes) {
2856
3325
  index = { static: /* @__PURE__ */ new Map(), methodsByStaticPath: /* @__PURE__ */ new Map(), dynamics: [] };
2857
3326
  for (const route of routes) {
2858
3327
  if (route.isDynamic) {
2859
- index.dynamics.push(route);
3328
+ index.dynamics.push({
3329
+ route,
3330
+ segments: route.urlPath.split("/").filter(Boolean)
3331
+ });
2860
3332
  } else {
2861
3333
  index.static.set(`${route.method}|${route.urlPath}`, route);
2862
3334
  let methods = index.methodsByStaticPath.get(route.urlPath);
@@ -2884,57 +3356,77 @@ function getWsIndex(routes) {
2884
3356
  wsIndexCache.set(routes, index);
2885
3357
  return index;
2886
3358
  }
2887
- function matchRoute(routes, method, path12) {
3359
+ function matchRoute(routes, method, path15) {
2888
3360
  const index = getHttpIndex(routes);
2889
- const staticHit = index.static.get(`${method}|${path12}`);
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}`);
2890
3371
  if (staticHit) {
2891
3372
  return { route: staticHit, params: {} };
2892
3373
  }
2893
- for (const route of index.dynamics) {
3374
+ for (const entry of index.dynamics) {
3375
+ const route = entry.route;
2894
3376
  if (route.method !== method) {
2895
3377
  continue;
2896
3378
  }
2897
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
3379
+ const params = matchSegments(entry.segments, path15, route.paramNames, route.isCatchAll);
2898
3380
  if (params !== null) {
2899
3381
  return { route, params };
2900
3382
  }
2901
3383
  }
2902
3384
  return null;
2903
3385
  }
2904
- function matchWsRoute(wsRoutes, path12) {
3386
+ function matchWsRoute(wsRoutes, path15) {
2905
3387
  const index = getWsIndex(wsRoutes);
2906
- const staticHit = index.static.get(path12);
3388
+ const staticHit = index.static.get(path15);
2907
3389
  if (staticHit) {
2908
3390
  return { route: staticHit, params: {} };
2909
3391
  }
2910
3392
  for (const route of index.dynamics) {
2911
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
3393
+ const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
2912
3394
  if (params !== null) {
2913
3395
  return { route, params };
2914
3396
  }
2915
3397
  }
2916
3398
  return null;
2917
3399
  }
2918
- function findAllowedMethods(routes, path12) {
3400
+ function findAllowedMethods(routes, path15) {
2919
3401
  const index = getHttpIndex(routes);
2920
3402
  const methods = /* @__PURE__ */ new Set();
2921
- const staticMethods = index.methodsByStaticPath.get(path12);
3403
+ const staticMethods = index.methodsByStaticPath.get(path15);
2922
3404
  if (staticMethods) {
2923
3405
  for (const method of staticMethods) {
2924
3406
  methods.add(method);
2925
3407
  }
2926
3408
  }
2927
- for (const route of index.dynamics) {
2928
- const params = matchDynamicPath(route.urlPath, path12, route.paramNames, route.isCatchAll);
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
+ );
2929
3416
  if (params !== null) {
2930
- methods.add(route.method);
3417
+ methods.add(entry.route.method);
2931
3418
  }
2932
3419
  }
3420
+ if (methods.has("GET")) {
3421
+ methods.add("HEAD");
3422
+ }
2933
3423
  return Array.from(methods);
2934
3424
  }
2935
- function matchDynamicPath(pattern, path12, paramNames, isCatchAll) {
2936
- const patternSegments = pattern.split("/").filter(Boolean);
2937
- const pathSegments = path12.split("/").filter(Boolean);
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);
2938
3430
  if (isCatchAll) {
2939
3431
  const nonCatchAllCount = patternSegments.length - 1;
2940
3432
  if (pathSegments.length <= nonCatchAllCount) {
@@ -3083,7 +3575,7 @@ async function resolveInputFromUrl(method, request, url) {
3083
3575
  }
3084
3576
  if (contentType.includes("application/x-www-form-urlencoded")) {
3085
3577
  const text2 = await request.text();
3086
- if (text2.trim() === "") return null;
3578
+ if (isBlankText(text2)) return null;
3087
3579
  const params = new URLSearchParams(text2);
3088
3580
  const obj = {};
3089
3581
  for (const [key, value] of params) {
@@ -3092,7 +3584,7 @@ async function resolveInputFromUrl(method, request, url) {
3092
3584
  return obj;
3093
3585
  }
3094
3586
  const text = await request.text();
3095
- if (text.trim() === "") {
3587
+ if (isBlankText(text)) {
3096
3588
  return null;
3097
3589
  }
3098
3590
  const result = parseJsonBody(text);
@@ -3111,6 +3603,26 @@ async function resolveInputFromUrl(method, request, url) {
3111
3603
  }
3112
3604
  return queryToObject(url.searchParams);
3113
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
+ }
3114
3626
 
3115
3627
  // src/response/sendNodeResponse.ts
3116
3628
  import { Readable } from "stream";
@@ -3123,11 +3635,24 @@ async function sendNodeResponse(response, res) {
3123
3635
  res.setHeader(key, value);
3124
3636
  }
3125
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
+ }
3126
3644
  if (response.body) {
3127
3645
  const nodeStream = Readable.fromWeb(response.body);
3128
3646
  await new Promise((resolve, reject) => {
3129
3647
  nodeStream.on("error", reject);
3130
- 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
+ });
3131
3656
  res.on("finish", resolve);
3132
3657
  nodeStream.pipe(res);
3133
3658
  });
@@ -3180,11 +3705,6 @@ function cors(options = {}) {
3180
3705
  } else if (Array.isArray(origin)) {
3181
3706
  allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
3182
3707
  }
3183
- if (!allowOrigin) {
3184
- await next();
3185
- return;
3186
- }
3187
- ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
3188
3708
  if (origin === true || Array.isArray(origin)) {
3189
3709
  const existingVary = ctx.headers.get("vary");
3190
3710
  if (existingVary) {
@@ -3195,6 +3715,11 @@ function cors(options = {}) {
3195
3715
  ctx.setHeader("Vary", "Origin");
3196
3716
  }
3197
3717
  }
3718
+ if (!allowOrigin) {
3719
+ await next();
3720
+ return;
3721
+ }
3722
+ ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
3198
3723
  ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
3199
3724
  if (allowedHeaders) {
3200
3725
  ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
@@ -3282,6 +3807,172 @@ function helmet(options = {}) {
3282
3807
  };
3283
3808
  }
3284
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
+
3285
3976
  // src/middleware/logger.ts
3286
3977
  function logger(options = {}) {
3287
3978
  return async (ctx, next) => {
@@ -3316,9 +4007,9 @@ function logger(options = {}) {
3316
4007
  }
3317
4008
 
3318
4009
  // src/server/handleWsUpgrade.ts
3319
- import fs8 from "fs";
4010
+ import fs10 from "fs";
3320
4011
  import { WebSocketServer, WebSocket } from "ws";
3321
- import path9 from "path";
4012
+ import path12 from "path";
3322
4013
 
3323
4014
  // src/server/serverUtils.ts
3324
4015
  function nodeHttpToWebHeaders(req) {
@@ -3374,7 +4065,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
3374
4065
  const dist = getDevDist();
3375
4066
  if (dist) {
3376
4067
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3377
- if (sourcePath && fs8.existsSync(sourcePath)) {
4068
+ if (sourcePath && fs10.existsSync(sourcePath)) {
3378
4069
  await ensureCompiled(sourcePath, rootDir, dist);
3379
4070
  }
3380
4071
  }
@@ -3397,9 +4088,9 @@ function bindEvents(rawSocket, handlers) {
3397
4088
  }
3398
4089
  }
3399
4090
  if (handlers.onMessage) {
3400
- rawSocket.on("message", (data) => {
3401
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
3402
- 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"));
3403
4094
  });
3404
4095
  }
3405
4096
  if (handlers.onClose) {
@@ -3413,12 +4104,22 @@ function bindEvents(rawSocket, handlers) {
3413
4104
  });
3414
4105
  }
3415
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
+ }
3416
4112
  async function sendResponseToSocket(socket, response) {
3417
4113
  const body = await response.text().catch(() => "");
3418
4114
  const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
3419
4115
  `;
3420
4116
  const headerLines = [];
3421
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
+ }
3422
4123
  for (const [key, value] of response.headers) {
3423
4124
  if (key.toLowerCase() === "content-length") {
3424
4125
  hasContentLength = true;
@@ -3432,9 +4133,33 @@ async function sendResponseToSocket(socket, response) {
3432
4133
  socket.destroy();
3433
4134
  }
3434
4135
  function attachWebSocket(options) {
3435
- const { server, routesRef, rootDir, config, globalMiddlewares, trustedProxy = false } = options;
4136
+ const {
4137
+ server,
4138
+ routesRef,
4139
+ rootDir,
4140
+ config,
4141
+ globalMiddlewares,
4142
+ trustedProxy = false,
4143
+ registries
4144
+ } = options;
3436
4145
  const wss = new WebSocketServer({ noServer: true });
3437
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) {
3438
4163
  const currentWsRoutes = routesRef.wsCurrent;
3439
4164
  const pathname = getPathname(req);
3440
4165
  const match = matchWsRoute(currentWsRoutes, pathname);
@@ -3448,13 +4173,13 @@ function attachWebSocket(options) {
3448
4173
  const host = req.headers.host ?? "localhost";
3449
4174
  const url = `http://${host}${req.url ?? "/"}`;
3450
4175
  const request = new Request(url, { method: "GET", headers });
3451
- const ctx = createContext(request, params, config, getClientIp(req, trustedProxy));
4176
+ const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
3452
4177
  const meta = ctx.meta;
3453
4178
  let upgraded = false;
3454
4179
  const finalHandler = async () => {
3455
4180
  let handlers;
3456
4181
  try {
3457
- const absoluteFilePath = path9.resolve(rootDir, route.filePath);
4182
+ const absoluteFilePath = path12.resolve(rootDir, route.filePath);
3458
4183
  handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
3459
4184
  } catch (err) {
3460
4185
  const reason = err instanceof Error ? err.message : String(err);
@@ -3478,6 +4203,7 @@ function attachWebSocket(options) {
3478
4203
  let response;
3479
4204
  try {
3480
4205
  if (route.middlewares === void 0 && route.middlewarePaths) {
4206
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
3481
4207
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3482
4208
  if (bundle) {
3483
4209
  route.middlewares = bundle.middlewares;
@@ -3503,13 +4229,13 @@ function attachWebSocket(options) {
3503
4229
  return;
3504
4230
  }
3505
4231
  await sendResponseToSocket(socket, mergeMeta(response, meta));
3506
- });
4232
+ }
3507
4233
  return wss;
3508
4234
  }
3509
4235
 
3510
4236
  // src/server/createServer.ts
3511
4237
  var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
3512
- function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4238
+ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT, requestSignal) {
3513
4239
  const forwardedProto = req.headers["x-forwarded-proto"];
3514
4240
  const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
3515
4241
  const host = req.headers.host ?? "localhost";
@@ -3517,7 +4243,10 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3517
4243
  const headers = nodeHttpToWebHeaders(req);
3518
4244
  const method = req.method ?? "GET";
3519
4245
  if (method === "GET" || method === "HEAD") {
3520
- return { request: new Request(url.toString(), { method, headers }), url };
4246
+ return {
4247
+ request: new Request(url.toString(), { method, headers, signal: requestSignal }),
4248
+ url
4249
+ };
3521
4250
  }
3522
4251
  const contentLength = req.headers["content-length"];
3523
4252
  if (contentLength !== void 0) {
@@ -3533,7 +4262,8 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3533
4262
  method,
3534
4263
  headers,
3535
4264
  body: limitedStream,
3536
- duplex: "half"
4265
+ duplex: "half",
4266
+ signal: requestSignal
3537
4267
  }),
3538
4268
  url
3539
4269
  };
@@ -3600,6 +4330,9 @@ function createServer(options) {
3600
4330
  middlewares: globalMiddlewares,
3601
4331
  injectors: globalInjectors,
3602
4332
  helmet: helmetOption,
4333
+ compression: compressionOption,
4334
+ etag: etagOption,
4335
+ registries,
3603
4336
  logger: loggerOption,
3604
4337
  bodyLimit = DEFAULT_BODY_LIMIT,
3605
4338
  http2: http2Option,
@@ -3607,6 +4340,10 @@ function createServer(options) {
3607
4340
  } = options;
3608
4341
  const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
3609
4342
  const configMiddlewares = [];
4343
+ if (compressionOption) {
4344
+ const compOpts = typeof compressionOption === "object" ? compressionOption : {};
4345
+ configMiddlewares.push(compression(compOpts));
4346
+ }
3610
4347
  const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
3611
4348
  if (corsMiddleware) configMiddlewares.push(corsMiddleware);
3612
4349
  if (helmetOption) {
@@ -3615,6 +4352,10 @@ function createServer(options) {
3615
4352
  }
3616
4353
  const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
3617
4354
  if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
4355
+ if (etagOption) {
4356
+ const etagOpts = typeof etagOption === "object" ? etagOption : {};
4357
+ configMiddlewares.push(etag(etagOpts));
4358
+ }
3618
4359
  const outerMiddlewares = [...configMiddlewares];
3619
4360
  if (globalMiddlewares && globalMiddlewares.length > 0) {
3620
4361
  outerMiddlewares.push(...globalMiddlewares);
@@ -3643,22 +4384,36 @@ function createServer(options) {
3643
4384
  config,
3644
4385
  globalInjectors,
3645
4386
  bodyLimit,
3646
- trustedProxy
4387
+ trustedProxy,
4388
+ registries
3647
4389
  ).catch(() => {
3648
4390
  res.statusCode = 500;
3649
4391
  res.end();
3650
4392
  });
3651
4393
  });
3652
- if (routesRef.wsCurrent.length > 0) {
3653
- attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares, trustedProxy });
3654
- }
4394
+ attachWebSocket({
4395
+ server,
4396
+ routesRef,
4397
+ rootDir,
4398
+ config,
4399
+ globalMiddlewares,
4400
+ trustedProxy,
4401
+ registries
4402
+ });
3655
4403
  return { server, routesRef };
3656
4404
  }
3657
- function prepareRequest(req, config, bodyLimit, trustedProxy) {
3658
- const { request, url } = toWebRequest(req, bodyLimit);
4405
+ function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
4406
+ const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
3659
4407
  const method = request.method.toUpperCase();
3660
4408
  const urlPath = url.pathname;
3661
- const ctx = createContextFromUrl(request, url, {}, config, getClientIp(req, trustedProxy));
4409
+ const ctx = createContextFromUrl(
4410
+ request,
4411
+ url,
4412
+ {},
4413
+ config,
4414
+ getClientIp(req, trustedProxy),
4415
+ registries
4416
+ );
3662
4417
  const meta = ctx.meta;
3663
4418
  return { request, url, ctx, meta, method, urlPath };
3664
4419
  }
@@ -3671,17 +4426,28 @@ function resolveRouteOrThrow(routes, method, urlPath) {
3671
4426
  }
3672
4427
  throw new RouteNotFoundError(urlPath);
3673
4428
  }
4429
+ var routePathCache = /* @__PURE__ */ new WeakMap();
4430
+ function getRoutePaths(route, rootDir, dist) {
4431
+ let cached = routePathCache.get(route);
4432
+ if (!cached) {
4433
+ cached = {
4434
+ absFilePath: path13.resolve(rootDir, route.filePath),
4435
+ schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
4436
+ };
4437
+ routePathCache.set(route, cached);
4438
+ }
4439
+ return cached;
4440
+ }
3674
4441
  function createRoutePipeline(opts) {
3675
4442
  const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
3676
4443
  return async () => {
3677
4444
  const match = resolveRouteOrThrow(routes, method, urlPath);
3678
4445
  ctx.params = match.params;
3679
4446
  const { route } = match;
3680
- const absoluteFilePath = path10.resolve(rootDir, route.filePath);
3681
- const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
4447
+ const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
4448
+ const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
3682
4449
  const input = await resolveInputFromUrl(route.method, request, url);
3683
4450
  const inputType = getInputTypeForMethod(route.method);
3684
- const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
3685
4451
  if (isDevOnDemandEnabled()) {
3686
4452
  const devDist = getDevDist();
3687
4453
  if (devDist) {
@@ -3692,8 +4458,9 @@ function createRoutePipeline(opts) {
3692
4458
  if (!result.valid) {
3693
4459
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
3694
4460
  }
3695
- const body = hasBody(route.method) ? result.data : void 0;
4461
+ const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
3696
4462
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
4463
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
3697
4464
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3698
4465
  if (bundle) {
3699
4466
  route.middlewares = bundle.middlewares;
@@ -3719,11 +4486,22 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
3719
4486
  }
3720
4487
  }
3721
4488
  }
3722
- async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy) {
4489
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
3723
4490
  let meta = { headers: {}, setCookies: [] };
3724
4491
  let ctx;
4492
+ const abortController = new AbortController();
4493
+ res.on("close", () => {
4494
+ if (!res.writableEnded) abortController.abort();
4495
+ });
3725
4496
  try {
3726
- const prepared = prepareRequest(req, config, bodyLimit, trustedProxy);
4497
+ const prepared = prepareRequest(
4498
+ req,
4499
+ config,
4500
+ bodyLimit,
4501
+ trustedProxy,
4502
+ registries,
4503
+ abortController.signal
4504
+ );
3727
4505
  ctx = prepared.ctx;
3728
4506
  meta = prepared.meta;
3729
4507
  const { request, url, method, urlPath } = prepared;
@@ -3741,6 +4519,7 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
3741
4519
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
3742
4520
  await sendSuccessResponse(response, res);
3743
4521
  } catch (err) {
4522
+ if (res.destroyed || res.writableEnded) return;
3744
4523
  await sendErrorResponse(err, meta, res, onError, ctx);
3745
4524
  }
3746
4525
  }
@@ -3764,7 +4543,7 @@ async function createTestServer(options) {
3764
4543
  } = options;
3765
4544
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3766
4545
  const sorted = sortRoutes(routes);
3767
- const schemaDist = dist ? path11.isAbsolute(dist) ? dist : path11.resolve(rootDir, dist) : await fs9.mkdtemp(path11.join(os.tmpdir(), "faapi-test-schema-"));
4546
+ const schemaDist = dist ? path14.isAbsolute(dist) ? dist : path14.resolve(rootDir, dist) : await fs11.mkdtemp(path14.join(os.tmpdir(), "faapi-test-schema-"));
3768
4547
  await generateSchemaFiles(sorted, rootDir, schemaDist);
3769
4548
  const { server } = createServer({
3770
4549
  routes: sorted,
@@ -3797,7 +4576,7 @@ async function createTestServer(options) {
3797
4576
  await new Promise((resolve) => {
3798
4577
  server.close(() => resolve());
3799
4578
  });
3800
- await fs9.rm(schemaDist, { recursive: true, force: true }).catch(() => {
4579
+ await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3801
4580
  });
3802
4581
  invalidateSchemaCache();
3803
4582
  }