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