@faapi/faapi 0.0.0-canary.0 → 0.0.0-canary.0f443f9
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 +1 -1
- package/dist/cli/index.js +417 -139
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +60 -11
- package/dist/index.js +67 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/cli/index.js
CHANGED
|
@@ -613,16 +613,17 @@ var init_normalizePatterns = __esm({
|
|
|
613
613
|
// src/cli/parseArgs.ts
|
|
614
614
|
function parseArgs(argv) {
|
|
615
615
|
const cli = cac("faapi");
|
|
616
|
-
cli.option("--port <port>", "Server port (env: PORT)").option("--app-dir <dir>", "App directory", { default: "
|
|
616
|
+
cli.option("--port <port>", "Server port (env: PORT)").option("--app-dir <dir>", "App directory (src by default)", { default: "src" }).option("--cors", "Enable CORS (default in dev mode)").option("--no-cors", "Disable CORS").option("--static <dir>", "Static files directory", { default: void 0 }).option("--no-static", "Disable static file serving").option("--types <path>", "Output path for generated types file").option("--config <path>", "Path to config file (faapi.config.ts)");
|
|
617
617
|
const { args, options } = cli.parse(["", "", ...argv]);
|
|
618
618
|
const rawPatterns = args.map(String).filter((a) => a !== "dev");
|
|
619
619
|
const patterns = normalizePatterns(rawPatterns);
|
|
620
620
|
const port = options.port ? Number(options.port) : Number(process.env.PORT) || 3e3;
|
|
621
|
-
const appDir = String(options.appDir ?? "
|
|
621
|
+
const appDir = String(options.appDir ?? "src");
|
|
622
622
|
const cors2 = options.cors !== false;
|
|
623
623
|
const staticDir = options.static === false ? void 0 : options.static;
|
|
624
|
+
const defaultPattern = appDir === "." ? "api/**/*.ts" : `${appDir}/api/**/*.ts`;
|
|
624
625
|
return {
|
|
625
|
-
patterns: patterns.length > 0 ? patterns : [
|
|
626
|
+
patterns: patterns.length > 0 ? patterns : [defaultPattern],
|
|
626
627
|
port,
|
|
627
628
|
appDir,
|
|
628
629
|
cors: cors2,
|
|
@@ -695,7 +696,7 @@ function isCatchAllSegment(segment) {
|
|
|
695
696
|
function isRouteGroup(segment) {
|
|
696
697
|
return /^\(.+\)$/.test(segment);
|
|
697
698
|
}
|
|
698
|
-
function filePathToUrlPath(filePath, appDir = "
|
|
699
|
+
function filePathToUrlPath(filePath, appDir = ".") {
|
|
699
700
|
const withoutPrefix = filePath.startsWith(appDir + "/") ? filePath.slice(appDir.length + 1) : filePath;
|
|
700
701
|
const lastSlashIndex = withoutPrefix.lastIndexOf("/");
|
|
701
702
|
const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
|
|
@@ -850,7 +851,7 @@ async function hasWsExport(absPath) {
|
|
|
850
851
|
}
|
|
851
852
|
}
|
|
852
853
|
async function scanRoutes(rootDir, patterns, appDir) {
|
|
853
|
-
const dir = appDir ?? "
|
|
854
|
+
const dir = appDir ?? ".";
|
|
854
855
|
const files = await fg(patterns, {
|
|
855
856
|
cwd: rootDir,
|
|
856
857
|
onlyFiles: true,
|
|
@@ -1232,7 +1233,7 @@ function formatSetCookie(name, value, options) {
|
|
|
1232
1233
|
if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
1233
1234
|
return cookie;
|
|
1234
1235
|
}
|
|
1235
|
-
function createContext(request, params, config = {}) {
|
|
1236
|
+
function createContext(request, params, config = {}, ip = "") {
|
|
1236
1237
|
const url = new URL(request.url);
|
|
1237
1238
|
const meta = { headers: {}, setCookies: [] };
|
|
1238
1239
|
const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
|
|
@@ -1247,6 +1248,7 @@ function createContext(request, params, config = {}) {
|
|
|
1247
1248
|
headers: request.headers,
|
|
1248
1249
|
method: request.method,
|
|
1249
1250
|
path: url.pathname,
|
|
1251
|
+
ip,
|
|
1250
1252
|
cookies: cookiesObj,
|
|
1251
1253
|
config,
|
|
1252
1254
|
meta,
|
|
@@ -1396,6 +1398,65 @@ var init_inputType = __esm({
|
|
|
1396
1398
|
}
|
|
1397
1399
|
});
|
|
1398
1400
|
|
|
1401
|
+
// src/errors/FaapiError.ts
|
|
1402
|
+
var FaapiError;
|
|
1403
|
+
var init_FaapiError = __esm({
|
|
1404
|
+
"src/errors/FaapiError.ts"() {
|
|
1405
|
+
"use strict";
|
|
1406
|
+
FaapiError = class extends Error {
|
|
1407
|
+
constructor(code, message, statusCode) {
|
|
1408
|
+
super(message);
|
|
1409
|
+
this.code = code;
|
|
1410
|
+
this.statusCode = statusCode;
|
|
1411
|
+
this.name = "FaapiError";
|
|
1412
|
+
}
|
|
1413
|
+
code;
|
|
1414
|
+
statusCode;
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
// src/errors/httpErrors.ts
|
|
1420
|
+
function deriveStatusCode(issues) {
|
|
1421
|
+
const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
|
|
1422
|
+
return has400 ? 400 : 422;
|
|
1423
|
+
}
|
|
1424
|
+
var ValidationError, RouteNotFoundError, MethodNotAllowedError, InternalError;
|
|
1425
|
+
var init_httpErrors = __esm({
|
|
1426
|
+
"src/errors/httpErrors.ts"() {
|
|
1427
|
+
"use strict";
|
|
1428
|
+
init_FaapiError();
|
|
1429
|
+
ValidationError = class extends FaapiError {
|
|
1430
|
+
constructor(message, issues) {
|
|
1431
|
+
super("VALIDATION_ERROR", message, deriveStatusCode(issues));
|
|
1432
|
+
this.issues = issues;
|
|
1433
|
+
this.name = "ValidationError";
|
|
1434
|
+
}
|
|
1435
|
+
issues;
|
|
1436
|
+
};
|
|
1437
|
+
RouteNotFoundError = class extends FaapiError {
|
|
1438
|
+
constructor(path10) {
|
|
1439
|
+
super("ROUTE_NOT_FOUND", `Route not found: ${path10}`, 404);
|
|
1440
|
+
this.name = "RouteNotFoundError";
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
MethodNotAllowedError = class extends FaapiError {
|
|
1444
|
+
constructor(method, path10, allowedMethods) {
|
|
1445
|
+
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path10}`, 405);
|
|
1446
|
+
this.allowedMethods = allowedMethods;
|
|
1447
|
+
this.name = "MethodNotAllowedError";
|
|
1448
|
+
}
|
|
1449
|
+
allowedMethods;
|
|
1450
|
+
};
|
|
1451
|
+
InternalError = class extends FaapiError {
|
|
1452
|
+
constructor(message) {
|
|
1453
|
+
super("INTERNAL_ERROR", message, 500);
|
|
1454
|
+
this.name = "InternalError";
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1399
1460
|
// src/runtime/resolveInput.ts
|
|
1400
1461
|
async function resolveInput(method, request) {
|
|
1401
1462
|
const inputType = getInputTypeForMethod(method);
|
|
@@ -1404,12 +1465,33 @@ async function resolveInput(method, request) {
|
|
|
1404
1465
|
if (contentType.includes("multipart/form-data")) {
|
|
1405
1466
|
return parseMultipart(request);
|
|
1406
1467
|
}
|
|
1468
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1469
|
+
const text2 = await request.text();
|
|
1470
|
+
if (text2.trim() === "") return null;
|
|
1471
|
+
const params = new URLSearchParams(text2);
|
|
1472
|
+
const obj = {};
|
|
1473
|
+
for (const [key, value] of params) {
|
|
1474
|
+
obj[key] = value;
|
|
1475
|
+
}
|
|
1476
|
+
return obj;
|
|
1477
|
+
}
|
|
1407
1478
|
const text = await request.text();
|
|
1479
|
+
if (text.trim() === "") {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1408
1482
|
const result = parseJsonBody(text);
|
|
1409
|
-
if (result.success) {
|
|
1410
|
-
|
|
1483
|
+
if (!result.success) {
|
|
1484
|
+
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
1485
|
+
{
|
|
1486
|
+
path: "body",
|
|
1487
|
+
code: "INVALID_FORMAT",
|
|
1488
|
+
expected: "JSON",
|
|
1489
|
+
received: "text",
|
|
1490
|
+
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
1491
|
+
}
|
|
1492
|
+
]);
|
|
1411
1493
|
}
|
|
1412
|
-
return
|
|
1494
|
+
return result.data;
|
|
1413
1495
|
}
|
|
1414
1496
|
const url = new URL(request.url);
|
|
1415
1497
|
return queryToObject(url.searchParams);
|
|
@@ -1421,6 +1503,7 @@ var init_resolveInput = __esm({
|
|
|
1421
1503
|
init_parseJsonBody();
|
|
1422
1504
|
init_parseMultipart();
|
|
1423
1505
|
init_inputType();
|
|
1506
|
+
init_httpErrors();
|
|
1424
1507
|
}
|
|
1425
1508
|
});
|
|
1426
1509
|
|
|
@@ -1623,6 +1706,7 @@ var init_resolveInjection = __esm({
|
|
|
1623
1706
|
ctx: "context",
|
|
1624
1707
|
// 别名
|
|
1625
1708
|
cookies: "cookies",
|
|
1709
|
+
ip: "ip",
|
|
1626
1710
|
files: "files",
|
|
1627
1711
|
fields: "fields"
|
|
1628
1712
|
};
|
|
@@ -1642,6 +1726,8 @@ function getBuiltinInjectionValue(type, ctx, body) {
|
|
|
1642
1726
|
return ctx;
|
|
1643
1727
|
case "cookies":
|
|
1644
1728
|
return ctx.cookies;
|
|
1729
|
+
case "ip":
|
|
1730
|
+
return ctx.ip;
|
|
1645
1731
|
case "body":
|
|
1646
1732
|
return body;
|
|
1647
1733
|
case "files":
|
|
@@ -1805,61 +1891,6 @@ var init_sendNodeResponse = __esm({
|
|
|
1805
1891
|
}
|
|
1806
1892
|
});
|
|
1807
1893
|
|
|
1808
|
-
// src/errors/FaapiError.ts
|
|
1809
|
-
var FaapiError;
|
|
1810
|
-
var init_FaapiError = __esm({
|
|
1811
|
-
"src/errors/FaapiError.ts"() {
|
|
1812
|
-
"use strict";
|
|
1813
|
-
FaapiError = class extends Error {
|
|
1814
|
-
constructor(code, message, statusCode) {
|
|
1815
|
-
super(message);
|
|
1816
|
-
this.code = code;
|
|
1817
|
-
this.statusCode = statusCode;
|
|
1818
|
-
this.name = "FaapiError";
|
|
1819
|
-
}
|
|
1820
|
-
code;
|
|
1821
|
-
statusCode;
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
// src/errors/httpErrors.ts
|
|
1827
|
-
var ValidationError, RouteNotFoundError, MethodNotAllowedError, InternalError;
|
|
1828
|
-
var init_httpErrors = __esm({
|
|
1829
|
-
"src/errors/httpErrors.ts"() {
|
|
1830
|
-
"use strict";
|
|
1831
|
-
init_FaapiError();
|
|
1832
|
-
ValidationError = class extends FaapiError {
|
|
1833
|
-
constructor(message, issues) {
|
|
1834
|
-
super("VALIDATION_ERROR", message, 400);
|
|
1835
|
-
this.issues = issues;
|
|
1836
|
-
this.name = "ValidationError";
|
|
1837
|
-
}
|
|
1838
|
-
issues;
|
|
1839
|
-
};
|
|
1840
|
-
RouteNotFoundError = class extends FaapiError {
|
|
1841
|
-
constructor(path10) {
|
|
1842
|
-
super("ROUTE_NOT_FOUND", `Route not found: ${path10}`, 404);
|
|
1843
|
-
this.name = "RouteNotFoundError";
|
|
1844
|
-
}
|
|
1845
|
-
};
|
|
1846
|
-
MethodNotAllowedError = class extends FaapiError {
|
|
1847
|
-
constructor(method, path10, allowedMethods) {
|
|
1848
|
-
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path10}`, 405);
|
|
1849
|
-
this.allowedMethods = allowedMethods;
|
|
1850
|
-
this.name = "MethodNotAllowedError";
|
|
1851
|
-
}
|
|
1852
|
-
allowedMethods;
|
|
1853
|
-
};
|
|
1854
|
-
InternalError = class extends FaapiError {
|
|
1855
|
-
constructor(message) {
|
|
1856
|
-
super("INTERNAL_ERROR", message, 500);
|
|
1857
|
-
this.name = "InternalError";
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
});
|
|
1862
|
-
|
|
1863
1894
|
// src/validator/schemaRegistry.ts
|
|
1864
1895
|
var SchemaRegistry, schemaRegistry;
|
|
1865
1896
|
var init_schemaRegistry = __esm({
|
|
@@ -1971,6 +2002,20 @@ function coerceValue(value, type, path10, issues) {
|
|
|
1971
2002
|
return value.map((item, i) => coerceValue(item, type.element, `${path10}[${i}]`, issues));
|
|
1972
2003
|
}
|
|
1973
2004
|
return value;
|
|
2005
|
+
case "tuple": {
|
|
2006
|
+
if (!Array.isArray(value)) return value;
|
|
2007
|
+
const restElement = type.elements.find((e) => e.rest);
|
|
2008
|
+
return value.map((item, i) => {
|
|
2009
|
+
const elem = type.elements[i];
|
|
2010
|
+
if (elem && !elem.rest) {
|
|
2011
|
+
return coerceValue(item, elem.type, `${path10}[${i}]`, issues);
|
|
2012
|
+
}
|
|
2013
|
+
if (restElement) {
|
|
2014
|
+
return coerceValue(item, restElement.type, `${path10}[${i}]`, issues);
|
|
2015
|
+
}
|
|
2016
|
+
return item;
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
1974
2019
|
case "union":
|
|
1975
2020
|
for (const member of type.members) {
|
|
1976
2021
|
const tempIssues = [];
|
|
@@ -1980,6 +2025,12 @@ function coerceValue(value, type, path10, issues) {
|
|
|
1980
2025
|
}
|
|
1981
2026
|
}
|
|
1982
2027
|
return value;
|
|
2028
|
+
case "literal":
|
|
2029
|
+
if (typeof type.value === "number" && typeof value === "string" && value.trim() !== "") {
|
|
2030
|
+
const num = Number(value);
|
|
2031
|
+
if (!Number.isNaN(num)) return num;
|
|
2032
|
+
}
|
|
2033
|
+
return value;
|
|
1983
2034
|
case "object":
|
|
1984
2035
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1985
2036
|
const obj = value;
|
|
@@ -2005,6 +2056,9 @@ function coerceStringToNumber(value, path10, issues) {
|
|
|
2005
2056
|
if (value.trim() === "") {
|
|
2006
2057
|
issues.push({
|
|
2007
2058
|
path: path10,
|
|
2059
|
+
code: "COERCE_FAILED",
|
|
2060
|
+
expected: "number",
|
|
2061
|
+
received: "string",
|
|
2008
2062
|
message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A number`
|
|
2009
2063
|
});
|
|
2010
2064
|
return value;
|
|
@@ -2013,6 +2067,9 @@ function coerceStringToNumber(value, path10, issues) {
|
|
|
2013
2067
|
if (Number.isNaN(num)) {
|
|
2014
2068
|
issues.push({
|
|
2015
2069
|
path: path10,
|
|
2070
|
+
code: "COERCE_FAILED",
|
|
2071
|
+
expected: "number",
|
|
2072
|
+
received: "string",
|
|
2016
2073
|
message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A number`
|
|
2017
2074
|
});
|
|
2018
2075
|
return value;
|
|
@@ -2028,6 +2085,9 @@ function coerceStringToBoolean(value, path10, issues) {
|
|
|
2028
2085
|
}
|
|
2029
2086
|
issues.push({
|
|
2030
2087
|
path: path10,
|
|
2088
|
+
code: "COERCE_FAILED",
|
|
2089
|
+
expected: "boolean",
|
|
2090
|
+
received: "string",
|
|
2031
2091
|
message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A boolean`
|
|
2032
2092
|
});
|
|
2033
2093
|
return value;
|
|
@@ -2074,6 +2134,28 @@ var init_validateInput = __esm({
|
|
|
2074
2134
|
}
|
|
2075
2135
|
});
|
|
2076
2136
|
|
|
2137
|
+
// src/utils/getClientIp.ts
|
|
2138
|
+
function getClientIp(req) {
|
|
2139
|
+
const xff = req.headers["x-forwarded-for"];
|
|
2140
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
2141
|
+
const first = xff.split(",")[0]?.trim();
|
|
2142
|
+
if (first) return first;
|
|
2143
|
+
}
|
|
2144
|
+
const remote = req.socket.remoteAddress;
|
|
2145
|
+
if (remote) {
|
|
2146
|
+
if (remote.startsWith("::ffff:")) {
|
|
2147
|
+
return remote.slice(7);
|
|
2148
|
+
}
|
|
2149
|
+
return remote;
|
|
2150
|
+
}
|
|
2151
|
+
return "";
|
|
2152
|
+
}
|
|
2153
|
+
var init_getClientIp = __esm({
|
|
2154
|
+
"src/utils/getClientIp.ts"() {
|
|
2155
|
+
"use strict";
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2077
2159
|
// src/middleware/cors.ts
|
|
2078
2160
|
function cors(options = {}) {
|
|
2079
2161
|
const {
|
|
@@ -2275,7 +2357,12 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
2275
2357
|
case ts2.SyntaxKind.BooleanKeyword:
|
|
2276
2358
|
return { kind: "boolean" };
|
|
2277
2359
|
case ts2.SyntaxKind.BigIntKeyword:
|
|
2278
|
-
|
|
2360
|
+
throw new SchemaExtractionError(
|
|
2361
|
+
typeNode.getText(),
|
|
2362
|
+
"bigint \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93,\u8BF7\u6539\u7528 string \u6216 number"
|
|
2363
|
+
);
|
|
2364
|
+
case ts2.SyntaxKind.SymbolKeyword:
|
|
2365
|
+
throw new SchemaExtractionError(typeNode.getText(), "symbol \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
|
|
2279
2366
|
case ts2.SyntaxKind.NullKeyword:
|
|
2280
2367
|
return { kind: "null" };
|
|
2281
2368
|
case ts2.SyntaxKind.UndefinedKeyword:
|
|
@@ -2320,8 +2407,35 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
2320
2407
|
};
|
|
2321
2408
|
}
|
|
2322
2409
|
if (ts2.isTupleTypeNode(typeNode)) {
|
|
2323
|
-
const elements = typeNode.elements.map((e) =>
|
|
2324
|
-
|
|
2410
|
+
const elements = typeNode.elements.map((e) => {
|
|
2411
|
+
if (ts2.isRestTypeNode(e)) {
|
|
2412
|
+
const inner = resolveTypeNode(e.type, checker, visited);
|
|
2413
|
+
if (inner.kind === "array") {
|
|
2414
|
+
return { type: inner.element, optional: false, rest: true };
|
|
2415
|
+
}
|
|
2416
|
+
return { type: inner, optional: false, rest: true };
|
|
2417
|
+
}
|
|
2418
|
+
if (ts2.isNamedTupleMember(e)) {
|
|
2419
|
+
return {
|
|
2420
|
+
type: resolveTypeNode(e.type, checker, visited),
|
|
2421
|
+
optional: !!e.questionToken,
|
|
2422
|
+
rest: false
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
if (ts2.isOptionalTypeNode(e)) {
|
|
2426
|
+
return {
|
|
2427
|
+
type: resolveTypeNode(e.type, checker, visited),
|
|
2428
|
+
optional: true,
|
|
2429
|
+
rest: false
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
type: resolveTypeNode(e, checker, visited),
|
|
2434
|
+
optional: false,
|
|
2435
|
+
rest: false
|
|
2436
|
+
};
|
|
2437
|
+
});
|
|
2438
|
+
return { kind: "tuple", elements };
|
|
2325
2439
|
}
|
|
2326
2440
|
if (ts2.isUnionTypeNode(typeNode)) {
|
|
2327
2441
|
const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
|
|
@@ -2492,6 +2606,9 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
2492
2606
|
"Promise \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u52FF\u5728 query/body \u7C7B\u578B\u4E2D\u4F7F\u7528"
|
|
2493
2607
|
);
|
|
2494
2608
|
}
|
|
2609
|
+
if (typeName === "Function") {
|
|
2610
|
+
throw new SchemaExtractionError(typeNode.getText(), "Function \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
|
|
2611
|
+
}
|
|
2495
2612
|
if (visited.has(typeName)) {
|
|
2496
2613
|
return { kind: "ref", name: typeName };
|
|
2497
2614
|
}
|
|
@@ -2507,11 +2624,38 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
2507
2624
|
if (ts2.isTypeAliasDeclaration(declaration)) {
|
|
2508
2625
|
return resolveTypeNode(declaration.type, checker, visited);
|
|
2509
2626
|
}
|
|
2627
|
+
if (ts2.isEnumDeclaration(declaration)) {
|
|
2628
|
+
return resolveEnumDeclaration(declaration);
|
|
2629
|
+
}
|
|
2510
2630
|
}
|
|
2511
2631
|
}
|
|
2512
2632
|
}
|
|
2513
2633
|
throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
|
|
2514
2634
|
}
|
|
2635
|
+
function resolveEnumDeclaration(node) {
|
|
2636
|
+
const members = [];
|
|
2637
|
+
let nextNumericValue = 0;
|
|
2638
|
+
for (const member of node.members) {
|
|
2639
|
+
if (member.initializer) {
|
|
2640
|
+
if (ts2.isStringLiteral(member.initializer)) {
|
|
2641
|
+
members.push({ kind: "literal", value: member.initializer.text });
|
|
2642
|
+
} else if (ts2.isNumericLiteral(member.initializer)) {
|
|
2643
|
+
const num = Number(member.initializer.text);
|
|
2644
|
+
members.push({ kind: "literal", value: num });
|
|
2645
|
+
nextNumericValue = num + 1;
|
|
2646
|
+
} else {
|
|
2647
|
+
throw new SchemaExtractionError(
|
|
2648
|
+
node.name.text,
|
|
2649
|
+
`enum \u6210\u5458 "${member.name.getText()}" \u7684\u521D\u59CB\u5316\u503C\u7C7B\u578B\u4E0D\u652F\u6301,\u4EC5\u652F\u6301 string/number \u5B57\u9762\u91CF`
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
} else {
|
|
2653
|
+
members.push({ kind: "literal", value: nextNumericValue });
|
|
2654
|
+
nextNumericValue++;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return { kind: "union", members };
|
|
2658
|
+
}
|
|
2515
2659
|
function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
|
|
2516
2660
|
const properties = [];
|
|
2517
2661
|
const propMap = /* @__PURE__ */ new Map();
|
|
@@ -2563,6 +2707,34 @@ var init_resolveTypeNode = __esm({
|
|
|
2563
2707
|
});
|
|
2564
2708
|
|
|
2565
2709
|
// src/ast/generateValidatorCode.ts
|
|
2710
|
+
function runtimeTypeToExpected(type) {
|
|
2711
|
+
switch (type.kind) {
|
|
2712
|
+
case "string":
|
|
2713
|
+
case "number":
|
|
2714
|
+
case "boolean":
|
|
2715
|
+
case "null":
|
|
2716
|
+
case "undefined":
|
|
2717
|
+
case "bigint":
|
|
2718
|
+
case "date":
|
|
2719
|
+
case "any":
|
|
2720
|
+
case "unknown":
|
|
2721
|
+
return type.kind;
|
|
2722
|
+
case "literal":
|
|
2723
|
+
return typeof type.value === "string" ? `'${type.value}'` : String(type.value);
|
|
2724
|
+
case "array":
|
|
2725
|
+
return `${runtimeTypeToExpected(type.element)}[]`;
|
|
2726
|
+
case "tuple":
|
|
2727
|
+
return `[${type.elements.map((e) => (e.rest ? "..." : "") + runtimeTypeToExpected(e.type) + (e.optional ? "?" : "")).join(", ")}]`;
|
|
2728
|
+
case "object":
|
|
2729
|
+
return "object";
|
|
2730
|
+
case "union":
|
|
2731
|
+
return type.members.map(runtimeTypeToExpected).join(" | ");
|
|
2732
|
+
case "record":
|
|
2733
|
+
return `Record<${runtimeTypeToExpected(type.key)}, ${runtimeTypeToExpected(type.value)}>`;
|
|
2734
|
+
case "ref":
|
|
2735
|
+
return type.name;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2566
2738
|
function generateValidatorSource(typeInfo, resolveType) {
|
|
2567
2739
|
const ctx = new CodeGenContext(resolveType);
|
|
2568
2740
|
collectNamedTypes(typeInfo.runtimeType, typeInfo.name, ctx);
|
|
@@ -2578,7 +2750,7 @@ function generateValidatorSource(typeInfo, resolveType) {
|
|
|
2578
2750
|
const body = generateObjectValidation(type, "value", "issues", ctx, "path");
|
|
2579
2751
|
return `function validate_${name}(value, path, issues) {
|
|
2580
2752
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
2581
|
-
issues.push({ path, message: '\u671F\u671B\u5BF9\u8C61' });
|
|
2753
|
+
issues.push({ path, code: 'TYPE_MISMATCH', expected: 'object', received: typeof value, message: '\u671F\u671B\u5BF9\u8C61' });
|
|
2582
2754
|
return;
|
|
2583
2755
|
}
|
|
2584
2756
|
if (!validate_${name}.__visited) validate_${name}.__visited = new WeakSet();
|
|
@@ -2613,6 +2785,11 @@ function collectNamedTypesFromType(type, ctx) {
|
|
|
2613
2785
|
case "array":
|
|
2614
2786
|
collectNamedTypesFromType(type.element, ctx);
|
|
2615
2787
|
break;
|
|
2788
|
+
case "tuple":
|
|
2789
|
+
for (const elem of type.elements) {
|
|
2790
|
+
collectNamedTypesFromType(elem.type, ctx);
|
|
2791
|
+
}
|
|
2792
|
+
break;
|
|
2616
2793
|
case "union":
|
|
2617
2794
|
for (const member of type.members) {
|
|
2618
2795
|
collectNamedTypesFromType(member, ctx);
|
|
@@ -2649,7 +2826,7 @@ function generateObjectValidation(type, varName, issuesVar, ctx, pathExpr) {
|
|
|
2649
2826
|
const hasCheck = `'${prop.name}' in ${varName}`;
|
|
2650
2827
|
if (!prop.optional) {
|
|
2651
2828
|
lines.push(
|
|
2652
|
-
`if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
|
|
2829
|
+
`if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, code: 'MISSING_FIELD', expected: '${prop.name}', received: 'undefined', message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
|
|
2653
2830
|
);
|
|
2654
2831
|
lines.push(`else {`);
|
|
2655
2832
|
} else {
|
|
@@ -2668,21 +2845,23 @@ function generateValueValidation(type, varName, issuesVar, ctx, pathExpr) {
|
|
|
2668
2845
|
return "";
|
|
2669
2846
|
// 不校验
|
|
2670
2847
|
case "string":
|
|
2671
|
-
return `if (typeof ${varName} !== 'string') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B string\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2848
|
+
return `if (typeof ${varName} !== 'string') ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'string', received: typeof ${varName}, message: '\u671F\u671B string\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2672
2849
|
case "number":
|
|
2673
|
-
return `if (typeof ${varName} !== 'number' || Number.isNaN(${varName})) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B number\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2850
|
+
return `if (typeof ${varName} !== 'number' || Number.isNaN(${varName})) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'number', received: typeof ${varName}, message: '\u671F\u671B number\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2674
2851
|
case "boolean":
|
|
2675
|
-
return `if (typeof ${varName} !== 'boolean') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B boolean\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2852
|
+
return `if (typeof ${varName} !== 'boolean') ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'boolean', received: typeof ${varName}, message: '\u671F\u671B boolean\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2676
2853
|
case "bigint":
|
|
2677
|
-
return `if (typeof ${varName} !== 'bigint') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B bigint\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2854
|
+
return `if (typeof ${varName} !== 'bigint') ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'bigint', received: typeof ${varName}, message: '\u671F\u671B bigint\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2678
2855
|
case "null":
|
|
2679
|
-
return `if (${varName} !== null) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B null\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2856
|
+
return `if (${varName} !== null) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'null', received: typeof ${varName}, message: '\u671F\u671B null\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
|
|
2680
2857
|
case "undefined":
|
|
2681
|
-
return `if (${varName} !== undefined) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B undefined' });`;
|
|
2858
|
+
return `if (${varName} !== undefined) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'undefined', received: typeof ${varName}, message: '\u671F\u671B undefined' });`;
|
|
2682
2859
|
case "literal":
|
|
2683
|
-
return `if (${varName} !== ${JSON.stringify(type.value)}) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B\u5B57\u9762\u91CF ${JSON.stringify(type.value)}\uFF0C\u5B9E\u9645 ' + JSON.stringify(${varName}) });`;
|
|
2860
|
+
return `if (${varName} !== ${JSON.stringify(type.value)}) ${issuesVar}.push({ path: ${path10}, code: 'INVALID_VALUE', expected: ${JSON.stringify(JSON.stringify(type.value))}, received: JSON.stringify(${varName}), message: '\u671F\u671B\u5B57\u9762\u91CF ${JSON.stringify(type.value)}\uFF0C\u5B9E\u9645 ' + JSON.stringify(${varName}) });`;
|
|
2684
2861
|
case "date":
|
|
2685
|
-
return `if (
|
|
2862
|
+
return `if (${varName} instanceof Date) { /* Date \u5B9E\u4F8B,\u901A\u8FC7 */ }
|
|
2863
|
+
else if (typeof ${varName} !== 'string') ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'Date | ISO 8601 string', received: typeof ${varName}, message: '\u671F\u671B Date \u6216 ISO 8601 \u5B57\u7B26\u4E32\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
|
|
2864
|
+
else if (!/^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?(Z|[+-]\\d{2}:?\\d{2})?)?$/.test(${varName}) || isNaN(new Date(${varName}).getTime())) ${issuesVar}.push({ path: ${path10}, code: 'INVALID_FORMAT', expected: 'ISO 8601', received: String(${varName}), message: '\u4E0D\u662F\u5408\u6CD5\u7684 ISO 8601 \u65E5\u671F\u5B57\u7B26\u4E32: ' + ${varName} });`;
|
|
2686
2865
|
case "array": {
|
|
2687
2866
|
const id = ctx.nextVarId();
|
|
2688
2867
|
const itemVar = `item${id}`;
|
|
@@ -2695,21 +2874,71 @@ function generateValueValidation(type, varName, issuesVar, ctx, pathExpr) {
|
|
|
2695
2874
|
ctx,
|
|
2696
2875
|
elemPath
|
|
2697
2876
|
);
|
|
2698
|
-
return `if (!Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B\u6570\u7EC4\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
|
|
2877
|
+
return `if (!Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'array', received: typeof ${varName}, message: '\u671F\u671B\u6570\u7EC4\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
|
|
2699
2878
|
else for (let ${indexVar} = 0; ${indexVar} < ${varName}.length; ${indexVar}++) { const ${itemVar} = ${varName}[${indexVar}]; ${elemValidation} }`;
|
|
2700
2879
|
}
|
|
2880
|
+
case "tuple": {
|
|
2881
|
+
const minRequired = type.elements.filter((e) => !e.optional && !e.rest).length;
|
|
2882
|
+
const fixedCount = type.elements.filter((e) => !e.rest).length;
|
|
2883
|
+
const restElement = type.elements.find((e) => e.rest);
|
|
2884
|
+
const id = ctx.nextVarId();
|
|
2885
|
+
const itemVar = `t${id}`;
|
|
2886
|
+
const indexVar = `ti${id}`;
|
|
2887
|
+
const lines = [];
|
|
2888
|
+
lines.push(
|
|
2889
|
+
`if (!Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'tuple', received: typeof ${varName}, message: '\u671F\u671B\u5143\u7EC4\uFF08\u6570\u7EC4\uFF09\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`
|
|
2890
|
+
);
|
|
2891
|
+
lines.push(`else {`);
|
|
2892
|
+
lines.push(
|
|
2893
|
+
`if (${varName}.length < ${minRequired}) ${issuesVar}.push({ path: ${path10}, code: 'MISSING_FIELD', expected: 'tuple length >= ${minRequired}', received: 'length ' + ${varName}.length, message: '\u5143\u7EC4\u957F\u5EA6\u4E0D\u8DB3\uFF0C\u671F\u671B\u81F3\u5C11 ${minRequired}\uFF0C\u5B9E\u9645 ' + ${varName}.length });`
|
|
2894
|
+
);
|
|
2895
|
+
if (!restElement) {
|
|
2896
|
+
lines.push(
|
|
2897
|
+
`if (${varName}.length > ${fixedCount}) ${issuesVar}.push({ path: ${path10}, code: 'INVALID_VALUE', expected: 'tuple length = ${fixedCount}', received: 'length ' + ${varName}.length, message: '\u5143\u7EC4\u957F\u5EA6\u8D85\u51FA\uFF0C\u671F\u671B ${fixedCount}\uFF0C\u5B9E\u9645 ' + ${varName}.length });`
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
for (let i = 0; i < type.elements.length; i++) {
|
|
2901
|
+
const elem = type.elements[i];
|
|
2902
|
+
if (elem.rest) continue;
|
|
2903
|
+
const elemPath = `${path10} + '[' + ${i} + ']'`;
|
|
2904
|
+
const elemAccess = `${varName}[${i}]`;
|
|
2905
|
+
if (elem.optional) {
|
|
2906
|
+
lines.push(`if (${i} < ${varName}.length && ${elemAccess} !== undefined) {`);
|
|
2907
|
+
} else {
|
|
2908
|
+
lines.push(`if (${i} < ${varName}.length) {`);
|
|
2909
|
+
}
|
|
2910
|
+
lines.push(` ${generateValueValidation(elem.type, elemAccess, issuesVar, ctx, elemPath)}`);
|
|
2911
|
+
lines.push(`}`);
|
|
2912
|
+
}
|
|
2913
|
+
if (restElement) {
|
|
2914
|
+
const restPath = `${path10} + '[' + ${indexVar} + ']'`;
|
|
2915
|
+
const restValidation = generateValueValidation(
|
|
2916
|
+
restElement.type,
|
|
2917
|
+
itemVar,
|
|
2918
|
+
issuesVar,
|
|
2919
|
+
ctx,
|
|
2920
|
+
restPath
|
|
2921
|
+
);
|
|
2922
|
+
lines.push(
|
|
2923
|
+
`for (let ${indexVar} = ${fixedCount}; ${indexVar} < ${varName}.length; ${indexVar}++) { const ${itemVar} = ${varName}[${indexVar}]; ${restValidation} }`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
lines.push(`}`);
|
|
2927
|
+
return lines.join("\n");
|
|
2928
|
+
}
|
|
2701
2929
|
case "union": {
|
|
2702
2930
|
const tempVar = `tempIssues_${Math.random().toString(36).slice(2, 8)}`;
|
|
2703
2931
|
const memberChecks = type.members.map((member) => {
|
|
2704
2932
|
const check = generateValueValidation(member, varName, tempVar, ctx, path10);
|
|
2705
2933
|
return `(() => { const ${tempVar} = []; ${check}; return ${tempVar}.length === 0; })()`;
|
|
2706
2934
|
});
|
|
2707
|
-
|
|
2935
|
+
const expected = runtimeTypeToExpected(type);
|
|
2936
|
+
return `if (!(${memberChecks.join(" || ")})) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: ${JSON.stringify(expected)}, received: typeof ${varName}, message: '\u503C ' + JSON.stringify(${varName}) + ' \u4E0D\u5339\u914D\u8054\u5408\u7C7B\u578B ${expected.replace(/'/g, "\\'")}' });`;
|
|
2708
2937
|
}
|
|
2709
2938
|
case "record": {
|
|
2710
2939
|
const valuePath = `${path10} + '.' + key`;
|
|
2711
2940
|
const valueValidation = generateValueValidation(type.value, "val", issuesVar, ctx, valuePath);
|
|
2712
|
-
return `if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B\u5BF9\u8C61\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
|
|
2941
|
+
return `if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'object', received: typeof ${varName}, message: '\u671F\u671B\u5BF9\u8C61\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
|
|
2713
2942
|
else for (const [key, val] of Object.entries(${varName})) { ${valueValidation} }`;
|
|
2714
2943
|
}
|
|
2715
2944
|
case "object": {
|
|
@@ -2723,7 +2952,7 @@ else for (const [key, val] of Object.entries(${varName})) { ${valueValidation} }
|
|
|
2723
2952
|
function generateInlineObjectValidation(type, varName, issuesVar, ctx, pathExpr) {
|
|
2724
2953
|
const lines = [];
|
|
2725
2954
|
lines.push(
|
|
2726
|
-
`if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) ${issuesVar}.push({ path: ${pathExpr}, message: '\u671F\u671B\u5BF9\u8C61\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`
|
|
2955
|
+
`if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) ${issuesVar}.push({ path: ${pathExpr}, code: 'TYPE_MISMATCH', expected: 'object', received: typeof ${varName}, message: '\u671F\u671B\u5BF9\u8C61\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`
|
|
2727
2956
|
);
|
|
2728
2957
|
lines.push("else {");
|
|
2729
2958
|
for (const prop of type.properties) {
|
|
@@ -2732,7 +2961,7 @@ function generateInlineObjectValidation(type, varName, issuesVar, ctx, pathExpr)
|
|
|
2732
2961
|
const hasCheck = `'${prop.name}' in ${varName}`;
|
|
2733
2962
|
if (!prop.optional) {
|
|
2734
2963
|
lines.push(
|
|
2735
|
-
` if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
|
|
2964
|
+
` if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, code: 'MISSING_FIELD', expected: '${prop.name}', received: 'undefined', message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
|
|
2736
2965
|
);
|
|
2737
2966
|
lines.push(` else {`);
|
|
2738
2967
|
} else {
|
|
@@ -3138,21 +3367,24 @@ function nodeHttpToWebHeaders(req) {
|
|
|
3138
3367
|
return headers;
|
|
3139
3368
|
}
|
|
3140
3369
|
function buildErrorResponse(err, ctx, errorFormat) {
|
|
3141
|
-
|
|
3142
|
-
return errorFormat ? errorFormat(err, ctx) : formatErrorResponse(err);
|
|
3143
|
-
} catch {
|
|
3370
|
+
if (errorFormat) {
|
|
3144
3371
|
try {
|
|
3145
|
-
|
|
3372
|
+
const res = errorFormat(err, ctx);
|
|
3373
|
+
if (res) return res;
|
|
3146
3374
|
} catch {
|
|
3147
|
-
return new Response(
|
|
3148
|
-
JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
|
|
3149
|
-
{
|
|
3150
|
-
status: 500,
|
|
3151
|
-
headers: { "Content-Type": "application/json" }
|
|
3152
|
-
}
|
|
3153
|
-
);
|
|
3154
3375
|
}
|
|
3155
3376
|
}
|
|
3377
|
+
try {
|
|
3378
|
+
return formatErrorResponse(err);
|
|
3379
|
+
} catch {
|
|
3380
|
+
return new Response(
|
|
3381
|
+
JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
|
|
3382
|
+
{
|
|
3383
|
+
status: 500,
|
|
3384
|
+
headers: { "Content-Type": "application/json" }
|
|
3385
|
+
}
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3156
3388
|
}
|
|
3157
3389
|
var init_serverUtils = __esm({
|
|
3158
3390
|
"src/server/serverUtils.ts"() {
|
|
@@ -3261,7 +3493,7 @@ function attachWebSocket(options) {
|
|
|
3261
3493
|
const host = req.headers.host ?? "localhost";
|
|
3262
3494
|
const url = `http://${host}${req.url ?? "/"}`;
|
|
3263
3495
|
const request = new Request(url, { method: "GET", headers });
|
|
3264
|
-
const ctx = createContext(request, params, config);
|
|
3496
|
+
const ctx = createContext(request, params, config, getClientIp(req));
|
|
3265
3497
|
const meta = ctx.meta;
|
|
3266
3498
|
let upgraded = false;
|
|
3267
3499
|
const finalHandler = async () => {
|
|
@@ -3318,6 +3550,7 @@ var init_handleWsUpgrade = __esm({
|
|
|
3318
3550
|
init_createContext();
|
|
3319
3551
|
init_invokeHandler();
|
|
3320
3552
|
init_importWithCacheBust();
|
|
3553
|
+
init_getClientIp();
|
|
3321
3554
|
init_serverUtils();
|
|
3322
3555
|
init_wsHandler();
|
|
3323
3556
|
}
|
|
@@ -3380,12 +3613,7 @@ function findAllowedMethods(routes, path10) {
|
|
|
3380
3613
|
continue;
|
|
3381
3614
|
}
|
|
3382
3615
|
if (route.isDynamic) {
|
|
3383
|
-
const params = matchDynamicPath(
|
|
3384
|
-
route.urlPath,
|
|
3385
|
-
path10,
|
|
3386
|
-
route.paramNames,
|
|
3387
|
-
route.isCatchAll
|
|
3388
|
-
);
|
|
3616
|
+
const params = matchDynamicPath(route.urlPath, path10, route.paramNames, route.isCatchAll);
|
|
3389
3617
|
if (params !== null) {
|
|
3390
3618
|
methods.add(route.method);
|
|
3391
3619
|
}
|
|
@@ -3446,7 +3674,7 @@ async function handleRequest(routes, rootDir, req, res, corsMiddleware, staticDi
|
|
|
3446
3674
|
const request = toWebRequest(req);
|
|
3447
3675
|
const method = request.method.toUpperCase();
|
|
3448
3676
|
const urlPath = new URL(request.url).pathname;
|
|
3449
|
-
const ctx = createContext(request, {}, config);
|
|
3677
|
+
const ctx = createContext(request, {}, config, getClientIp(req));
|
|
3450
3678
|
const meta = ctx.meta;
|
|
3451
3679
|
const routePipeline = async () => {
|
|
3452
3680
|
const match = matchRoute(routes, method, urlPath);
|
|
@@ -3531,6 +3759,7 @@ var init_createServer = __esm({
|
|
|
3531
3759
|
init_httpErrors();
|
|
3532
3760
|
init_validateInput();
|
|
3533
3761
|
init_inputType();
|
|
3762
|
+
init_getClientIp();
|
|
3534
3763
|
init_cors();
|
|
3535
3764
|
init_serveStatic();
|
|
3536
3765
|
init_schemaRegistry();
|
|
@@ -3555,7 +3784,8 @@ function startServer(options) {
|
|
|
3555
3784
|
config,
|
|
3556
3785
|
wsRoutes,
|
|
3557
3786
|
middlewares,
|
|
3558
|
-
injectors
|
|
3787
|
+
injectors,
|
|
3788
|
+
beforeListen
|
|
3559
3789
|
} = options;
|
|
3560
3790
|
const server = createServer({
|
|
3561
3791
|
routes,
|
|
@@ -3570,26 +3800,57 @@ function startServer(options) {
|
|
|
3570
3800
|
middlewares,
|
|
3571
3801
|
injectors
|
|
3572
3802
|
});
|
|
3573
|
-
return
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
console.log(
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
console.log(` WS ${route.urlPath} ${route.filePath}`);
|
|
3803
|
+
return (async () => {
|
|
3804
|
+
if (beforeListen) {
|
|
3805
|
+
await beforeListen(server);
|
|
3806
|
+
}
|
|
3807
|
+
return new Promise((resolve3) => {
|
|
3808
|
+
server.listen(port, () => {
|
|
3809
|
+
const address = server.address();
|
|
3810
|
+
const actualPort = typeof address === "object" && address !== null ? address.port : port;
|
|
3811
|
+
console.log("faapi dev server started");
|
|
3812
|
+
console.log(`- Local: http://localhost:${actualPort}`);
|
|
3813
|
+
console.log("- Loaded routes:");
|
|
3814
|
+
for (const route of routes) {
|
|
3815
|
+
const method = route.method.padEnd(6);
|
|
3816
|
+
console.log(` ${method}${route.urlPath} ${route.filePath}`);
|
|
3588
3817
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3818
|
+
if (wsRoutes && wsRoutes.length > 0) {
|
|
3819
|
+
console.log("- WebSocket routes:");
|
|
3820
|
+
for (const route of wsRoutes) {
|
|
3821
|
+
console.log(` WS ${route.urlPath} ${route.filePath}`);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
resolve3(server);
|
|
3825
|
+
});
|
|
3591
3826
|
});
|
|
3592
|
-
});
|
|
3827
|
+
})();
|
|
3828
|
+
}
|
|
3829
|
+
function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
|
|
3830
|
+
if (handlerWrappers.length > 0) {
|
|
3831
|
+
const listeners = server.listeners("request");
|
|
3832
|
+
const original = listeners[0];
|
|
3833
|
+
if (original) {
|
|
3834
|
+
server.removeAllListeners("request");
|
|
3835
|
+
let handler = original;
|
|
3836
|
+
for (const wrap of handlerWrappers) {
|
|
3837
|
+
handler = wrap(handler);
|
|
3838
|
+
}
|
|
3839
|
+
server.on("request", handler);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
if (upgradeWrappers.length > 0) {
|
|
3843
|
+
const listeners = server.listeners("upgrade");
|
|
3844
|
+
const original = listeners[0];
|
|
3845
|
+
server.removeAllListeners("upgrade");
|
|
3846
|
+
let upgrade = original;
|
|
3847
|
+
for (const wrap of upgradeWrappers) {
|
|
3848
|
+
upgrade = wrap(upgrade);
|
|
3849
|
+
}
|
|
3850
|
+
if (upgrade) {
|
|
3851
|
+
server.on("upgrade", upgrade);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3593
3854
|
}
|
|
3594
3855
|
var init_startServer = __esm({
|
|
3595
3856
|
"src/server/startServer.ts"() {
|
|
@@ -5531,7 +5792,20 @@ var init_loadConfig = __esm({
|
|
|
5531
5792
|
|
|
5532
5793
|
// src/cli/loadPlugins.ts
|
|
5533
5794
|
async function loadPlugins(declarations, ctx) {
|
|
5534
|
-
|
|
5795
|
+
const handlerWrappers = [];
|
|
5796
|
+
const upgradeWrappers = [];
|
|
5797
|
+
if (!declarations || declarations.length === 0) {
|
|
5798
|
+
return { handlerWrappers, upgradeWrappers };
|
|
5799
|
+
}
|
|
5800
|
+
const fullCtx = {
|
|
5801
|
+
...ctx,
|
|
5802
|
+
wrapHandler: (fn) => {
|
|
5803
|
+
handlerWrappers.push(fn);
|
|
5804
|
+
},
|
|
5805
|
+
wrapUpgradeHandler: (fn) => {
|
|
5806
|
+
upgradeWrappers.push(fn);
|
|
5807
|
+
}
|
|
5808
|
+
};
|
|
5535
5809
|
const loaded = /* @__PURE__ */ new Set();
|
|
5536
5810
|
for (const decl of declarations) {
|
|
5537
5811
|
const { specifier, options, enable } = resolveDeclaration(decl);
|
|
@@ -5548,12 +5822,15 @@ async function loadPlugins(declarations, ctx) {
|
|
|
5548
5822
|
console.warn(`! Plugin ${specifier} has no setup function, skipping`);
|
|
5549
5823
|
continue;
|
|
5550
5824
|
}
|
|
5551
|
-
await plugin.setup({ ...
|
|
5825
|
+
await plugin.setup({ ...fullCtx, options });
|
|
5552
5826
|
console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
|
|
5553
5827
|
} catch (err) {
|
|
5554
|
-
console.warn(
|
|
5828
|
+
console.warn(
|
|
5829
|
+
`! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`
|
|
5830
|
+
);
|
|
5555
5831
|
}
|
|
5556
5832
|
}
|
|
5833
|
+
return { handlerWrappers, upgradeWrappers };
|
|
5557
5834
|
}
|
|
5558
5835
|
function resolveDeclaration(decl) {
|
|
5559
5836
|
if (typeof decl === "string") {
|
|
@@ -5590,10 +5867,11 @@ function isProductionMode(rootDir) {
|
|
|
5590
5867
|
return fs6.existsSync(path9.resolve(rootDir, "dist", "faapi-schema.js"));
|
|
5591
5868
|
}
|
|
5592
5869
|
function adjustForProd(patterns, appDir) {
|
|
5593
|
-
const prodPatterns = patterns.map(
|
|
5594
|
-
(p
|
|
5595
|
-
|
|
5596
|
-
|
|
5870
|
+
const prodPatterns = patterns.map((p) => p.replace(/\.ts$/g, ".js")).map((p) => {
|
|
5871
|
+
if (p.startsWith("dist/")) return p;
|
|
5872
|
+
return `dist/${p}`;
|
|
5873
|
+
});
|
|
5874
|
+
const prodAppDir = appDir === "." ? "dist" : `dist/${appDir}`;
|
|
5597
5875
|
return { patterns: prodPatterns, appDir: prodAppDir };
|
|
5598
5876
|
}
|
|
5599
5877
|
async function startCommand(argv) {
|
|
@@ -5637,6 +5915,7 @@ async function startCommand(argv) {
|
|
|
5637
5915
|
await generateTypes(sorted, rootDir, typesPath);
|
|
5638
5916
|
console.log(`- Types generated: ${typesPath}`);
|
|
5639
5917
|
}
|
|
5918
|
+
const pluginConfig = config ? Object.fromEntries(Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))) : {};
|
|
5640
5919
|
const server = await startServer({
|
|
5641
5920
|
port: args.port,
|
|
5642
5921
|
routes: sorted,
|
|
@@ -5649,7 +5928,17 @@ async function startCommand(argv) {
|
|
|
5649
5928
|
config: config ?? void 0,
|
|
5650
5929
|
wsRoutes,
|
|
5651
5930
|
middlewares: config?.middlewares,
|
|
5652
|
-
injectors: config?.injectors
|
|
5931
|
+
injectors: config?.injectors,
|
|
5932
|
+
// beforeListen:加载插件并应用 handler 包装(在 server.listen 之前)
|
|
5933
|
+
beforeListen: async (server2) => {
|
|
5934
|
+
const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
|
|
5935
|
+
rootDir,
|
|
5936
|
+
routes: sorted,
|
|
5937
|
+
server: server2,
|
|
5938
|
+
config: pluginConfig
|
|
5939
|
+
});
|
|
5940
|
+
applyPluginWrappers(server2, handlerWrappers, upgradeWrappers);
|
|
5941
|
+
}
|
|
5653
5942
|
});
|
|
5654
5943
|
if (config?.lifecycle?.onReady) {
|
|
5655
5944
|
await config.lifecycle.onReady({
|
|
@@ -5682,15 +5971,6 @@ async function startCommand(argv) {
|
|
|
5682
5971
|
types: args.types
|
|
5683
5972
|
});
|
|
5684
5973
|
}
|
|
5685
|
-
const pluginConfig = config ? Object.fromEntries(
|
|
5686
|
-
Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))
|
|
5687
|
-
) : {};
|
|
5688
|
-
await loadPlugins(config?.plugins, {
|
|
5689
|
-
rootDir,
|
|
5690
|
-
routes: sorted,
|
|
5691
|
-
server,
|
|
5692
|
-
config: pluginConfig
|
|
5693
|
-
});
|
|
5694
5974
|
}
|
|
5695
5975
|
function isFaapiConfigKey(key) {
|
|
5696
5976
|
return FAAPI_CONFIG_KEYS.has(key);
|
|
@@ -5712,8 +5992,6 @@ var init_startCommand = __esm({
|
|
|
5712
5992
|
init_loadPlugins();
|
|
5713
5993
|
FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
5714
5994
|
"port",
|
|
5715
|
-
"appDir",
|
|
5716
|
-
"patterns",
|
|
5717
5995
|
"staticDir",
|
|
5718
5996
|
"cors",
|
|
5719
5997
|
"responseFormat",
|