@faapi/faapi 0.0.0-canary.2a7b6a3 → 0.0.0-canary.4e89b9b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +465 -209
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +54 -10
- package/dist/index.js +396 -338
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -290,10 +290,35 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
290
290
|
const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
|
|
291
291
|
return { kind: "object", properties };
|
|
292
292
|
}
|
|
293
|
-
if (typeName === "Map"
|
|
293
|
+
if (typeName === "Map") {
|
|
294
|
+
if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
|
|
295
|
+
throw new SchemaExtractionError(
|
|
296
|
+
typeNode.getText(),
|
|
297
|
+
"Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
kind: "map",
|
|
302
|
+
key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
|
|
303
|
+
value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (typeName === "Set") {
|
|
307
|
+
if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
|
|
308
|
+
throw new SchemaExtractionError(
|
|
309
|
+
typeNode.getText(),
|
|
310
|
+
"Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
kind: "set",
|
|
315
|
+
element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (typeName === "WeakMap" || typeName === "WeakSet") {
|
|
294
319
|
throw new SchemaExtractionError(
|
|
295
320
|
typeNode.getText(),
|
|
296
|
-
`${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528\u5BF9\u8C61
|
|
321
|
+
`${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
|
|
297
322
|
);
|
|
298
323
|
}
|
|
299
324
|
if (typeName === "Promise") {
|
|
@@ -650,6 +675,7 @@ import ts4 from "typescript";
|
|
|
650
675
|
var PARAM_TYPE_MAP = {
|
|
651
676
|
query: "query",
|
|
652
677
|
body: "body",
|
|
678
|
+
form: "form",
|
|
653
679
|
headers: "headers",
|
|
654
680
|
params: "params",
|
|
655
681
|
context: "context",
|
|
@@ -807,9 +833,16 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
807
833
|
const inputType = getInputTypeForMethod(method);
|
|
808
834
|
const schemaName = getSchemaName(method, inputType);
|
|
809
835
|
const meta = analyzeInjection(code, method);
|
|
810
|
-
const param = meta.params.find((p) => p.type === inputType);
|
|
836
|
+
const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
|
|
837
|
+
const isForm = param?.type === "form";
|
|
811
838
|
const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
|
|
812
|
-
sources.push({
|
|
839
|
+
sources.push({
|
|
840
|
+
urlPath: entry.urlPath,
|
|
841
|
+
filePath,
|
|
842
|
+
schemaName,
|
|
843
|
+
typeInfo,
|
|
844
|
+
coerce: isForm || void 0
|
|
845
|
+
});
|
|
813
846
|
}
|
|
814
847
|
}
|
|
815
848
|
return { sources, allTypesByFile, mergedAllTypes };
|
|
@@ -882,8 +915,8 @@ function cors(options = {}) {
|
|
|
882
915
|
|
|
883
916
|
// src/middleware/logger.ts
|
|
884
917
|
function logger(options = {}) {
|
|
885
|
-
const { log = console.log } = options;
|
|
886
918
|
return async (ctx, next) => {
|
|
919
|
+
const log = options.log ?? console.log;
|
|
887
920
|
const start = Date.now();
|
|
888
921
|
try {
|
|
889
922
|
const response = await next();
|
|
@@ -995,8 +1028,8 @@ async function importWithCacheBust(filePath) {
|
|
|
995
1028
|
|
|
996
1029
|
// src/config/loadConfig.ts
|
|
997
1030
|
var CONFIG_PRODUCT_FILE = "faapi-config.js";
|
|
998
|
-
async function loadConfig(rootDir,
|
|
999
|
-
const configProductPath = path2.resolve(rootDir,
|
|
1031
|
+
async function loadConfig(rootDir, dist) {
|
|
1032
|
+
const configProductPath = path2.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
|
|
1000
1033
|
if (fs.existsSync(configProductPath)) {
|
|
1001
1034
|
const module = await importWithCacheBust(configProductPath);
|
|
1002
1035
|
return module.default ?? {};
|
|
@@ -1004,7 +1037,7 @@ async function loadConfig(rootDir, outDir) {
|
|
|
1004
1037
|
const hasSourceConfig = fs.existsSync(path2.join(rootDir, "faapi.config.ts")) || fs.existsSync(path2.join(rootDir, "faapi.config.js"));
|
|
1005
1038
|
if (hasSourceConfig) {
|
|
1006
1039
|
throw new Error(
|
|
1007
|
-
`[faapi] ${
|
|
1040
|
+
`[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
1008
1041
|
);
|
|
1009
1042
|
}
|
|
1010
1043
|
return null;
|
|
@@ -1062,182 +1095,6 @@ var ModuleLoadError = class extends FaapiError {
|
|
|
1062
1095
|
}
|
|
1063
1096
|
};
|
|
1064
1097
|
|
|
1065
|
-
// src/cli/createAppCore.ts
|
|
1066
|
-
import fs4 from "fs";
|
|
1067
|
-
import path7 from "path";
|
|
1068
|
-
import { PassThrough } from "stream";
|
|
1069
|
-
|
|
1070
|
-
// src/router/sortRoutes.ts
|
|
1071
|
-
function sortRoutes(routes) {
|
|
1072
|
-
return [...routes].sort((a, b) => {
|
|
1073
|
-
if (a.isDynamic !== b.isDynamic) {
|
|
1074
|
-
return a.isDynamic ? 1 : -1;
|
|
1075
|
-
}
|
|
1076
|
-
if (a.isCatchAll !== b.isCatchAll) {
|
|
1077
|
-
return a.isCatchAll ? 1 : -1;
|
|
1078
|
-
}
|
|
1079
|
-
const aSegments = a.urlPath.split("/").filter(Boolean).length;
|
|
1080
|
-
const bSegments = b.urlPath.split("/").filter(Boolean).length;
|
|
1081
|
-
if (aSegments !== bSegments) {
|
|
1082
|
-
return aSegments - bSegments;
|
|
1083
|
-
}
|
|
1084
|
-
return a.urlPath.localeCompare(b.urlPath);
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// src/router/detectRouteConflicts.ts
|
|
1089
|
-
function detectRouteConflicts(routes) {
|
|
1090
|
-
const map = /* @__PURE__ */ new Map();
|
|
1091
|
-
for (const route of routes) {
|
|
1092
|
-
const key = `${route.method} ${route.urlPath}`;
|
|
1093
|
-
const existing = map.get(key);
|
|
1094
|
-
if (existing) {
|
|
1095
|
-
existing.files.push(route.filePath);
|
|
1096
|
-
} else {
|
|
1097
|
-
map.set(key, {
|
|
1098
|
-
method: route.method,
|
|
1099
|
-
urlPath: route.urlPath,
|
|
1100
|
-
files: [route.filePath]
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
const conflicts = [];
|
|
1105
|
-
for (const conflict of map.values()) {
|
|
1106
|
-
if (conflict.files.length > 1) {
|
|
1107
|
-
conflicts.push(conflict);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return conflicts;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// src/server/createServer.ts
|
|
1114
|
-
import {
|
|
1115
|
-
createServer as createHttpServer
|
|
1116
|
-
} from "http";
|
|
1117
|
-
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
1118
|
-
import { readFileSync } from "fs";
|
|
1119
|
-
import { Readable as Readable2 } from "stream";
|
|
1120
|
-
import path5 from "path";
|
|
1121
|
-
|
|
1122
|
-
// src/router/matchRoute.ts
|
|
1123
|
-
function matchRoute(routes, method, path9) {
|
|
1124
|
-
for (const route of routes) {
|
|
1125
|
-
if (route.method !== method) {
|
|
1126
|
-
continue;
|
|
1127
|
-
}
|
|
1128
|
-
if (!route.isDynamic) {
|
|
1129
|
-
if (route.urlPath === path9) {
|
|
1130
|
-
return { route, params: {} };
|
|
1131
|
-
}
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1135
|
-
if (params !== null) {
|
|
1136
|
-
return { route, params };
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
return null;
|
|
1140
|
-
}
|
|
1141
|
-
function matchWsRoute(wsRoutes, path9) {
|
|
1142
|
-
for (const route of wsRoutes) {
|
|
1143
|
-
if (!route.isDynamic) {
|
|
1144
|
-
if (route.urlPath === path9) {
|
|
1145
|
-
return { route, params: {} };
|
|
1146
|
-
}
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1150
|
-
if (params !== null) {
|
|
1151
|
-
return { route, params };
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return null;
|
|
1155
|
-
}
|
|
1156
|
-
function matchDynamicPath(pattern, path9, paramNames, isCatchAll) {
|
|
1157
|
-
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1158
|
-
const pathSegments = path9.split("/").filter(Boolean);
|
|
1159
|
-
if (isCatchAll) {
|
|
1160
|
-
const nonCatchAllCount = patternSegments.length - 1;
|
|
1161
|
-
if (pathSegments.length <= nonCatchAllCount) {
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
const params2 = {};
|
|
1165
|
-
for (let i = 0; i < nonCatchAllCount; i++) {
|
|
1166
|
-
const patternSeg = patternSegments[i];
|
|
1167
|
-
const pathSeg = pathSegments[i];
|
|
1168
|
-
if (patternSeg.startsWith(":")) {
|
|
1169
|
-
const paramName = patternSeg.slice(1);
|
|
1170
|
-
params2[paramName] = pathSeg;
|
|
1171
|
-
} else if (patternSeg !== pathSeg) {
|
|
1172
|
-
return null;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
|
|
1176
|
-
const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
|
|
1177
|
-
params2[catchAllParamName] = catchAllValue;
|
|
1178
|
-
if (Object.keys(params2).length !== paramNames.length) {
|
|
1179
|
-
return null;
|
|
1180
|
-
}
|
|
1181
|
-
return params2;
|
|
1182
|
-
}
|
|
1183
|
-
if (patternSegments.length !== pathSegments.length) {
|
|
1184
|
-
return null;
|
|
1185
|
-
}
|
|
1186
|
-
const params = {};
|
|
1187
|
-
for (let i = 0; i < patternSegments.length; i++) {
|
|
1188
|
-
const patternSeg = patternSegments[i];
|
|
1189
|
-
const pathSeg = pathSegments[i];
|
|
1190
|
-
if (patternSeg.startsWith(":")) {
|
|
1191
|
-
const paramName = patternSeg.slice(1);
|
|
1192
|
-
params[paramName] = pathSeg;
|
|
1193
|
-
} else if (patternSeg !== pathSeg) {
|
|
1194
|
-
return null;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
if (Object.keys(params).length !== paramNames.length) {
|
|
1198
|
-
return null;
|
|
1199
|
-
}
|
|
1200
|
-
return params;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// src/loader/resolveExports.ts
|
|
1204
|
-
function resolveExport(module, exportName) {
|
|
1205
|
-
if (exportName in module && typeof module[exportName] !== "undefined") {
|
|
1206
|
-
return module[exportName];
|
|
1207
|
-
}
|
|
1208
|
-
const defaultExport = module.default;
|
|
1209
|
-
if (defaultExport !== null && typeof defaultExport === "object") {
|
|
1210
|
-
const value = defaultExport[exportName];
|
|
1211
|
-
if (value !== void 0) {
|
|
1212
|
-
return value;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
return void 0;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// src/loader/validateRouteModule.ts
|
|
1219
|
-
function validateRouteModule(value, method, filePath) {
|
|
1220
|
-
if (typeof value !== "function") {
|
|
1221
|
-
throw new Error(
|
|
1222
|
-
`Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// src/loader/loadRouteModule.ts
|
|
1228
|
-
async function loadRouteModule(filePath, method) {
|
|
1229
|
-
let module;
|
|
1230
|
-
try {
|
|
1231
|
-
module = await importWithCacheBust(filePath);
|
|
1232
|
-
} catch (err) {
|
|
1233
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1234
|
-
throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
|
|
1235
|
-
}
|
|
1236
|
-
const handler = resolveExport(module, method);
|
|
1237
|
-
validateRouteModule(handler, method, filePath);
|
|
1238
|
-
return { handler, method };
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
1098
|
// src/runtime/sse.ts
|
|
1242
1099
|
function encodeSseEvent(event) {
|
|
1243
1100
|
let out = "";
|
|
@@ -1442,111 +1299,22 @@ function createContext(request, params, config = {}, ip = "") {
|
|
|
1442
1299
|
return ctx;
|
|
1443
1300
|
}
|
|
1444
1301
|
|
|
1445
|
-
// src/utils/
|
|
1446
|
-
function
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
result[key] = value;
|
|
1302
|
+
// src/utils/isPlainObject.ts
|
|
1303
|
+
function isPlainObject(value) {
|
|
1304
|
+
if (value === null || typeof value !== "object") {
|
|
1305
|
+
return false;
|
|
1450
1306
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
// src/utils/parseJsonBody.ts
|
|
1455
|
-
function parseJsonBody(text) {
|
|
1456
|
-
try {
|
|
1457
|
-
const data = JSON.parse(text);
|
|
1458
|
-
return { success: true, data };
|
|
1459
|
-
} catch {
|
|
1460
|
-
return { success: false, error: "Invalid JSON body" };
|
|
1307
|
+
if (Array.isArray(value)) {
|
|
1308
|
+
return false;
|
|
1461
1309
|
}
|
|
1310
|
+
const proto = Object.getPrototypeOf(value);
|
|
1311
|
+
return proto === null || proto === Object.prototype;
|
|
1462
1312
|
}
|
|
1463
1313
|
|
|
1464
|
-
// src/
|
|
1465
|
-
async function
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const files = [];
|
|
1469
|
-
for (const [key, value] of formData.entries()) {
|
|
1470
|
-
if (value instanceof File) {
|
|
1471
|
-
files.push({
|
|
1472
|
-
name: key,
|
|
1473
|
-
filename: value.name,
|
|
1474
|
-
type: value.type,
|
|
1475
|
-
size: value.size,
|
|
1476
|
-
arrayBuffer: () => value.arrayBuffer()
|
|
1477
|
-
});
|
|
1478
|
-
} else {
|
|
1479
|
-
if (key in fields) {
|
|
1480
|
-
const existing = fields[key];
|
|
1481
|
-
if (Array.isArray(existing)) {
|
|
1482
|
-
existing.push(value);
|
|
1483
|
-
} else {
|
|
1484
|
-
fields[key] = [existing, value];
|
|
1485
|
-
}
|
|
1486
|
-
} else {
|
|
1487
|
-
fields[key] = value;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
return { fields, files };
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// src/runtime/resolveInput.ts
|
|
1495
|
-
async function resolveInput(method, request) {
|
|
1496
|
-
const inputType = getInputTypeForMethod(method);
|
|
1497
|
-
if (inputType === "body") {
|
|
1498
|
-
const contentType = request.headers.get("content-type") ?? "";
|
|
1499
|
-
if (contentType.includes("multipart/form-data")) {
|
|
1500
|
-
return parseMultipart(request);
|
|
1501
|
-
}
|
|
1502
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1503
|
-
const text2 = await request.text();
|
|
1504
|
-
if (text2.trim() === "") return null;
|
|
1505
|
-
const params = new URLSearchParams(text2);
|
|
1506
|
-
const obj = {};
|
|
1507
|
-
for (const [key, value] of params) {
|
|
1508
|
-
obj[key] = value;
|
|
1509
|
-
}
|
|
1510
|
-
return obj;
|
|
1511
|
-
}
|
|
1512
|
-
const text = await request.text();
|
|
1513
|
-
if (text.trim() === "") {
|
|
1514
|
-
return null;
|
|
1515
|
-
}
|
|
1516
|
-
const result = parseJsonBody(text);
|
|
1517
|
-
if (!result.success) {
|
|
1518
|
-
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
1519
|
-
{
|
|
1520
|
-
path: "body",
|
|
1521
|
-
code: "INVALID_FORMAT",
|
|
1522
|
-
expected: "JSON",
|
|
1523
|
-
received: "text",
|
|
1524
|
-
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
1525
|
-
}
|
|
1526
|
-
]);
|
|
1527
|
-
}
|
|
1528
|
-
return result.data;
|
|
1529
|
-
}
|
|
1530
|
-
const url = new URL(request.url);
|
|
1531
|
-
return queryToObject(url.searchParams);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// src/utils/isPlainObject.ts
|
|
1535
|
-
function isPlainObject(value) {
|
|
1536
|
-
if (value === null || typeof value !== "object") {
|
|
1537
|
-
return false;
|
|
1538
|
-
}
|
|
1539
|
-
if (Array.isArray(value)) {
|
|
1540
|
-
return false;
|
|
1541
|
-
}
|
|
1542
|
-
const proto = Object.getPrototypeOf(value);
|
|
1543
|
-
return proto === null || proto === Object.prototype;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
// src/response/toResponse.ts
|
|
1547
|
-
async function toResponse(value, meta) {
|
|
1548
|
-
if (value instanceof Promise) {
|
|
1549
|
-
return toResponse(await value, meta);
|
|
1314
|
+
// src/response/toResponse.ts
|
|
1315
|
+
async function toResponse(value, meta) {
|
|
1316
|
+
if (value instanceof Promise) {
|
|
1317
|
+
return toResponse(await value, meta);
|
|
1550
1318
|
}
|
|
1551
1319
|
const applyMeta = (headers2) => {
|
|
1552
1320
|
if (!meta) return;
|
|
@@ -1632,6 +1400,15 @@ async function toResponse(value, meta) {
|
|
|
1632
1400
|
});
|
|
1633
1401
|
}
|
|
1634
1402
|
|
|
1403
|
+
// src/utils/queryToObject.ts
|
|
1404
|
+
function queryToObject(params) {
|
|
1405
|
+
const result = {};
|
|
1406
|
+
for (const [key, value] of params) {
|
|
1407
|
+
result[key] = value;
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1635
1412
|
// src/injection/injectParams.ts
|
|
1636
1413
|
function getBuiltinInjectionValue(type, ctx, body) {
|
|
1637
1414
|
switch (type) {
|
|
@@ -1649,6 +1426,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
|
|
|
1649
1426
|
return ctx.ip;
|
|
1650
1427
|
case "body":
|
|
1651
1428
|
return body;
|
|
1429
|
+
// form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
|
|
1430
|
+
// 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
|
|
1431
|
+
case "form":
|
|
1432
|
+
return body;
|
|
1652
1433
|
case "files":
|
|
1653
1434
|
if (body && typeof body === "object" && "files" in body) {
|
|
1654
1435
|
return body.files;
|
|
@@ -1767,6 +1548,262 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
1767
1548
|
return await compose(middlewares, ctx, finalHandler);
|
|
1768
1549
|
}
|
|
1769
1550
|
|
|
1551
|
+
// src/cli/createAppCore.ts
|
|
1552
|
+
import fs4 from "fs";
|
|
1553
|
+
import path7 from "path";
|
|
1554
|
+
import { PassThrough } from "stream";
|
|
1555
|
+
|
|
1556
|
+
// src/router/sortRoutes.ts
|
|
1557
|
+
function sortRoutes(routes) {
|
|
1558
|
+
return [...routes].sort((a, b) => {
|
|
1559
|
+
if (a.isDynamic !== b.isDynamic) {
|
|
1560
|
+
return a.isDynamic ? 1 : -1;
|
|
1561
|
+
}
|
|
1562
|
+
if (a.isCatchAll !== b.isCatchAll) {
|
|
1563
|
+
return a.isCatchAll ? 1 : -1;
|
|
1564
|
+
}
|
|
1565
|
+
const aSegments = a.urlPath.split("/").filter(Boolean).length;
|
|
1566
|
+
const bSegments = b.urlPath.split("/").filter(Boolean).length;
|
|
1567
|
+
if (aSegments !== bSegments) {
|
|
1568
|
+
return aSegments - bSegments;
|
|
1569
|
+
}
|
|
1570
|
+
return a.urlPath.localeCompare(b.urlPath);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/router/detectRouteConflicts.ts
|
|
1575
|
+
function detectRouteConflicts(routes) {
|
|
1576
|
+
const map = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const route of routes) {
|
|
1578
|
+
const key = `${route.method} ${route.urlPath}`;
|
|
1579
|
+
const existing = map.get(key);
|
|
1580
|
+
if (existing) {
|
|
1581
|
+
existing.files.push(route.filePath);
|
|
1582
|
+
} else {
|
|
1583
|
+
map.set(key, {
|
|
1584
|
+
method: route.method,
|
|
1585
|
+
urlPath: route.urlPath,
|
|
1586
|
+
files: [route.filePath]
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const conflicts = [];
|
|
1591
|
+
for (const conflict of map.values()) {
|
|
1592
|
+
if (conflict.files.length > 1) {
|
|
1593
|
+
conflicts.push(conflict);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return conflicts;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/server/createServer.ts
|
|
1600
|
+
import {
|
|
1601
|
+
createServer as createHttpServer
|
|
1602
|
+
} from "http";
|
|
1603
|
+
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
1604
|
+
import { readFileSync } from "fs";
|
|
1605
|
+
import { Readable as Readable2 } from "stream";
|
|
1606
|
+
import path5 from "path";
|
|
1607
|
+
|
|
1608
|
+
// src/router/matchRoute.ts
|
|
1609
|
+
function matchRoute(routes, method, path9) {
|
|
1610
|
+
for (const route of routes) {
|
|
1611
|
+
if (route.method !== method) {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (!route.isDynamic) {
|
|
1615
|
+
if (route.urlPath === path9) {
|
|
1616
|
+
return { route, params: {} };
|
|
1617
|
+
}
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1621
|
+
if (params !== null) {
|
|
1622
|
+
return { route, params };
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function matchWsRoute(wsRoutes, path9) {
|
|
1628
|
+
for (const route of wsRoutes) {
|
|
1629
|
+
if (!route.isDynamic) {
|
|
1630
|
+
if (route.urlPath === path9) {
|
|
1631
|
+
return { route, params: {} };
|
|
1632
|
+
}
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const params = matchDynamicPath(route.urlPath, path9, route.paramNames, route.isCatchAll);
|
|
1636
|
+
if (params !== null) {
|
|
1637
|
+
return { route, params };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
function matchDynamicPath(pattern, path9, paramNames, isCatchAll) {
|
|
1643
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1644
|
+
const pathSegments = path9.split("/").filter(Boolean);
|
|
1645
|
+
if (isCatchAll) {
|
|
1646
|
+
const nonCatchAllCount = patternSegments.length - 1;
|
|
1647
|
+
if (pathSegments.length <= nonCatchAllCount) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
const params2 = {};
|
|
1651
|
+
for (let i = 0; i < nonCatchAllCount; i++) {
|
|
1652
|
+
const patternSeg = patternSegments[i];
|
|
1653
|
+
const pathSeg = pathSegments[i];
|
|
1654
|
+
if (patternSeg.startsWith(":")) {
|
|
1655
|
+
const paramName = patternSeg.slice(1);
|
|
1656
|
+
params2[paramName] = pathSeg;
|
|
1657
|
+
} else if (patternSeg !== pathSeg) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
|
|
1662
|
+
const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
|
|
1663
|
+
params2[catchAllParamName] = catchAllValue;
|
|
1664
|
+
if (Object.keys(params2).length !== paramNames.length) {
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
return params2;
|
|
1668
|
+
}
|
|
1669
|
+
if (patternSegments.length !== pathSegments.length) {
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
const params = {};
|
|
1673
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
1674
|
+
const patternSeg = patternSegments[i];
|
|
1675
|
+
const pathSeg = pathSegments[i];
|
|
1676
|
+
if (patternSeg.startsWith(":")) {
|
|
1677
|
+
const paramName = patternSeg.slice(1);
|
|
1678
|
+
params[paramName] = pathSeg;
|
|
1679
|
+
} else if (patternSeg !== pathSeg) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (Object.keys(params).length !== paramNames.length) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
return params;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/loader/resolveExports.ts
|
|
1690
|
+
function resolveExport(module, exportName) {
|
|
1691
|
+
if (exportName in module && typeof module[exportName] !== "undefined") {
|
|
1692
|
+
return module[exportName];
|
|
1693
|
+
}
|
|
1694
|
+
const defaultExport = module.default;
|
|
1695
|
+
if (defaultExport !== null && typeof defaultExport === "object") {
|
|
1696
|
+
const value = defaultExport[exportName];
|
|
1697
|
+
if (value !== void 0) {
|
|
1698
|
+
return value;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/loader/validateRouteModule.ts
|
|
1705
|
+
function validateRouteModule(value, method, filePath) {
|
|
1706
|
+
if (typeof value !== "function") {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/loader/loadRouteModule.ts
|
|
1714
|
+
async function loadRouteModule(filePath, method) {
|
|
1715
|
+
let module;
|
|
1716
|
+
try {
|
|
1717
|
+
module = await importWithCacheBust(filePath);
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1720
|
+
throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
|
|
1721
|
+
}
|
|
1722
|
+
const handler = resolveExport(module, method);
|
|
1723
|
+
validateRouteModule(handler, method, filePath);
|
|
1724
|
+
return { handler, method };
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/utils/parseJsonBody.ts
|
|
1728
|
+
function parseJsonBody(text) {
|
|
1729
|
+
try {
|
|
1730
|
+
const data = JSON.parse(text);
|
|
1731
|
+
return { success: true, data };
|
|
1732
|
+
} catch {
|
|
1733
|
+
return { success: false, error: "Invalid JSON body" };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/utils/parseMultipart.ts
|
|
1738
|
+
async function parseMultipart(request) {
|
|
1739
|
+
const formData = await request.formData();
|
|
1740
|
+
const fields = {};
|
|
1741
|
+
const files = [];
|
|
1742
|
+
for (const [key, value] of formData.entries()) {
|
|
1743
|
+
if (value instanceof File) {
|
|
1744
|
+
files.push({
|
|
1745
|
+
name: key,
|
|
1746
|
+
filename: value.name,
|
|
1747
|
+
type: value.type,
|
|
1748
|
+
size: value.size,
|
|
1749
|
+
arrayBuffer: () => value.arrayBuffer()
|
|
1750
|
+
});
|
|
1751
|
+
} else {
|
|
1752
|
+
if (key in fields) {
|
|
1753
|
+
const existing = fields[key];
|
|
1754
|
+
if (Array.isArray(existing)) {
|
|
1755
|
+
existing.push(value);
|
|
1756
|
+
} else {
|
|
1757
|
+
fields[key] = [existing, value];
|
|
1758
|
+
}
|
|
1759
|
+
} else {
|
|
1760
|
+
fields[key] = value;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return { fields, files };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/runtime/resolveInput.ts
|
|
1768
|
+
async function resolveInput(method, request) {
|
|
1769
|
+
const inputType = getInputTypeForMethod(method);
|
|
1770
|
+
if (inputType === "body") {
|
|
1771
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
1772
|
+
if (contentType.includes("multipart/form-data")) {
|
|
1773
|
+
return parseMultipart(request);
|
|
1774
|
+
}
|
|
1775
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1776
|
+
const text2 = await request.text();
|
|
1777
|
+
if (text2.trim() === "") return null;
|
|
1778
|
+
const params = new URLSearchParams(text2);
|
|
1779
|
+
const obj = {};
|
|
1780
|
+
for (const [key, value] of params) {
|
|
1781
|
+
obj[key] = value;
|
|
1782
|
+
}
|
|
1783
|
+
return obj;
|
|
1784
|
+
}
|
|
1785
|
+
const text = await request.text();
|
|
1786
|
+
if (text.trim() === "") {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
const result = parseJsonBody(text);
|
|
1790
|
+
if (!result.success) {
|
|
1791
|
+
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
1792
|
+
{
|
|
1793
|
+
path: "body",
|
|
1794
|
+
code: "INVALID_FORMAT",
|
|
1795
|
+
expected: "JSON",
|
|
1796
|
+
received: "text",
|
|
1797
|
+
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
1798
|
+
}
|
|
1799
|
+
]);
|
|
1800
|
+
}
|
|
1801
|
+
return result.data;
|
|
1802
|
+
}
|
|
1803
|
+
const url = new URL(request.url);
|
|
1804
|
+
return queryToObject(url.searchParams);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1770
1807
|
// src/response/sendNodeResponse.ts
|
|
1771
1808
|
import { Readable } from "stream";
|
|
1772
1809
|
async function sendNodeResponse(response, res) {
|
|
@@ -2175,6 +2212,13 @@ function collectNamedTypes(type, ctx) {
|
|
|
2175
2212
|
collectNamedTypes(type.key, ctx);
|
|
2176
2213
|
collectNamedTypes(type.value, ctx);
|
|
2177
2214
|
return;
|
|
2215
|
+
case "map":
|
|
2216
|
+
collectNamedTypes(type.key, ctx);
|
|
2217
|
+
collectNamedTypes(type.value, ctx);
|
|
2218
|
+
return;
|
|
2219
|
+
case "set":
|
|
2220
|
+
collectNamedTypes(type.element, ctx);
|
|
2221
|
+
return;
|
|
2178
2222
|
case "ref": {
|
|
2179
2223
|
if (ctx.namedTypes.has(type.name)) return;
|
|
2180
2224
|
ctx.namedTypes.set(type.name, { kind: "any" });
|
|
@@ -2264,6 +2308,10 @@ function baseExpression(type, ctx) {
|
|
|
2264
2308
|
return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
|
|
2265
2309
|
case "record":
|
|
2266
2310
|
return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
|
|
2311
|
+
case "map":
|
|
2312
|
+
return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
|
|
2313
|
+
case "set":
|
|
2314
|
+
return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
|
|
2267
2315
|
case "ref":
|
|
2268
2316
|
if (type.name === ctx.entryTypeName) {
|
|
2269
2317
|
return `${ctx.entryExportName}Schema`;
|
|
@@ -2273,17 +2321,21 @@ function baseExpression(type, ctx) {
|
|
|
2273
2321
|
}
|
|
2274
2322
|
var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
|
|
2275
2323
|
var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
|
|
2324
|
+
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);';
|
|
2325
|
+
var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
|
|
2276
2326
|
var HELPERS_FILENAME = "faapi-helpers.js";
|
|
2277
2327
|
function generateHelpersFileSource() {
|
|
2278
2328
|
return [
|
|
2279
2329
|
"// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
|
|
2280
2330
|
COERCE_NUMBER_HELPER,
|
|
2281
2331
|
COERCE_BOOLEAN_HELPER,
|
|
2332
|
+
COERCE_MAP_HELPER,
|
|
2333
|
+
COERCE_SET_HELPER,
|
|
2282
2334
|
""
|
|
2283
2335
|
].join("\n");
|
|
2284
2336
|
}
|
|
2285
2337
|
function usesCoerceHelpers(code) {
|
|
2286
|
-
return code.includes("coerceNumber") || code.includes("coerceBoolean");
|
|
2338
|
+
return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
|
|
2287
2339
|
}
|
|
2288
2340
|
function wrapCoercePreprocess(kind, inner) {
|
|
2289
2341
|
if (kind === "number") {
|
|
@@ -2370,6 +2422,10 @@ function containsRef(type, visited) {
|
|
|
2370
2422
|
return type.members.some((m) => containsRef(m, visited));
|
|
2371
2423
|
case "record":
|
|
2372
2424
|
return containsRef(type.key, visited) || containsRef(type.value, visited);
|
|
2425
|
+
case "map":
|
|
2426
|
+
return containsRef(type.key, visited) || containsRef(type.value, visited);
|
|
2427
|
+
case "set":
|
|
2428
|
+
return containsRef(type.element, visited);
|
|
2373
2429
|
default:
|
|
2374
2430
|
return false;
|
|
2375
2431
|
}
|
|
@@ -2400,25 +2456,25 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
|
|
|
2400
2456
|
}
|
|
2401
2457
|
|
|
2402
2458
|
// src/cli/generateSchemaFiles.ts
|
|
2403
|
-
function getSchemaOutputPath(sourceFile,
|
|
2459
|
+
function getSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
2404
2460
|
let rel = sourceFile.replace(/\\/g, "/");
|
|
2405
|
-
if (
|
|
2406
|
-
rel = rel.slice(
|
|
2461
|
+
if (rel.startsWith("src/")) {
|
|
2462
|
+
rel = rel.slice(4);
|
|
2407
2463
|
}
|
|
2408
2464
|
const idx = rel.lastIndexOf("/");
|
|
2409
2465
|
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2410
|
-
return path4.resolve(rootDir,
|
|
2466
|
+
return path4.resolve(rootDir, dist, relDir, "zod.js");
|
|
2411
2467
|
}
|
|
2412
|
-
function getRuntimeSchemaPath(filePath,
|
|
2468
|
+
function getRuntimeSchemaPath(filePath, dist, rootDir) {
|
|
2413
2469
|
let rel = filePath.replace(/\\/g, "/");
|
|
2414
|
-
if (
|
|
2415
|
-
rel = rel.slice(
|
|
2416
|
-
} else if (rel.startsWith(`${
|
|
2417
|
-
rel = rel.slice(
|
|
2470
|
+
if (rel.startsWith("src/")) {
|
|
2471
|
+
rel = rel.slice(4);
|
|
2472
|
+
} else if (rel.startsWith(`${dist}/`)) {
|
|
2473
|
+
rel = rel.slice(dist.length + 1);
|
|
2418
2474
|
}
|
|
2419
2475
|
const idx = rel.lastIndexOf("/");
|
|
2420
2476
|
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2421
|
-
return path4.resolve(rootDir,
|
|
2477
|
+
return path4.resolve(rootDir, dist, relDir, "zod.js");
|
|
2422
2478
|
}
|
|
2423
2479
|
function getHelpersImportPath(relDir) {
|
|
2424
2480
|
if (!relDir) return `./${HELPERS_FILENAME}`;
|
|
@@ -2434,7 +2490,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
|
2434
2490
|
if (!typeInfo) {
|
|
2435
2491
|
continue;
|
|
2436
2492
|
}
|
|
2437
|
-
const coerce = /(?:Query|Params)$/.test(schemaName);
|
|
2493
|
+
const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
|
|
2438
2494
|
const block = [`// ${schemaName}`];
|
|
2439
2495
|
const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
|
|
2440
2496
|
/^import \{ z \} from 'zod';\s*\n\s*\n/,
|
|
@@ -2446,13 +2502,15 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
|
2446
2502
|
}
|
|
2447
2503
|
const allSchemaCode = schemaBlocks.join("\n");
|
|
2448
2504
|
if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
|
|
2449
|
-
lines.push(
|
|
2505
|
+
lines.push(
|
|
2506
|
+
`import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
|
|
2507
|
+
);
|
|
2450
2508
|
}
|
|
2451
2509
|
lines.push("");
|
|
2452
2510
|
lines.push(...schemaBlocks);
|
|
2453
2511
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
2454
2512
|
}
|
|
2455
|
-
async function generateSchemaFiles(routes, rootDir,
|
|
2513
|
+
async function generateSchemaFiles(routes, rootDir, dist) {
|
|
2456
2514
|
if (routes.length === 0) return;
|
|
2457
2515
|
const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
|
|
2458
2516
|
const sourcesByFile = /* @__PURE__ */ new Map();
|
|
@@ -2467,11 +2525,11 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
|
|
|
2467
2525
|
const fileEntries = [];
|
|
2468
2526
|
for (const [filePath, fileSources] of sourcesByFile) {
|
|
2469
2527
|
const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
2470
|
-
const outputPath = getSchemaOutputPath(relFile,
|
|
2528
|
+
const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
|
|
2471
2529
|
const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
|
|
2472
2530
|
let relForDir = relFile;
|
|
2473
|
-
if (
|
|
2474
|
-
relForDir = relForDir.slice(
|
|
2531
|
+
if (relForDir.startsWith("src/")) {
|
|
2532
|
+
relForDir = relForDir.slice(4);
|
|
2475
2533
|
}
|
|
2476
2534
|
const dirIdx = relForDir.lastIndexOf("/");
|
|
2477
2535
|
const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
|
|
@@ -2481,7 +2539,7 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
|
|
|
2481
2539
|
}
|
|
2482
2540
|
const allSourceCode = fileEntries.map((e) => e.source).join("\n");
|
|
2483
2541
|
if (usesCoerceHelpers(allSourceCode)) {
|
|
2484
|
-
const helpersPath = path4.resolve(rootDir,
|
|
2542
|
+
const helpersPath = path4.resolve(rootDir, dist, HELPERS_FILENAME);
|
|
2485
2543
|
await writeSchemaFile(helpersPath, generateHelpersFileSource());
|
|
2486
2544
|
}
|
|
2487
2545
|
await Promise.all(
|
|
@@ -2558,8 +2616,7 @@ function createServer(options) {
|
|
|
2558
2616
|
const {
|
|
2559
2617
|
routes,
|
|
2560
2618
|
rootDir,
|
|
2561
|
-
|
|
2562
|
-
outDir,
|
|
2619
|
+
dist,
|
|
2563
2620
|
cors: corsOption,
|
|
2564
2621
|
onError,
|
|
2565
2622
|
config,
|
|
@@ -2567,6 +2624,7 @@ function createServer(options) {
|
|
|
2567
2624
|
middlewares: globalMiddlewares,
|
|
2568
2625
|
injectors: globalInjectors,
|
|
2569
2626
|
helmet: helmetOption,
|
|
2627
|
+
logger: loggerOption,
|
|
2570
2628
|
bodyLimit = DEFAULT_BODY_LIMIT,
|
|
2571
2629
|
http2: http2Option
|
|
2572
2630
|
} = options;
|
|
@@ -2578,6 +2636,8 @@ function createServer(options) {
|
|
|
2578
2636
|
const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
|
|
2579
2637
|
configMiddlewares.push(helmet(helmOpts));
|
|
2580
2638
|
}
|
|
2639
|
+
const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
|
|
2640
|
+
if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
|
|
2581
2641
|
const server = (() => {
|
|
2582
2642
|
if (http2Option) {
|
|
2583
2643
|
const h2Opts = typeof http2Option === "object" ? http2Option : {};
|
|
@@ -2594,8 +2654,7 @@ function createServer(options) {
|
|
|
2594
2654
|
handleRequest(
|
|
2595
2655
|
currentRoutes,
|
|
2596
2656
|
rootDir,
|
|
2597
|
-
|
|
2598
|
-
outDir,
|
|
2657
|
+
dist,
|
|
2599
2658
|
req,
|
|
2600
2659
|
res,
|
|
2601
2660
|
configMiddlewares,
|
|
@@ -2614,7 +2673,7 @@ function createServer(options) {
|
|
|
2614
2673
|
}
|
|
2615
2674
|
return { server, routesRef };
|
|
2616
2675
|
}
|
|
2617
|
-
async function handleRequest(routes, rootDir,
|
|
2676
|
+
async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
|
|
2618
2677
|
const request = toWebRequest(req, bodyLimit);
|
|
2619
2678
|
const method = request.method.toUpperCase();
|
|
2620
2679
|
const urlPath = new URL(request.url).pathname;
|
|
@@ -2635,7 +2694,7 @@ async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMi
|
|
|
2635
2694
|
const routeModule = await loadRouteModule(absoluteFilePath, route.method);
|
|
2636
2695
|
const input = await resolveInput(route.method, request);
|
|
2637
2696
|
const inputType = getInputTypeForMethod(route.method);
|
|
2638
|
-
const schemaPath = getRuntimeSchemaPath(route.filePath,
|
|
2697
|
+
const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
|
|
2639
2698
|
const result = await validateInput(schemaPath, route.method, inputType, input);
|
|
2640
2699
|
if (!result.valid) {
|
|
2641
2700
|
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
|
|
@@ -2862,10 +2921,10 @@ function resolveDeclaration(decl) {
|
|
|
2862
2921
|
}
|
|
2863
2922
|
|
|
2864
2923
|
// src/cli/createAppCore.ts
|
|
2865
|
-
var
|
|
2866
|
-
var DEFAULT_APP_DIR = "src";
|
|
2924
|
+
var DEFAULT_DIST = "dist";
|
|
2867
2925
|
var DEFAULT_PORT = 3e3;
|
|
2868
2926
|
var ROUTES_FILE = "faapi-routes.js";
|
|
2927
|
+
var PATTERNS = ["src/api/**/*.ts"];
|
|
2869
2928
|
var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2870
2929
|
"cors",
|
|
2871
2930
|
"lifecycle",
|
|
@@ -2883,16 +2942,14 @@ function isFaapiConfigKey(key) {
|
|
|
2883
2942
|
}
|
|
2884
2943
|
async function createAppBase(options) {
|
|
2885
2944
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
2886
|
-
const
|
|
2887
|
-
const routesPath = path7.resolve(rootDir,
|
|
2945
|
+
const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
|
|
2946
|
+
const routesPath = path7.resolve(rootDir, dist, ROUTES_FILE);
|
|
2888
2947
|
if (!fs4.existsSync(routesPath)) {
|
|
2889
2948
|
throw new Error(
|
|
2890
|
-
`[faapi] ${
|
|
2949
|
+
`[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
2891
2950
|
);
|
|
2892
2951
|
}
|
|
2893
|
-
const config = await loadConfig(rootDir,
|
|
2894
|
-
const appDir = options?.appDir ?? process.env.FAAPI_APP_DIR ?? DEFAULT_APP_DIR;
|
|
2895
|
-
const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
|
|
2952
|
+
const config = await loadConfig(rootDir, dist);
|
|
2896
2953
|
const serialized = await importWithCacheBust(routesPath);
|
|
2897
2954
|
const hydrated = await hydrateRoutes(serialized);
|
|
2898
2955
|
let sorted = sortRoutes(hydrated.routes);
|
|
@@ -2910,8 +2967,7 @@ async function createAppBase(options) {
|
|
|
2910
2967
|
const { server, routesRef } = createServer({
|
|
2911
2968
|
routes: sorted,
|
|
2912
2969
|
rootDir,
|
|
2913
|
-
|
|
2914
|
-
outDir,
|
|
2970
|
+
dist,
|
|
2915
2971
|
cors: config?.cors ?? true,
|
|
2916
2972
|
onError: config?.lifecycle?.onError,
|
|
2917
2973
|
config: config ?? void 0,
|
|
@@ -2919,6 +2975,7 @@ async function createAppBase(options) {
|
|
|
2919
2975
|
middlewares: config?.middlewares,
|
|
2920
2976
|
injectors: config?.injectors,
|
|
2921
2977
|
helmet: config?.helmet,
|
|
2978
|
+
logger: config?.logger,
|
|
2922
2979
|
bodyLimit: config?.bodyLimit,
|
|
2923
2980
|
http2: config?.http2
|
|
2924
2981
|
});
|
|
@@ -3071,9 +3128,8 @@ async function createAppBase(options) {
|
|
|
3071
3128
|
};
|
|
3072
3129
|
const ctx = {
|
|
3073
3130
|
rootDir,
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
patterns,
|
|
3131
|
+
dist,
|
|
3132
|
+
patterns: PATTERNS,
|
|
3077
3133
|
server,
|
|
3078
3134
|
routesRef,
|
|
3079
3135
|
config,
|
|
@@ -3139,8 +3195,8 @@ function isCatchAllSegment(segment) {
|
|
|
3139
3195
|
function isRouteGroup(segment) {
|
|
3140
3196
|
return /^\(.+\)$/.test(segment);
|
|
3141
3197
|
}
|
|
3142
|
-
function filePathToUrlPath(filePath
|
|
3143
|
-
const withoutPrefix = filePath.startsWith(
|
|
3198
|
+
function filePathToUrlPath(filePath) {
|
|
3199
|
+
const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
|
|
3144
3200
|
const lastSlashIndex = withoutPrefix.lastIndexOf("/");
|
|
3145
3201
|
const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
|
|
3146
3202
|
if (!dirPath) {
|
|
@@ -3151,24 +3207,25 @@ function filePathToUrlPath(filePath, appDir = ".") {
|
|
|
3151
3207
|
}
|
|
3152
3208
|
|
|
3153
3209
|
// src/router/scanRoutes.ts
|
|
3154
|
-
|
|
3210
|
+
var APP_DIR = "src";
|
|
3211
|
+
function toProdAbsPath(sourceAbsPath, rootDir, dist) {
|
|
3155
3212
|
let rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
3156
|
-
if (
|
|
3157
|
-
rel = rel.slice(
|
|
3213
|
+
if (rel.startsWith(`${APP_DIR}/`)) {
|
|
3214
|
+
rel = rel.slice(APP_DIR.length + 1);
|
|
3158
3215
|
}
|
|
3159
|
-
const prodRel = `${
|
|
3216
|
+
const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
|
|
3160
3217
|
return path8.resolve(rootDir, prodRel);
|
|
3161
3218
|
}
|
|
3162
|
-
async function findMergedMiddlewares(routeFilePath, rootDir,
|
|
3219
|
+
async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
|
|
3163
3220
|
const routeDir = path8.dirname(routeFilePath);
|
|
3164
3221
|
const resolvedRoot = path8.resolve(rootDir);
|
|
3165
3222
|
const mwPaths = [];
|
|
3166
3223
|
let currentDir = path8.resolve(rootDir, routeDir);
|
|
3167
3224
|
while (true) {
|
|
3168
|
-
if (
|
|
3225
|
+
if (dist) {
|
|
3169
3226
|
const mwPath = path8.join(currentDir, "middlewares.js");
|
|
3170
3227
|
const absMwPath = path8.resolve(rootDir, mwPath);
|
|
3171
|
-
const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir,
|
|
3228
|
+
const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
|
|
3172
3229
|
if (fs5.existsSync(prodAbsMwPath)) {
|
|
3173
3230
|
mwPaths.push(prodAbsMwPath);
|
|
3174
3231
|
}
|
|
@@ -3233,8 +3290,7 @@ async function hasWsExport(absPath) {
|
|
|
3233
3290
|
return false;
|
|
3234
3291
|
}
|
|
3235
3292
|
}
|
|
3236
|
-
async function scanRoutes(rootDir, patterns,
|
|
3237
|
-
const dir = appDir ?? ".";
|
|
3293
|
+
async function scanRoutes(rootDir, patterns, dist) {
|
|
3238
3294
|
const files = await fg(patterns, {
|
|
3239
3295
|
cwd: rootDir,
|
|
3240
3296
|
onlyFiles: true,
|
|
@@ -3247,12 +3303,12 @@ async function scanRoutes(rootDir, patterns, appDir, prodDir) {
|
|
|
3247
3303
|
const fileName = normalizedFile.split("/").pop();
|
|
3248
3304
|
if (fileName === "handler.ts" || fileName === "handler.js") {
|
|
3249
3305
|
const absPath = path8.resolve(rootDir, normalizedFile);
|
|
3250
|
-
const importPath =
|
|
3251
|
-
const urlPath = filePathToUrlPath(normalizedFile
|
|
3306
|
+
const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
|
|
3307
|
+
const urlPath = filePathToUrlPath(normalizedFile);
|
|
3252
3308
|
const paramNames = extractParamNames(urlPath);
|
|
3253
3309
|
const isDynamic = paramNames.length > 0;
|
|
3254
3310
|
const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
|
|
3255
|
-
const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir,
|
|
3311
|
+
const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
|
|
3256
3312
|
const methods = await extractMethodsFromHandler(importPath);
|
|
3257
3313
|
for (const method of methods) {
|
|
3258
3314
|
routes.push({
|
|
@@ -3293,9 +3349,9 @@ async function createDevApp(options) {
|
|
|
3293
3349
|
invalidateMiddlewareCache();
|
|
3294
3350
|
invalidateProgramCache();
|
|
3295
3351
|
invalidateSchemaCache();
|
|
3296
|
-
const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.
|
|
3352
|
+
const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
|
|
3297
3353
|
const sorted = sortRoutes(reScanned.routes);
|
|
3298
|
-
await generateSchemaFiles(sorted, ctx.rootDir, ctx.
|
|
3354
|
+
await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
|
|
3299
3355
|
ctx.updateRoutes(sorted, reScanned.wsRoutes);
|
|
3300
3356
|
};
|
|
3301
3357
|
return devApp;
|
|
@@ -3317,6 +3373,7 @@ export {
|
|
|
3317
3373
|
collectRouteSchemaSources,
|
|
3318
3374
|
cors,
|
|
3319
3375
|
createProdApp as createApp,
|
|
3376
|
+
createContext,
|
|
3320
3377
|
createDevApp,
|
|
3321
3378
|
createProdApp,
|
|
3322
3379
|
createProgram,
|
|
@@ -3324,6 +3381,7 @@ export {
|
|
|
3324
3381
|
getInputTypeForMethod,
|
|
3325
3382
|
helmet,
|
|
3326
3383
|
invalidateProgramCache,
|
|
3384
|
+
invokeHandler,
|
|
3327
3385
|
loadConfig,
|
|
3328
3386
|
logger,
|
|
3329
3387
|
resolveTypeNode
|