@faapi/faapi 6.17.0 → 6.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.js CHANGED
@@ -1,3 +1,12 @@
1
+ // src/response/bufferedBody.ts
2
+ var bufferedBodyResponses = /* @__PURE__ */ new WeakSet();
3
+ function markBufferedBody(response) {
4
+ bufferedBodyResponses.add(response);
5
+ }
6
+ function hasBufferedBody(response) {
7
+ return bufferedBodyResponses.has(response);
8
+ }
9
+
1
10
  // src/utils/isPlainObject.ts
2
11
  function isPlainObject(value) {
3
12
  if (value === null || typeof value !== "object") {
@@ -12,8 +21,50 @@ function isPlainObject(value) {
12
21
 
13
22
  // src/utils/stringifyJson.ts
14
23
  function stringifyJson(value) {
24
+ if (!needsConversion(value, /* @__PURE__ */ new Set())) {
25
+ return JSON.stringify(value);
26
+ }
15
27
  return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
16
28
  }
29
+ function needsConversion(value, ancestors) {
30
+ if (value === null) return false;
31
+ const type = typeof value;
32
+ if (type === "bigint") return true;
33
+ if (type === "number") return !Number.isFinite(value);
34
+ if (type !== "object") return false;
35
+ const obj = value;
36
+ if (ancestors.has(obj)) {
37
+ throw new TypeError("[faapi] Converting circular structure to JSON");
38
+ }
39
+ if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
40
+ return true;
41
+ }
42
+ if (typeof obj.toJSON === "function") return true;
43
+ if (Array.isArray(obj)) {
44
+ ancestors.add(obj);
45
+ for (let i = 0; i < obj.length; i++) {
46
+ if (needsConversion(obj[i], ancestors)) {
47
+ ancestors.delete(obj);
48
+ return true;
49
+ }
50
+ }
51
+ ancestors.delete(obj);
52
+ return false;
53
+ }
54
+ if (isPlainObject(obj)) {
55
+ ancestors.add(obj);
56
+ for (const key in obj) {
57
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
58
+ if (needsConversion(obj[key], ancestors)) {
59
+ ancestors.delete(obj);
60
+ return true;
61
+ }
62
+ }
63
+ ancestors.delete(obj);
64
+ return false;
65
+ }
66
+ return false;
67
+ }
17
68
  function convertValue(value, ancestors) {
18
69
  if (value === null) return value;
19
70
  const type = typeof value;
@@ -108,16 +159,30 @@ function createSseWriter() {
108
159
  let controller = null;
109
160
  let closed = false;
110
161
  let aborted = false;
111
- const stream = new ReadableStream({
112
- start(c) {
113
- controller = c;
162
+ let drainWaiters = [];
163
+ const resolveDrainWaiters = () => {
164
+ const waiters = drainWaiters;
165
+ drainWaiters = [];
166
+ for (const resolve of waiters) resolve();
167
+ };
168
+ const stream = new ReadableStream(
169
+ {
170
+ start(c) {
171
+ controller = c;
172
+ },
173
+ // 消费者排空缓冲后再要数据 → 背压解除,唤醒 waitForDrain 等待方
174
+ pull() {
175
+ resolveDrainWaiters();
176
+ },
177
+ cancel() {
178
+ aborted = true;
179
+ closed = true;
180
+ controller = null;
181
+ resolveDrainWaiters();
182
+ }
114
183
  },
115
- cancel() {
116
- aborted = true;
117
- closed = true;
118
- controller = null;
119
- }
120
- });
184
+ { highWaterMark: 16 }
185
+ );
121
186
  const response = new Response(stream, {
122
187
  status: 200,
123
188
  headers: {
@@ -152,6 +217,7 @@ function createSseWriter() {
152
217
  close() {
153
218
  if (closed) return;
154
219
  closed = true;
220
+ resolveDrainWaiters();
155
221
  if (controller) {
156
222
  try {
157
223
  controller.close();
@@ -163,6 +229,16 @@ function createSseWriter() {
163
229
  get closed() {
164
230
  return closed;
165
231
  },
232
+ get desiredSize() {
233
+ return controller ? controller.desiredSize : null;
234
+ },
235
+ waitForDrain() {
236
+ if (closed || !controller) return Promise.resolve();
237
+ if ((controller.desiredSize ?? 0) > 0) return Promise.resolve();
238
+ return new Promise((resolve) => {
239
+ drainWaiters.push(resolve);
240
+ });
241
+ },
166
242
  get aborted() {
167
243
  return aborted;
168
244
  },
@@ -199,14 +275,14 @@ var ValidationError = class extends FaapiError {
199
275
  issues;
200
276
  };
201
277
  var RouteNotFoundError = class extends FaapiError {
202
- constructor(path15) {
203
- super("ROUTE_NOT_FOUND", `Route not found: ${path15}`, 404);
278
+ constructor(path16) {
279
+ super("ROUTE_NOT_FOUND", `Route not found: ${path16}`, 404);
204
280
  this.name = "RouteNotFoundError";
205
281
  }
206
282
  };
207
283
  var MethodNotAllowedError = class extends FaapiError {
208
- constructor(method, path15, allowedMethods) {
209
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path15}`, 405);
284
+ constructor(method, path16, allowedMethods) {
285
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path16}`, 405);
210
286
  this.allowedMethods = allowedMethods;
211
287
  this.name = "MethodNotAllowedError";
212
288
  }
@@ -252,7 +328,9 @@ function jsonRaw(body, status, extraHeaders) {
252
328
  const extra = new Headers(extraHeaders);
253
329
  extra.forEach((value, key) => headers.set(key, value));
254
330
  }
255
- return new Response(stringifyJson(body), { status, headers });
331
+ const response = new Response(stringifyJson(body), { status, headers });
332
+ markBufferedBody(response);
333
+ return response;
256
334
  }
257
335
  function wrapOkResult(result, config) {
258
336
  if (result instanceof Response) return result;
@@ -269,12 +347,9 @@ function formatFailResponse(options, config) {
269
347
  }
270
348
  function formatErrorResponse(error, config) {
271
349
  const failFn = resolveFailFn(config);
350
+ const buildFailBody = (e) => failFn({ status: e.statusCode, code: e.code, message: e.message });
272
351
  if (error instanceof ValidationError) {
273
- const body2 = failFn({
274
- status: error.statusCode,
275
- code: error.code,
276
- message: error.message
277
- });
352
+ const body2 = buildFailBody(error);
278
353
  const bodyObj = typeof body2 === "object" && body2 !== null ? { ...body2 } : { error: body2 };
279
354
  const existingError = bodyObj.error;
280
355
  if (existingError && typeof existingError === "object") {
@@ -287,28 +362,15 @@ function formatErrorResponse(error, config) {
287
362
  return jsonOk(bodyObj, error.statusCode);
288
363
  }
289
364
  if (error instanceof MethodNotAllowedError) {
290
- const body2 = failFn({
291
- status: error.statusCode,
292
- code: error.code,
293
- message: error.message
365
+ return jsonOk(buildFailBody(error), error.statusCode, {
366
+ Allow: error.allowedMethods.join(", ")
294
367
  });
295
- return jsonOk(body2, error.statusCode, { Allow: error.allowedMethods.join(", ") });
296
368
  }
297
369
  if (error instanceof PayloadTooLargeError) {
298
- const body2 = failFn({
299
- status: error.statusCode,
300
- code: error.code,
301
- message: error.message
302
- });
303
- return jsonOk(body2, error.statusCode);
370
+ return jsonOk(buildFailBody(error), error.statusCode);
304
371
  }
305
372
  if (error instanceof FaapiError) {
306
- const body2 = failFn({
307
- status: error.statusCode,
308
- code: error.code,
309
- message: error.message
310
- });
311
- return jsonOk(body2, error.statusCode);
373
+ return jsonOk(buildFailBody(error), error.statusCode);
312
374
  }
313
375
  const message = error instanceof Error ? error.message : "An unknown error occurred";
314
376
  const body = failFn({ status: 500, code: "INTERNAL_ERROR", message });
@@ -513,10 +575,12 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
513
575
  },
514
576
  html(html, status) {
515
577
  const headers = { "Content-Type": "text/html; charset=utf-8" };
516
- return new Response(html, {
578
+ const response = new Response(html, {
517
579
  status: status ?? 200,
518
580
  headers
519
581
  });
582
+ markBufferedBody(response);
583
+ return response;
520
584
  },
521
585
  getCookie(name) {
522
586
  return parsedCookies.get(name);
@@ -584,7 +648,7 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
584
648
  function createTestContext(options) {
585
649
  const {
586
650
  method = "GET",
587
- path: path15,
651
+ path: path16,
588
652
  query,
589
653
  headers,
590
654
  params = {},
@@ -592,7 +656,7 @@ function createTestContext(options) {
592
656
  ip = "",
593
657
  registries
594
658
  } = options;
595
- const url = new URL(`http://localhost${path15}`);
659
+ const url = new URL(`http://localhost${path16}`);
596
660
  if (query) {
597
661
  for (const [key, value] of Object.entries(query)) {
598
662
  if (Array.isArray(value)) {
@@ -676,51 +740,67 @@ async function toResponse(value, meta) {
676
740
  if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
677
741
  const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
678
742
  applyMeta(headers2);
679
- return new Response(value, {
680
- status: meta?.status ?? 200,
681
- headers: headers2
682
- });
743
+ return markBuffered(
744
+ new Response(value, {
745
+ status: meta?.status ?? 200,
746
+ headers: headers2
747
+ })
748
+ );
683
749
  }
684
750
  if (value instanceof Uint8Array) {
685
751
  const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
686
752
  applyMeta(headers2);
687
- return new Response(value, {
688
- status: meta?.status ?? 200,
689
- headers: headers2
690
- });
753
+ return markBuffered(
754
+ new Response(value, {
755
+ status: meta?.status ?? 200,
756
+ headers: headers2
757
+ })
758
+ );
691
759
  }
692
760
  if (isPlainObject(value) || Array.isArray(value)) {
693
761
  const body2 = stringifyJson(value);
694
762
  const headers2 = new Headers({ "Content-Type": "application/json" });
695
763
  applyMeta(headers2);
696
- return new Response(body2, {
697
- status: meta?.status ?? 200,
698
- headers: headers2
699
- });
764
+ return markBuffered(
765
+ new Response(body2, {
766
+ status: meta?.status ?? 200,
767
+ headers: headers2
768
+ })
769
+ );
700
770
  }
701
771
  if (typeof value === "string") {
702
772
  const headers2 = new Headers({ "Content-Type": "text/plain" });
703
773
  applyMeta(headers2);
704
- return new Response(value, {
705
- status: meta?.status ?? 200,
706
- headers: headers2
707
- });
774
+ return markBuffered(
775
+ new Response(value, {
776
+ status: meta?.status ?? 200,
777
+ headers: headers2
778
+ })
779
+ );
708
780
  }
709
781
  if (typeof value === "number" || typeof value === "boolean") {
710
782
  const headers2 = new Headers({ "Content-Type": "text/plain" });
711
783
  applyMeta(headers2);
712
- return new Response(String(value), {
713
- status: meta?.status ?? 200,
714
- headers: headers2
715
- });
784
+ return markBuffered(
785
+ new Response(String(value), {
786
+ status: meta?.status ?? 200,
787
+ headers: headers2
788
+ })
789
+ );
716
790
  }
717
791
  const body = stringifyJson(value);
718
792
  const headers = new Headers({ "Content-Type": "application/json" });
719
793
  applyMeta(headers);
720
- return new Response(body, {
721
- status: meta?.status ?? 200,
722
- headers
723
- });
794
+ return markBuffered(
795
+ new Response(body, {
796
+ status: meta?.status ?? 200,
797
+ headers
798
+ })
799
+ );
800
+ }
801
+ function markBuffered(response) {
802
+ markBufferedBody(response);
803
+ return response;
724
804
  }
725
805
 
726
806
  // src/injection/resolveInjection.ts
@@ -825,7 +905,10 @@ function extractParamName(param, names) {
825
905
  }
826
906
 
827
907
  // src/utils/queryToObject.ts
908
+ var queryCache = /* @__PURE__ */ new WeakMap();
828
909
  function queryToObject(params) {
910
+ const cached = queryCache.get(params);
911
+ if (cached) return cached;
829
912
  const result = {};
830
913
  for (const [key, value] of params) {
831
914
  const existing = result[key];
@@ -837,6 +920,7 @@ function queryToObject(params) {
837
920
  result[key] = [existing, value];
838
921
  }
839
922
  }
923
+ queryCache.set(params, result);
840
924
  return result;
841
925
  }
842
926
 
@@ -1157,18 +1241,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1157
1241
  sseWriter.close();
1158
1242
  }
1159
1243
  };
1160
- if (!middlewares || middlewares.length === 0) {
1161
- try {
1162
- const result = await injectParamsAsync(handler, ctx, body, injectors);
1163
- const sseResponse = pickSseAndAutoClose();
1164
- if (sseResponse) return sseResponse;
1165
- return toResponse(wrapResult(result, ctx), meta);
1166
- } catch (err) {
1167
- autoCloseSseOnError();
1168
- throw err;
1169
- }
1170
- }
1171
- const finalHandler = async () => {
1244
+ const executeAndFormat = async () => {
1172
1245
  try {
1173
1246
  const result = await injectParamsAsync(handler, ctx, body, injectors);
1174
1247
  const sseResponse = pickSseAndAutoClose();
@@ -1179,27 +1252,31 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1179
1252
  throw err;
1180
1253
  }
1181
1254
  };
1255
+ if (!middlewares || middlewares.length === 0) {
1256
+ return executeAndFormat();
1257
+ }
1258
+ const finalHandler = executeAndFormat;
1182
1259
  return await compose(middlewares, ctx, finalHandler);
1183
1260
  }
1184
1261
 
1185
1262
  // src/testServer.ts
1186
- import path14 from "path";
1263
+ import path15 from "path";
1187
1264
  import os from "os";
1188
- import fs11 from "fs/promises";
1265
+ import fs12 from "fs/promises";
1189
1266
 
1190
1267
  // src/router/scanRoutes.ts
1191
1268
  import fg from "fast-glob";
1192
1269
  import path2 from "path";
1193
- import fs2 from "fs";
1270
+ import fs3 from "fs";
1194
1271
 
1195
1272
  // src/router/constants.ts
1196
1273
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1197
1274
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
1198
1275
 
1199
1276
  // src/utils/normalizePath.ts
1200
- function normalizePath(path15) {
1201
- if (!path15) return "";
1202
- let result = path15.replace(/\\/g, "/");
1277
+ function normalizePath(path16) {
1278
+ if (!path16) return "";
1279
+ let result = path16.replace(/\\/g, "/");
1203
1280
  result = result.replace(/\/+/g, "/");
1204
1281
  result = result.replace(/\/+$/, "");
1205
1282
  if (result && !result.startsWith("/")) {
@@ -1246,6 +1323,7 @@ function filePathToUrlPath(filePath) {
1246
1323
  }
1247
1324
 
1248
1325
  // src/utils/importWithCacheBust.ts
1326
+ import fs from "fs";
1249
1327
  import { pathToFileURL } from "url";
1250
1328
  var loadTs;
1251
1329
  function getVitestImportActual() {
@@ -1253,13 +1331,26 @@ function getVitestImportActual() {
1253
1331
  if (typeof vi?.importActual !== "function") return void 0;
1254
1332
  return vi.importActual.bind(vi);
1255
1333
  }
1334
+ var bustCache = /* @__PURE__ */ new Map();
1335
+ var bustCounter = 0;
1256
1336
  async function importWithCacheBust(filePath, bustViteCache = false) {
1257
1337
  const importActual = getVitestImportActual();
1258
1338
  if (importActual) {
1259
1339
  if (bustViteCache) {
1340
+ let mtimeMs = 0;
1341
+ try {
1342
+ mtimeMs = fs.statSync(filePath).mtimeMs;
1343
+ } catch {
1344
+ }
1345
+ const cached = bustCache.get(filePath);
1346
+ if (cached && cached.mtimeMs === mtimeMs) {
1347
+ return cached.module;
1348
+ }
1260
1349
  let url2 = pathToFileURL(filePath).href;
1261
- url2 += `?t=${Date.now()}`;
1262
- return await import(url2);
1350
+ url2 += `?t=${Date.now()}-${++bustCounter}`;
1351
+ const module = await import(url2);
1352
+ bustCache.set(filePath, { mtimeMs, module });
1353
+ return module;
1263
1354
  }
1264
1355
  return await importActual(filePath);
1265
1356
  }
@@ -1347,7 +1438,7 @@ async function loadMergedMiddlewares(middlewarePaths) {
1347
1438
 
1348
1439
  // src/utils/prodPaths.ts
1349
1440
  import path from "path";
1350
- import fs from "fs";
1441
+ import fs2 from "fs";
1351
1442
  var APP_DIR = "src";
1352
1443
  function toProdFilePath(filePath, dist) {
1353
1444
  let rel = filePath.replace(/\\/g, "/");
@@ -1365,7 +1456,7 @@ function toProdExtension(filePath) {
1365
1456
  }
1366
1457
  function toRealPath(p) {
1367
1458
  try {
1368
- return fs.realpathSync(p);
1459
+ return fs2.realpathSync(p);
1369
1460
  } catch {
1370
1461
  return p;
1371
1462
  }
@@ -1400,7 +1491,7 @@ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
1400
1491
  const mwJsPath = path2.join(currentDir, "middlewares.js");
1401
1492
  const absTsPath = path2.resolve(rootDir, mwTsPath);
1402
1493
  const absJsPath = path2.resolve(rootDir, mwJsPath);
1403
- const absMwPath = fs2.existsSync(absTsPath) ? absTsPath : fs2.existsSync(absJsPath) ? absJsPath : null;
1494
+ const absMwPath = fs3.existsSync(absTsPath) ? absTsPath : fs3.existsSync(absJsPath) ? absJsPath : null;
1404
1495
  if (absMwPath) {
1405
1496
  const relMwPath = path2.relative(rootDir, absMwPath);
1406
1497
  const prodAbsPath = path2.resolve(rootDir, toProdFilePath(relMwPath, dist));
@@ -1410,7 +1501,7 @@ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
1410
1501
  for (const ext of [".ts", ".js"]) {
1411
1502
  const mwPath = path2.join(currentDir, `middlewares${ext}`);
1412
1503
  const absMwPath = path2.resolve(rootDir, mwPath);
1413
- if (fs2.existsSync(absMwPath)) {
1504
+ if (fs3.existsSync(absMwPath)) {
1414
1505
  paths.push(absMwPath);
1415
1506
  break;
1416
1507
  }
@@ -1449,7 +1540,7 @@ async function scanRoutes(rootDir, patterns, dist) {
1449
1540
  const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
1450
1541
  middlewareBundle = await loadMergedMiddlewares(mwPaths);
1451
1542
  }
1452
- const source = await fs2.promises.readFile(absPath, "utf8").catch(() => "");
1543
+ const source = await fs3.promises.readFile(absPath, "utf8").catch(() => "");
1453
1544
  const exportNames = extractExportsFromSource(source);
1454
1545
  const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
1455
1546
  for (const method of methods) {
@@ -1502,34 +1593,24 @@ function sortRoutes(routes) {
1502
1593
  }
1503
1594
 
1504
1595
  // src/cli/generateSchemaFiles.ts
1505
- import path6 from "path";
1506
-
1507
- // src/utils/atomicWrite.ts
1508
- import path3 from "path";
1509
- import fs3 from "fs";
1510
- async function atomicWriteFile(outputPath, content) {
1511
- await fs3.promises.mkdir(path3.dirname(outputPath), { recursive: true });
1512
- const tmp = `${outputPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1513
- await fs3.promises.writeFile(tmp, content, "utf-8");
1514
- await fs3.promises.rename(tmp, outputPath);
1515
- }
1596
+ import path7 from "path";
1516
1597
 
1517
1598
  // src/ast/createProgram.ts
1518
1599
  import ts2 from "typescript";
1519
1600
  import fs4 from "fs";
1520
- import path4 from "path";
1601
+ import path3 from "path";
1521
1602
  var programCache = /* @__PURE__ */ new Map();
1522
1603
  var tsConfigCache = /* @__PURE__ */ new Map();
1523
1604
  function findTsConfig(filePath) {
1524
- let dir = path4.dirname(filePath);
1525
- const root = path4.parse(dir).root;
1605
+ let dir = path3.dirname(filePath);
1606
+ const root = path3.parse(dir).root;
1526
1607
  while (true) {
1527
- const candidate = path4.join(dir, "tsconfig.json");
1608
+ const candidate = path3.join(dir, "tsconfig.json");
1528
1609
  if (fs4.existsSync(candidate)) {
1529
1610
  return candidate;
1530
1611
  }
1531
1612
  if (dir === root) return null;
1532
- const parent = path4.dirname(dir);
1613
+ const parent = path3.dirname(dir);
1533
1614
  if (parent === dir) return null;
1534
1615
  dir = parent;
1535
1616
  }
@@ -1545,7 +1626,7 @@ function parseTsConfig(tsconfigPath) {
1545
1626
  return result;
1546
1627
  }
1547
1628
  const config = configFile.config ?? {};
1548
- const basePath = path4.dirname(tsconfigPath);
1629
+ const basePath = path3.dirname(tsconfigPath);
1549
1630
  const parsed = ts2.parseJsonConfigFileContent(
1550
1631
  config,
1551
1632
  ts2.sys,
@@ -2545,11 +2626,11 @@ function extractSchema(typeNode, sourceFile) {
2545
2626
  }
2546
2627
 
2547
2628
  // src/cli/collectRouteSchemaSources.ts
2548
- import path5 from "path";
2629
+ import path4 from "path";
2549
2630
  function collectRouteSchemaSources(routes, rootDir) {
2550
2631
  const methodsByFile = /* @__PURE__ */ new Map();
2551
2632
  for (const route of routes) {
2552
- const filePath = rootDir ? path5.resolve(rootDir, route.filePath) : route.filePath;
2633
+ const filePath = rootDir ? path4.resolve(rootDir, route.filePath) : route.filePath;
2553
2634
  let entry = methodsByFile.get(filePath);
2554
2635
  if (!entry) {
2555
2636
  entry = { urlPath: route.urlPath, methods: /* @__PURE__ */ new Set() };
@@ -2893,6 +2974,63 @@ function generateZodSchemaSourceParts(typeInfo, resolveType, exportName, coerce
2893
2974
  return { namedTypeDeclarations, entryDeclaration };
2894
2975
  }
2895
2976
 
2977
+ // src/cli/generateZodArtifacts.ts
2978
+ import path6 from "path";
2979
+ import { existsSync } from "fs";
2980
+
2981
+ // src/utils/atomicWrite.ts
2982
+ import path5 from "path";
2983
+ import fs5 from "fs";
2984
+ async function atomicWriteFile(outputPath, content) {
2985
+ await fs5.promises.mkdir(path5.dirname(outputPath), { recursive: true });
2986
+ const tmp = `${outputPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2987
+ await fs5.promises.writeFile(tmp, content, "utf-8");
2988
+ await fs5.promises.rename(tmp, outputPath);
2989
+ }
2990
+
2991
+ // src/cli/generateZodArtifacts.ts
2992
+ async function generateZodArtifacts(sources, options) {
2993
+ const { generateFileSource, resolversByFile, rootDir, dist } = options;
2994
+ if (sources.length === 0) return;
2995
+ const sourcesByFile = /* @__PURE__ */ new Map();
2996
+ for (const source of sources) {
2997
+ let list = sourcesByFile.get(source.filePath);
2998
+ if (!list) {
2999
+ list = [];
3000
+ sourcesByFile.set(source.filePath, list);
3001
+ }
3002
+ list.push(source);
3003
+ }
3004
+ const fileEntries = [];
3005
+ for (const [filePath, fileSources] of sourcesByFile) {
3006
+ const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
3007
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
3008
+ const resolver = resolversByFile.get(filePath);
3009
+ let relForDir = relFile;
3010
+ if (relForDir.startsWith("src/")) {
3011
+ relForDir = relForDir.slice(4);
3012
+ }
3013
+ const dirIdx = relForDir.lastIndexOf("/");
3014
+ const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
3015
+ const source = generateFileSource(
3016
+ fileSources,
3017
+ (name) => resolver?.resolve(name)?.runtimeType,
3018
+ getHelpersImportPath(zodRelDir)
3019
+ );
3020
+ fileEntries.push({ outputPath, source });
3021
+ }
3022
+ const allSourceCode = fileEntries.map((e) => e.source).join("\n");
3023
+ if (usesCoerceHelpers(allSourceCode)) {
3024
+ const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
3025
+ if (!existsSync(helpersPath)) {
3026
+ await atomicWriteFile(helpersPath, generateHelpersFileSource());
3027
+ }
3028
+ }
3029
+ await Promise.all(
3030
+ fileEntries.map(({ outputPath, source }) => atomicWriteFile(outputPath, source))
3031
+ );
3032
+ }
3033
+
2896
3034
  // src/cli/generateSchemaFiles.ts
2897
3035
  function getSchemaOutputPath(sourceFile, dist, rootDir) {
2898
3036
  let rel = sourceFile.replace(/\\/g, "/");
@@ -2901,7 +3039,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
2901
3039
  }
2902
3040
  const idx = rel.lastIndexOf("/");
2903
3041
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2904
- return path6.resolve(rootDir, dist, relDir, "zod.js");
3042
+ return path7.resolve(rootDir, dist, relDir, "zod.js");
2905
3043
  }
2906
3044
  function getRuntimeSchemaPath(filePath, dist, rootDir) {
2907
3045
  let rel = filePath.replace(/\\/g, "/");
@@ -2912,7 +3050,7 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
2912
3050
  }
2913
3051
  const idx = rel.lastIndexOf("/");
2914
3052
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2915
- return path6.resolve(rootDir, dist, relDir, "zod.js");
3053
+ return path7.resolve(rootDir, dist, relDir, "zod.js");
2916
3054
  }
2917
3055
  function getHelpersImportPath(relDir) {
2918
3056
  if (!relDir) return `./${HELPERS_FILENAME}`;
@@ -2960,60 +3098,27 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
2960
3098
  async function generateSchemaFiles(routes, rootDir, dist) {
2961
3099
  if (routes.length === 0) return;
2962
3100
  const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
2963
- const sourcesByFile = /* @__PURE__ */ new Map();
2964
- for (const source of sources) {
2965
- let list = sourcesByFile.get(source.filePath);
2966
- if (!list) {
2967
- list = [];
2968
- sourcesByFile.set(source.filePath, list);
2969
- }
2970
- list.push(source);
2971
- }
2972
- const fileEntries = [];
2973
- for (const [filePath, fileSources] of sourcesByFile) {
2974
- const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
2975
- const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2976
- let relForDir = relFile;
2977
- if (relForDir.startsWith("src/")) {
2978
- relForDir = relForDir.slice(4);
2979
- }
2980
- const dirIdx = relForDir.lastIndexOf("/");
2981
- const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2982
- const helpersImportPath = getHelpersImportPath(zodRelDir);
2983
- const resolver = resolversByFile.get(filePath);
2984
- const source = generateSchemaFileSource(
2985
- fileSources,
2986
- (name) => resolver?.resolve(name)?.runtimeType,
2987
- helpersImportPath
2988
- );
2989
- fileEntries.push({ outputPath, source });
2990
- }
2991
- const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2992
- if (usesCoerceHelpers(allSourceCode)) {
2993
- const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
2994
- await writeSchemaFile(helpersPath, generateHelpersFileSource());
2995
- }
2996
- await Promise.all(
2997
- fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2998
- );
2999
- }
3000
- async function writeSchemaFile(outputPath, source) {
3001
- await atomicWriteFile(outputPath, source);
3101
+ await generateZodArtifacts(sources, {
3102
+ generateFileSource: generateSchemaFileSource,
3103
+ resolversByFile,
3104
+ rootDir,
3105
+ dist
3106
+ });
3002
3107
  }
3003
3108
 
3004
3109
  // src/cli/compileOnDemand.ts
3005
- import path11 from "path";
3006
- import fs9 from "fs";
3110
+ import path12 from "path";
3111
+ import fs10 from "fs";
3007
3112
 
3008
3113
  // src/cli/compileSourceFiles.ts
3009
- import path9 from "path";
3010
- import fs7 from "fs";
3114
+ import path10 from "path";
3115
+ import fs8 from "fs";
3011
3116
  import fg2 from "fast-glob";
3012
3117
 
3013
3118
  // src/cli/aliasPlugin.ts
3014
3119
  import ts7 from "typescript";
3015
- import path8 from "path";
3016
- import fs6 from "fs";
3120
+ import path9 from "path";
3121
+ import fs7 from "fs";
3017
3122
 
3018
3123
  // src/utils/resolveAlias.ts
3019
3124
  function resolveAlias(specifier, config) {
@@ -3040,15 +3145,15 @@ function resolveAlias(specifier, config) {
3040
3145
 
3041
3146
  // src/utils/readTsconfig.ts
3042
3147
  import ts6 from "typescript";
3043
- import path7 from "path";
3044
- import fs5 from "fs";
3148
+ import path8 from "path";
3149
+ import fs6 from "fs";
3045
3150
  var tsconfigCache = /* @__PURE__ */ new Map();
3046
3151
  function readTsconfig(rootDir) {
3047
- const tsconfigPath = path7.resolve(rootDir, "tsconfig.json");
3048
- if (!fs5.existsSync(tsconfigPath)) return null;
3152
+ const tsconfigPath = path8.resolve(rootDir, "tsconfig.json");
3153
+ if (!fs6.existsSync(tsconfigPath)) return null;
3049
3154
  let mtimeMs;
3050
3155
  try {
3051
- mtimeMs = fs5.statSync(tsconfigPath).mtimeMs;
3156
+ mtimeMs = fs6.statSync(tsconfigPath).mtimeMs;
3052
3157
  } catch {
3053
3158
  return null;
3054
3159
  }
@@ -3064,7 +3169,7 @@ function readTsconfig(rootDir) {
3064
3169
  const config = rawPaths ? (() => {
3065
3170
  const paths = {};
3066
3171
  for (const [pattern, targets] of Object.entries(rawPaths)) {
3067
- paths[pattern] = targets.map((t) => path7.resolve(baseUrl, t));
3172
+ paths[pattern] = targets.map((t) => path8.resolve(baseUrl, t));
3068
3173
  }
3069
3174
  return { baseUrl, paths };
3070
3175
  })() : null;
@@ -3074,26 +3179,26 @@ function readTsconfig(rootDir) {
3074
3179
 
3075
3180
  // src/cli/aliasPlugin.ts
3076
3181
  function toProdImportPath(sourceFile, importer) {
3077
- const importerDir = path8.dirname(importer);
3078
- let rel = path8.relative(importerDir, sourceFile);
3079
- rel = rel.split(path8.sep).join("/");
3182
+ const importerDir = path9.dirname(importer);
3183
+ let rel = path9.relative(importerDir, sourceFile);
3184
+ rel = rel.split(path9.sep).join("/");
3080
3185
  if (!rel.startsWith(".")) rel = "./" + rel;
3081
3186
  return toProdExtension(rel);
3082
3187
  }
3083
3188
  function toStrippedProdImportPath(sourceFile, rootDir) {
3084
- const appDirAbs = toRealPath(path8.resolve(rootDir, APP_DIR));
3189
+ const appDirAbs = toRealPath(path9.resolve(rootDir, APP_DIR));
3085
3190
  const sourceReal = toRealPath(sourceFile);
3086
- let rel = path8.relative(appDirAbs, sourceReal);
3087
- rel = rel.split(path8.sep).join("/");
3191
+ let rel = path9.relative(appDirAbs, sourceReal);
3192
+ rel = rel.split(path9.sep).join("/");
3088
3193
  if (!rel.startsWith(".")) rel = "./" + rel;
3089
3194
  return toProdExtension(rel);
3090
3195
  }
3091
3196
  function toProdImportFromImporter(importer, rootDir, relFromDist) {
3092
- const importerRel = path8.relative(toRealPath(path8.resolve(rootDir)), toRealPath(importer)).split(path8.sep).join("/");
3093
- const importerProdDir = path8.posix.dirname(toProdExtension(importerRel));
3197
+ const importerRel = path9.relative(toRealPath(path9.resolve(rootDir)), toRealPath(importer)).split(path9.sep).join("/");
3198
+ const importerProdDir = path9.posix.dirname(toProdExtension(importerRel));
3094
3199
  if (importerProdDir === ".") return relFromDist;
3095
3200
  const target = relFromDist.replace(/^\.\//, "");
3096
- let rel = path8.posix.relative(importerProdDir, target);
3201
+ let rel = path9.posix.relative(importerProdDir, target);
3097
3202
  if (!rel.startsWith(".")) rel = "./" + rel;
3098
3203
  return rel;
3099
3204
  }
@@ -3108,41 +3213,41 @@ var INDEX_EXTS = [
3108
3213
  "/index.cjs"
3109
3214
  ];
3110
3215
  function resolveRelativeSpecifier(importer, specifier) {
3111
- const importerDir = path8.dirname(importer);
3112
- const base = path8.resolve(importerDir, specifier);
3216
+ const importerDir = path9.dirname(importer);
3217
+ const base = path9.resolve(importerDir, specifier);
3113
3218
  if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
3114
- if (fs6.existsSync(base)) return base;
3219
+ if (fs7.existsSync(base)) return base;
3115
3220
  if (base.endsWith(".js")) {
3116
3221
  for (const ext of [".ts", ".tsx", ".jsx"]) {
3117
3222
  const file = base.slice(0, -3) + ext;
3118
- if (fs6.existsSync(file)) return file;
3223
+ if (fs7.existsSync(file)) return file;
3119
3224
  }
3120
3225
  }
3121
3226
  return null;
3122
3227
  }
3123
3228
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
3124
- return fs6.existsSync(base) ? base : null;
3229
+ return fs7.existsSync(base) ? base : null;
3125
3230
  }
3126
3231
  for (const ext of SOURCE_EXTS) {
3127
3232
  const file = base + ext;
3128
- if (fs6.existsSync(file)) return file;
3233
+ if (fs7.existsSync(file)) return file;
3129
3234
  }
3130
3235
  for (const indexExt of INDEX_EXTS) {
3131
3236
  const file = base + indexExt;
3132
- if (fs6.existsSync(file)) return file;
3237
+ if (fs7.existsSync(file)) return file;
3133
3238
  }
3134
3239
  return null;
3135
3240
  }
3136
3241
  function createAliasPlugin(config, options) {
3137
- const appDirAbs = options?.rootDir ? toRealPath(path8.resolve(options.rootDir, APP_DIR)) : null;
3242
+ const appDirAbs = options?.rootDir ? toRealPath(path9.resolve(options.rootDir, APP_DIR)) : null;
3138
3243
  const probeCandidateFile = (candidate) => {
3139
3244
  for (const ext of SOURCE_EXTS) {
3140
3245
  const file = candidate + ext;
3141
- if (fs6.existsSync(file)) return file;
3246
+ if (fs7.existsSync(file)) return file;
3142
3247
  }
3143
3248
  for (const indexExt of INDEX_EXTS) {
3144
3249
  const file = candidate + indexExt;
3145
- if (fs6.existsSync(file)) return file;
3250
+ if (fs7.existsSync(file)) return file;
3146
3251
  }
3147
3252
  return null;
3148
3253
  };
@@ -3152,7 +3257,7 @@ function createAliasPlugin(config, options) {
3152
3257
  build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
3153
3258
  let source;
3154
3259
  try {
3155
- source = fs6.readFileSync(args.path, "utf8");
3260
+ source = fs7.readFileSync(args.path, "utf8");
3156
3261
  } catch {
3157
3262
  return void 0;
3158
3263
  }
@@ -3277,11 +3382,11 @@ async function compileSourceFiles(options) {
3277
3382
  if (entryPoints.length === 0) {
3278
3383
  return { compiledFiles: [] };
3279
3384
  }
3280
- const absDist = path9.resolve(rootDir, dist);
3281
- await fs7.promises.mkdir(absDist, { recursive: true });
3385
+ const absDist = path10.resolve(rootDir, dist);
3386
+ await fs8.promises.mkdir(absDist, { recursive: true });
3282
3387
  const plugins = buildAliasPlugins(rootDir);
3283
3388
  const esbuild = await import("esbuild");
3284
- const outbase = path9.resolve(rootDir, APP_DIR);
3389
+ const outbase = path10.resolve(rootDir, APP_DIR);
3285
3390
  const result = await esbuild.build({
3286
3391
  entryPoints,
3287
3392
  outdir: absDist,
@@ -3301,10 +3406,10 @@ async function compileSourceFiles(options) {
3301
3406
  if (atomicWrite && result.outputFiles) {
3302
3407
  await Promise.all(
3303
3408
  result.outputFiles.map(async (file) => {
3304
- await fs7.promises.mkdir(path9.dirname(file.path), { recursive: true });
3409
+ await fs8.promises.mkdir(path10.dirname(file.path), { recursive: true });
3305
3410
  const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
3306
- await fs7.promises.writeFile(tmp, file.contents);
3307
- await fs7.promises.rename(tmp, file.path);
3411
+ await fs8.promises.writeFile(tmp, file.contents);
3412
+ await fs8.promises.rename(tmp, file.path);
3308
3413
  })
3309
3414
  );
3310
3415
  }
@@ -3317,8 +3422,8 @@ async function compileDevRoutes(options) {
3317
3422
  }
3318
3423
 
3319
3424
  // src/cli/collectImports.ts
3320
- import path10 from "path";
3321
- import fs8 from "fs";
3425
+ import path11 from "path";
3426
+ import fs9 from "fs";
3322
3427
  var SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
3323
3428
  function extractImportSpecifiers(source) {
3324
3429
  const specifiers = [];
@@ -3330,10 +3435,10 @@ function extractImportSpecifiers(source) {
3330
3435
  return specifiers;
3331
3436
  }
3332
3437
  function resolveSpecifierFromDir(dir, specifier) {
3333
- return resolveRelativeSpecifier(path10.join(dir, "__faapi_probe__.ts"), specifier);
3438
+ return resolveRelativeSpecifier(path11.join(dir, "__faapi_probe__.ts"), specifier);
3334
3439
  }
3335
3440
  async function collectRelativeImports(entryFiles, rootDir) {
3336
- const appDirAbs = toRealPath(path10.resolve(rootDir, "src"));
3441
+ const appDirAbs = toRealPath(path11.resolve(rootDir, "src"));
3337
3442
  const tsconfig = readTsconfig(rootDir);
3338
3443
  const visited = /* @__PURE__ */ new Set();
3339
3444
  const insideFiles = /* @__PURE__ */ new Set();
@@ -3343,7 +3448,7 @@ async function collectRelativeImports(entryFiles, rootDir) {
3343
3448
  visited.add(filePath);
3344
3449
  let source;
3345
3450
  try {
3346
- source = await fs8.promises.readFile(filePath, "utf8");
3451
+ source = await fs9.promises.readFile(filePath, "utf8");
3347
3452
  } catch {
3348
3453
  return;
3349
3454
  }
@@ -3365,7 +3470,7 @@ async function collectRelativeImports(entryFiles, rootDir) {
3365
3470
  resolved = null;
3366
3471
  }
3367
3472
  if (!resolved) continue;
3368
- if (!isInsideDir(toRealPath(resolved), toRealPath(path10.resolve(rootDir)))) continue;
3473
+ if (!isInsideDir(toRealPath(resolved), toRealPath(path11.resolve(rootDir)))) continue;
3369
3474
  if (isInsideDir(toRealPath(resolved), appDirAbs)) {
3370
3475
  insideFiles.add(resolved);
3371
3476
  } else {
@@ -3386,8 +3491,8 @@ async function collectRelativeImports(entryFiles, rootDir) {
3386
3491
  // src/cli/compileOnDemand.ts
3387
3492
  function isProductFresh(sourceAbsPath, productAbsPath) {
3388
3493
  try {
3389
- const srcStat = fs9.statSync(sourceAbsPath);
3390
- const prodStat = fs9.statSync(productAbsPath);
3494
+ const srcStat = fs10.statSync(sourceAbsPath);
3495
+ const prodStat = fs10.statSync(productAbsPath);
3391
3496
  return prodStat.mtimeMs >= srcStat.mtimeMs;
3392
3497
  } catch {
3393
3498
  return false;
@@ -3407,14 +3512,13 @@ var state = createDevOnDemandState();
3407
3512
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
3408
3513
  const inFlight2 = state.inFlightCompilations.get(sourceAbsPath);
3409
3514
  if (inFlight2) {
3410
- await inFlight2.catch(() => {
3411
- });
3515
+ await inFlight2;
3412
3516
  return false;
3413
3517
  }
3414
3518
  if (state.compiledFiles.has(sourceAbsPath)) {
3415
3519
  return false;
3416
3520
  }
3417
- if (!fs9.existsSync(sourceAbsPath)) {
3521
+ if (!fs10.existsSync(sourceAbsPath)) {
3418
3522
  return false;
3419
3523
  }
3420
3524
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
@@ -3464,25 +3568,24 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
3464
3568
  }
3465
3569
  }
3466
3570
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
3467
- const rel = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3571
+ const rel = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3468
3572
  if (!rel.startsWith("src/")) return null;
3469
3573
  const relWithoutSrc = rel.slice(4);
3470
3574
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
3471
- return path11.resolve(rootDir, dist, jsRel);
3575
+ return path12.resolve(rootDir, dist, jsRel);
3472
3576
  }
3473
3577
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3474
3578
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3475
3579
  if (inFlight2) {
3476
- await inFlight2.catch(() => {
3477
- });
3580
+ await inFlight2;
3478
3581
  return false;
3479
3582
  }
3480
3583
  if (state.generatedSchemas.has(schemaPath)) {
3481
3584
  return false;
3482
3585
  }
3483
- const prodAbsPath = path11.resolve(rootDir, routeFilePath);
3586
+ const prodAbsPath = path12.resolve(rootDir, routeFilePath);
3484
3587
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
3485
- if (!fs9.existsSync(sourceAbsPath)) {
3588
+ if (!fs10.existsSync(sourceAbsPath)) {
3486
3589
  return false;
3487
3590
  }
3488
3591
  if (isProductFresh(sourceAbsPath, schemaPath)) {
@@ -3493,7 +3596,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3493
3596
  if (fileRoutes.length === 0) {
3494
3597
  return false;
3495
3598
  }
3496
- const sourceRelPath = path11.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3599
+ const sourceRelPath = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3497
3600
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3498
3601
  const generatePromise = (async () => {
3499
3602
  await generateSchemaFiles(sourceRoutes, rootDir, dist);
@@ -3511,19 +3614,19 @@ var sourcePathCache = /* @__PURE__ */ new Map();
3511
3614
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
3512
3615
  const cached = sourcePathCache.get(prodAbsPath);
3513
3616
  if (cached) return cached;
3514
- const rel = path11.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
3617
+ const rel = path12.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
3515
3618
  let relWithoutDist = rel;
3516
3619
  if (relWithoutDist.startsWith(`${dist}/`)) {
3517
3620
  relWithoutDist = relWithoutDist.slice(dist.length + 1);
3518
3621
  }
3519
3622
  const srcRel = `src/${relWithoutDist}`;
3520
3623
  const tsRel = srcRel.replace(/\.js$/, ".ts");
3521
- const tsAbs = path11.resolve(rootDir, tsRel);
3624
+ const tsAbs = path12.resolve(rootDir, tsRel);
3522
3625
  let result;
3523
- if (fs9.existsSync(tsAbs)) {
3626
+ if (fs10.existsSync(tsAbs)) {
3524
3627
  result = tsAbs;
3525
3628
  } else {
3526
- result = path11.resolve(rootDir, srcRel);
3629
+ result = path12.resolve(rootDir, srcRel);
3527
3630
  }
3528
3631
  sourcePathCache.set(prodAbsPath, result);
3529
3632
  return result;
@@ -3576,9 +3679,9 @@ async function validateInput(schemaPath, method, inputType, input) {
3576
3679
  function mapZodIssues(error) {
3577
3680
  return error.issues.map((issue) => {
3578
3681
  const code = mapZodCode(issue);
3579
- const path15 = issue.path.map(String).join(".") || "";
3682
+ const path16 = issue.path.map(String).join(".") || "";
3580
3683
  return {
3581
- path: path15,
3684
+ path: path16,
3582
3685
  code,
3583
3686
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
3584
3687
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -3628,7 +3731,7 @@ import {
3628
3731
  import { createSecureServer as createHttp2SecureServer } from "http2";
3629
3732
  import { readFileSync } from "fs";
3630
3733
  import { Readable as Readable2 } from "stream";
3631
- import path13 from "path";
3734
+ import path14 from "path";
3632
3735
 
3633
3736
  // src/router/matchRoute.ts
3634
3737
  var httpIndexCache = /* @__PURE__ */ new WeakMap();
@@ -3670,65 +3773,72 @@ function getWsIndex(routes) {
3670
3773
  wsIndexCache.set(routes, index);
3671
3774
  return index;
3672
3775
  }
3673
- function matchRoute(routes, method, path15) {
3776
+ function matchRoute(routes, method, path16) {
3674
3777
  const index = getHttpIndex(routes);
3675
3778
  const upper = method.toUpperCase();
3676
- const hit = matchByMethod(index, upper, path15);
3779
+ const hit = matchByMethod(index, upper, path16);
3677
3780
  if (hit) return hit;
3678
3781
  if (upper === "HEAD") {
3679
- return matchByMethod(index, "GET", path15);
3782
+ return matchByMethod(index, "GET", path16);
3680
3783
  }
3681
3784
  return null;
3682
3785
  }
3683
- function matchByMethod(index, method, path15) {
3684
- const staticHit = index.static.get(`${method}|${path15}`);
3786
+ function matchByMethod(index, method, path16) {
3787
+ const staticHit = index.static.get(`${method}|${path16}`);
3685
3788
  if (staticHit) {
3686
3789
  return { route: staticHit, params: {} };
3687
3790
  }
3791
+ if (index.dynamics.length === 0) {
3792
+ return null;
3793
+ }
3794
+ const pathSegments = path16.split("/").filter(Boolean);
3688
3795
  for (const entry of index.dynamics) {
3689
3796
  const route = entry.route;
3690
3797
  if (route.method !== method) {
3691
3798
  continue;
3692
3799
  }
3693
- const params = matchSegments(entry.segments, path15, route.paramNames, route.isCatchAll);
3800
+ const params = matchSegments(entry.segments, pathSegments, route.paramNames, route.isCatchAll);
3694
3801
  if (params !== null) {
3695
3802
  return { route, params };
3696
3803
  }
3697
3804
  }
3698
3805
  return null;
3699
3806
  }
3700
- function matchWsRoute(wsRoutes, path15) {
3807
+ function matchWsRoute(wsRoutes, path16) {
3701
3808
  const index = getWsIndex(wsRoutes);
3702
- const staticHit = index.static.get(path15);
3809
+ const staticHit = index.static.get(path16);
3703
3810
  if (staticHit) {
3704
3811
  return { route: staticHit, params: {} };
3705
3812
  }
3706
3813
  for (const route of index.dynamics) {
3707
- const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
3814
+ const params = matchDynamicPath(route.urlPath, path16, route.paramNames, route.isCatchAll);
3708
3815
  if (params !== null) {
3709
3816
  return { route, params };
3710
3817
  }
3711
3818
  }
3712
3819
  return null;
3713
3820
  }
3714
- function findAllowedMethods(routes, path15) {
3821
+ function findAllowedMethods(routes, path16) {
3715
3822
  const index = getHttpIndex(routes);
3716
3823
  const methods = /* @__PURE__ */ new Set();
3717
- const staticMethods = index.methodsByStaticPath.get(path15);
3824
+ const staticMethods = index.methodsByStaticPath.get(path16);
3718
3825
  if (staticMethods) {
3719
3826
  for (const method of staticMethods) {
3720
3827
  methods.add(method);
3721
3828
  }
3722
3829
  }
3723
- for (const entry of index.dynamics) {
3724
- const params = matchSegments(
3725
- entry.segments,
3726
- path15,
3727
- entry.route.paramNames,
3728
- entry.route.isCatchAll
3729
- );
3730
- if (params !== null) {
3731
- methods.add(entry.route.method);
3830
+ if (index.dynamics.length > 0) {
3831
+ const pathSegments = path16.split("/").filter(Boolean);
3832
+ for (const entry of index.dynamics) {
3833
+ const params = matchSegments(
3834
+ entry.segments,
3835
+ pathSegments,
3836
+ entry.route.paramNames,
3837
+ entry.route.isCatchAll
3838
+ );
3839
+ if (params !== null) {
3840
+ methods.add(entry.route.method);
3841
+ }
3732
3842
  }
3733
3843
  }
3734
3844
  if (methods.has("GET")) {
@@ -3736,11 +3846,15 @@ function findAllowedMethods(routes, path15) {
3736
3846
  }
3737
3847
  return Array.from(methods);
3738
3848
  }
3739
- function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
3740
- return matchSegments(pattern.split("/").filter(Boolean), path15, paramNames, isCatchAll);
3849
+ function matchDynamicPath(pattern, path16, paramNames, isCatchAll) {
3850
+ return matchSegments(
3851
+ pattern.split("/").filter(Boolean),
3852
+ path16.split("/").filter(Boolean),
3853
+ paramNames,
3854
+ isCatchAll
3855
+ );
3741
3856
  }
3742
- function matchSegments(patternSegments, path15, paramNames, isCatchAll) {
3743
- const pathSegments = path15.split("/").filter(Boolean);
3857
+ function matchSegments(patternSegments, pathSegments, paramNames, isCatchAll) {
3744
3858
  if (isCatchAll) {
3745
3859
  const nonCatchAllCount = patternSegments.length - 1;
3746
3860
  if (pathSegments.length <= nonCatchAllCount) {
@@ -3956,6 +4070,20 @@ async function sendNodeResponse(response, res) {
3956
4070
  }
3957
4071
  }
3958
4072
  if (response.body) {
4073
+ if (hasBufferedBody(response)) {
4074
+ const buffer = await readBufferedBody(response.body);
4075
+ await new Promise((resolve) => {
4076
+ res.on("close", () => {
4077
+ if (!res.writableEnded) res.destroy();
4078
+ });
4079
+ if (res.destroyed) {
4080
+ resolve();
4081
+ return;
4082
+ }
4083
+ res.end(buffer, () => resolve());
4084
+ });
4085
+ return;
4086
+ }
3959
4087
  const nodeStream = Readable.fromWeb(response.body);
3960
4088
  await new Promise((resolve, reject) => {
3961
4089
  nodeStream.on("error", reject);
@@ -3974,6 +4102,25 @@ async function sendNodeResponse(response, res) {
3974
4102
  }
3975
4103
  res.end();
3976
4104
  }
4105
+ async function readBufferedBody(body) {
4106
+ const reader = body.getReader();
4107
+ const chunks = [];
4108
+ try {
4109
+ for (; ; ) {
4110
+ const { done, value } = await reader.read();
4111
+ if (done) break;
4112
+ chunks.push(value);
4113
+ }
4114
+ } finally {
4115
+ try {
4116
+ reader.releaseLock();
4117
+ } catch {
4118
+ }
4119
+ }
4120
+ if (chunks.length === 0) return new Uint8Array(0);
4121
+ if (chunks.length === 1) return chunks[0];
4122
+ return Buffer.concat(chunks);
4123
+ }
3977
4124
 
3978
4125
  // src/utils/getClientIp.ts
3979
4126
  function getClientIp(req, trustedProxy = false) {
@@ -3994,6 +4141,20 @@ function getClientIp(req, trustedProxy = false) {
3994
4141
  return "";
3995
4142
  }
3996
4143
 
4144
+ // src/middleware/mergeVary.ts
4145
+ function mergeVary(meta, value) {
4146
+ const existing = meta.headers["Vary"] ?? meta.headers["vary"];
4147
+ if (!existing) {
4148
+ meta.headers["Vary"] = value;
4149
+ return;
4150
+ }
4151
+ const segments = existing.split(",").map((s) => s.trim().toLowerCase());
4152
+ if (!segments.includes(value.toLowerCase())) {
4153
+ const key = meta.headers["Vary"] !== void 0 ? "Vary" : "vary";
4154
+ meta.headers[key] = `${existing}, ${value}`;
4155
+ }
4156
+ }
4157
+
3997
4158
  // src/middleware/cors.ts
3998
4159
  var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
3999
4160
  function cors(options = {}) {
@@ -4006,6 +4167,7 @@ function cors(options = {}) {
4006
4167
  maxAge
4007
4168
  } = options;
4008
4169
  return async (ctx, next) => {
4170
+ const meta = ctx.meta;
4009
4171
  const reqOrigin = ctx.headers.get("origin");
4010
4172
  if (!reqOrigin) {
4011
4173
  await next();
@@ -4020,14 +4182,7 @@ function cors(options = {}) {
4020
4182
  allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
4021
4183
  }
4022
4184
  if (origin === true || Array.isArray(origin)) {
4023
- const existingVary = ctx.headers.get("vary");
4024
- if (existingVary) {
4025
- if (!existingVary.toLowerCase().includes("origin")) {
4026
- ctx.setHeader("Vary", `${existingVary}, Origin`);
4027
- }
4028
- } else {
4029
- ctx.setHeader("Vary", "Origin");
4030
- }
4185
+ mergeVary(meta, "Origin");
4031
4186
  }
4032
4187
  if (!allowOrigin) {
4033
4188
  await next();
@@ -4178,17 +4333,6 @@ async function compressBody(encoding, body) {
4178
4333
  return deflateAsync(buf);
4179
4334
  }
4180
4335
  }
4181
- function mergeVary(meta, value) {
4182
- const existing = meta.headers["Vary"] ?? meta.headers["vary"];
4183
- if (!existing) {
4184
- meta.headers["Vary"] = value;
4185
- return;
4186
- }
4187
- if (!existing.toLowerCase().includes(value.toLowerCase())) {
4188
- const key = meta.headers["Vary"] !== void 0 ? "Vary" : "vary";
4189
- meta.headers[key] = `${existing}, ${value}`;
4190
- }
4191
- }
4192
4336
  function compression(options = {}) {
4193
4337
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
4194
4338
  return async (ctx, next) => {
@@ -4338,9 +4482,9 @@ function logger(options = {}) {
4338
4482
  }
4339
4483
 
4340
4484
  // src/server/handleWsUpgrade.ts
4341
- import fs10 from "fs";
4485
+ import fs11 from "fs";
4342
4486
  import { WebSocketServer, WebSocket } from "ws";
4343
- import path12 from "path";
4487
+ import path13 from "path";
4344
4488
 
4345
4489
  // src/server/serverUtils.ts
4346
4490
  function nodeHttpToWebHeaders(req) {
@@ -4359,13 +4503,15 @@ function buildErrorResponse(err, config) {
4359
4503
  try {
4360
4504
  return formatErrorResponse(err, config);
4361
4505
  } catch {
4362
- return new Response(
4506
+ const fallback = new Response(
4363
4507
  JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
4364
4508
  {
4365
4509
  status: 500,
4366
4510
  headers: { "Content-Type": "application/json" }
4367
4511
  }
4368
4512
  );
4513
+ markBufferedBody(fallback);
4514
+ return fallback;
4369
4515
  }
4370
4516
  }
4371
4517
 
@@ -4396,7 +4542,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
4396
4542
  const dist = getDevDist();
4397
4543
  if (dist) {
4398
4544
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
4399
- if (sourcePath && fs10.existsSync(sourcePath)) {
4545
+ if (sourcePath && fs11.existsSync(sourcePath)) {
4400
4546
  await ensureCompiled(sourcePath, rootDir, dist);
4401
4547
  }
4402
4548
  }
@@ -4411,27 +4557,47 @@ async function loadWsHandler(filePath, ctx, rootDir) {
4411
4557
  function bindEvents(rawSocket, handlers) {
4412
4558
  if (!handlers) return;
4413
4559
  const ws = wrapWsSocket(rawSocket);
4560
+ const guard = (event, fn) => {
4561
+ try {
4562
+ fn();
4563
+ } catch (err) {
4564
+ const error = err instanceof Error ? err : new Error(String(err));
4565
+ if (handlers.onError) {
4566
+ try {
4567
+ handlers.onError(ws, error);
4568
+ } catch (hookErr) {
4569
+ console.error(`[faapi] WS onError \u56DE\u8C03\u629B\u9519 (after ${event}):`, hookErr);
4570
+ }
4571
+ } else {
4572
+ console.error(`[faapi] WS ${event} \u56DE\u8C03\u629B\u9519:`, error);
4573
+ }
4574
+ }
4575
+ };
4414
4576
  if (handlers.onOpen) {
4415
4577
  if (rawSocket.readyState === WebSocket.OPEN) {
4416
- handlers.onOpen(ws);
4578
+ guard("onOpen", () => handlers.onOpen(ws));
4417
4579
  } else {
4418
- rawSocket.once("open", () => handlers.onOpen(ws));
4580
+ rawSocket.once("open", () => guard("onOpen", () => handlers.onOpen(ws)));
4419
4581
  }
4420
4582
  }
4421
4583
  if (handlers.onMessage) {
4422
4584
  rawSocket.on("message", (data, isBinary) => {
4423
4585
  const buf = toWsBuffer(data);
4424
- handlers.onMessage(ws, isBinary ? buf : buf.toString("utf8"));
4586
+ guard("onMessage", () => handlers.onMessage(ws, isBinary ? buf : buf.toString("utf8")));
4425
4587
  });
4426
4588
  }
4427
4589
  if (handlers.onClose) {
4428
4590
  rawSocket.on("close", (code, reason) => {
4429
- handlers.onClose(ws, code, reason.toString("utf8"));
4591
+ guard("onClose", () => handlers.onClose(ws, code, reason.toString("utf8")));
4430
4592
  });
4431
4593
  }
4432
4594
  if (handlers.onError) {
4433
4595
  rawSocket.on("error", (err) => {
4434
- handlers.onError(ws, err);
4596
+ try {
4597
+ handlers.onError(ws, err);
4598
+ } catch (hookErr) {
4599
+ console.error("[faapi] WS onError \u56DE\u8C03\u629B\u9519 (after onError):", hookErr);
4600
+ }
4435
4601
  });
4436
4602
  }
4437
4603
  }
@@ -4510,7 +4676,7 @@ function attachWebSocket(options) {
4510
4676
  const finalHandler = async () => {
4511
4677
  let handlers;
4512
4678
  try {
4513
- const absoluteFilePath = path12.resolve(rootDir, route.filePath);
4679
+ const absoluteFilePath = path13.resolve(rootDir, route.filePath);
4514
4680
  handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
4515
4681
  } catch (err) {
4516
4682
  const reason = err instanceof Error ? err.message : String(err);
@@ -4715,9 +4881,17 @@ function createServer(options) {
4715
4881
  bodyLimit,
4716
4882
  trustedProxy,
4717
4883
  registries
4718
- ).catch(() => {
4719
- res.statusCode = 500;
4720
- res.end();
4884
+ ).catch((err) => {
4885
+ console.error("[faapi] Request pipeline failed after error response attempt:", err);
4886
+ try {
4887
+ if (!res.headersSent) {
4888
+ res.statusCode = 500;
4889
+ res.end();
4890
+ } else if (!res.writableEnded) {
4891
+ res.end();
4892
+ }
4893
+ } catch {
4894
+ }
4721
4895
  });
4722
4896
  });
4723
4897
  attachWebSocket({
@@ -4760,13 +4934,14 @@ function getRoutePaths(route, rootDir, dist) {
4760
4934
  let cached = routePathCache.get(route);
4761
4935
  if (!cached) {
4762
4936
  cached = {
4763
- absFilePath: path13.resolve(rootDir, route.filePath),
4937
+ absFilePath: path14.resolve(rootDir, route.filePath),
4764
4938
  schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
4765
4939
  };
4766
4940
  routePathCache.set(route, cached);
4767
4941
  }
4768
4942
  return cached;
4769
4943
  }
4944
+ var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
4770
4945
  function createRoutePipeline(opts) {
4771
4946
  const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
4772
4947
  return async () => {
@@ -4799,7 +4974,15 @@ function createRoutePipeline(opts) {
4799
4974
  route.injectors = {};
4800
4975
  }
4801
4976
  }
4802
- const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
4977
+ let mergedInjectors = route.injectors;
4978
+ if (globalInjectors) {
4979
+ let merged = mergedInjectorsCache.get(route);
4980
+ if (!merged) {
4981
+ merged = { ...globalInjectors, ...route.injectors };
4982
+ mergedInjectorsCache.set(route, merged);
4983
+ }
4984
+ mergedInjectors = merged;
4985
+ }
4803
4986
  return await invokeHandler(routeModule.handler, ctx, body, route.middlewares, mergedInjectors);
4804
4987
  };
4805
4988
  }
@@ -4849,6 +5032,10 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
4849
5032
  await sendSuccessResponse(response, res);
4850
5033
  } catch (err) {
4851
5034
  if (res.destroyed || res.writableEnded) return;
5035
+ if (err instanceof PayloadTooLargeError) {
5036
+ res.setHeader("Connection", "close");
5037
+ res.once("finish", () => req.destroy());
5038
+ }
4852
5039
  await sendErrorResponse(err, meta, res, onError, ctx);
4853
5040
  }
4854
5041
  }
@@ -4871,7 +5058,7 @@ async function createTestServer(options) {
4871
5058
  } = options;
4872
5059
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
4873
5060
  const sorted = sortRoutes(routes);
4874
- const schemaDist = dist ? path14.isAbsolute(dist) ? dist : path14.resolve(rootDir, dist) : await fs11.mkdtemp(path14.join(os.tmpdir(), "faapi-test-schema-"));
5061
+ const schemaDist = dist ? path15.isAbsolute(dist) ? dist : path15.resolve(rootDir, dist) : await fs12.mkdtemp(path15.join(os.tmpdir(), "faapi-test-schema-"));
4875
5062
  await generateSchemaFiles(sorted, rootDir, schemaDist);
4876
5063
  const { server } = createServer({
4877
5064
  routes: sorted,
@@ -4903,7 +5090,7 @@ async function createTestServer(options) {
4903
5090
  await new Promise((resolve) => {
4904
5091
  server.close(() => resolve());
4905
5092
  });
4906
- await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
5093
+ await fs12.rm(schemaDist, { recursive: true, force: true }).catch(() => {
4907
5094
  });
4908
5095
  invalidateSchemaCache();
4909
5096
  }