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