@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/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: "app" }).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)");
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 ?? "app");
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 : [`${appDir}/api/**/*.ts`],
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 = "app") {
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 ?? "app";
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
- return result.data;
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 null;
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
- return { kind: "bigint" };
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) => resolveTypeNode(e, checker, visited));
2324
- return { kind: "array", element: { kind: "union", members: elements } };
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 (!(${varName} instanceof Date) || Number.isNaN(${varName}.getTime())) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B Date \u5B9E\u4F8B' });`;
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
- return `if (!(${memberChecks.join(" || ")})) ${issuesVar}.push({ path: ${path10}, message: '\u503C ' + JSON.stringify(${varName}) + ' \u4E0D\u5339\u914D\u8054\u5408\u7C7B\u578B' });`;
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
- try {
3142
- return errorFormat ? errorFormat(err, ctx) : formatErrorResponse(err);
3143
- } catch {
3370
+ if (errorFormat) {
3144
3371
  try {
3145
- return formatErrorResponse(err);
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 new Promise((resolve3) => {
3574
- server.listen(port, () => {
3575
- const address = server.address();
3576
- const actualPort = typeof address === "object" && address !== null ? address.port : port;
3577
- console.log("faapi dev server started");
3578
- console.log(`- Local: http://localhost:${actualPort}`);
3579
- console.log("- Loaded routes:");
3580
- for (const route of routes) {
3581
- const method = route.method.padEnd(6);
3582
- console.log(` ${method}${route.urlPath} ${route.filePath}`);
3583
- }
3584
- if (wsRoutes && wsRoutes.length > 0) {
3585
- console.log("- WebSocket routes:");
3586
- for (const route of wsRoutes) {
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
- resolve3(server);
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
- if (!declarations || declarations.length === 0) return;
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({ ...ctx, options });
5825
+ await plugin.setup({ ...fullCtx, options });
5552
5826
  console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
5553
5827
  } catch (err) {
5554
- console.warn(`! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`);
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) => p.replace(/\.ts$/g, ".js").replace(/^app\//, "dist/app/")
5595
- );
5596
- const prodAppDir = appDir === "app" ? "dist/app" : `dist/${appDir}`;
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",