@faapi/faapi 1.5.0 → 2.0.1-canary.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.
@@ -0,0 +1,3578 @@
1
+ // src/runtime/sse.ts
2
+ function encodeSseEvent(event) {
3
+ let out = "";
4
+ if (event.comment !== void 0) {
5
+ out += `: ${event.comment}
6
+ `;
7
+ }
8
+ if (event.event !== void 0) {
9
+ out += `event: ${event.event}
10
+ `;
11
+ }
12
+ if (event.id !== void 0) {
13
+ out += `id: ${event.id}
14
+ `;
15
+ }
16
+ if (event.retry !== void 0) {
17
+ out += `retry: ${event.retry}
18
+ `;
19
+ }
20
+ if (event.data !== void 0) {
21
+ let dataStr;
22
+ if (typeof event.data === "string") {
23
+ dataStr = event.data;
24
+ } else if (event.data === null) {
25
+ dataStr = "null";
26
+ } else {
27
+ dataStr = JSON.stringify(event.data);
28
+ }
29
+ const lines = dataStr.split("\n");
30
+ for (const line of lines) {
31
+ out += `data: ${line}
32
+ `;
33
+ }
34
+ }
35
+ out += "\n";
36
+ return out;
37
+ }
38
+ function createSseWriter() {
39
+ const encoder = new TextEncoder();
40
+ let controller = null;
41
+ let closed = false;
42
+ let aborted = false;
43
+ const stream = new ReadableStream({
44
+ start(c) {
45
+ controller = c;
46
+ },
47
+ cancel() {
48
+ aborted = true;
49
+ closed = true;
50
+ controller = null;
51
+ }
52
+ });
53
+ const response = new Response(stream, {
54
+ status: 200,
55
+ headers: {
56
+ "Content-Type": "text/event-stream",
57
+ "Cache-Control": "no-cache",
58
+ Connection: "keep-alive"
59
+ }
60
+ });
61
+ const writer = {
62
+ send(event) {
63
+ if (closed || !controller) return;
64
+ const text = encodeSseEvent(event);
65
+ controller.enqueue(encoder.encode(text));
66
+ },
67
+ sendRaw(chunk) {
68
+ if (closed || !controller) return;
69
+ const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
70
+ controller.enqueue(bytes);
71
+ },
72
+ sendError(error) {
73
+ if (closed || !controller) return;
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ const text = encodeSseEvent({ event: "error", data: message });
76
+ try {
77
+ controller.enqueue(encoder.encode(text));
78
+ } finally {
79
+ writer.close();
80
+ }
81
+ },
82
+ close() {
83
+ if (closed) return;
84
+ closed = true;
85
+ if (controller) {
86
+ try {
87
+ controller.close();
88
+ } catch {
89
+ }
90
+ controller = null;
91
+ }
92
+ },
93
+ get closed() {
94
+ return closed;
95
+ },
96
+ get aborted() {
97
+ return aborted;
98
+ },
99
+ get response() {
100
+ return response;
101
+ }
102
+ };
103
+ return writer;
104
+ }
105
+
106
+ // src/runtime/createContext.ts
107
+ function parseCookies(cookieHeader) {
108
+ const cookies = /* @__PURE__ */ new Map();
109
+ if (!cookieHeader) return cookies;
110
+ for (const pair of cookieHeader.split(";")) {
111
+ const [name, ...rest] = pair.split("=");
112
+ const trimmed = name?.trim();
113
+ if (trimmed) {
114
+ cookies.set(trimmed, rest.join("=").trim());
115
+ }
116
+ }
117
+ return cookies;
118
+ }
119
+ function formatSetCookie(name, value, options) {
120
+ let cookie = `${name}=${value}`;
121
+ if (options?.domain) cookie += `; Domain=${options.domain}`;
122
+ if (options?.path) cookie += `; Path=${options.path}`;
123
+ if (options?.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
124
+ if (options?.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
125
+ if (options?.httpOnly) cookie += `; HttpOnly`;
126
+ if (options?.secure) cookie += `; Secure`;
127
+ if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
128
+ return cookie;
129
+ }
130
+ function createContext(request, params, config = {}, ip = "") {
131
+ const url = new URL(request.url);
132
+ const meta = { headers: {}, setCookies: [] };
133
+ const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
134
+ const cookiesObj = {};
135
+ for (const [key, val] of parsedCookies) {
136
+ cookiesObj[key] = val;
137
+ }
138
+ const ctx = {
139
+ request,
140
+ params,
141
+ query: url.searchParams,
142
+ headers: request.headers,
143
+ method: request.method,
144
+ path: url.pathname,
145
+ ip,
146
+ ua: request.headers.get("user-agent") ?? "",
147
+ cookies: cookiesObj,
148
+ config,
149
+ meta,
150
+ setStatus(status) {
151
+ meta.status = status;
152
+ },
153
+ setHeader(key, value) {
154
+ meta.headers[key] = value;
155
+ },
156
+ setETag(value) {
157
+ meta.headers["etag"] = value;
158
+ },
159
+ redirect(url2, status = 302) {
160
+ return new Response(null, {
161
+ status,
162
+ headers: { Location: url2 }
163
+ });
164
+ },
165
+ json(data, status) {
166
+ const headers = { "Content-Type": "application/json" };
167
+ return new Response(JSON.stringify(data), {
168
+ status: status ?? 200,
169
+ headers
170
+ });
171
+ },
172
+ html(html, status) {
173
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
174
+ return new Response(html, {
175
+ status: status ?? 200,
176
+ headers
177
+ });
178
+ },
179
+ getCookie(name) {
180
+ return parsedCookies.get(name);
181
+ },
182
+ setCookie(name, value, options) {
183
+ meta.setCookies.push(formatSetCookie(name, value, options));
184
+ },
185
+ deleteCookie(name) {
186
+ meta.setCookies.push(formatSetCookie(name, "", { maxAge: 0 }));
187
+ },
188
+ /**
189
+ * 创建 SSE writer,用于流式推送事件
190
+ *
191
+ * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
192
+ * 作为 HTTP 响应(Content-Type: text/event-stream)。
193
+ *
194
+ * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
195
+ */
196
+ sse() {
197
+ const writer = createSseWriter();
198
+ const ctxWithSse = ctx;
199
+ ctxWithSse.__sseResponse = writer.response;
200
+ ctxWithSse.__sseWriter = writer;
201
+ return writer;
202
+ },
203
+ /**
204
+ * 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
205
+ *
206
+ * 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
207
+ * handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
208
+ */
209
+ ok(data) {
210
+ const responseConfig = config.response;
211
+ const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
212
+ const body = okFn(data);
213
+ return ctx.json(body);
214
+ },
215
+ /**
216
+ * 返回错误响应(对象形式参数,status 和 code 均可省略)
217
+ *
218
+ * - status 省略时 HTTP 状态码默认 500
219
+ * - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
220
+ * - status 和 code 独立无关联
221
+ *
222
+ * body 用 config.response.fail(或默认实现)包装。
223
+ */
224
+ fail(options) {
225
+ const responseConfig = config.response;
226
+ const failFn = responseConfig?.fail ?? ((e) => {
227
+ const error = { message: e.message };
228
+ if (e.code !== void 0) error.code = e.code;
229
+ return { error };
230
+ });
231
+ const body = failFn({
232
+ status: options.status,
233
+ code: options.code,
234
+ message: options.message
235
+ });
236
+ return ctx.json(body, options.status ?? 500);
237
+ }
238
+ };
239
+ const extend = config?.extendContext;
240
+ if (typeof extend === "function") {
241
+ extend(ctx);
242
+ }
243
+ return ctx;
244
+ }
245
+ function createTestContext(options) {
246
+ const { method = "GET", path: path11, query, headers, params = {}, config = {}, ip = "" } = options;
247
+ const url = new URL(`http://localhost${path11}`);
248
+ if (query) {
249
+ for (const [key, value] of Object.entries(query)) {
250
+ if (Array.isArray(value)) {
251
+ for (const item of value) {
252
+ url.searchParams.append(key, String(item));
253
+ }
254
+ } else {
255
+ url.searchParams.set(key, String(value));
256
+ }
257
+ }
258
+ }
259
+ const request = new Request(url.toString(), {
260
+ method,
261
+ headers
262
+ });
263
+ return createContext(request, params, config, ip);
264
+ }
265
+
266
+ // src/utils/isPlainObject.ts
267
+ function isPlainObject(value) {
268
+ if (value === null || typeof value !== "object") {
269
+ return false;
270
+ }
271
+ if (Array.isArray(value)) {
272
+ return false;
273
+ }
274
+ const proto = Object.getPrototypeOf(value);
275
+ return proto === null || proto === Object.prototype;
276
+ }
277
+
278
+ // src/response/toResponse.ts
279
+ async function toResponse(value, meta) {
280
+ if (value instanceof Promise) {
281
+ return toResponse(await value, meta);
282
+ }
283
+ const applyMeta = (headers2) => {
284
+ if (!meta) return;
285
+ for (const [key, val] of Object.entries(meta.headers)) {
286
+ headers2.set(key, val);
287
+ }
288
+ for (const cookie of meta.setCookies ?? []) {
289
+ headers2.append("set-cookie", cookie);
290
+ }
291
+ };
292
+ if (value instanceof Response) {
293
+ if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
294
+ const headers2 = new Headers(value.headers);
295
+ applyMeta(headers2);
296
+ return new Response(value.body, {
297
+ status: meta.status ?? value.status,
298
+ headers: headers2
299
+ });
300
+ }
301
+ return value;
302
+ }
303
+ if (value === null || value === void 0) {
304
+ const status = meta?.status ?? 204;
305
+ const headers2 = new Headers();
306
+ applyMeta(headers2);
307
+ return new Response(null, { status, headers: headers2 });
308
+ }
309
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
310
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
311
+ applyMeta(headers2);
312
+ return new Response(value, {
313
+ status: meta?.status ?? 200,
314
+ headers: headers2
315
+ });
316
+ }
317
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
318
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
319
+ applyMeta(headers2);
320
+ return new Response(value, {
321
+ status: meta?.status ?? 200,
322
+ headers: headers2
323
+ });
324
+ }
325
+ if (value instanceof Uint8Array) {
326
+ const headers2 = new Headers({ "Content-Type": "application/octet-stream" });
327
+ applyMeta(headers2);
328
+ return new Response(value, {
329
+ status: meta?.status ?? 200,
330
+ headers: headers2
331
+ });
332
+ }
333
+ if (isPlainObject(value) || Array.isArray(value)) {
334
+ const body2 = JSON.stringify(value);
335
+ const headers2 = new Headers({ "Content-Type": "application/json" });
336
+ applyMeta(headers2);
337
+ return new Response(body2, {
338
+ status: meta?.status ?? 200,
339
+ headers: headers2
340
+ });
341
+ }
342
+ if (typeof value === "string") {
343
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
344
+ applyMeta(headers2);
345
+ return new Response(value, {
346
+ status: meta?.status ?? 200,
347
+ headers: headers2
348
+ });
349
+ }
350
+ if (typeof value === "number" || typeof value === "boolean") {
351
+ const headers2 = new Headers({ "Content-Type": "text/plain" });
352
+ applyMeta(headers2);
353
+ return new Response(String(value), {
354
+ status: meta?.status ?? 200,
355
+ headers: headers2
356
+ });
357
+ }
358
+ const body = JSON.stringify(value);
359
+ const headers = new Headers({ "Content-Type": "application/json" });
360
+ applyMeta(headers);
361
+ return new Response(body, {
362
+ status: meta?.status ?? 200,
363
+ headers
364
+ });
365
+ }
366
+
367
+ // src/injection/resolveInjection.ts
368
+ import ts from "typescript";
369
+ var PARAM_TYPE_MAP = {
370
+ query: "query",
371
+ body: "body",
372
+ form: "form",
373
+ headers: "headers",
374
+ params: "params",
375
+ context: "context",
376
+ ctx: "context",
377
+ // 别名
378
+ cookies: "cookies",
379
+ ip: "ip",
380
+ ua: "ua",
381
+ files: "files",
382
+ fields: "fields"
383
+ };
384
+ function resolveInjection(fn) {
385
+ const fnStr = fn.toString();
386
+ const params = extractParamsWithAst(fnStr);
387
+ return params.map((param) => {
388
+ const type = PARAM_TYPE_MAP[param.name] || "unknown";
389
+ return {
390
+ name: param.name,
391
+ type,
392
+ hasType: false
393
+ // 运行时类型已擦除
394
+ };
395
+ });
396
+ }
397
+ function extractParamsWithAst(fnStr) {
398
+ const sourceFile = ts.createSourceFile(
399
+ "__faapi_injection__.ts",
400
+ fnStr,
401
+ ts.ScriptTarget.Latest,
402
+ true
403
+ );
404
+ const paramNames = [];
405
+ function visit(node) {
406
+ if (ts.isFunctionDeclaration(node) && node.parameters.length > 0) {
407
+ for (const param of node.parameters) {
408
+ extractParamName(param, paramNames);
409
+ }
410
+ return;
411
+ }
412
+ if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.parameters.length > 0) {
413
+ for (const param of node.parameters) {
414
+ extractParamName(param, paramNames);
415
+ }
416
+ return;
417
+ }
418
+ ts.forEachChild(node, visit);
419
+ }
420
+ visit(sourceFile);
421
+ return paramNames.map((name) => ({ name }));
422
+ }
423
+ function extractParamName(param, names) {
424
+ const name = param.name;
425
+ if (ts.isIdentifier(name)) {
426
+ names.push(name.text);
427
+ return;
428
+ }
429
+ if (ts.isObjectBindingPattern(name)) {
430
+ for (const element of name.elements) {
431
+ if (ts.isBindingElement(element)) {
432
+ const elemName = element.name;
433
+ if (ts.isIdentifier(elemName)) {
434
+ names.push(elemName.text);
435
+ }
436
+ }
437
+ }
438
+ return;
439
+ }
440
+ if (ts.isArrayBindingPattern(name)) {
441
+ for (const element of name.elements) {
442
+ if (element && ts.isBindingElement(element)) {
443
+ const elemName = element.name;
444
+ if (ts.isIdentifier(elemName)) {
445
+ names.push(elemName.text);
446
+ }
447
+ }
448
+ }
449
+ return;
450
+ }
451
+ }
452
+
453
+ // src/utils/queryToObject.ts
454
+ function queryToObject(params) {
455
+ const result = {};
456
+ for (const [key, value] of params) {
457
+ result[key] = value;
458
+ }
459
+ return result;
460
+ }
461
+
462
+ // src/injection/injectParams.ts
463
+ function getBuiltinInjectionValue(type, ctx, body) {
464
+ switch (type) {
465
+ case "query":
466
+ return queryToObject(ctx.query);
467
+ case "params":
468
+ return ctx.params;
469
+ case "headers":
470
+ return ctx.headers;
471
+ case "context":
472
+ return ctx;
473
+ case "cookies":
474
+ return ctx.cookies;
475
+ case "ip":
476
+ return ctx.ip;
477
+ case "ua":
478
+ return ctx.ua;
479
+ case "body":
480
+ return body;
481
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
482
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
483
+ case "form":
484
+ return body;
485
+ case "files":
486
+ if (body && typeof body === "object" && "files" in body) {
487
+ return body.files;
488
+ }
489
+ return [];
490
+ case "fields":
491
+ if (body && typeof body === "object" && "fields" in body) {
492
+ return body.fields;
493
+ }
494
+ return {};
495
+ default:
496
+ return void 0;
497
+ }
498
+ }
499
+ async function injectParamsAsync(handler, ctx, body, injectors) {
500
+ const injections = resolveInjection(handler);
501
+ if (injections.length === 0) {
502
+ return await handler();
503
+ }
504
+ const args = await Promise.all(
505
+ injections.map(async (injection) => {
506
+ if (injection.type !== "unknown") {
507
+ return getBuiltinInjectionValue(injection.type, ctx, body);
508
+ }
509
+ if (injectors && injection.name in injectors) {
510
+ return await injectors[injection.name](ctx);
511
+ }
512
+ return void 0;
513
+ })
514
+ );
515
+ return await handler(...args);
516
+ }
517
+
518
+ // src/runtime/invokeHandler.ts
519
+ function wrapResult(result, ctx) {
520
+ if (result instanceof Response) return result;
521
+ const responseConfig = ctx.config.response;
522
+ const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
523
+ return okFn(result);
524
+ }
525
+ function mergeMeta(response, meta) {
526
+ const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
527
+ if (!hasMeta) return response;
528
+ const headers = new Headers(response.headers);
529
+ for (const [key, value] of Object.entries(meta.headers)) {
530
+ headers.set(key, value);
531
+ }
532
+ for (const cookie of meta.setCookies) {
533
+ headers.append("set-cookie", cookie);
534
+ }
535
+ return new Response(response.body, {
536
+ status: meta.status ?? response.status,
537
+ headers
538
+ });
539
+ }
540
+ async function compose(middlewares, ctx, finalHandler) {
541
+ const meta = ctx.meta;
542
+ let index = -1;
543
+ async function dispatch(i) {
544
+ if (i <= index) {
545
+ throw new Error("next() called multiple times");
546
+ }
547
+ index = i;
548
+ if (i >= middlewares.length) {
549
+ return await finalHandler();
550
+ }
551
+ const mw = middlewares[i];
552
+ let innerResponse;
553
+ const next = async () => {
554
+ innerResponse = await dispatch(i + 1);
555
+ return innerResponse;
556
+ };
557
+ const result = await mw(ctx, next);
558
+ if (result instanceof Response) {
559
+ return mergeMeta(result, meta);
560
+ }
561
+ if (innerResponse !== void 0) {
562
+ return innerResponse;
563
+ }
564
+ throw new Error("\u4E2D\u95F4\u4EF6\u5FC5\u987B await next() \u6216\u8FD4\u56DE Response");
565
+ }
566
+ return await dispatch(0);
567
+ }
568
+ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
569
+ const meta = ctx.meta;
570
+ const pickSseAndAutoClose = () => {
571
+ const sseWriter = ctx.__sseWriter;
572
+ if (!sseWriter) return null;
573
+ if (!sseWriter.closed && !sseWriter.aborted) {
574
+ sseWriter.close();
575
+ }
576
+ return mergeMeta(sseWriter.response, meta);
577
+ };
578
+ const autoCloseSseOnError = () => {
579
+ const sseWriter = ctx.__sseWriter;
580
+ if (sseWriter && !sseWriter.closed && !sseWriter.aborted) {
581
+ sseWriter.close();
582
+ }
583
+ };
584
+ if (!middlewares || middlewares.length === 0) {
585
+ try {
586
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
587
+ const sseResponse = pickSseAndAutoClose();
588
+ if (sseResponse) return sseResponse;
589
+ return toResponse(wrapResult(result, ctx), meta);
590
+ } catch (err) {
591
+ autoCloseSseOnError();
592
+ throw err;
593
+ }
594
+ }
595
+ const finalHandler = async () => {
596
+ try {
597
+ const result = await injectParamsAsync(handler, ctx, body, injectors);
598
+ const sseResponse = pickSseAndAutoClose();
599
+ if (sseResponse) return sseResponse;
600
+ return toResponse(wrapResult(result, ctx), meta);
601
+ } catch (err) {
602
+ autoCloseSseOnError();
603
+ throw err;
604
+ }
605
+ };
606
+ return await compose(middlewares, ctx, finalHandler);
607
+ }
608
+
609
+ // src/testServer.ts
610
+ import path10 from "path";
611
+ import os from "os";
612
+ import fs9 from "fs/promises";
613
+
614
+ // src/router/scanRoutes.ts
615
+ import fg from "fast-glob";
616
+ import path from "path";
617
+ import fs from "fs";
618
+
619
+ // src/router/constants.ts
620
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
621
+ var HTTP_METHOD_SET = new Set(HTTP_METHODS);
622
+
623
+ // src/utils/normalizePath.ts
624
+ function normalizePath(path11) {
625
+ if (!path11) return "";
626
+ let result = path11.replace(/\\/g, "/");
627
+ result = result.replace(/\/+/g, "/");
628
+ result = result.replace(/\/+$/, "");
629
+ if (result && !result.startsWith("/")) {
630
+ result = "/" + result;
631
+ }
632
+ return result;
633
+ }
634
+
635
+ // src/router/parseRouteFile.ts
636
+ function dynamicSegmentToParam(segment) {
637
+ const match = segment.match(/^\[(.+)\]$/);
638
+ if (match) {
639
+ return ":" + match[1];
640
+ }
641
+ return segment;
642
+ }
643
+ function extractParamNames(urlPath) {
644
+ const params = [];
645
+ const segments = urlPath.split("/");
646
+ for (const segment of segments) {
647
+ if (segment.startsWith(":...")) {
648
+ params.push(segment.slice(4));
649
+ } else if (segment.startsWith(":")) {
650
+ params.push(segment.slice(1));
651
+ }
652
+ }
653
+ return params;
654
+ }
655
+ function isCatchAllSegment(segment) {
656
+ return /^\[\.\.\..+\]$/.test(segment);
657
+ }
658
+ function isRouteGroup(segment) {
659
+ return /^\(.+\)$/.test(segment);
660
+ }
661
+ function filePathToUrlPath(filePath) {
662
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
663
+ const lastSlashIndex = withoutPrefix.lastIndexOf("/");
664
+ const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
665
+ if (!dirPath) {
666
+ return "";
667
+ }
668
+ const segments = dirPath.split("/").filter((s) => !isRouteGroup(s)).map(dynamicSegmentToParam);
669
+ return normalizePath(segments.join("/"));
670
+ }
671
+
672
+ // src/utils/importWithCacheBust.ts
673
+ import { pathToFileURL } from "url";
674
+ var loadTs;
675
+ function getVitestImportActual() {
676
+ const vi = globalThis.vi;
677
+ if (typeof vi?.importActual !== "function") return void 0;
678
+ return vi.importActual.bind(vi);
679
+ }
680
+ async function importWithCacheBust(filePath, bustViteCache = false) {
681
+ const importActual = getVitestImportActual();
682
+ if (importActual) {
683
+ if (bustViteCache) {
684
+ let url2 = pathToFileURL(filePath).href;
685
+ url2 += `?t=${Date.now()}`;
686
+ return await import(url2);
687
+ }
688
+ return await importActual(filePath);
689
+ }
690
+ let url = pathToFileURL(filePath).href;
691
+ if (loadTs !== void 0) {
692
+ url += `?t=${loadTs}`;
693
+ }
694
+ return await import(url);
695
+ }
696
+
697
+ // src/middleware/loadMiddlewares.ts
698
+ var middlewareCache = /* @__PURE__ */ new Map();
699
+ function getCachedMiddlewares(absPath) {
700
+ return middlewareCache.get(absPath);
701
+ }
702
+ function setCachedMiddlewares(absPath, bundle) {
703
+ middlewareCache.set(absPath, bundle);
704
+ }
705
+ async function loadMiddlewaresFile(filePath) {
706
+ try {
707
+ const module = await importWithCacheBust(filePath);
708
+ const middlewares = module.default ?? module.middlewares ?? [];
709
+ if (!Array.isArray(middlewares)) {
710
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
711
+ return { middlewares: [], injectors: {} };
712
+ }
713
+ const validMiddlewares = middlewares.filter((m) => {
714
+ if (typeof m !== "function") {
715
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
716
+ return false;
717
+ }
718
+ return true;
719
+ });
720
+ const injectors = module.injectors ?? {};
721
+ if (typeof injectors !== "object" || injectors === null) {
722
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
723
+ return { middlewares: validMiddlewares, injectors: {} };
724
+ }
725
+ const validInjectors = {};
726
+ for (const [name, injector] of Object.entries(injectors)) {
727
+ if (typeof injector !== "function") {
728
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
729
+ continue;
730
+ }
731
+ validInjectors[name] = injector;
732
+ }
733
+ return { middlewares: validMiddlewares, injectors: validInjectors };
734
+ } catch {
735
+ return { middlewares: [], injectors: {} };
736
+ }
737
+ }
738
+ async function loadMergedMiddlewares(middlewarePaths) {
739
+ if (middlewarePaths.length === 0) return void 0;
740
+ const mergedMiddlewares = [];
741
+ const mergedInjectors = {};
742
+ for (const absMwPath of middlewarePaths) {
743
+ let bundle = getCachedMiddlewares(absMwPath);
744
+ if (bundle === void 0) {
745
+ bundle = await loadMiddlewaresFile(absMwPath);
746
+ setCachedMiddlewares(absMwPath, bundle);
747
+ }
748
+ mergedMiddlewares.push(...bundle.middlewares);
749
+ for (const [name, injector] of Object.entries(bundle.injectors)) {
750
+ mergedInjectors[name] = injector;
751
+ }
752
+ }
753
+ if (mergedMiddlewares.length === 0 && Object.keys(mergedInjectors).length === 0) {
754
+ return void 0;
755
+ }
756
+ return { middlewares: mergedMiddlewares, injectors: mergedInjectors };
757
+ }
758
+
759
+ // src/router/scanRoutes.ts
760
+ var HTTP_OR_WS_EXPORT_RE = new RegExp(
761
+ String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|WS)\b`,
762
+ "g"
763
+ );
764
+ function extractExportsFromSource(source) {
765
+ const names = /* @__PURE__ */ new Set();
766
+ let match;
767
+ HTTP_OR_WS_EXPORT_RE.lastIndex = 0;
768
+ while ((match = HTTP_OR_WS_EXPORT_RE.exec(source)) !== null) {
769
+ names.add(match[1]);
770
+ }
771
+ return names;
772
+ }
773
+ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
774
+ const routeDir = path.dirname(routeFilePath);
775
+ const resolvedRoot = path.resolve(rootDir);
776
+ const paths = [];
777
+ let currentDir = path.resolve(rootDir, routeDir);
778
+ while (true) {
779
+ if (dist) {
780
+ const mwTsPath = path.join(currentDir, "middlewares.ts");
781
+ const mwJsPath = path.join(currentDir, "middlewares.js");
782
+ const absTsPath = path.resolve(rootDir, mwTsPath);
783
+ const absJsPath = path.resolve(rootDir, mwJsPath);
784
+ const absMwPath = fs.existsSync(absTsPath) ? absTsPath : fs.existsSync(absJsPath) ? absJsPath : null;
785
+ if (absMwPath) {
786
+ const relMwPath = path.relative(rootDir, absMwPath);
787
+ const prodAbsPath = path.resolve(rootDir, toProdFilePath(relMwPath, dist));
788
+ paths.push(prodAbsPath);
789
+ }
790
+ } else {
791
+ for (const ext of [".ts", ".js"]) {
792
+ const mwPath = path.join(currentDir, `middlewares${ext}`);
793
+ const absMwPath = path.resolve(rootDir, mwPath);
794
+ if (fs.existsSync(absMwPath)) {
795
+ paths.push(absMwPath);
796
+ break;
797
+ }
798
+ }
799
+ }
800
+ if (currentDir === resolvedRoot) break;
801
+ const parentDir = path.dirname(currentDir);
802
+ if (parentDir === currentDir) break;
803
+ currentDir = parentDir;
804
+ }
805
+ paths.reverse();
806
+ return paths;
807
+ }
808
+ function toProdFilePath(filePath, dist) {
809
+ let rel = filePath.replace(/\\/g, "/");
810
+ if (rel.startsWith("src/")) {
811
+ rel = rel.slice(4);
812
+ }
813
+ const jsPath = rel.replace(/\.ts$/, ".js");
814
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
815
+ }
816
+ async function scanRoutes(rootDir, patterns, dist) {
817
+ const files = await fg(patterns, {
818
+ cwd: rootDir,
819
+ onlyFiles: true,
820
+ absolute: false
821
+ });
822
+ const routes = [];
823
+ const wsRoutes = [];
824
+ for (const file of files) {
825
+ const normalizedFile = file.replace(/\\/g, "/");
826
+ const fileName = normalizedFile.split("/").pop();
827
+ if (fileName === "handler.ts" || fileName === "handler.js") {
828
+ const absPath = path.resolve(rootDir, normalizedFile);
829
+ const urlPath = filePathToUrlPath(normalizedFile);
830
+ const paramNames = extractParamNames(urlPath);
831
+ const isDynamic = paramNames.length > 0;
832
+ const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
833
+ let middlewarePaths;
834
+ let middlewareBundle;
835
+ if (dist) {
836
+ middlewarePaths = collectMiddlewarePaths(normalizedFile, rootDir, dist);
837
+ } else {
838
+ const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
839
+ middlewareBundle = await loadMergedMiddlewares(mwPaths);
840
+ }
841
+ const source = await fs.promises.readFile(absPath, "utf8").catch(() => "");
842
+ const exportNames = extractExportsFromSource(source);
843
+ const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
844
+ for (const method of methods) {
845
+ routes.push({
846
+ method,
847
+ urlPath,
848
+ filePath: normalizedFile,
849
+ paramNames,
850
+ isDynamic,
851
+ isCatchAll: isCatchAll || void 0,
852
+ middlewarePaths,
853
+ middlewares: middlewareBundle?.middlewares,
854
+ injectors: middlewareBundle?.injectors
855
+ });
856
+ }
857
+ if (exportNames.has("WS")) {
858
+ wsRoutes.push({
859
+ urlPath,
860
+ filePath: normalizedFile,
861
+ paramNames,
862
+ isDynamic,
863
+ isCatchAll: isCatchAll || void 0,
864
+ middlewarePaths,
865
+ middlewares: middlewareBundle?.middlewares,
866
+ injectors: middlewareBundle?.injectors
867
+ });
868
+ }
869
+ continue;
870
+ }
871
+ }
872
+ return { routes, wsRoutes };
873
+ }
874
+
875
+ // src/router/sortRoutes.ts
876
+ function sortRoutes(routes) {
877
+ return [...routes].sort((a, b) => {
878
+ if (a.isDynamic !== b.isDynamic) {
879
+ return a.isDynamic ? 1 : -1;
880
+ }
881
+ if (a.isCatchAll !== b.isCatchAll) {
882
+ return a.isCatchAll ? 1 : -1;
883
+ }
884
+ const aSegments = a.urlPath.split("/").filter(Boolean).length;
885
+ const bSegments = b.urlPath.split("/").filter(Boolean).length;
886
+ if (aSegments !== bSegments) {
887
+ return aSegments - bSegments;
888
+ }
889
+ return a.urlPath.localeCompare(b.urlPath);
890
+ });
891
+ }
892
+
893
+ // src/cli/generateSchemaFiles.ts
894
+ import path3 from "path";
895
+ import fs2 from "fs/promises";
896
+
897
+ // src/ast/createProgram.ts
898
+ import ts2 from "typescript";
899
+ var programCache = /* @__PURE__ */ new Map();
900
+ function createProgram(filePath) {
901
+ const cached = programCache.get(filePath);
902
+ if (cached) {
903
+ return cached;
904
+ }
905
+ const program = ts2.createProgram([filePath], {
906
+ strict: true,
907
+ target: ts2.ScriptTarget.ES2022,
908
+ module: ts2.ModuleKind.NodeNext,
909
+ moduleResolution: ts2.ModuleResolutionKind.NodeNext,
910
+ skipLibCheck: true,
911
+ noEmit: true
912
+ });
913
+ programCache.set(filePath, program);
914
+ return program;
915
+ }
916
+
917
+ // src/ast/extractHandlerTypes.ts
918
+ import ts4 from "typescript";
919
+
920
+ // src/ast/resolveTypeNode.ts
921
+ import ts3 from "typescript";
922
+ var SchemaExtractionError = class extends Error {
923
+ constructor(typeText, reason, options) {
924
+ super(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}`, options);
925
+ this.typeText = typeText;
926
+ this.reason = reason;
927
+ this.name = "SchemaExtractionError";
928
+ }
929
+ typeText;
930
+ reason;
931
+ };
932
+ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
933
+ const kind = typeNode.kind;
934
+ switch (kind) {
935
+ case ts3.SyntaxKind.StringKeyword:
936
+ return { kind: "string" };
937
+ case ts3.SyntaxKind.NumberKeyword:
938
+ return { kind: "number" };
939
+ case ts3.SyntaxKind.BooleanKeyword:
940
+ return { kind: "boolean" };
941
+ case ts3.SyntaxKind.BigIntKeyword:
942
+ throw new SchemaExtractionError(
943
+ typeNode.getText(),
944
+ "bigint \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93,\u8BF7\u6539\u7528 string \u6216 number"
945
+ );
946
+ case ts3.SyntaxKind.SymbolKeyword:
947
+ throw new SchemaExtractionError(typeNode.getText(), "symbol \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
948
+ case ts3.SyntaxKind.NullKeyword:
949
+ return { kind: "null" };
950
+ case ts3.SyntaxKind.UndefinedKeyword:
951
+ return { kind: "undefined" };
952
+ case ts3.SyntaxKind.UnknownKeyword:
953
+ return { kind: "any" };
954
+ case ts3.SyntaxKind.AnyKeyword:
955
+ throw new SchemaExtractionError(typeNode.getText(), "any \u4E0D\u652F\u6301\uFF0C\u8BF7\u4F7F\u7528 unknown \u8868\u793A\u4E0D\u6821\u9A8C");
956
+ case ts3.SyntaxKind.VoidKeyword:
957
+ throw new SchemaExtractionError(typeNode.getText(), "void \u4E0D\u652F\u6301\u8FD0\u884C\u65F6\u6821\u9A8C");
958
+ case ts3.SyntaxKind.NeverKeyword:
959
+ throw new SchemaExtractionError(typeNode.getText(), "never \u4E0D\u652F\u6301\u8FD0\u884C\u65F6\u6821\u9A8C");
960
+ case ts3.SyntaxKind.ObjectKeyword:
961
+ throw new SchemaExtractionError(
962
+ typeNode.getText(),
963
+ "object \u4E0D\u652F\u6301\uFF0C\u8BF7\u4F7F\u7528\u5177\u4F53\u5BF9\u8C61\u7C7B\u578B\u6216 unknown"
964
+ );
965
+ }
966
+ if (ts3.isLiteralTypeNode(typeNode)) {
967
+ const literal = typeNode.literal;
968
+ if (ts3.isStringLiteral(literal)) {
969
+ return { kind: "literal", value: literal.text };
970
+ }
971
+ if (ts3.isNumericLiteral(literal)) {
972
+ return { kind: "literal", value: Number(literal.text) };
973
+ }
974
+ if (literal.kind === ts3.SyntaxKind.TrueKeyword) {
975
+ return { kind: "literal", value: true };
976
+ }
977
+ if (literal.kind === ts3.SyntaxKind.FalseKeyword) {
978
+ return { kind: "literal", value: false };
979
+ }
980
+ if (literal.kind === ts3.SyntaxKind.NullKeyword) {
981
+ return { kind: "null" };
982
+ }
983
+ throw new SchemaExtractionError(typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u5B57\u9762\u91CF\u7C7B\u578B");
984
+ }
985
+ if (ts3.isArrayTypeNode(typeNode)) {
986
+ return {
987
+ kind: "array",
988
+ element: resolveTypeNode(typeNode.elementType, checker, visited)
989
+ };
990
+ }
991
+ if (ts3.isTupleTypeNode(typeNode)) {
992
+ const elements = typeNode.elements.map((e) => {
993
+ if (ts3.isRestTypeNode(e)) {
994
+ const inner = resolveTypeNode(e.type, checker, visited);
995
+ if (inner.kind === "array") {
996
+ return { type: inner.element, optional: false, rest: true };
997
+ }
998
+ return { type: inner, optional: false, rest: true };
999
+ }
1000
+ if (ts3.isNamedTupleMember(e)) {
1001
+ return {
1002
+ type: resolveTypeNode(e.type, checker, visited),
1003
+ optional: !!e.questionToken,
1004
+ rest: false
1005
+ };
1006
+ }
1007
+ if (ts3.isOptionalTypeNode(e)) {
1008
+ return {
1009
+ type: resolveTypeNode(e.type, checker, visited),
1010
+ optional: true,
1011
+ rest: false
1012
+ };
1013
+ }
1014
+ return {
1015
+ type: resolveTypeNode(e, checker, visited),
1016
+ optional: false,
1017
+ rest: false
1018
+ };
1019
+ });
1020
+ return { kind: "tuple", elements };
1021
+ }
1022
+ if (ts3.isUnionTypeNode(typeNode)) {
1023
+ const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
1024
+ return { kind: "union", members };
1025
+ }
1026
+ if (ts3.isIntersectionTypeNode(typeNode)) {
1027
+ const properties = [];
1028
+ for (const t of typeNode.types) {
1029
+ const resolved = resolveTypeNode(t, checker, visited);
1030
+ if (resolved.kind === "object") {
1031
+ properties.push(...resolved.properties);
1032
+ }
1033
+ }
1034
+ return { kind: "object", properties };
1035
+ }
1036
+ if (ts3.isTypeLiteralNode(typeNode)) {
1037
+ return resolveTypeLiteral(typeNode, checker, visited);
1038
+ }
1039
+ if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.KeyOfKeyword) {
1040
+ return resolveKeyOf(typeNode, checker);
1041
+ }
1042
+ if (ts3.isTypeOperatorNode(typeNode) && typeNode.operator === ts3.SyntaxKind.ReadonlyKeyword) {
1043
+ return resolveTypeNode(typeNode.type, checker, visited);
1044
+ }
1045
+ if (ts3.isTypeReferenceNode(typeNode)) {
1046
+ return resolveTypeReference(typeNode, checker, visited);
1047
+ }
1048
+ throw new SchemaExtractionError(typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
1049
+ }
1050
+ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1051
+ const properties = [];
1052
+ for (const member of typeNode.members) {
1053
+ if (ts3.isPropertySignature(member) && member.name) {
1054
+ const name = member.name.getText();
1055
+ const optional = !!member.questionToken;
1056
+ const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1057
+ const constraints = extractConstraintsFromJsDoc(member, name);
1058
+ validateConstraints(constraints, type, name);
1059
+ properties.push(
1060
+ constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
1061
+ );
1062
+ }
1063
+ if (ts3.isIndexSignatureDeclaration(member)) {
1064
+ const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1065
+ const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1066
+ return { kind: "record", key: keyType, value: valueType };
1067
+ }
1068
+ }
1069
+ return { kind: "object", properties };
1070
+ }
1071
+ function extractLiteralKeys(type) {
1072
+ if (type.kind === "literal" && typeof type.value === "string") {
1073
+ return [type.value];
1074
+ }
1075
+ if (type.kind === "union") {
1076
+ const keys = [];
1077
+ for (const member of type.members) {
1078
+ if (member.kind === "literal" && typeof member.value === "string") {
1079
+ keys.push(member.value);
1080
+ } else {
1081
+ return null;
1082
+ }
1083
+ }
1084
+ return keys;
1085
+ }
1086
+ return null;
1087
+ }
1088
+ function extractKeysFromChecker(typeNode, checker) {
1089
+ if (!checker) return null;
1090
+ const type = checker.getTypeFromTypeNode(typeNode);
1091
+ if (type.isUnion()) {
1092
+ const keys = [];
1093
+ for (const member of type.types) {
1094
+ if (member.isStringLiteral()) {
1095
+ keys.push(member.value);
1096
+ } else if (member.isNumberLiteral()) {
1097
+ keys.push(String(member.value));
1098
+ } else {
1099
+ return null;
1100
+ }
1101
+ }
1102
+ return keys;
1103
+ }
1104
+ if (type.isStringLiteral()) {
1105
+ return [type.value];
1106
+ }
1107
+ if (type.isNumberLiteral()) {
1108
+ return [String(type.value)];
1109
+ }
1110
+ return null;
1111
+ }
1112
+ function resolveKeyOf(typeNode, checker) {
1113
+ if (!checker) {
1114
+ throw new SchemaExtractionError(typeNode.getText(), "keyof T \u9700\u8981 checker \u624D\u80FD\u89E3\u6790");
1115
+ }
1116
+ const type = checker.getTypeFromTypeNode(typeNode);
1117
+ if (type.isUnion()) {
1118
+ const members = [];
1119
+ for (const member of type.types) {
1120
+ if (member.isStringLiteral()) {
1121
+ members.push({ kind: "literal", value: member.value });
1122
+ } else if (member.isNumberLiteral()) {
1123
+ members.push({ kind: "literal", value: member.value });
1124
+ } else {
1125
+ throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u5305\u542B\u975E\u5B57\u9762\u91CF\u7C7B\u578B");
1126
+ }
1127
+ }
1128
+ return { kind: "union", members };
1129
+ }
1130
+ if (type.isStringLiteral()) {
1131
+ return { kind: "literal", value: type.value };
1132
+ }
1133
+ if (type.isNumberLiteral()) {
1134
+ return { kind: "literal", value: type.value };
1135
+ }
1136
+ throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u8054\u5408");
1137
+ }
1138
+ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1139
+ const typeName = typeNode.typeName.getText();
1140
+ if (typeName === "Date") {
1141
+ return { kind: "date" };
1142
+ }
1143
+ if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
1144
+ return {
1145
+ kind: "array",
1146
+ element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1147
+ };
1148
+ }
1149
+ if (typeName === "Record" && typeNode.typeArguments?.length === 2) {
1150
+ return {
1151
+ kind: "record",
1152
+ key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1153
+ value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1154
+ };
1155
+ }
1156
+ if ((typeName === "Partial" || typeName === "Required" || typeName === "Readonly") && typeNode.typeArguments?.length === 1) {
1157
+ const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1158
+ if (inner.kind === "object" && typeName === "Partial") {
1159
+ return {
1160
+ kind: "object",
1161
+ properties: inner.properties.map((p) => ({ ...p, optional: true }))
1162
+ };
1163
+ }
1164
+ return inner;
1165
+ }
1166
+ if ((typeName === "Pick" || typeName === "Omit") && typeNode.typeArguments?.length === 2) {
1167
+ const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
1168
+ if (innerType.kind !== "object") {
1169
+ throw new SchemaExtractionError(
1170
+ typeNode.getText(),
1171
+ `${typeName} \u7684 T \u5FC5\u987B\u662F\u5BF9\u8C61\u7C7B\u578B\uFF0C\u5B9E\u9645\u4E3A ${innerType.kind}`
1172
+ );
1173
+ }
1174
+ const keyTypeNode = typeNode.typeArguments[1];
1175
+ let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited));
1176
+ if (keys === null) {
1177
+ keys = extractKeysFromChecker(keyTypeNode, checker);
1178
+ }
1179
+ if (keys === null) {
1180
+ throw new SchemaExtractionError(typeNode.getText(), `${typeName} \u7684 K \u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u96C6\u5408`);
1181
+ }
1182
+ const keySet = new Set(keys);
1183
+ const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
1184
+ return { kind: "object", properties };
1185
+ }
1186
+ if (typeName === "Map") {
1187
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
1188
+ throw new SchemaExtractionError(
1189
+ typeNode.getText(),
1190
+ "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
1191
+ );
1192
+ }
1193
+ return {
1194
+ kind: "map",
1195
+ key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
1196
+ value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
1197
+ };
1198
+ }
1199
+ if (typeName === "Set") {
1200
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
1201
+ throw new SchemaExtractionError(
1202
+ typeNode.getText(),
1203
+ "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
1204
+ );
1205
+ }
1206
+ return {
1207
+ kind: "set",
1208
+ element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
1209
+ };
1210
+ }
1211
+ if (typeName === "WeakMap" || typeName === "WeakSet") {
1212
+ throw new SchemaExtractionError(
1213
+ typeNode.getText(),
1214
+ `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
1215
+ );
1216
+ }
1217
+ if (typeName === "Promise") {
1218
+ throw new SchemaExtractionError(
1219
+ typeNode.getText(),
1220
+ "Promise \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u52FF\u5728 query/body \u7C7B\u578B\u4E2D\u4F7F\u7528"
1221
+ );
1222
+ }
1223
+ if (typeName === "Function") {
1224
+ throw new SchemaExtractionError(typeNode.getText(), "Function \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
1225
+ }
1226
+ if (visited.has(typeName)) {
1227
+ return { kind: "ref", name: typeName };
1228
+ }
1229
+ visited.add(typeName);
1230
+ if (checker) {
1231
+ const symbol = typeNode.typeName.kind === ts3.SyntaxKind.Identifier ? checker.getSymbolAtLocation(typeNode.typeName) : void 0;
1232
+ if (symbol) {
1233
+ const declaration = symbol.declarations?.[0];
1234
+ if (declaration) {
1235
+ if (ts3.isInterfaceDeclaration(declaration)) {
1236
+ return resolveInterfaceDeclaration(declaration, checker, visited);
1237
+ }
1238
+ if (ts3.isTypeAliasDeclaration(declaration)) {
1239
+ return resolveTypeNode(declaration.type, checker, visited);
1240
+ }
1241
+ if (ts3.isEnumDeclaration(declaration)) {
1242
+ return resolveEnumDeclaration(declaration);
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
1248
+ }
1249
+ function resolveEnumDeclaration(node) {
1250
+ const members = [];
1251
+ let nextNumericValue = 0;
1252
+ for (const member of node.members) {
1253
+ if (member.initializer) {
1254
+ if (ts3.isStringLiteral(member.initializer)) {
1255
+ members.push({ kind: "literal", value: member.initializer.text });
1256
+ } else if (ts3.isNumericLiteral(member.initializer)) {
1257
+ const num = Number(member.initializer.text);
1258
+ members.push({ kind: "literal", value: num });
1259
+ nextNumericValue = num + 1;
1260
+ } else {
1261
+ throw new SchemaExtractionError(
1262
+ node.name.text,
1263
+ `enum \u6210\u5458 "${member.name.getText()}" \u7684\u521D\u59CB\u5316\u503C\u7C7B\u578B\u4E0D\u652F\u6301,\u4EC5\u652F\u6301 string/number \u5B57\u9762\u91CF`
1264
+ );
1265
+ }
1266
+ } else {
1267
+ members.push({ kind: "literal", value: nextNumericValue });
1268
+ nextNumericValue++;
1269
+ }
1270
+ }
1271
+ return { kind: "union", members };
1272
+ }
1273
+ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
1274
+ const properties = [];
1275
+ const propMap = /* @__PURE__ */ new Map();
1276
+ for (const heritageClause of node.heritageClauses ?? []) {
1277
+ if (heritageClause.token === ts3.SyntaxKind.ExtendsKeyword) {
1278
+ for (const expr of heritageClause.types) {
1279
+ const parentType = resolveTypeNode(expr, checker, visited);
1280
+ if (parentType.kind === "object") {
1281
+ for (const prop of parentType.properties) {
1282
+ propMap.set(prop.name, prop);
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ for (const member of node.members) {
1289
+ if (ts3.isPropertySignature(member) && member.name) {
1290
+ const name = member.name.getText();
1291
+ const optional = !!member.questionToken;
1292
+ const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1293
+ const constraints = extractConstraintsFromJsDoc(member, name);
1294
+ validateConstraints(constraints, type, name);
1295
+ propMap.set(
1296
+ name,
1297
+ constraints.length > 0 ? { name, type, optional, constraints } : { name, type, optional }
1298
+ );
1299
+ }
1300
+ if (ts3.isIndexSignatureDeclaration(member)) {
1301
+ const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
1302
+ const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
1303
+ return { kind: "record", key: keyType, value: valueType };
1304
+ }
1305
+ }
1306
+ for (const prop of propMap.values()) {
1307
+ properties.push(prop);
1308
+ }
1309
+ return { kind: "object", properties };
1310
+ }
1311
+ var NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
1312
+ "max",
1313
+ "min",
1314
+ "int",
1315
+ "positive",
1316
+ "negative",
1317
+ "nonnegative",
1318
+ "nonpositive"
1319
+ ]);
1320
+ var LENGTH_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
1321
+ "maxLength",
1322
+ "minLength",
1323
+ "length"
1324
+ ]);
1325
+ var STRING_FORMAT_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
1326
+ "regex",
1327
+ "email",
1328
+ "url",
1329
+ "uuid"
1330
+ ]);
1331
+ function extractConstraintsFromJsDoc(node, fieldName) {
1332
+ const jsDocs = ts3.getJSDocCommentsAndTags(node).filter((entry) => ts3.isJSDoc(entry));
1333
+ if (jsDocs.length === 0) return [];
1334
+ const constraints = [];
1335
+ for (const jsDoc of jsDocs) {
1336
+ if (!jsDoc.tags) continue;
1337
+ for (const tag of jsDoc.tags) {
1338
+ const constraint = parseJsDocTag(tag, fieldName);
1339
+ if (constraint) constraints.push(constraint);
1340
+ }
1341
+ }
1342
+ return constraints;
1343
+ }
1344
+ function getTagCommentText(tag) {
1345
+ const comment = tag.comment;
1346
+ if (typeof comment === "string") return comment;
1347
+ return void 0;
1348
+ }
1349
+ function parseJsDocTag(tag, fieldName) {
1350
+ const tagName = tag.tagName.text;
1351
+ switch (tagName) {
1352
+ // 数值约束(带值)
1353
+ case "max":
1354
+ case "min": {
1355
+ const value = parseNumberValue(tag, fieldName, tagName);
1356
+ return { kind: tagName, value };
1357
+ }
1358
+ // 长度约束(带值)
1359
+ case "maxLength":
1360
+ case "minLength":
1361
+ case "length": {
1362
+ const value = parseNumberValue(tag, fieldName, tagName);
1363
+ return { kind: tagName, value };
1364
+ }
1365
+ // 正则约束(带 /pattern/flags 值)
1366
+ case "regex":
1367
+ case "pattern": {
1368
+ const text = getTagCommentText(tag);
1369
+ if (!text) {
1370
+ throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981 /pattern/flags \u5F62\u5F0F\u7684\u503C`);
1371
+ }
1372
+ const regex = parseRegexLiteral(text.trim(), fieldName);
1373
+ return { kind: "regex", pattern: regex.pattern, flags: regex.flags };
1374
+ }
1375
+ // 数值约束(无值)
1376
+ case "int":
1377
+ case "positive":
1378
+ case "negative":
1379
+ case "nonnegative":
1380
+ case "nonpositive":
1381
+ return { kind: tagName };
1382
+ // 字符串格式约束(无值)
1383
+ case "email":
1384
+ case "url":
1385
+ case "uuid":
1386
+ return { kind: tagName };
1387
+ default:
1388
+ return null;
1389
+ }
1390
+ }
1391
+ function parseNumberValue(tag, fieldName, tagName) {
1392
+ const text = getTagCommentText(tag);
1393
+ if (!text) {
1394
+ throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u9700\u8981\u4E00\u4E2A\u6570\u5B57\u503C`);
1395
+ }
1396
+ const trimmed = text.trim();
1397
+ const num = Number(trimmed);
1398
+ if (!Number.isFinite(num)) {
1399
+ throw new SchemaExtractionError(fieldName, `@${tagName} \u6807\u7B7E\u7684\u503C "${trimmed}" \u4E0D\u662F\u6709\u6548\u6570\u5B57`);
1400
+ }
1401
+ return num;
1402
+ }
1403
+ function parseRegexLiteral(text, fieldName) {
1404
+ const match = /^\/(.+)\/([gimsuy]*)$/.exec(text);
1405
+ if (!match) {
1406
+ throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u4E0D\u662F /pattern/flags \u5F62\u5F0F`);
1407
+ }
1408
+ const [, pattern, flags] = match;
1409
+ if (!pattern) {
1410
+ throw new SchemaExtractionError(fieldName, `\u6B63\u5219\u503C "${text}" \u7684 pattern \u90E8\u5206\u4E3A\u7A7A`);
1411
+ }
1412
+ return flags ? { pattern, flags } : { pattern };
1413
+ }
1414
+ function validateConstraints(constraints, type, fieldName) {
1415
+ if (constraints.length === 0) return;
1416
+ for (const constraint of constraints) {
1417
+ const kind = constraint.kind;
1418
+ if (NUMBER_CONSTRAINT_KINDS.has(kind)) {
1419
+ if (type.kind !== "number") {
1420
+ throw new SchemaExtractionError(
1421
+ fieldName,
1422
+ `@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E number \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
1423
+ );
1424
+ }
1425
+ continue;
1426
+ }
1427
+ if (LENGTH_CONSTRAINT_KINDS.has(kind)) {
1428
+ if (type.kind !== "string" && type.kind !== "array") {
1429
+ throw new SchemaExtractionError(
1430
+ fieldName,
1431
+ `@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u6216 array \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
1432
+ );
1433
+ }
1434
+ continue;
1435
+ }
1436
+ if (STRING_FORMAT_CONSTRAINT_KINDS.has(kind)) {
1437
+ if (type.kind !== "string") {
1438
+ throw new SchemaExtractionError(
1439
+ fieldName,
1440
+ `@${kind} \u7EA6\u675F\u4EC5\u9002\u7528\u4E8E string \u5B57\u6BB5\uFF0C\u5B9E\u9645\u4E3A ${type.kind}`
1441
+ );
1442
+ }
1443
+ continue;
1444
+ }
1445
+ }
1446
+ }
1447
+
1448
+ // src/ast/extractHandlerTypes.ts
1449
+ function extractTypeInfo(program, filePath, typeName) {
1450
+ const sourceFile = program.getSourceFile(filePath);
1451
+ if (!sourceFile) return null;
1452
+ const checker = program.getTypeChecker();
1453
+ let result = null;
1454
+ ts4.forEachChild(sourceFile, (node) => {
1455
+ if (result) return;
1456
+ if (ts4.isInterfaceDeclaration(node) && node.name.text === typeName) {
1457
+ const visited = /* @__PURE__ */ new Set();
1458
+ visited.add(typeName);
1459
+ const runtimeType = withFileContext(
1460
+ filePath,
1461
+ typeName,
1462
+ () => resolveInterfaceDeclaration(node, checker, visited)
1463
+ );
1464
+ result = {
1465
+ name: typeName,
1466
+ properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1467
+ runtimeType
1468
+ };
1469
+ return;
1470
+ }
1471
+ if (ts4.isTypeAliasDeclaration(node) && node.name.text === typeName) {
1472
+ const visited = /* @__PURE__ */ new Set();
1473
+ visited.add(typeName);
1474
+ const runtimeType = withFileContext(
1475
+ filePath,
1476
+ typeName,
1477
+ () => resolveTypeNode(node.type, checker, visited)
1478
+ );
1479
+ result = {
1480
+ name: typeName,
1481
+ properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1482
+ runtimeType
1483
+ };
1484
+ return;
1485
+ }
1486
+ });
1487
+ return result;
1488
+ }
1489
+ function extractAllTypes(program, filePath) {
1490
+ const sourceFile = program.getSourceFile(filePath);
1491
+ if (!sourceFile) return /* @__PURE__ */ new Map();
1492
+ const checker = program.getTypeChecker();
1493
+ const result = /* @__PURE__ */ new Map();
1494
+ ts4.forEachChild(sourceFile, (node) => {
1495
+ if (ts4.isInterfaceDeclaration(node)) {
1496
+ const visited = /* @__PURE__ */ new Set();
1497
+ visited.add(node.name.text);
1498
+ const runtimeType = withFileContext(
1499
+ filePath,
1500
+ node.name.text,
1501
+ () => resolveInterfaceDeclaration(node, checker, visited)
1502
+ );
1503
+ result.set(node.name.text, {
1504
+ name: node.name.text,
1505
+ properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1506
+ runtimeType
1507
+ });
1508
+ return;
1509
+ }
1510
+ if (ts4.isTypeAliasDeclaration(node)) {
1511
+ const visited = /* @__PURE__ */ new Set();
1512
+ visited.add(node.name.text);
1513
+ const runtimeType = withFileContext(
1514
+ filePath,
1515
+ node.name.text,
1516
+ () => resolveTypeNode(node.type, checker, visited)
1517
+ );
1518
+ result.set(node.name.text, {
1519
+ name: node.name.text,
1520
+ properties: runtimeType.kind === "object" ? runtimeType.properties : [],
1521
+ runtimeType
1522
+ });
1523
+ return;
1524
+ }
1525
+ });
1526
+ return result;
1527
+ }
1528
+ function withFileContext(filePath, typeName, fn) {
1529
+ try {
1530
+ return fn();
1531
+ } catch (err) {
1532
+ if (err instanceof SchemaExtractionError) {
1533
+ const fileName = filePath.split("/").pop() ?? filePath;
1534
+ const enriched = new SchemaExtractionError(
1535
+ err.typeText,
1536
+ `${err.reason}\uFF08\u6587\u4EF6: ${fileName}, \u7C7B\u578B: ${typeName}\uFF09`,
1537
+ { cause: err }
1538
+ );
1539
+ throw enriched;
1540
+ }
1541
+ throw err;
1542
+ }
1543
+ }
1544
+
1545
+ // src/runtime/inputType.ts
1546
+ function getInputTypeForMethod(method) {
1547
+ const upper = method.toUpperCase();
1548
+ if (upper === "GET" || upper === "DELETE" || upper === "HEAD") {
1549
+ return "query";
1550
+ }
1551
+ return "body";
1552
+ }
1553
+ function hasBody(method) {
1554
+ const upper = method.toUpperCase();
1555
+ return upper === "POST" || upper === "PUT" || upper === "PATCH" || upper === "DELETE";
1556
+ }
1557
+
1558
+ // src/validator/schemaName.ts
1559
+ function getSchemaName(method, inputType) {
1560
+ return `${method.toUpperCase()}${inputType.charAt(0).toUpperCase() + inputType.slice(1)}`;
1561
+ }
1562
+
1563
+ // src/injection/analyzeInjection.ts
1564
+ import ts5 from "typescript";
1565
+ function analyzeInjection(code, functionName) {
1566
+ const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
1567
+ const params = [];
1568
+ ts5.forEachChild(sourceFile, (node) => {
1569
+ if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
1570
+ for (const param of node.parameters) {
1571
+ const paramMeta = analyzeParam(param, sourceFile);
1572
+ params.push(paramMeta);
1573
+ }
1574
+ }
1575
+ });
1576
+ return { params };
1577
+ }
1578
+ function analyzeParam(param, sourceFile) {
1579
+ const name = param.name.getText(sourceFile);
1580
+ const type = PARAM_TYPE_MAP[name] || "unknown";
1581
+ const result = { name, type };
1582
+ if (param.type) {
1583
+ if (ts5.isTypeReferenceNode(param.type)) {
1584
+ result.typeName = param.type.typeName.getText(sourceFile);
1585
+ } else if (ts5.isTypeLiteralNode(param.type)) {
1586
+ result.schema = extractSchema(param.type, sourceFile);
1587
+ }
1588
+ }
1589
+ return result;
1590
+ }
1591
+ function extractSchema(typeNode, sourceFile) {
1592
+ const schema = [];
1593
+ for (const member of typeNode.members) {
1594
+ if (ts5.isPropertySignature(member) && member.name && ts5.isIdentifier(member.name)) {
1595
+ const propName = member.name.text;
1596
+ const optional = !!member.questionToken;
1597
+ const propType = member.type?.getText(sourceFile) || "unknown";
1598
+ schema.push({
1599
+ name: propName,
1600
+ type: propType,
1601
+ optional
1602
+ });
1603
+ }
1604
+ }
1605
+ return schema;
1606
+ }
1607
+
1608
+ // src/cli/collectRouteSchemaSources.ts
1609
+ import path2 from "path";
1610
+ function collectRouteSchemaSources(routes, rootDir) {
1611
+ const methodsByFile = /* @__PURE__ */ new Map();
1612
+ for (const route of routes) {
1613
+ const filePath = rootDir ? path2.resolve(rootDir, route.filePath) : route.filePath;
1614
+ let entry = methodsByFile.get(filePath);
1615
+ if (!entry) {
1616
+ entry = { urlPath: route.urlPath, methods: /* @__PURE__ */ new Set() };
1617
+ methodsByFile.set(filePath, entry);
1618
+ }
1619
+ entry.methods.add(route.method);
1620
+ }
1621
+ const programByFile = /* @__PURE__ */ new Map();
1622
+ const allTypesByFile = /* @__PURE__ */ new Map();
1623
+ const mergedAllTypes = /* @__PURE__ */ new Map();
1624
+ for (const filePath of methodsByFile.keys()) {
1625
+ const program = createProgram(filePath);
1626
+ programByFile.set(filePath, program);
1627
+ const allTypes = extractAllTypes(program, filePath);
1628
+ allTypesByFile.set(filePath, allTypes);
1629
+ for (const [name, info] of allTypes) {
1630
+ mergedAllTypes.set(name, info);
1631
+ }
1632
+ }
1633
+ const sources = [];
1634
+ for (const [filePath, entry] of methodsByFile) {
1635
+ const program = programByFile.get(filePath);
1636
+ const sourceFile = program.getSourceFile(filePath);
1637
+ const code = sourceFile?.text ?? "";
1638
+ for (const method of entry.methods) {
1639
+ const inputType = getInputTypeForMethod(method);
1640
+ const schemaName = getSchemaName(method, inputType);
1641
+ const meta = analyzeInjection(code, method);
1642
+ const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1643
+ const isForm = param?.type === "form";
1644
+ const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
1645
+ sources.push({
1646
+ urlPath: entry.urlPath,
1647
+ filePath,
1648
+ schemaName,
1649
+ typeInfo,
1650
+ coerce: isForm || void 0
1651
+ });
1652
+ }
1653
+ }
1654
+ return { sources, allTypesByFile, mergedAllTypes };
1655
+ }
1656
+
1657
+ // src/ast/generateZodSchema.ts
1658
+ var CodeGenContext = class {
1659
+ /** 命名类型集合:name → RuntimeType */
1660
+ namedTypes = /* @__PURE__ */ new Map();
1661
+ /** 类型解析器(用于解析 ref 的实际类型) */
1662
+ resolveType;
1663
+ /** 入口类型原始名(typeInfo.name,用于识别入口类型的自引用) */
1664
+ entryTypeName = "";
1665
+ /** 入口类型导出名(exportName,自引用时用此名生成变量名) */
1666
+ entryExportName = "";
1667
+ /**
1668
+ * 是否生成 coerce 逻辑(query/params 场景,URL 来源均为 string)
1669
+ *
1670
+ * true 时为 number/boolean 字段包 z.preprocess,把合法的字符串转成对应类型。
1671
+ * 嵌套类型(array/object/tuple/union 等)的元素递归处理。
1672
+ */
1673
+ coerce = false;
1674
+ constructor(resolveType) {
1675
+ this.resolveType = resolveType;
1676
+ }
1677
+ };
1678
+ function collectNamedTypes(type, ctx) {
1679
+ switch (type.kind) {
1680
+ case "string":
1681
+ case "number":
1682
+ case "boolean":
1683
+ case "bigint":
1684
+ case "null":
1685
+ case "undefined":
1686
+ case "any":
1687
+ case "unknown":
1688
+ case "literal":
1689
+ case "date":
1690
+ return;
1691
+ case "array":
1692
+ collectNamedTypes(type.element, ctx);
1693
+ return;
1694
+ case "tuple":
1695
+ for (const el of type.elements) {
1696
+ collectNamedTypes(el.type, ctx);
1697
+ }
1698
+ return;
1699
+ case "object":
1700
+ for (const prop of type.properties) {
1701
+ collectNamedTypes(prop.type, ctx);
1702
+ }
1703
+ return;
1704
+ case "union":
1705
+ for (const member of type.members) {
1706
+ collectNamedTypes(member, ctx);
1707
+ }
1708
+ return;
1709
+ case "record":
1710
+ collectNamedTypes(type.key, ctx);
1711
+ collectNamedTypes(type.value, ctx);
1712
+ return;
1713
+ case "map":
1714
+ collectNamedTypes(type.key, ctx);
1715
+ collectNamedTypes(type.value, ctx);
1716
+ return;
1717
+ case "set":
1718
+ collectNamedTypes(type.element, ctx);
1719
+ return;
1720
+ case "ref": {
1721
+ if (ctx.namedTypes.has(type.name)) return;
1722
+ ctx.namedTypes.set(type.name, { kind: "any" });
1723
+ const resolved = ctx.resolveType(type.name);
1724
+ if (resolved) {
1725
+ ctx.namedTypes.set(type.name, resolved);
1726
+ collectNamedTypes(resolved, ctx);
1727
+ }
1728
+ return;
1729
+ }
1730
+ }
1731
+ }
1732
+ function runtimeTypeToZodExpression(type, ctx, constraints) {
1733
+ const expr = baseExpression(type, ctx);
1734
+ const withConstraints = constraints && constraints.length > 0 ? applyConstraints(expr, constraints, type.kind) : expr;
1735
+ if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
1736
+ return wrapCoercePreprocess(type.kind, withConstraints);
1737
+ }
1738
+ return withConstraints;
1739
+ }
1740
+ function applyConstraints(baseExpr, constraints, typeKind) {
1741
+ const suffix = constraints.map((c) => constraintToZodChain(c, typeKind)).join("");
1742
+ return `${baseExpr}${suffix}`;
1743
+ }
1744
+ function constraintToZodChain(constraint, _typeKind) {
1745
+ switch (constraint.kind) {
1746
+ case "max":
1747
+ return `.max(${constraint.value})`;
1748
+ case "min":
1749
+ return `.min(${constraint.value})`;
1750
+ case "int":
1751
+ return ".int()";
1752
+ case "positive":
1753
+ return ".positive()";
1754
+ case "negative":
1755
+ return ".negative()";
1756
+ case "nonnegative":
1757
+ return ".nonnegative()";
1758
+ case "nonpositive":
1759
+ return ".nonpositive()";
1760
+ case "maxLength":
1761
+ return `.max(${constraint.value})`;
1762
+ case "minLength":
1763
+ return `.min(${constraint.value})`;
1764
+ case "length":
1765
+ return `.length(${constraint.value})`;
1766
+ case "regex": {
1767
+ const flags = constraint.flags ?? "";
1768
+ return `.regex(new RegExp(${JSON.stringify(constraint.pattern)}${flags ? `, ${JSON.stringify(flags)}` : ""}))`;
1769
+ }
1770
+ case "email":
1771
+ return ".email()";
1772
+ case "url":
1773
+ return ".url()";
1774
+ case "uuid":
1775
+ return ".uuid()";
1776
+ }
1777
+ }
1778
+ function baseExpression(type, ctx) {
1779
+ switch (type.kind) {
1780
+ case "string":
1781
+ return "z.string()";
1782
+ case "number":
1783
+ return "z.number()";
1784
+ case "boolean":
1785
+ return "z.boolean()";
1786
+ case "bigint":
1787
+ return "z.never()";
1788
+ case "null":
1789
+ return "z.null()";
1790
+ case "undefined":
1791
+ return "z.undefined()";
1792
+ case "any":
1793
+ case "unknown":
1794
+ return "z.unknown()";
1795
+ case "literal":
1796
+ return `z.literal(${JSON.stringify(type.value)})`;
1797
+ case "array":
1798
+ return `z.array(${runtimeTypeToZodExpression(type.element, ctx)})`;
1799
+ case "tuple":
1800
+ return generateTupleExpression(type.elements, ctx);
1801
+ case "object":
1802
+ return generateObjectExpression(type.properties, ctx);
1803
+ case "union":
1804
+ return generateUnionExpression(type.members, ctx);
1805
+ case "date":
1806
+ return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
1807
+ case "record":
1808
+ return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
1809
+ case "map":
1810
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
1811
+ case "set":
1812
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
1813
+ case "ref":
1814
+ if (type.name === ctx.entryTypeName) {
1815
+ return `${ctx.entryExportName}Schema`;
1816
+ }
1817
+ return `${type.name}Schema`;
1818
+ }
1819
+ }
1820
+ var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
1821
+ var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
1822
+ 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);';
1823
+ var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
1824
+ var HELPERS_FILENAME = "faapi-helpers.js";
1825
+ function generateHelpersFileSource() {
1826
+ return [
1827
+ "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
1828
+ COERCE_NUMBER_HELPER,
1829
+ COERCE_BOOLEAN_HELPER,
1830
+ COERCE_MAP_HELPER,
1831
+ COERCE_SET_HELPER,
1832
+ ""
1833
+ ].join("\n");
1834
+ }
1835
+ function usesCoerceHelpers(code) {
1836
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
1837
+ }
1838
+ function wrapCoercePreprocess(kind, inner) {
1839
+ if (kind === "number") {
1840
+ return `z.preprocess(coerceNumber, ${inner})`;
1841
+ }
1842
+ return `z.preprocess(coerceBoolean, ${inner})`;
1843
+ }
1844
+ function generateTupleExpression(elements, ctx) {
1845
+ const fixedExprs = [];
1846
+ const fixedOptional = [];
1847
+ let restExpression = "";
1848
+ let restStarted = false;
1849
+ for (const el of elements) {
1850
+ if (el.rest) {
1851
+ restExpression = runtimeTypeToZodExpression(el.type, ctx);
1852
+ restStarted = true;
1853
+ } else if (!restStarted) {
1854
+ fixedExprs.push(runtimeTypeToZodExpression(el.type, ctx));
1855
+ fixedOptional.push(el.optional);
1856
+ }
1857
+ }
1858
+ if (restExpression) {
1859
+ return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
1860
+ }
1861
+ const hasOptional = fixedOptional.some((o) => o);
1862
+ if (!hasOptional) {
1863
+ return `z.tuple([${fixedExprs.join(", ")}])`;
1864
+ }
1865
+ const variants = [];
1866
+ for (let len = fixedExprs.length; len >= 0; len--) {
1867
+ const removed = fixedOptional.slice(len);
1868
+ if (removed.length > 0 && removed.some((o) => !o)) {
1869
+ break;
1870
+ }
1871
+ const subset = fixedExprs.slice(0, len);
1872
+ variants.push(`z.tuple([${subset.join(", ")}])`);
1873
+ }
1874
+ variants.reverse();
1875
+ if (variants.length === 1) {
1876
+ return variants[0];
1877
+ }
1878
+ return `z.union([${variants.join(", ")}])`;
1879
+ }
1880
+ function generateObjectExpression(properties, ctx) {
1881
+ const fields = properties.map((prop) => {
1882
+ const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
1883
+ const finalExpr = prop.optional ? `${expr}.optional()` : expr;
1884
+ return `${JSON.stringify(prop.name)}: ${finalExpr}`;
1885
+ });
1886
+ return `z.object({ ${fields.join(", ")} })`;
1887
+ }
1888
+ function generateUnionExpression(members, ctx) {
1889
+ const hasNull = members.some((m) => m.kind === "null");
1890
+ const nonNull = members.filter((m) => m.kind !== "null");
1891
+ if (hasNull && nonNull.length === 1) {
1892
+ return `${runtimeTypeToZodExpression(nonNull[0], ctx)}.nullable()`;
1893
+ }
1894
+ if (hasNull) {
1895
+ const unionInner2 = nonNull.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1896
+ return `z.union([${unionInner2}]).nullable()`;
1897
+ }
1898
+ const unionInner = members.map((m) => runtimeTypeToZodExpression(m, ctx)).join(", ");
1899
+ return `z.union([${unionInner}])`;
1900
+ }
1901
+ function generateNamedTypeDeclaration(name, type, ctx) {
1902
+ const expr = runtimeTypeToZodExpression(type, ctx);
1903
+ const hasRef = containsRef(type, /* @__PURE__ */ new Set([name]));
1904
+ if (hasRef) {
1905
+ return `const ${name}Schema = z.lazy(() => ${expr});`;
1906
+ }
1907
+ return `const ${name}Schema = ${expr};`;
1908
+ }
1909
+ function containsRef(type, visited) {
1910
+ switch (type.kind) {
1911
+ case "ref":
1912
+ return visited.has(type.name);
1913
+ case "array":
1914
+ return containsRef(type.element, visited);
1915
+ case "tuple":
1916
+ return type.elements.some((el) => containsRef(el.type, visited));
1917
+ case "object":
1918
+ return type.properties.some((prop) => containsRef(prop.type, visited));
1919
+ case "union":
1920
+ return type.members.some((m) => containsRef(m, visited));
1921
+ case "record":
1922
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1923
+ case "map":
1924
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1925
+ case "set":
1926
+ return containsRef(type.element, visited);
1927
+ default:
1928
+ return false;
1929
+ }
1930
+ }
1931
+ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
1932
+ const ctx = new CodeGenContext(resolveType);
1933
+ const name = exportName ?? typeInfo.name;
1934
+ ctx.entryTypeName = typeInfo.name;
1935
+ ctx.entryExportName = name;
1936
+ ctx.coerce = coerce;
1937
+ collectNamedTypes(typeInfo.runtimeType, ctx);
1938
+ ctx.namedTypes.delete(typeInfo.name);
1939
+ const lines = [];
1940
+ lines.push("import { z } from 'zod';");
1941
+ lines.push("");
1942
+ for (const [n, type] of ctx.namedTypes) {
1943
+ lines.push(generateNamedTypeDeclaration(n, type, ctx));
1944
+ }
1945
+ if (ctx.namedTypes.size > 0) lines.push("");
1946
+ const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
1947
+ const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
1948
+ if (hasSelfRef) {
1949
+ lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
1950
+ } else {
1951
+ lines.push(`export const ${name}Schema = ${entryExpr};`);
1952
+ }
1953
+ return lines.join("\n");
1954
+ }
1955
+
1956
+ // src/cli/generateSchemaFiles.ts
1957
+ function getSchemaOutputPath(sourceFile, dist, rootDir) {
1958
+ let rel = sourceFile.replace(/\\/g, "/");
1959
+ if (rel.startsWith("src/")) {
1960
+ rel = rel.slice(4);
1961
+ }
1962
+ const idx = rel.lastIndexOf("/");
1963
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1964
+ return path3.resolve(rootDir, dist, relDir, "zod.js");
1965
+ }
1966
+ function getRuntimeSchemaPath(filePath, dist, rootDir) {
1967
+ let rel = filePath.replace(/\\/g, "/");
1968
+ if (rel.startsWith("src/")) {
1969
+ rel = rel.slice(4);
1970
+ } else if (rel.startsWith(`${dist}/`)) {
1971
+ rel = rel.slice(dist.length + 1);
1972
+ }
1973
+ const idx = rel.lastIndexOf("/");
1974
+ const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1975
+ return path3.resolve(rootDir, dist, relDir, "zod.js");
1976
+ }
1977
+ function getHelpersImportPath(relDir) {
1978
+ if (!relDir) return `./${HELPERS_FILENAME}`;
1979
+ const depth = relDir.split("/").filter(Boolean).length;
1980
+ return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
1981
+ }
1982
+ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1983
+ const resolveType = (name) => allTypes.get(name)?.runtimeType;
1984
+ const lines = ["import { z } from 'zod';"];
1985
+ const schemaBlocks = [];
1986
+ for (const source of sources) {
1987
+ const { schemaName, typeInfo } = source;
1988
+ if (!typeInfo) {
1989
+ continue;
1990
+ }
1991
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
1992
+ const block = [`// ${schemaName}`];
1993
+ const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1994
+ /^import \{ z \} from 'zod';\s*\n\s*\n/,
1995
+ ""
1996
+ );
1997
+ block.push(schemaCode);
1998
+ block.push("");
1999
+ schemaBlocks.push(block.join("\n"));
2000
+ }
2001
+ const allSchemaCode = schemaBlocks.join("\n");
2002
+ if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2003
+ lines.push(
2004
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2005
+ );
2006
+ }
2007
+ lines.push("");
2008
+ lines.push(...schemaBlocks);
2009
+ return lines.join("\n").replace(/\n+$/, "\n");
2010
+ }
2011
+ async function generateSchemaFiles(routes, rootDir, dist) {
2012
+ if (routes.length === 0) return;
2013
+ const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2014
+ const sourcesByFile = /* @__PURE__ */ new Map();
2015
+ for (const source of sources) {
2016
+ let list = sourcesByFile.get(source.filePath);
2017
+ if (!list) {
2018
+ list = [];
2019
+ sourcesByFile.set(source.filePath, list);
2020
+ }
2021
+ list.push(source);
2022
+ }
2023
+ const fileEntries = [];
2024
+ for (const [filePath, fileSources] of sourcesByFile) {
2025
+ const relFile = path3.relative(rootDir, filePath).replace(/\\/g, "/");
2026
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2027
+ const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2028
+ let relForDir = relFile;
2029
+ if (relForDir.startsWith("src/")) {
2030
+ relForDir = relForDir.slice(4);
2031
+ }
2032
+ const dirIdx = relForDir.lastIndexOf("/");
2033
+ const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2034
+ const helpersImportPath = getHelpersImportPath(zodRelDir);
2035
+ const source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
2036
+ fileEntries.push({ outputPath, source });
2037
+ }
2038
+ const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2039
+ if (usesCoerceHelpers(allSourceCode)) {
2040
+ const helpersPath = path3.resolve(rootDir, dist, HELPERS_FILENAME);
2041
+ await writeSchemaFile(helpersPath, generateHelpersFileSource());
2042
+ }
2043
+ await Promise.all(
2044
+ fileEntries.map(({ outputPath, source }) => writeSchemaFile(outputPath, source))
2045
+ );
2046
+ }
2047
+ async function writeSchemaFile(outputPath, source) {
2048
+ await fs2.mkdir(path3.dirname(outputPath), { recursive: true });
2049
+ await fs2.writeFile(outputPath, source, "utf-8");
2050
+ }
2051
+
2052
+ // src/errors/FaapiError.ts
2053
+ var FaapiError = class extends Error {
2054
+ constructor(code, message, statusCode) {
2055
+ super(message);
2056
+ this.code = code;
2057
+ this.statusCode = statusCode;
2058
+ this.name = "FaapiError";
2059
+ }
2060
+ code;
2061
+ statusCode;
2062
+ };
2063
+
2064
+ // src/errors/httpErrors.ts
2065
+ function deriveStatusCode(issues) {
2066
+ const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
2067
+ return has400 ? 400 : 422;
2068
+ }
2069
+ var ValidationError = class extends FaapiError {
2070
+ constructor(message, issues) {
2071
+ super("VALIDATION_ERROR", message, deriveStatusCode(issues));
2072
+ this.issues = issues;
2073
+ this.name = "ValidationError";
2074
+ }
2075
+ issues;
2076
+ };
2077
+ var RouteNotFoundError = class extends FaapiError {
2078
+ constructor(path11) {
2079
+ super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
2080
+ this.name = "RouteNotFoundError";
2081
+ }
2082
+ };
2083
+ var MethodNotAllowedError = class extends FaapiError {
2084
+ constructor(method, path11, allowedMethods) {
2085
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
2086
+ this.allowedMethods = allowedMethods;
2087
+ this.name = "MethodNotAllowedError";
2088
+ }
2089
+ allowedMethods;
2090
+ };
2091
+ var InternalError = class extends FaapiError {
2092
+ constructor(message) {
2093
+ super("INTERNAL_ERROR", message, 500);
2094
+ this.name = "InternalError";
2095
+ }
2096
+ };
2097
+
2098
+ // src/cli/compileOnDemand.ts
2099
+ import path7 from "path";
2100
+ import fs6 from "fs";
2101
+
2102
+ // src/cli/compileDevRoutes.ts
2103
+ import path6 from "path";
2104
+ import fs5 from "fs";
2105
+ import fg2 from "fast-glob";
2106
+
2107
+ // src/cli/aliasPlugin.ts
2108
+ import path5 from "path";
2109
+ import fs4 from "fs";
2110
+
2111
+ // src/utils/resolveAlias.ts
2112
+ function resolveAlias(specifier, config) {
2113
+ const candidates = [];
2114
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2115
+ const wildcardIndex = pattern.indexOf("*");
2116
+ if (wildcardIndex === -1) {
2117
+ if (specifier === pattern) {
2118
+ candidates.push(...targets);
2119
+ }
2120
+ continue;
2121
+ }
2122
+ const prefix = pattern.slice(0, wildcardIndex);
2123
+ const suffix = pattern.slice(wildcardIndex + 1);
2124
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
2125
+ const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
2126
+ for (const target of targets) {
2127
+ candidates.push(target.replace("*", captured));
2128
+ }
2129
+ }
2130
+ }
2131
+ return candidates;
2132
+ }
2133
+
2134
+ // src/utils/readTsconfig.ts
2135
+ import ts6 from "typescript";
2136
+ import path4 from "path";
2137
+ import fs3 from "fs";
2138
+ function readTsconfig(rootDir) {
2139
+ const tsconfigPath = path4.resolve(rootDir, "tsconfig.json");
2140
+ if (!fs3.existsSync(tsconfigPath)) return null;
2141
+ const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
2142
+ if (configFile.error || !configFile.config) return null;
2143
+ const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
2144
+ const baseUrl = parsed.options.baseUrl ?? rootDir;
2145
+ const rawPaths = parsed.options.paths;
2146
+ if (!rawPaths) return null;
2147
+ const paths = {};
2148
+ for (const [pattern, targets] of Object.entries(rawPaths)) {
2149
+ paths[pattern] = targets.map((t) => path4.resolve(baseUrl, t));
2150
+ }
2151
+ return { baseUrl, paths };
2152
+ }
2153
+
2154
+ // src/cli/aliasPlugin.ts
2155
+ function toProdExtension(filePath) {
2156
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
2157
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
2158
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
2159
+ return filePath;
2160
+ }
2161
+ function toProdImportPath(sourceFile, importer) {
2162
+ const importerDir = path5.dirname(importer);
2163
+ let rel = path5.relative(importerDir, sourceFile);
2164
+ rel = rel.split(path5.sep).join("/");
2165
+ if (!rel.startsWith(".")) rel = "./" + rel;
2166
+ return toProdExtension(rel);
2167
+ }
2168
+ function toRealPath(p) {
2169
+ try {
2170
+ return fs4.realpathSync(p);
2171
+ } catch {
2172
+ return p;
2173
+ }
2174
+ }
2175
+ function isInsideDir(filePath, dir) {
2176
+ const rel = path5.relative(dir, filePath);
2177
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
2178
+ }
2179
+ var APP_DIR = "src";
2180
+ function toStrippedProdImportPath(sourceFile, rootDir) {
2181
+ const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
2182
+ const sourceReal = toRealPath(sourceFile);
2183
+ let rel = path5.relative(appDirAbs, sourceReal);
2184
+ rel = rel.split(path5.sep).join("/");
2185
+ if (!rel.startsWith(".")) rel = "./" + rel;
2186
+ return toProdExtension(rel);
2187
+ }
2188
+ var PROD_EXTS = [".js", ".mjs", ".cjs"];
2189
+ var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2190
+ var INDEX_EXTS = [
2191
+ "/index.ts",
2192
+ "/index.tsx",
2193
+ "/index.js",
2194
+ "/index.jsx",
2195
+ "/index.mjs",
2196
+ "/index.cjs"
2197
+ ];
2198
+ function resolveRelativeSpecifier(importer, specifier) {
2199
+ const importerDir = path5.dirname(importer);
2200
+ const base = path5.resolve(importerDir, specifier);
2201
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2202
+ return fs4.existsSync(base) ? base : null;
2203
+ }
2204
+ if (/\.(ts|tsx|jsx)$/.test(specifier)) {
2205
+ return fs4.existsSync(base) ? base : null;
2206
+ }
2207
+ for (const ext of SOURCE_EXTS) {
2208
+ const file = base + ext;
2209
+ if (fs4.existsSync(file)) return file;
2210
+ }
2211
+ for (const indexExt of INDEX_EXTS) {
2212
+ const file = base + indexExt;
2213
+ if (fs4.existsSync(file)) return file;
2214
+ }
2215
+ return null;
2216
+ }
2217
+ function createAliasPlugin(config, options) {
2218
+ const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
2219
+ const appDirAbs = options?.rootDir ? toRealPath(path5.resolve(options.rootDir, APP_DIR)) : null;
2220
+ return {
2221
+ name: "faapi-alias",
2222
+ setup(build) {
2223
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
2224
+ let source;
2225
+ try {
2226
+ source = fs4.readFileSync(args.path, "utf8");
2227
+ } catch {
2228
+ return void 0;
2229
+ }
2230
+ const importer = args.path;
2231
+ const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
2232
+ let modified = false;
2233
+ const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2234
+ if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
2235
+ return full;
2236
+ }
2237
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2238
+ const resolved = resolveRelativeSpecifier(importer, specifier);
2239
+ if (resolved) {
2240
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
2241
+ return full;
2242
+ }
2243
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
2244
+ modified = true;
2245
+ return `${prefix}${quote}${toStrippedProdImportPath(
2246
+ resolved,
2247
+ options.rootDir
2248
+ )}${quote}`;
2249
+ }
2250
+ modified = true;
2251
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
2252
+ }
2253
+ return full;
2254
+ }
2255
+ const candidates = resolveAlias(specifier, config);
2256
+ for (const candidate of candidates) {
2257
+ for (const ext of SOURCE_EXTS) {
2258
+ const file = candidate + ext;
2259
+ if (fs4.existsSync(file)) {
2260
+ modified = true;
2261
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2262
+ return `${prefix}${quote}${toStrippedProdImportPath(
2263
+ file,
2264
+ options.rootDir
2265
+ )}${quote}`;
2266
+ }
2267
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2268
+ }
2269
+ }
2270
+ for (const indexExt of INDEX_EXTS) {
2271
+ const file = candidate + indexExt;
2272
+ if (fs4.existsSync(file)) {
2273
+ modified = true;
2274
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2275
+ return `${prefix}${quote}${toStrippedProdImportPath(
2276
+ file,
2277
+ options.rootDir
2278
+ )}${quote}`;
2279
+ }
2280
+ return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
2281
+ }
2282
+ }
2283
+ }
2284
+ return full;
2285
+ });
2286
+ if (!modified) return void 0;
2287
+ return { contents: newSource, loader: "default" };
2288
+ });
2289
+ }
2290
+ };
2291
+ }
2292
+ function buildAliasPlugins(rootDir) {
2293
+ const tsconfig = readTsconfig(rootDir);
2294
+ return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
2295
+ }
2296
+
2297
+ // src/cli/compileDevRoutes.ts
2298
+ var APP_DIR2 = "src";
2299
+ async function compileDevRoutes(options) {
2300
+ const { rootDir, dist, files, logLevel = "silent" } = options;
2301
+ const entryPoints = files ?? await fg2([`${APP_DIR2}/**/*.ts`], {
2302
+ cwd: rootDir,
2303
+ onlyFiles: true,
2304
+ absolute: true,
2305
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
2306
+ });
2307
+ if (entryPoints.length === 0) {
2308
+ return { compiledFiles: [] };
2309
+ }
2310
+ const absDist = path6.resolve(rootDir, dist);
2311
+ await fs5.promises.mkdir(absDist, { recursive: true });
2312
+ const plugins = buildAliasPlugins(rootDir);
2313
+ const esbuild = await import("esbuild");
2314
+ const outbase = path6.resolve(rootDir, APP_DIR2);
2315
+ const result = await esbuild.build({
2316
+ entryPoints,
2317
+ outdir: absDist,
2318
+ outbase,
2319
+ bundle: false,
2320
+ platform: "node",
2321
+ format: "esm",
2322
+ sourcemap: true,
2323
+ packages: "external",
2324
+ plugins,
2325
+ logLevel,
2326
+ write: false
2327
+ });
2328
+ if (result.outputFiles) {
2329
+ await Promise.all(
2330
+ result.outputFiles.map(async (file) => {
2331
+ await fs5.promises.mkdir(path6.dirname(file.path), { recursive: true });
2332
+ const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2333
+ await fs5.promises.writeFile(tmp, file.contents);
2334
+ await fs5.promises.rename(tmp, file.path);
2335
+ })
2336
+ );
2337
+ }
2338
+ return { compiledFiles: entryPoints };
2339
+ }
2340
+
2341
+ // src/cli/compileOnDemand.ts
2342
+ function isProductFresh(sourceAbsPath, productAbsPath) {
2343
+ try {
2344
+ const srcStat = fs6.statSync(sourceAbsPath);
2345
+ const prodStat = fs6.statSync(productAbsPath);
2346
+ return prodStat.mtimeMs >= srcStat.mtimeMs;
2347
+ } catch {
2348
+ return false;
2349
+ }
2350
+ }
2351
+ var compiledFiles = /* @__PURE__ */ new Set();
2352
+ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2353
+ if (compiledFiles.has(sourceAbsPath)) {
2354
+ return false;
2355
+ }
2356
+ if (!fs6.existsSync(sourceAbsPath)) {
2357
+ return false;
2358
+ }
2359
+ const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2360
+ if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2361
+ compiledFiles.add(sourceAbsPath);
2362
+ return false;
2363
+ }
2364
+ await compileDevRoutes({
2365
+ rootDir,
2366
+ dist,
2367
+ files: [sourceAbsPath],
2368
+ logLevel: "silent"
2369
+ });
2370
+ compiledFiles.add(sourceAbsPath);
2371
+ return true;
2372
+ }
2373
+ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2374
+ const rel = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2375
+ if (!rel.startsWith("src/")) return null;
2376
+ const relWithoutSrc = rel.slice(4);
2377
+ const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2378
+ return path7.resolve(rootDir, dist, jsRel);
2379
+ }
2380
+ var generatedSchemas = /* @__PURE__ */ new Set();
2381
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2382
+ if (generatedSchemas.has(schemaPath)) {
2383
+ return false;
2384
+ }
2385
+ const prodAbsPath = path7.resolve(rootDir, routeFilePath);
2386
+ const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
2387
+ if (!fs6.existsSync(sourceAbsPath)) {
2388
+ return false;
2389
+ }
2390
+ if (isProductFresh(sourceAbsPath, schemaPath)) {
2391
+ generatedSchemas.add(schemaPath);
2392
+ return false;
2393
+ }
2394
+ const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
2395
+ if (fileRoutes.length === 0) {
2396
+ return false;
2397
+ }
2398
+ const sourceRelPath = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2399
+ const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2400
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2401
+ generatedSchemas.add(schemaPath);
2402
+ return true;
2403
+ }
2404
+ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2405
+ const rel = path7.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2406
+ let relWithoutDist = rel;
2407
+ if (relWithoutDist.startsWith(`${dist}/`)) {
2408
+ relWithoutDist = relWithoutDist.slice(dist.length + 1);
2409
+ }
2410
+ const srcRel = `src/${relWithoutDist}`;
2411
+ const tsRel = srcRel.replace(/\.js$/, ".ts");
2412
+ const tsAbs = path7.resolve(rootDir, tsRel);
2413
+ if (fs6.existsSync(tsAbs)) return tsAbs;
2414
+ return path7.resolve(rootDir, srcRel);
2415
+ }
2416
+ var devOnDemandEnabled = false;
2417
+ function isDevOnDemandEnabled() {
2418
+ return devOnDemandEnabled;
2419
+ }
2420
+ var devDistDir;
2421
+ function getDevDist() {
2422
+ return devDistDir;
2423
+ }
2424
+
2425
+ // src/validator/validateInput.ts
2426
+ var moduleCache = /* @__PURE__ */ new Map();
2427
+ function invalidateSchemaCache() {
2428
+ moduleCache.clear();
2429
+ }
2430
+ async function loadSchemaModule(schemaPath) {
2431
+ let mod = moduleCache.get(schemaPath);
2432
+ if (!mod) {
2433
+ mod = await importWithCacheBust(schemaPath, isDevOnDemandEnabled());
2434
+ moduleCache.set(schemaPath, mod);
2435
+ }
2436
+ return mod;
2437
+ }
2438
+ async function validateInput(schemaPath, method, inputType, input) {
2439
+ const schemaName = getSchemaName(method, inputType);
2440
+ const schemaKey = `${schemaName}Schema`;
2441
+ let mod;
2442
+ try {
2443
+ mod = await loadSchemaModule(schemaPath);
2444
+ } catch (err) {
2445
+ const reason = err instanceof Error ? err.message : String(err);
2446
+ throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
2447
+ }
2448
+ const schema = mod[schemaKey];
2449
+ if (schema === void 0 || schema === null) {
2450
+ const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2451
+ return { valid: true, issues: [], data };
2452
+ }
2453
+ if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
2454
+ throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
2455
+ }
2456
+ const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
2457
+ const zodSchema = schema;
2458
+ const result = zodSchema.safeParse(inputObj);
2459
+ if (result.success) {
2460
+ const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
2461
+ return { valid: true, issues: [], data };
2462
+ }
2463
+ const issues = mapZodIssues(result.error);
2464
+ return { valid: false, issues, data: inputObj };
2465
+ }
2466
+ function mapZodIssues(error) {
2467
+ return error.issues.map((issue) => {
2468
+ const code = mapZodCode(issue.code, issue.message);
2469
+ const path11 = issue.path.map(String).join(".") || "";
2470
+ return {
2471
+ path: path11,
2472
+ code,
2473
+ expected: issue.expected ?? mapExpectedFromMessage(issue.message),
2474
+ received: issue.received ?? mapReceivedFromMessage(issue.message),
2475
+ message: issue.message
2476
+ };
2477
+ });
2478
+ }
2479
+ function mapZodCode(zodCode, message) {
2480
+ switch (zodCode) {
2481
+ case "invalid_type":
2482
+ case "invalid_union":
2483
+ case "invalid_union_discriminator":
2484
+ return "TYPE_MISMATCH";
2485
+ case "unrecognized_keys":
2486
+ return "INVALID_FORMAT";
2487
+ case "invalid_value":
2488
+ case "invalid_string":
2489
+ case "too_small":
2490
+ case "too_big":
2491
+ case "invalid_intersection_types":
2492
+ case "not_multiple_of":
2493
+ return "INVALID_VALUE";
2494
+ case "custom":
2495
+ return "INVALID_VALUE";
2496
+ default:
2497
+ if (message.includes("Required") || message.includes("required")) {
2498
+ return "MISSING_FIELD";
2499
+ }
2500
+ return "INVALID_VALUE";
2501
+ }
2502
+ }
2503
+ function mapExpectedFromMessage(message) {
2504
+ const match = message.match(/Expected\s+(\w+)/i);
2505
+ return match ? match[1].toLowerCase() : "unknown";
2506
+ }
2507
+ function mapReceivedFromMessage(message) {
2508
+ const match = message.match(/received\s+(\w+)/i);
2509
+ return match ? match[1].toLowerCase() : "unknown";
2510
+ }
2511
+
2512
+ // src/server/createServer.ts
2513
+ import {
2514
+ createServer as createHttpServer
2515
+ } from "http";
2516
+ import { createSecureServer as createHttp2SecureServer } from "http2";
2517
+ import { readFileSync } from "fs";
2518
+ import { Readable as Readable2 } from "stream";
2519
+ import path9 from "path";
2520
+
2521
+ // src/router/matchRoute.ts
2522
+ function matchRoute(routes, method, path11) {
2523
+ for (const route of routes) {
2524
+ if (route.method !== method) {
2525
+ continue;
2526
+ }
2527
+ if (!route.isDynamic) {
2528
+ if (route.urlPath === path11) {
2529
+ return { route, params: {} };
2530
+ }
2531
+ continue;
2532
+ }
2533
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2534
+ if (params !== null) {
2535
+ return { route, params };
2536
+ }
2537
+ }
2538
+ return null;
2539
+ }
2540
+ function matchWsRoute(wsRoutes, path11) {
2541
+ for (const route of wsRoutes) {
2542
+ if (!route.isDynamic) {
2543
+ if (route.urlPath === path11) {
2544
+ return { route, params: {} };
2545
+ }
2546
+ continue;
2547
+ }
2548
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
2549
+ if (params !== null) {
2550
+ return { route, params };
2551
+ }
2552
+ }
2553
+ return null;
2554
+ }
2555
+ function matchDynamicPath(pattern, path11, paramNames, isCatchAll) {
2556
+ const patternSegments = pattern.split("/").filter(Boolean);
2557
+ const pathSegments = path11.split("/").filter(Boolean);
2558
+ if (isCatchAll) {
2559
+ const nonCatchAllCount = patternSegments.length - 1;
2560
+ if (pathSegments.length <= nonCatchAllCount) {
2561
+ return null;
2562
+ }
2563
+ const params2 = {};
2564
+ for (let i = 0; i < nonCatchAllCount; i++) {
2565
+ const patternSeg = patternSegments[i];
2566
+ const pathSeg = pathSegments[i];
2567
+ if (patternSeg.startsWith(":")) {
2568
+ const paramName = patternSeg.slice(1);
2569
+ params2[paramName] = pathSeg;
2570
+ } else if (patternSeg !== pathSeg) {
2571
+ return null;
2572
+ }
2573
+ }
2574
+ const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
2575
+ const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
2576
+ params2[catchAllParamName] = catchAllValue;
2577
+ if (Object.keys(params2).length !== paramNames.length) {
2578
+ return null;
2579
+ }
2580
+ return params2;
2581
+ }
2582
+ if (patternSegments.length !== pathSegments.length) {
2583
+ return null;
2584
+ }
2585
+ const params = {};
2586
+ for (let i = 0; i < patternSegments.length; i++) {
2587
+ const patternSeg = patternSegments[i];
2588
+ const pathSeg = pathSegments[i];
2589
+ if (patternSeg.startsWith(":")) {
2590
+ const paramName = patternSeg.slice(1);
2591
+ params[paramName] = pathSeg;
2592
+ } else if (patternSeg !== pathSeg) {
2593
+ return null;
2594
+ }
2595
+ }
2596
+ if (Object.keys(params).length !== paramNames.length) {
2597
+ return null;
2598
+ }
2599
+ return params;
2600
+ }
2601
+
2602
+ // src/loader/loadRouteModule.ts
2603
+ import fs7 from "fs";
2604
+
2605
+ // src/loader/resolveExports.ts
2606
+ function resolveExport(module, exportName) {
2607
+ if (exportName in module && typeof module[exportName] !== "undefined") {
2608
+ return module[exportName];
2609
+ }
2610
+ const defaultExport = module.default;
2611
+ if (defaultExport !== null && typeof defaultExport === "object") {
2612
+ const value = defaultExport[exportName];
2613
+ if (value !== void 0) {
2614
+ return value;
2615
+ }
2616
+ }
2617
+ return void 0;
2618
+ }
2619
+
2620
+ // src/loader/validateRouteModule.ts
2621
+ function validateRouteModule(value, method, filePath) {
2622
+ if (typeof value !== "function") {
2623
+ throw new Error(
2624
+ `Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
2625
+ );
2626
+ }
2627
+ }
2628
+
2629
+ // src/loader/loadRouteModule.ts
2630
+ async function loadRouteModule(filePath, method, rootDir) {
2631
+ if (isDevOnDemandEnabled() && rootDir) {
2632
+ const dist = getDevDist();
2633
+ if (dist) {
2634
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2635
+ if (sourcePath && fs7.existsSync(sourcePath)) {
2636
+ try {
2637
+ await ensureCompiled(sourcePath, rootDir, dist);
2638
+ } catch (compileErr) {
2639
+ const reason = compileErr instanceof Error ? compileErr.message : String(compileErr);
2640
+ throw new Error(`Failed to compile route module "${sourcePath}": ${reason}`, {
2641
+ cause: compileErr
2642
+ });
2643
+ }
2644
+ }
2645
+ }
2646
+ }
2647
+ let module;
2648
+ try {
2649
+ module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
2650
+ } catch (err) {
2651
+ const reason = err instanceof Error ? err.message : String(err);
2652
+ throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
2653
+ }
2654
+ const handler = resolveExport(module, method);
2655
+ validateRouteModule(handler, method, filePath);
2656
+ return { handler, method };
2657
+ }
2658
+
2659
+ // src/utils/parseJsonBody.ts
2660
+ function parseJsonBody(text) {
2661
+ try {
2662
+ const data = JSON.parse(text);
2663
+ return { success: true, data };
2664
+ } catch {
2665
+ return { success: false, error: "Invalid JSON body" };
2666
+ }
2667
+ }
2668
+
2669
+ // src/utils/parseMultipart.ts
2670
+ async function parseMultipart(request) {
2671
+ const formData = await request.formData();
2672
+ const fields = {};
2673
+ const files = [];
2674
+ for (const [key, value] of formData.entries()) {
2675
+ if (value instanceof File) {
2676
+ files.push({
2677
+ name: key,
2678
+ filename: value.name,
2679
+ type: value.type,
2680
+ size: value.size,
2681
+ arrayBuffer: () => value.arrayBuffer()
2682
+ });
2683
+ } else {
2684
+ if (key in fields) {
2685
+ const existing = fields[key];
2686
+ if (Array.isArray(existing)) {
2687
+ existing.push(value);
2688
+ } else {
2689
+ fields[key] = [existing, value];
2690
+ }
2691
+ } else {
2692
+ fields[key] = value;
2693
+ }
2694
+ }
2695
+ }
2696
+ return { fields, files };
2697
+ }
2698
+
2699
+ // src/runtime/resolveInput.ts
2700
+ async function resolveInput(method, request) {
2701
+ const inputType = getInputTypeForMethod(method);
2702
+ if (inputType === "body") {
2703
+ const contentType = request.headers.get("content-type") ?? "";
2704
+ if (contentType.includes("multipart/form-data")) {
2705
+ return parseMultipart(request);
2706
+ }
2707
+ if (contentType.includes("application/x-www-form-urlencoded")) {
2708
+ const text2 = await request.text();
2709
+ if (text2.trim() === "") return null;
2710
+ const params = new URLSearchParams(text2);
2711
+ const obj = {};
2712
+ for (const [key, value] of params) {
2713
+ obj[key] = value;
2714
+ }
2715
+ return obj;
2716
+ }
2717
+ const text = await request.text();
2718
+ if (text.trim() === "") {
2719
+ return null;
2720
+ }
2721
+ const result = parseJsonBody(text);
2722
+ if (!result.success) {
2723
+ throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
2724
+ {
2725
+ path: "body",
2726
+ code: "INVALID_FORMAT",
2727
+ expected: "JSON",
2728
+ received: "text",
2729
+ message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
2730
+ }
2731
+ ]);
2732
+ }
2733
+ return result.data;
2734
+ }
2735
+ const url = new URL(request.url);
2736
+ return queryToObject(url.searchParams);
2737
+ }
2738
+
2739
+ // src/response/sendNodeResponse.ts
2740
+ import { Readable } from "stream";
2741
+ async function sendNodeResponse(response, res) {
2742
+ res.statusCode = response.status;
2743
+ for (const [key, value] of response.headers) {
2744
+ if (key.toLowerCase() === "set-cookie") {
2745
+ res.appendHeader(key, value);
2746
+ } else {
2747
+ res.setHeader(key, value);
2748
+ }
2749
+ }
2750
+ if (response.body) {
2751
+ const nodeStream = Readable.fromWeb(response.body);
2752
+ await new Promise((resolve, reject) => {
2753
+ nodeStream.on("error", reject);
2754
+ res.on("error", reject);
2755
+ res.on("finish", resolve);
2756
+ nodeStream.pipe(res);
2757
+ });
2758
+ return;
2759
+ }
2760
+ res.end();
2761
+ }
2762
+
2763
+ // src/utils/getClientIp.ts
2764
+ function getClientIp(req) {
2765
+ const xff = req.headers["x-forwarded-for"];
2766
+ if (typeof xff === "string" && xff.length > 0) {
2767
+ const first = xff.split(",")[0]?.trim();
2768
+ if (first) return first;
2769
+ }
2770
+ const remote = req.socket?.remoteAddress;
2771
+ if (remote) {
2772
+ if (remote.startsWith("::ffff:")) {
2773
+ return remote.slice(7);
2774
+ }
2775
+ return remote;
2776
+ }
2777
+ return "";
2778
+ }
2779
+
2780
+ // src/middleware/cors.ts
2781
+ var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
2782
+ function cors(options = {}) {
2783
+ const {
2784
+ origin = true,
2785
+ methods = DEFAULT_METHODS,
2786
+ allowedHeaders,
2787
+ exposeHeaders,
2788
+ credentials = false,
2789
+ maxAge
2790
+ } = options;
2791
+ return async (ctx, next) => {
2792
+ const reqOrigin = ctx.headers.get("origin");
2793
+ if (!reqOrigin) {
2794
+ await next();
2795
+ return;
2796
+ }
2797
+ let allowOrigin = null;
2798
+ if (origin === true) {
2799
+ allowOrigin = reqOrigin;
2800
+ } else if (typeof origin === "string") {
2801
+ allowOrigin = reqOrigin === origin ? origin : null;
2802
+ } else if (Array.isArray(origin)) {
2803
+ allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
2804
+ }
2805
+ if (!allowOrigin) {
2806
+ await next();
2807
+ return;
2808
+ }
2809
+ ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
2810
+ if (origin === true || Array.isArray(origin)) {
2811
+ const existingVary = ctx.headers.get("vary");
2812
+ if (existingVary) {
2813
+ if (!existingVary.toLowerCase().includes("origin")) {
2814
+ ctx.setHeader("Vary", `${existingVary}, Origin`);
2815
+ }
2816
+ } else {
2817
+ ctx.setHeader("Vary", "Origin");
2818
+ }
2819
+ }
2820
+ ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
2821
+ if (allowedHeaders) {
2822
+ ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
2823
+ } else {
2824
+ const requestHeaders = ctx.headers.get("access-control-request-headers");
2825
+ if (requestHeaders) {
2826
+ ctx.setHeader("Access-Control-Allow-Headers", requestHeaders);
2827
+ }
2828
+ }
2829
+ if (exposeHeaders && exposeHeaders.length > 0) {
2830
+ ctx.setHeader("Access-Control-Expose-Headers", exposeHeaders.join(", "));
2831
+ }
2832
+ if (credentials) {
2833
+ ctx.setHeader("Access-Control-Allow-Credentials", "true");
2834
+ }
2835
+ if (maxAge !== void 0) {
2836
+ ctx.setHeader("Access-Control-Max-Age", String(maxAge));
2837
+ }
2838
+ if (ctx.method === "OPTIONS") {
2839
+ return new Response(null, { status: 204 });
2840
+ }
2841
+ await next();
2842
+ };
2843
+ }
2844
+
2845
+ // src/middleware/helmet.ts
2846
+ var DEFAULTS = {
2847
+ contentSecurityPolicy: "default-src 'self'",
2848
+ xFrameOptions: "SAMEORIGIN",
2849
+ xContentTypeOptions: true,
2850
+ referrerPolicy: "no-referrer",
2851
+ strictTransportSecurity: "max-age=31536000; includeSubDomains",
2852
+ xDnsPrefetchControl: true,
2853
+ xDownloadOptions: true,
2854
+ xPermittedCrossDomainPolicies: "none",
2855
+ crossOriginOpenerPolicy: "same-origin",
2856
+ crossOriginResourcePolicy: "same-origin",
2857
+ crossOriginEmbedderPolicy: false,
2858
+ originAgentCluster: true,
2859
+ xPoweredBy: true
2860
+ };
2861
+ function helmet(options = {}) {
2862
+ const opts = { ...DEFAULTS, ...options };
2863
+ return async (ctx, next) => {
2864
+ if (opts.contentSecurityPolicy !== false) {
2865
+ ctx.setHeader("Content-Security-Policy", opts.contentSecurityPolicy);
2866
+ }
2867
+ if (opts.xFrameOptions !== false) {
2868
+ ctx.setHeader("X-Frame-Options", opts.xFrameOptions);
2869
+ }
2870
+ if (opts.xContentTypeOptions) {
2871
+ ctx.setHeader("X-Content-Type-Options", "nosniff");
2872
+ }
2873
+ if (opts.referrerPolicy !== false) {
2874
+ ctx.setHeader("Referrer-Policy", opts.referrerPolicy);
2875
+ }
2876
+ if (opts.strictTransportSecurity !== false) {
2877
+ ctx.setHeader("Strict-Transport-Security", opts.strictTransportSecurity);
2878
+ }
2879
+ if (opts.xDnsPrefetchControl) {
2880
+ ctx.setHeader("X-DNS-Prefetch-Control", "off");
2881
+ }
2882
+ if (opts.xDownloadOptions) {
2883
+ ctx.setHeader("X-Download-Options", "noopen");
2884
+ }
2885
+ if (opts.xPermittedCrossDomainPolicies !== false) {
2886
+ ctx.setHeader("X-Permitted-Cross-Domain-Policies", opts.xPermittedCrossDomainPolicies);
2887
+ }
2888
+ if (opts.crossOriginOpenerPolicy !== false) {
2889
+ ctx.setHeader("Cross-Origin-Opener-Policy", opts.crossOriginOpenerPolicy);
2890
+ }
2891
+ if (opts.crossOriginResourcePolicy !== false) {
2892
+ ctx.setHeader("Cross-Origin-Resource-Policy", opts.crossOriginResourcePolicy);
2893
+ }
2894
+ if (opts.crossOriginEmbedderPolicy !== false) {
2895
+ ctx.setHeader("Cross-Origin-Embedder-Policy", opts.crossOriginEmbedderPolicy);
2896
+ }
2897
+ if (opts.originAgentCluster) {
2898
+ ctx.setHeader("Origin-Agent-Cluster", "?1");
2899
+ }
2900
+ if (opts.xPoweredBy) {
2901
+ ctx.setHeader("X-Powered-By", "faapi");
2902
+ }
2903
+ return await next();
2904
+ };
2905
+ }
2906
+
2907
+ // src/middleware/logger.ts
2908
+ function logger(options = {}) {
2909
+ return async (ctx, next) => {
2910
+ const log = options.log ?? console.log;
2911
+ const start = Date.now();
2912
+ try {
2913
+ const response = await next();
2914
+ const duration = Date.now() - start;
2915
+ const entry = {
2916
+ method: ctx.method,
2917
+ path: ctx.path,
2918
+ status: response.status,
2919
+ durationMs: duration
2920
+ };
2921
+ log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
2922
+ return response;
2923
+ } catch (err) {
2924
+ const duration = Date.now() - start;
2925
+ const message = err instanceof Error ? err.message : String(err);
2926
+ const status = err?.statusCode ?? 500;
2927
+ const entry = {
2928
+ method: ctx.method,
2929
+ path: ctx.path,
2930
+ status,
2931
+ durationMs: duration,
2932
+ error: message
2933
+ };
2934
+ log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
2935
+ throw err;
2936
+ }
2937
+ };
2938
+ }
2939
+
2940
+ // src/server/handleWsUpgrade.ts
2941
+ import fs8 from "fs";
2942
+ import { WebSocketServer, WebSocket } from "ws";
2943
+ import path8 from "path";
2944
+
2945
+ // src/errors/formatErrorResponse.ts
2946
+ function formatErrorResponse(error) {
2947
+ if (error instanceof ValidationError) {
2948
+ const body2 = {
2949
+ code: error.code,
2950
+ message: error.message,
2951
+ issues: error.issues
2952
+ };
2953
+ return new Response(JSON.stringify({ error: body2 }), {
2954
+ status: error.statusCode,
2955
+ headers: { "Content-Type": "application/json" }
2956
+ });
2957
+ }
2958
+ if (error instanceof MethodNotAllowedError) {
2959
+ const body2 = {
2960
+ code: error.code,
2961
+ message: error.message
2962
+ };
2963
+ return new Response(JSON.stringify({ error: body2 }), {
2964
+ status: error.statusCode,
2965
+ headers: {
2966
+ "Content-Type": "application/json",
2967
+ Allow: error.allowedMethods.join(", ")
2968
+ }
2969
+ });
2970
+ }
2971
+ if (error instanceof FaapiError) {
2972
+ const body2 = {
2973
+ code: error.code,
2974
+ message: error.message
2975
+ };
2976
+ return new Response(JSON.stringify({ error: body2 }), {
2977
+ status: error.statusCode,
2978
+ headers: { "Content-Type": "application/json" }
2979
+ });
2980
+ }
2981
+ const body = {
2982
+ code: "INTERNAL_ERROR",
2983
+ message: error instanceof Error ? error.message : "An unknown error occurred"
2984
+ };
2985
+ return new Response(JSON.stringify({ error: body }), {
2986
+ status: 500,
2987
+ headers: { "Content-Type": "application/json" }
2988
+ });
2989
+ }
2990
+
2991
+ // src/server/serverUtils.ts
2992
+ function nodeHttpToWebHeaders(req) {
2993
+ const headers = new Headers();
2994
+ for (const [key, value] of Object.entries(req.headers)) {
2995
+ if (value === void 0) continue;
2996
+ if (Array.isArray(value)) {
2997
+ for (const v of value) headers.append(key, v);
2998
+ } else {
2999
+ headers.set(key, value);
3000
+ }
3001
+ }
3002
+ return headers;
3003
+ }
3004
+ function buildErrorResponse(err) {
3005
+ try {
3006
+ return formatErrorResponse(err);
3007
+ } catch {
3008
+ return new Response(
3009
+ JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
3010
+ {
3011
+ status: 500,
3012
+ headers: { "Content-Type": "application/json" }
3013
+ }
3014
+ );
3015
+ }
3016
+ }
3017
+
3018
+ // src/runtime/wsHandler.ts
3019
+ function wrapWsSocket(rawSocket) {
3020
+ return {
3021
+ send(data) {
3022
+ const payload = typeof data === "string" || Buffer.isBuffer(data) ? data : JSON.stringify(data);
3023
+ rawSocket.send(payload);
3024
+ },
3025
+ close(code, reason) {
3026
+ rawSocket.close(code, reason);
3027
+ },
3028
+ get readyState() {
3029
+ return rawSocket.readyState;
3030
+ }
3031
+ };
3032
+ }
3033
+
3034
+ // src/server/handleWsUpgrade.ts
3035
+ function getPathname(req) {
3036
+ const url = req.url ?? "/";
3037
+ const idx = url.indexOf("?");
3038
+ return idx >= 0 ? url.slice(0, idx) : url;
3039
+ }
3040
+ async function loadWsHandler(filePath, ctx, rootDir) {
3041
+ if (isDevOnDemandEnabled() && rootDir) {
3042
+ const dist = getDevDist();
3043
+ if (dist) {
3044
+ const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3045
+ if (sourcePath && fs8.existsSync(sourcePath)) {
3046
+ await ensureCompiled(sourcePath, rootDir, dist);
3047
+ }
3048
+ }
3049
+ }
3050
+ const module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
3051
+ const handler = module["WS"];
3052
+ if (typeof handler !== "function") {
3053
+ throw new Error(`WS export not found in ${filePath}`);
3054
+ }
3055
+ return handler(ctx);
3056
+ }
3057
+ function bindEvents(rawSocket, handlers) {
3058
+ if (!handlers) return;
3059
+ const ws = wrapWsSocket(rawSocket);
3060
+ if (handlers.onOpen) {
3061
+ if (rawSocket.readyState === WebSocket.OPEN) {
3062
+ handlers.onOpen(ws);
3063
+ } else {
3064
+ rawSocket.once("open", () => handlers.onOpen(ws));
3065
+ }
3066
+ }
3067
+ if (handlers.onMessage) {
3068
+ rawSocket.on("message", (data) => {
3069
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
3070
+ handlers.onMessage(ws, buf.toString("utf8"));
3071
+ });
3072
+ }
3073
+ if (handlers.onClose) {
3074
+ rawSocket.on("close", (code, reason) => {
3075
+ handlers.onClose(ws, code, reason.toString("utf8"));
3076
+ });
3077
+ }
3078
+ if (handlers.onError) {
3079
+ rawSocket.on("error", (err) => {
3080
+ handlers.onError(ws, err);
3081
+ });
3082
+ }
3083
+ }
3084
+ async function sendResponseToSocket(socket, response) {
3085
+ const body = await response.text().catch(() => "");
3086
+ const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
3087
+ `;
3088
+ const headerLines = [];
3089
+ let hasContentLength = false;
3090
+ for (const [key, value] of response.headers) {
3091
+ if (key.toLowerCase() === "content-length") {
3092
+ hasContentLength = true;
3093
+ }
3094
+ headerLines.push(`${key}: ${value}`);
3095
+ }
3096
+ if (!hasContentLength) {
3097
+ headerLines.push(`Content-Length: ${Buffer.byteLength(body)}`);
3098
+ }
3099
+ socket.write(statusLine + headerLines.join("\r\n") + "\r\n\r\n" + body);
3100
+ socket.destroy();
3101
+ }
3102
+ function attachWebSocket(options) {
3103
+ const { server, routesRef, rootDir, config, globalMiddlewares } = options;
3104
+ const wss = new WebSocketServer({ noServer: true });
3105
+ server.on("upgrade", async (req, socket, head) => {
3106
+ const currentWsRoutes = routesRef.wsCurrent;
3107
+ const pathname = getPathname(req);
3108
+ const match = matchWsRoute(currentWsRoutes, pathname);
3109
+ if (!match) {
3110
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
3111
+ socket.destroy();
3112
+ return;
3113
+ }
3114
+ const { route, params } = match;
3115
+ const headers = nodeHttpToWebHeaders(req);
3116
+ const host = req.headers.host ?? "localhost";
3117
+ const url = `http://${host}${req.url ?? "/"}`;
3118
+ const request = new Request(url, { method: "GET", headers });
3119
+ const ctx = createContext(request, params, config, getClientIp(req));
3120
+ const meta = ctx.meta;
3121
+ let upgraded = false;
3122
+ const finalHandler = async () => {
3123
+ let handlers;
3124
+ try {
3125
+ const absoluteFilePath = path8.resolve(rootDir, route.filePath);
3126
+ handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
3127
+ } catch (err) {
3128
+ const reason = err instanceof Error ? err.message : String(err);
3129
+ console.error(`[faapi] WS handler \u52A0\u8F7D\u5931\u8D25 ${route.filePath}: ${reason}`);
3130
+ return new Response("Internal Server Error", { status: 500 });
3131
+ }
3132
+ await new Promise((resolve, reject) => {
3133
+ wss.handleUpgrade(req, socket, head, (rawSocket) => {
3134
+ try {
3135
+ bindEvents(rawSocket, handlers);
3136
+ wss.emit("connection", rawSocket, req);
3137
+ upgraded = true;
3138
+ resolve();
3139
+ } catch (err) {
3140
+ reject(err);
3141
+ }
3142
+ });
3143
+ });
3144
+ return new Response(null, { status: 200 });
3145
+ };
3146
+ let response;
3147
+ try {
3148
+ if (route.middlewares === void 0 && route.middlewarePaths) {
3149
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3150
+ if (bundle) {
3151
+ route.middlewares = bundle.middlewares;
3152
+ } else {
3153
+ route.middlewares = [];
3154
+ }
3155
+ }
3156
+ const dirMiddlewares = route.middlewares ?? [];
3157
+ const allMiddlewares = globalMiddlewares && globalMiddlewares.length > 0 ? [...globalMiddlewares, ...dirMiddlewares] : dirMiddlewares;
3158
+ if (allMiddlewares.length > 0) {
3159
+ response = await compose(allMiddlewares, ctx, finalHandler);
3160
+ } else {
3161
+ response = await finalHandler();
3162
+ }
3163
+ } catch (err) {
3164
+ if (upgraded) {
3165
+ console.error("[faapi] WS \u63E1\u624B\u540E\u4E2D\u95F4\u4EF6\u629B\u9519:", err);
3166
+ return;
3167
+ }
3168
+ response = buildErrorResponse(err);
3169
+ }
3170
+ if (upgraded) {
3171
+ return;
3172
+ }
3173
+ await sendResponseToSocket(socket, mergeMeta(response, meta));
3174
+ });
3175
+ return wss;
3176
+ }
3177
+
3178
+ // src/server/createServer.ts
3179
+ var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
3180
+ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3181
+ const forwardedProto = req.headers["x-forwarded-proto"];
3182
+ const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
3183
+ const host = req.headers.host ?? "localhost";
3184
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
3185
+ const headers = nodeHttpToWebHeaders(req);
3186
+ const method = req.method ?? "GET";
3187
+ if (method === "GET" || method === "HEAD") {
3188
+ return new Request(url.toString(), { method, headers });
3189
+ }
3190
+ const stream = Readable2.toWeb(req);
3191
+ const limitedStream = limitStreamSize(stream, bodyLimit);
3192
+ return new Request(url.toString(), {
3193
+ method,
3194
+ headers,
3195
+ body: limitedStream,
3196
+ duplex: "half"
3197
+ });
3198
+ }
3199
+ function limitStreamSize(stream, maxSize) {
3200
+ let totalSize = 0;
3201
+ const reader = stream.getReader();
3202
+ return new ReadableStream({
3203
+ async pull(controller) {
3204
+ const { done, value } = await reader.read();
3205
+ if (done) {
3206
+ controller.close();
3207
+ reader.releaseLock();
3208
+ return;
3209
+ }
3210
+ totalSize += value.byteLength;
3211
+ if (totalSize > maxSize) {
3212
+ controller.error(new Error(`\u8BF7\u6C42\u4F53\u8D85\u8FC7\u5927\u5C0F\u9650\u5236 ${maxSize} \u5B57\u8282`));
3213
+ reader.releaseLock();
3214
+ return;
3215
+ }
3216
+ controller.enqueue(value);
3217
+ },
3218
+ cancel(reason) {
3219
+ reader.cancel(reason);
3220
+ }
3221
+ });
3222
+ }
3223
+ function findAllowedMethods(routes, path11) {
3224
+ const methods = /* @__PURE__ */ new Set();
3225
+ for (const route of routes) {
3226
+ if (route.urlPath === path11) {
3227
+ methods.add(route.method);
3228
+ continue;
3229
+ }
3230
+ if (route.isDynamic) {
3231
+ const params = matchDynamicPath(route.urlPath, path11, route.paramNames, route.isCatchAll);
3232
+ if (params !== null) {
3233
+ methods.add(route.method);
3234
+ }
3235
+ }
3236
+ }
3237
+ return Array.from(methods);
3238
+ }
3239
+ function createServer(options) {
3240
+ const {
3241
+ routes,
3242
+ rootDir,
3243
+ dist,
3244
+ cors: corsOption,
3245
+ onError,
3246
+ config,
3247
+ wsRoutes,
3248
+ middlewares: globalMiddlewares,
3249
+ injectors: globalInjectors,
3250
+ helmet: helmetOption,
3251
+ logger: loggerOption,
3252
+ bodyLimit = DEFAULT_BODY_LIMIT,
3253
+ http2: http2Option
3254
+ } = options;
3255
+ const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
3256
+ const configMiddlewares = [];
3257
+ const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
3258
+ if (corsMiddleware) configMiddlewares.push(corsMiddleware);
3259
+ if (helmetOption) {
3260
+ const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
3261
+ configMiddlewares.push(helmet(helmOpts));
3262
+ }
3263
+ const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
3264
+ if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
3265
+ const server = (() => {
3266
+ if (http2Option) {
3267
+ const h2Opts = typeof http2Option === "object" ? http2Option : {};
3268
+ return createHttp2SecureServer({
3269
+ key: h2Opts.key ? readFileSync(h2Opts.key) : void 0,
3270
+ cert: h2Opts.cert ? readFileSync(h2Opts.cert) : void 0,
3271
+ allowHTTP1: true
3272
+ });
3273
+ }
3274
+ return createHttpServer();
3275
+ })();
3276
+ server.on("request", (req, res) => {
3277
+ const currentRoutes = routesRef.current;
3278
+ handleRequest(
3279
+ currentRoutes,
3280
+ rootDir,
3281
+ dist,
3282
+ req,
3283
+ res,
3284
+ configMiddlewares,
3285
+ onError,
3286
+ config,
3287
+ globalMiddlewares,
3288
+ globalInjectors,
3289
+ bodyLimit
3290
+ ).catch(() => {
3291
+ res.statusCode = 500;
3292
+ res.end();
3293
+ });
3294
+ });
3295
+ if (routesRef.wsCurrent.length > 0) {
3296
+ attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares });
3297
+ }
3298
+ return { server, routesRef };
3299
+ }
3300
+ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3301
+ const request = toWebRequest(req, bodyLimit);
3302
+ const method = request.method.toUpperCase();
3303
+ const urlPath = new URL(request.url).pathname;
3304
+ const ctx = createContext(request, {}, config, getClientIp(req));
3305
+ const meta = ctx.meta;
3306
+ const routePipeline = async () => {
3307
+ const match = matchRoute(routes, method, urlPath);
3308
+ if (!match) {
3309
+ const allowedMethods = findAllowedMethods(routes, urlPath);
3310
+ if (allowedMethods.length > 0) {
3311
+ throw new MethodNotAllowedError(method, urlPath, allowedMethods);
3312
+ }
3313
+ throw new RouteNotFoundError(urlPath);
3314
+ }
3315
+ ctx.params = match.params;
3316
+ const { route } = match;
3317
+ const absoluteFilePath = path9.resolve(rootDir, route.filePath);
3318
+ const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
3319
+ const input = await resolveInput(route.method, request);
3320
+ const inputType = getInputTypeForMethod(route.method);
3321
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
3322
+ if (isDevOnDemandEnabled()) {
3323
+ const devDist = getDevDist();
3324
+ if (devDist) {
3325
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
3326
+ }
3327
+ }
3328
+ const result = await validateInput(schemaPath, route.method, inputType, input);
3329
+ if (!result.valid) {
3330
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
3331
+ }
3332
+ const body = hasBody(route.method) ? result.data : void 0;
3333
+ if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
3334
+ const bundle = await loadMergedMiddlewares(route.middlewarePaths);
3335
+ if (bundle) {
3336
+ route.middlewares = bundle.middlewares;
3337
+ route.injectors = bundle.injectors;
3338
+ } else {
3339
+ route.middlewares = [];
3340
+ route.injectors = {};
3341
+ }
3342
+ }
3343
+ const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
3344
+ const response = await invokeHandler(
3345
+ routeModule.handler,
3346
+ ctx,
3347
+ body,
3348
+ route.middlewares,
3349
+ mergedInjectors
3350
+ );
3351
+ return response;
3352
+ };
3353
+ try {
3354
+ let response;
3355
+ const outerMiddlewares = [];
3356
+ if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3357
+ if (globalMiddlewares && globalMiddlewares.length > 0) {
3358
+ outerMiddlewares.push(...globalMiddlewares);
3359
+ }
3360
+ if (outerMiddlewares.length > 0) {
3361
+ response = await compose(outerMiddlewares, ctx, routePipeline);
3362
+ } else {
3363
+ response = await routePipeline();
3364
+ }
3365
+ await sendNodeResponse(response, res);
3366
+ } catch (err) {
3367
+ const errorResponse = buildErrorResponse(err);
3368
+ await sendNodeResponse(mergeMeta(errorResponse, meta), res);
3369
+ if (onError) {
3370
+ try {
3371
+ await onError(err, ctx);
3372
+ } catch {
3373
+ }
3374
+ }
3375
+ }
3376
+ }
3377
+
3378
+ // src/testServer.ts
3379
+ var DEFAULT_PATTERNS = ["src/api/**/*.ts"];
3380
+ var DEFAULT_BODY_LIMIT2 = 10 * 1024 * 1024;
3381
+ async function createTestServer(options) {
3382
+ const {
3383
+ rootDir,
3384
+ patterns = DEFAULT_PATTERNS,
3385
+ dist,
3386
+ cors: cors2 = false,
3387
+ helmet: helmet2 = false,
3388
+ logger: logger2 = false,
3389
+ middlewares,
3390
+ injectors,
3391
+ onError,
3392
+ config,
3393
+ bodyLimit = DEFAULT_BODY_LIMIT2
3394
+ } = options;
3395
+ const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
3396
+ const sorted = sortRoutes(routes);
3397
+ const schemaDist = dist ? path10.isAbsolute(dist) ? dist : path10.resolve(rootDir, dist) : await fs9.mkdtemp(path10.join(os.tmpdir(), "faapi-test-schema-"));
3398
+ await generateSchemaFiles(sorted, rootDir, schemaDist);
3399
+ const { server } = createServer({
3400
+ routes: sorted,
3401
+ rootDir,
3402
+ dist: schemaDist,
3403
+ cors: cors2,
3404
+ helmet: helmet2,
3405
+ logger: logger2,
3406
+ middlewares,
3407
+ injectors,
3408
+ onError,
3409
+ config,
3410
+ wsRoutes,
3411
+ bodyLimit
3412
+ });
3413
+ const baseUrl = await listenOnRandomPort(server);
3414
+ let closed = false;
3415
+ const testServer = {
3416
+ server,
3417
+ baseUrl,
3418
+ routes: sorted,
3419
+ wsRoutes,
3420
+ schemaDist,
3421
+ async close() {
3422
+ if (closed) return;
3423
+ closed = true;
3424
+ const s = server;
3425
+ s.closeAllConnections?.();
3426
+ s.closeIdleConnections?.();
3427
+ await new Promise((resolve) => {
3428
+ server.close(() => resolve());
3429
+ });
3430
+ await fs9.rm(schemaDist, { recursive: true, force: true }).catch(() => {
3431
+ });
3432
+ invalidateSchemaCache();
3433
+ }
3434
+ };
3435
+ return testServer;
3436
+ }
3437
+ function listenOnRandomPort(server) {
3438
+ return new Promise((resolve, reject) => {
3439
+ server.listen(0, () => {
3440
+ const addr = server.address();
3441
+ if (typeof addr === "object" && addr !== null) {
3442
+ resolve(`http://localhost:${addr.port}`);
3443
+ } else {
3444
+ reject(new Error("Failed to get server address"));
3445
+ }
3446
+ });
3447
+ server.on("error", (err) => {
3448
+ reject(err);
3449
+ });
3450
+ });
3451
+ }
3452
+
3453
+ // src/wsTestClient.ts
3454
+ import { WebSocket as WebSocket2 } from "ws";
3455
+ var MessageQueue = class {
3456
+ queue = [];
3457
+ waiters = [];
3458
+ listener;
3459
+ constructor(ws) {
3460
+ this.listener = (data) => {
3461
+ const msg = normalizeRawData(data);
3462
+ const waiter = this.waiters.shift();
3463
+ if (waiter) {
3464
+ waiter(msg);
3465
+ } else {
3466
+ this.queue.push(msg);
3467
+ }
3468
+ };
3469
+ ws.on("message", this.listener);
3470
+ }
3471
+ /**
3472
+ * 取下一条消息
3473
+ *
3474
+ * 队列有则立即 resolve,无则注册 waiter 等待下一条 'message' 事件。
3475
+ * 超时未到 → reject('WebSocket message timeout'),waiter 被清理。
3476
+ *
3477
+ * @param timeout 超时毫秒,默认 2000
3478
+ */
3479
+ next(timeout = 2e3) {
3480
+ return new Promise((resolve, reject) => {
3481
+ const wrapped = (msg2) => {
3482
+ clearTimeout(timer);
3483
+ resolve(msg2);
3484
+ };
3485
+ const timer = setTimeout(() => {
3486
+ const idx = this.waiters.indexOf(wrapped);
3487
+ if (idx >= 0) this.waiters.splice(idx, 1);
3488
+ reject(new Error("WebSocket message timeout"));
3489
+ }, timeout);
3490
+ const msg = this.queue.shift();
3491
+ if (msg !== void 0) {
3492
+ wrapped(msg);
3493
+ } else {
3494
+ this.waiters.push(wrapped);
3495
+ }
3496
+ });
3497
+ }
3498
+ };
3499
+ function normalizeRawData(data) {
3500
+ if (Buffer.isBuffer(data)) {
3501
+ return data.toString("utf8");
3502
+ }
3503
+ if (Array.isArray(data)) {
3504
+ return Buffer.concat(data).toString("utf8");
3505
+ }
3506
+ return Buffer.from(data).toString("utf8");
3507
+ }
3508
+ function waitForWsOpen(ws, timeout = 2e3) {
3509
+ return new Promise((resolve, reject) => {
3510
+ const timer = setTimeout(() => {
3511
+ reject(new Error("WebSocket open timeout"));
3512
+ }, timeout);
3513
+ const cleanup = () => {
3514
+ clearTimeout(timer);
3515
+ ws.removeListener("open", onOpen);
3516
+ ws.removeListener("error", onError);
3517
+ ws.removeListener("close", onClose);
3518
+ };
3519
+ const onOpen = () => {
3520
+ cleanup();
3521
+ resolve();
3522
+ };
3523
+ const onError = (err) => {
3524
+ cleanup();
3525
+ reject(err);
3526
+ };
3527
+ const onClose = () => {
3528
+ cleanup();
3529
+ reject(new Error("WebSocket closed before open"));
3530
+ };
3531
+ ws.once("open", onOpen);
3532
+ ws.once("error", onError);
3533
+ ws.once("close", onClose);
3534
+ });
3535
+ }
3536
+ async function connectWs(baseUrl, pathname, options = {}) {
3537
+ const { timeout = 2e3, headers, protocols } = options;
3538
+ const wsBaseUrl = baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
3539
+ const url = `${wsBaseUrl}${pathname}`;
3540
+ const ws = new WebSocket2(url, protocols, headers ? { headers } : void 0);
3541
+ const queue = new MessageQueue(ws);
3542
+ try {
3543
+ await waitForWsOpen(ws, timeout);
3544
+ } catch (err) {
3545
+ if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3546
+ ws.close();
3547
+ }
3548
+ throw err;
3549
+ }
3550
+ let closed = false;
3551
+ return {
3552
+ ws,
3553
+ queue,
3554
+ async close() {
3555
+ if (closed) return;
3556
+ closed = true;
3557
+ if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
3558
+ ws.close();
3559
+ }
3560
+ await new Promise((resolve) => {
3561
+ const timer = setTimeout(resolve, 1e3);
3562
+ ws.once("close", () => {
3563
+ clearTimeout(timer);
3564
+ resolve();
3565
+ });
3566
+ });
3567
+ }
3568
+ };
3569
+ }
3570
+ export {
3571
+ MessageQueue,
3572
+ connectWs,
3573
+ createTestContext,
3574
+ createTestServer,
3575
+ invokeHandler,
3576
+ waitForWsOpen
3577
+ };
3578
+ //# sourceMappingURL=testing.js.map