@faapi/faapi 0.0.0-canary.0 → 0.0.0-canary.0e994fe

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 (root by default)", { default: "." }).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 ?? ".");
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,
@@ -1396,6 +1397,65 @@ var init_inputType = __esm({
1396
1397
  }
1397
1398
  });
1398
1399
 
1400
+ // src/errors/FaapiError.ts
1401
+ var FaapiError;
1402
+ var init_FaapiError = __esm({
1403
+ "src/errors/FaapiError.ts"() {
1404
+ "use strict";
1405
+ FaapiError = class extends Error {
1406
+ constructor(code, message, statusCode) {
1407
+ super(message);
1408
+ this.code = code;
1409
+ this.statusCode = statusCode;
1410
+ this.name = "FaapiError";
1411
+ }
1412
+ code;
1413
+ statusCode;
1414
+ };
1415
+ }
1416
+ });
1417
+
1418
+ // src/errors/httpErrors.ts
1419
+ function deriveStatusCode(issues) {
1420
+ const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
1421
+ return has400 ? 400 : 422;
1422
+ }
1423
+ var ValidationError, RouteNotFoundError, MethodNotAllowedError, InternalError;
1424
+ var init_httpErrors = __esm({
1425
+ "src/errors/httpErrors.ts"() {
1426
+ "use strict";
1427
+ init_FaapiError();
1428
+ ValidationError = class extends FaapiError {
1429
+ constructor(message, issues) {
1430
+ super("VALIDATION_ERROR", message, deriveStatusCode(issues));
1431
+ this.issues = issues;
1432
+ this.name = "ValidationError";
1433
+ }
1434
+ issues;
1435
+ };
1436
+ RouteNotFoundError = class extends FaapiError {
1437
+ constructor(path10) {
1438
+ super("ROUTE_NOT_FOUND", `Route not found: ${path10}`, 404);
1439
+ this.name = "RouteNotFoundError";
1440
+ }
1441
+ };
1442
+ MethodNotAllowedError = class extends FaapiError {
1443
+ constructor(method, path10, allowedMethods) {
1444
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path10}`, 405);
1445
+ this.allowedMethods = allowedMethods;
1446
+ this.name = "MethodNotAllowedError";
1447
+ }
1448
+ allowedMethods;
1449
+ };
1450
+ InternalError = class extends FaapiError {
1451
+ constructor(message) {
1452
+ super("INTERNAL_ERROR", message, 500);
1453
+ this.name = "InternalError";
1454
+ }
1455
+ };
1456
+ }
1457
+ });
1458
+
1399
1459
  // src/runtime/resolveInput.ts
1400
1460
  async function resolveInput(method, request) {
1401
1461
  const inputType = getInputTypeForMethod(method);
@@ -1404,12 +1464,33 @@ async function resolveInput(method, request) {
1404
1464
  if (contentType.includes("multipart/form-data")) {
1405
1465
  return parseMultipart(request);
1406
1466
  }
1467
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1468
+ const text2 = await request.text();
1469
+ if (text2.trim() === "") return null;
1470
+ const params = new URLSearchParams(text2);
1471
+ const obj = {};
1472
+ for (const [key, value] of params) {
1473
+ obj[key] = value;
1474
+ }
1475
+ return obj;
1476
+ }
1407
1477
  const text = await request.text();
1478
+ if (text.trim() === "") {
1479
+ return null;
1480
+ }
1408
1481
  const result = parseJsonBody(text);
1409
- if (result.success) {
1410
- return result.data;
1482
+ if (!result.success) {
1483
+ throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
1484
+ {
1485
+ path: "body",
1486
+ code: "INVALID_FORMAT",
1487
+ expected: "JSON",
1488
+ received: "text",
1489
+ message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
1490
+ }
1491
+ ]);
1411
1492
  }
1412
- return null;
1493
+ return result.data;
1413
1494
  }
1414
1495
  const url = new URL(request.url);
1415
1496
  return queryToObject(url.searchParams);
@@ -1421,6 +1502,7 @@ var init_resolveInput = __esm({
1421
1502
  init_parseJsonBody();
1422
1503
  init_parseMultipart();
1423
1504
  init_inputType();
1505
+ init_httpErrors();
1424
1506
  }
1425
1507
  });
1426
1508
 
@@ -1805,61 +1887,6 @@ var init_sendNodeResponse = __esm({
1805
1887
  }
1806
1888
  });
1807
1889
 
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
1890
  // src/validator/schemaRegistry.ts
1864
1891
  var SchemaRegistry, schemaRegistry;
1865
1892
  var init_schemaRegistry = __esm({
@@ -1971,6 +1998,20 @@ function coerceValue(value, type, path10, issues) {
1971
1998
  return value.map((item, i) => coerceValue(item, type.element, `${path10}[${i}]`, issues));
1972
1999
  }
1973
2000
  return value;
2001
+ case "tuple": {
2002
+ if (!Array.isArray(value)) return value;
2003
+ const restElement = type.elements.find((e) => e.rest);
2004
+ return value.map((item, i) => {
2005
+ const elem = type.elements[i];
2006
+ if (elem && !elem.rest) {
2007
+ return coerceValue(item, elem.type, `${path10}[${i}]`, issues);
2008
+ }
2009
+ if (restElement) {
2010
+ return coerceValue(item, restElement.type, `${path10}[${i}]`, issues);
2011
+ }
2012
+ return item;
2013
+ });
2014
+ }
1974
2015
  case "union":
1975
2016
  for (const member of type.members) {
1976
2017
  const tempIssues = [];
@@ -1980,6 +2021,12 @@ function coerceValue(value, type, path10, issues) {
1980
2021
  }
1981
2022
  }
1982
2023
  return value;
2024
+ case "literal":
2025
+ if (typeof type.value === "number" && typeof value === "string" && value.trim() !== "") {
2026
+ const num = Number(value);
2027
+ if (!Number.isNaN(num)) return num;
2028
+ }
2029
+ return value;
1983
2030
  case "object":
1984
2031
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1985
2032
  const obj = value;
@@ -2005,6 +2052,9 @@ function coerceStringToNumber(value, path10, issues) {
2005
2052
  if (value.trim() === "") {
2006
2053
  issues.push({
2007
2054
  path: path10,
2055
+ code: "COERCE_FAILED",
2056
+ expected: "number",
2057
+ received: "string",
2008
2058
  message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A number`
2009
2059
  });
2010
2060
  return value;
@@ -2013,6 +2063,9 @@ function coerceStringToNumber(value, path10, issues) {
2013
2063
  if (Number.isNaN(num)) {
2014
2064
  issues.push({
2015
2065
  path: path10,
2066
+ code: "COERCE_FAILED",
2067
+ expected: "number",
2068
+ received: "string",
2016
2069
  message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A number`
2017
2070
  });
2018
2071
  return value;
@@ -2028,6 +2081,9 @@ function coerceStringToBoolean(value, path10, issues) {
2028
2081
  }
2029
2082
  issues.push({
2030
2083
  path: path10,
2084
+ code: "COERCE_FAILED",
2085
+ expected: "boolean",
2086
+ received: "string",
2031
2087
  message: `\u5B57\u6BB5 "${path10}" \u7C7B\u578B\u8F6C\u6362\u5931\u8D25\uFF1A\u65E0\u6CD5\u5C06 "${value}" \u8F6C\u4E3A boolean`
2032
2088
  });
2033
2089
  return value;
@@ -2275,7 +2331,12 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
2275
2331
  case ts2.SyntaxKind.BooleanKeyword:
2276
2332
  return { kind: "boolean" };
2277
2333
  case ts2.SyntaxKind.BigIntKeyword:
2278
- return { kind: "bigint" };
2334
+ throw new SchemaExtractionError(
2335
+ typeNode.getText(),
2336
+ "bigint \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93,\u8BF7\u6539\u7528 string \u6216 number"
2337
+ );
2338
+ case ts2.SyntaxKind.SymbolKeyword:
2339
+ throw new SchemaExtractionError(typeNode.getText(), "symbol \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
2279
2340
  case ts2.SyntaxKind.NullKeyword:
2280
2341
  return { kind: "null" };
2281
2342
  case ts2.SyntaxKind.UndefinedKeyword:
@@ -2320,8 +2381,35 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
2320
2381
  };
2321
2382
  }
2322
2383
  if (ts2.isTupleTypeNode(typeNode)) {
2323
- const elements = typeNode.elements.map((e) => resolveTypeNode(e, checker, visited));
2324
- return { kind: "array", element: { kind: "union", members: elements } };
2384
+ const elements = typeNode.elements.map((e) => {
2385
+ if (ts2.isRestTypeNode(e)) {
2386
+ const inner = resolveTypeNode(e.type, checker, visited);
2387
+ if (inner.kind === "array") {
2388
+ return { type: inner.element, optional: false, rest: true };
2389
+ }
2390
+ return { type: inner, optional: false, rest: true };
2391
+ }
2392
+ if (ts2.isNamedTupleMember(e)) {
2393
+ return {
2394
+ type: resolveTypeNode(e.type, checker, visited),
2395
+ optional: !!e.questionToken,
2396
+ rest: false
2397
+ };
2398
+ }
2399
+ if (ts2.isOptionalTypeNode(e)) {
2400
+ return {
2401
+ type: resolveTypeNode(e.type, checker, visited),
2402
+ optional: true,
2403
+ rest: false
2404
+ };
2405
+ }
2406
+ return {
2407
+ type: resolveTypeNode(e, checker, visited),
2408
+ optional: false,
2409
+ rest: false
2410
+ };
2411
+ });
2412
+ return { kind: "tuple", elements };
2325
2413
  }
2326
2414
  if (ts2.isUnionTypeNode(typeNode)) {
2327
2415
  const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
@@ -2492,6 +2580,9 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
2492
2580
  "Promise \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u52FF\u5728 query/body \u7C7B\u578B\u4E2D\u4F7F\u7528"
2493
2581
  );
2494
2582
  }
2583
+ if (typeName === "Function") {
2584
+ throw new SchemaExtractionError(typeNode.getText(), "Function \u65E0\u6CD5\u901A\u8FC7 HTTP/JSON \u4F20\u8F93");
2585
+ }
2495
2586
  if (visited.has(typeName)) {
2496
2587
  return { kind: "ref", name: typeName };
2497
2588
  }
@@ -2507,11 +2598,38 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
2507
2598
  if (ts2.isTypeAliasDeclaration(declaration)) {
2508
2599
  return resolveTypeNode(declaration.type, checker, visited);
2509
2600
  }
2601
+ if (ts2.isEnumDeclaration(declaration)) {
2602
+ return resolveEnumDeclaration(declaration);
2603
+ }
2510
2604
  }
2511
2605
  }
2512
2606
  }
2513
2607
  throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
2514
2608
  }
2609
+ function resolveEnumDeclaration(node) {
2610
+ const members = [];
2611
+ let nextNumericValue = 0;
2612
+ for (const member of node.members) {
2613
+ if (member.initializer) {
2614
+ if (ts2.isStringLiteral(member.initializer)) {
2615
+ members.push({ kind: "literal", value: member.initializer.text });
2616
+ } else if (ts2.isNumericLiteral(member.initializer)) {
2617
+ const num = Number(member.initializer.text);
2618
+ members.push({ kind: "literal", value: num });
2619
+ nextNumericValue = num + 1;
2620
+ } else {
2621
+ throw new SchemaExtractionError(
2622
+ node.name.text,
2623
+ `enum \u6210\u5458 "${member.name.getText()}" \u7684\u521D\u59CB\u5316\u503C\u7C7B\u578B\u4E0D\u652F\u6301,\u4EC5\u652F\u6301 string/number \u5B57\u9762\u91CF`
2624
+ );
2625
+ }
2626
+ } else {
2627
+ members.push({ kind: "literal", value: nextNumericValue });
2628
+ nextNumericValue++;
2629
+ }
2630
+ }
2631
+ return { kind: "union", members };
2632
+ }
2515
2633
  function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
2516
2634
  const properties = [];
2517
2635
  const propMap = /* @__PURE__ */ new Map();
@@ -2563,6 +2681,34 @@ var init_resolveTypeNode = __esm({
2563
2681
  });
2564
2682
 
2565
2683
  // src/ast/generateValidatorCode.ts
2684
+ function runtimeTypeToExpected(type) {
2685
+ switch (type.kind) {
2686
+ case "string":
2687
+ case "number":
2688
+ case "boolean":
2689
+ case "null":
2690
+ case "undefined":
2691
+ case "bigint":
2692
+ case "date":
2693
+ case "any":
2694
+ case "unknown":
2695
+ return type.kind;
2696
+ case "literal":
2697
+ return typeof type.value === "string" ? `'${type.value}'` : String(type.value);
2698
+ case "array":
2699
+ return `${runtimeTypeToExpected(type.element)}[]`;
2700
+ case "tuple":
2701
+ return `[${type.elements.map((e) => (e.rest ? "..." : "") + runtimeTypeToExpected(e.type) + (e.optional ? "?" : "")).join(", ")}]`;
2702
+ case "object":
2703
+ return "object";
2704
+ case "union":
2705
+ return type.members.map(runtimeTypeToExpected).join(" | ");
2706
+ case "record":
2707
+ return `Record<${runtimeTypeToExpected(type.key)}, ${runtimeTypeToExpected(type.value)}>`;
2708
+ case "ref":
2709
+ return type.name;
2710
+ }
2711
+ }
2566
2712
  function generateValidatorSource(typeInfo, resolveType) {
2567
2713
  const ctx = new CodeGenContext(resolveType);
2568
2714
  collectNamedTypes(typeInfo.runtimeType, typeInfo.name, ctx);
@@ -2578,7 +2724,7 @@ function generateValidatorSource(typeInfo, resolveType) {
2578
2724
  const body = generateObjectValidation(type, "value", "issues", ctx, "path");
2579
2725
  return `function validate_${name}(value, path, issues) {
2580
2726
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
2581
- issues.push({ path, message: '\u671F\u671B\u5BF9\u8C61' });
2727
+ issues.push({ path, code: 'TYPE_MISMATCH', expected: 'object', received: typeof value, message: '\u671F\u671B\u5BF9\u8C61' });
2582
2728
  return;
2583
2729
  }
2584
2730
  if (!validate_${name}.__visited) validate_${name}.__visited = new WeakSet();
@@ -2613,6 +2759,11 @@ function collectNamedTypesFromType(type, ctx) {
2613
2759
  case "array":
2614
2760
  collectNamedTypesFromType(type.element, ctx);
2615
2761
  break;
2762
+ case "tuple":
2763
+ for (const elem of type.elements) {
2764
+ collectNamedTypesFromType(elem.type, ctx);
2765
+ }
2766
+ break;
2616
2767
  case "union":
2617
2768
  for (const member of type.members) {
2618
2769
  collectNamedTypesFromType(member, ctx);
@@ -2649,7 +2800,7 @@ function generateObjectValidation(type, varName, issuesVar, ctx, pathExpr) {
2649
2800
  const hasCheck = `'${prop.name}' in ${varName}`;
2650
2801
  if (!prop.optional) {
2651
2802
  lines.push(
2652
- `if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
2803
+ `if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, code: 'MISSING_FIELD', expected: '${prop.name}', received: 'undefined', message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
2653
2804
  );
2654
2805
  lines.push(`else {`);
2655
2806
  } else {
@@ -2668,21 +2819,23 @@ function generateValueValidation(type, varName, issuesVar, ctx, pathExpr) {
2668
2819
  return "";
2669
2820
  // 不校验
2670
2821
  case "string":
2671
- return `if (typeof ${varName} !== 'string') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B string\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
2822
+ 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
2823
  case "number":
2673
- return `if (typeof ${varName} !== 'number' || Number.isNaN(${varName})) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B number\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
2824
+ 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
2825
  case "boolean":
2675
- return `if (typeof ${varName} !== 'boolean') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B boolean\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
2826
+ 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
2827
  case "bigint":
2677
- return `if (typeof ${varName} !== 'bigint') ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B bigint\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
2828
+ 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
2829
  case "null":
2679
- return `if (${varName} !== null) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B null\uFF0C\u5B9E\u9645 ' + typeof ${varName} });`;
2830
+ 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
2831
  case "undefined":
2681
- return `if (${varName} !== undefined) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B undefined' });`;
2832
+ return `if (${varName} !== undefined) ${issuesVar}.push({ path: ${path10}, code: 'TYPE_MISMATCH', expected: 'undefined', received: typeof ${varName}, message: '\u671F\u671B undefined' });`;
2682
2833
  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}) });`;
2834
+ 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
2835
  case "date":
2685
- return `if (!(${varName} instanceof Date) || Number.isNaN(${varName}.getTime())) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B Date \u5B9E\u4F8B' });`;
2836
+ return `if (${varName} instanceof Date) { /* Date \u5B9E\u4F8B,\u901A\u8FC7 */ }
2837
+ 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} });
2838
+ 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
2839
  case "array": {
2687
2840
  const id = ctx.nextVarId();
2688
2841
  const itemVar = `item${id}`;
@@ -2695,21 +2848,71 @@ function generateValueValidation(type, varName, issuesVar, ctx, pathExpr) {
2695
2848
  ctx,
2696
2849
  elemPath
2697
2850
  );
2698
- return `if (!Array.isArray(${varName})) ${issuesVar}.push({ path: ${path10}, message: '\u671F\u671B\u6570\u7EC4\uFF0C\u5B9E\u9645 ' + typeof ${varName} });
2851
+ 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
2852
  else for (let ${indexVar} = 0; ${indexVar} < ${varName}.length; ${indexVar}++) { const ${itemVar} = ${varName}[${indexVar}]; ${elemValidation} }`;
2700
2853
  }
2854
+ case "tuple": {
2855
+ const minRequired = type.elements.filter((e) => !e.optional && !e.rest).length;
2856
+ const fixedCount = type.elements.filter((e) => !e.rest).length;
2857
+ const restElement = type.elements.find((e) => e.rest);
2858
+ const id = ctx.nextVarId();
2859
+ const itemVar = `t${id}`;
2860
+ const indexVar = `ti${id}`;
2861
+ const lines = [];
2862
+ lines.push(
2863
+ `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} });`
2864
+ );
2865
+ lines.push(`else {`);
2866
+ lines.push(
2867
+ `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 });`
2868
+ );
2869
+ if (!restElement) {
2870
+ lines.push(
2871
+ `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 });`
2872
+ );
2873
+ }
2874
+ for (let i = 0; i < type.elements.length; i++) {
2875
+ const elem = type.elements[i];
2876
+ if (elem.rest) continue;
2877
+ const elemPath = `${path10} + '[' + ${i} + ']'`;
2878
+ const elemAccess = `${varName}[${i}]`;
2879
+ if (elem.optional) {
2880
+ lines.push(`if (${i} < ${varName}.length && ${elemAccess} !== undefined) {`);
2881
+ } else {
2882
+ lines.push(`if (${i} < ${varName}.length) {`);
2883
+ }
2884
+ lines.push(` ${generateValueValidation(elem.type, elemAccess, issuesVar, ctx, elemPath)}`);
2885
+ lines.push(`}`);
2886
+ }
2887
+ if (restElement) {
2888
+ const restPath = `${path10} + '[' + ${indexVar} + ']'`;
2889
+ const restValidation = generateValueValidation(
2890
+ restElement.type,
2891
+ itemVar,
2892
+ issuesVar,
2893
+ ctx,
2894
+ restPath
2895
+ );
2896
+ lines.push(
2897
+ `for (let ${indexVar} = ${fixedCount}; ${indexVar} < ${varName}.length; ${indexVar}++) { const ${itemVar} = ${varName}[${indexVar}]; ${restValidation} }`
2898
+ );
2899
+ }
2900
+ lines.push(`}`);
2901
+ return lines.join("\n");
2902
+ }
2701
2903
  case "union": {
2702
2904
  const tempVar = `tempIssues_${Math.random().toString(36).slice(2, 8)}`;
2703
2905
  const memberChecks = type.members.map((member) => {
2704
2906
  const check = generateValueValidation(member, varName, tempVar, ctx, path10);
2705
2907
  return `(() => { const ${tempVar} = []; ${check}; return ${tempVar}.length === 0; })()`;
2706
2908
  });
2707
- return `if (!(${memberChecks.join(" || ")})) ${issuesVar}.push({ path: ${path10}, message: '\u503C ' + JSON.stringify(${varName}) + ' \u4E0D\u5339\u914D\u8054\u5408\u7C7B\u578B' });`;
2909
+ const expected = runtimeTypeToExpected(type);
2910
+ 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
2911
  }
2709
2912
  case "record": {
2710
2913
  const valuePath = `${path10} + '.' + key`;
2711
2914
  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} });
2915
+ 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
2916
  else for (const [key, val] of Object.entries(${varName})) { ${valueValidation} }`;
2714
2917
  }
2715
2918
  case "object": {
@@ -2723,7 +2926,7 @@ else for (const [key, val] of Object.entries(${varName})) { ${valueValidation} }
2723
2926
  function generateInlineObjectValidation(type, varName, issuesVar, ctx, pathExpr) {
2724
2927
  const lines = [];
2725
2928
  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} });`
2929
+ `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
2930
  );
2728
2931
  lines.push("else {");
2729
2932
  for (const prop of type.properties) {
@@ -2732,7 +2935,7 @@ function generateInlineObjectValidation(type, varName, issuesVar, ctx, pathExpr)
2732
2935
  const hasCheck = `'${prop.name}' in ${varName}`;
2733
2936
  if (!prop.optional) {
2734
2937
  lines.push(
2735
- ` if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
2938
+ ` if (!${hasCheck}) ${issuesVar}.push({ path: ${propPath}, code: 'MISSING_FIELD', expected: '${prop.name}', received: 'undefined', message: '\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 "${prop.name}"' });`
2736
2939
  );
2737
2940
  lines.push(` else {`);
2738
2941
  } else {
@@ -3138,21 +3341,24 @@ function nodeHttpToWebHeaders(req) {
3138
3341
  return headers;
3139
3342
  }
3140
3343
  function buildErrorResponse(err, ctx, errorFormat) {
3141
- try {
3142
- return errorFormat ? errorFormat(err, ctx) : formatErrorResponse(err);
3143
- } catch {
3344
+ if (errorFormat) {
3144
3345
  try {
3145
- return formatErrorResponse(err);
3346
+ const res = errorFormat(err, ctx);
3347
+ if (res) return res;
3146
3348
  } 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
3349
  }
3155
3350
  }
3351
+ try {
3352
+ return formatErrorResponse(err);
3353
+ } catch {
3354
+ return new Response(
3355
+ JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
3356
+ {
3357
+ status: 500,
3358
+ headers: { "Content-Type": "application/json" }
3359
+ }
3360
+ );
3361
+ }
3156
3362
  }
3157
3363
  var init_serverUtils = __esm({
3158
3364
  "src/server/serverUtils.ts"() {
@@ -3380,12 +3586,7 @@ function findAllowedMethods(routes, path10) {
3380
3586
  continue;
3381
3587
  }
3382
3588
  if (route.isDynamic) {
3383
- const params = matchDynamicPath(
3384
- route.urlPath,
3385
- path10,
3386
- route.paramNames,
3387
- route.isCatchAll
3388
- );
3589
+ const params = matchDynamicPath(route.urlPath, path10, route.paramNames, route.isCatchAll);
3389
3590
  if (params !== null) {
3390
3591
  methods.add(route.method);
3391
3592
  }
@@ -3555,7 +3756,8 @@ function startServer(options) {
3555
3756
  config,
3556
3757
  wsRoutes,
3557
3758
  middlewares,
3558
- injectors
3759
+ injectors,
3760
+ beforeListen
3559
3761
  } = options;
3560
3762
  const server = createServer({
3561
3763
  routes,
@@ -3570,26 +3772,57 @@ function startServer(options) {
3570
3772
  middlewares,
3571
3773
  injectors
3572
3774
  });
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}`);
3775
+ return (async () => {
3776
+ if (beforeListen) {
3777
+ await beforeListen(server);
3778
+ }
3779
+ return new Promise((resolve3) => {
3780
+ server.listen(port, () => {
3781
+ const address = server.address();
3782
+ const actualPort = typeof address === "object" && address !== null ? address.port : port;
3783
+ console.log("faapi dev server started");
3784
+ console.log(`- Local: http://localhost:${actualPort}`);
3785
+ console.log("- Loaded routes:");
3786
+ for (const route of routes) {
3787
+ const method = route.method.padEnd(6);
3788
+ console.log(` ${method}${route.urlPath} ${route.filePath}`);
3588
3789
  }
3589
- }
3590
- resolve3(server);
3790
+ if (wsRoutes && wsRoutes.length > 0) {
3791
+ console.log("- WebSocket routes:");
3792
+ for (const route of wsRoutes) {
3793
+ console.log(` WS ${route.urlPath} ${route.filePath}`);
3794
+ }
3795
+ }
3796
+ resolve3(server);
3797
+ });
3591
3798
  });
3592
- });
3799
+ })();
3800
+ }
3801
+ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
3802
+ if (handlerWrappers.length > 0) {
3803
+ const listeners = server.listeners("request");
3804
+ const original = listeners[0];
3805
+ if (original) {
3806
+ server.removeAllListeners("request");
3807
+ let handler = original;
3808
+ for (const wrap of handlerWrappers) {
3809
+ handler = wrap(handler);
3810
+ }
3811
+ server.on("request", handler);
3812
+ }
3813
+ }
3814
+ if (upgradeWrappers.length > 0) {
3815
+ const listeners = server.listeners("upgrade");
3816
+ const original = listeners[0];
3817
+ server.removeAllListeners("upgrade");
3818
+ let upgrade = original;
3819
+ for (const wrap of upgradeWrappers) {
3820
+ upgrade = wrap(upgrade);
3821
+ }
3822
+ if (upgrade) {
3823
+ server.on("upgrade", upgrade);
3824
+ }
3825
+ }
3593
3826
  }
3594
3827
  var init_startServer = __esm({
3595
3828
  "src/server/startServer.ts"() {
@@ -5531,7 +5764,20 @@ var init_loadConfig = __esm({
5531
5764
 
5532
5765
  // src/cli/loadPlugins.ts
5533
5766
  async function loadPlugins(declarations, ctx) {
5534
- if (!declarations || declarations.length === 0) return;
5767
+ const handlerWrappers = [];
5768
+ const upgradeWrappers = [];
5769
+ if (!declarations || declarations.length === 0) {
5770
+ return { handlerWrappers, upgradeWrappers };
5771
+ }
5772
+ const fullCtx = {
5773
+ ...ctx,
5774
+ wrapHandler: (fn) => {
5775
+ handlerWrappers.push(fn);
5776
+ },
5777
+ wrapUpgradeHandler: (fn) => {
5778
+ upgradeWrappers.push(fn);
5779
+ }
5780
+ };
5535
5781
  const loaded = /* @__PURE__ */ new Set();
5536
5782
  for (const decl of declarations) {
5537
5783
  const { specifier, options, enable } = resolveDeclaration(decl);
@@ -5548,12 +5794,15 @@ async function loadPlugins(declarations, ctx) {
5548
5794
  console.warn(`! Plugin ${specifier} has no setup function, skipping`);
5549
5795
  continue;
5550
5796
  }
5551
- await plugin.setup({ ...ctx, options });
5797
+ await plugin.setup({ ...fullCtx, options });
5552
5798
  console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
5553
5799
  } catch (err) {
5554
- console.warn(`! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`);
5800
+ console.warn(
5801
+ `! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`
5802
+ );
5555
5803
  }
5556
5804
  }
5805
+ return { handlerWrappers, upgradeWrappers };
5557
5806
  }
5558
5807
  function resolveDeclaration(decl) {
5559
5808
  if (typeof decl === "string") {
@@ -5590,10 +5839,11 @@ function isProductionMode(rootDir) {
5590
5839
  return fs6.existsSync(path9.resolve(rootDir, "dist", "faapi-schema.js"));
5591
5840
  }
5592
5841
  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}`;
5842
+ const prodPatterns = patterns.map((p) => p.replace(/\.ts$/g, ".js")).map((p) => {
5843
+ if (p.startsWith("dist/")) return p;
5844
+ return `dist/${p}`;
5845
+ });
5846
+ const prodAppDir = appDir === "." ? "dist" : `dist/${appDir}`;
5597
5847
  return { patterns: prodPatterns, appDir: prodAppDir };
5598
5848
  }
5599
5849
  async function startCommand(argv) {
@@ -5637,6 +5887,7 @@ async function startCommand(argv) {
5637
5887
  await generateTypes(sorted, rootDir, typesPath);
5638
5888
  console.log(`- Types generated: ${typesPath}`);
5639
5889
  }
5890
+ const pluginConfig = config ? Object.fromEntries(Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))) : {};
5640
5891
  const server = await startServer({
5641
5892
  port: args.port,
5642
5893
  routes: sorted,
@@ -5649,7 +5900,17 @@ async function startCommand(argv) {
5649
5900
  config: config ?? void 0,
5650
5901
  wsRoutes,
5651
5902
  middlewares: config?.middlewares,
5652
- injectors: config?.injectors
5903
+ injectors: config?.injectors,
5904
+ // beforeListen:加载插件并应用 handler 包装(在 server.listen 之前)
5905
+ beforeListen: async (server2) => {
5906
+ const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
5907
+ rootDir,
5908
+ routes: sorted,
5909
+ server: server2,
5910
+ config: pluginConfig
5911
+ });
5912
+ applyPluginWrappers(server2, handlerWrappers, upgradeWrappers);
5913
+ }
5653
5914
  });
5654
5915
  if (config?.lifecycle?.onReady) {
5655
5916
  await config.lifecycle.onReady({
@@ -5682,15 +5943,6 @@ async function startCommand(argv) {
5682
5943
  types: args.types
5683
5944
  });
5684
5945
  }
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
5946
  }
5695
5947
  function isFaapiConfigKey(key) {
5696
5948
  return FAAPI_CONFIG_KEYS.has(key);
@@ -5712,8 +5964,6 @@ var init_startCommand = __esm({
5712
5964
  init_loadPlugins();
5713
5965
  FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
5714
5966
  "port",
5715
- "appDir",
5716
- "patterns",
5717
5967
  "staticDir",
5718
5968
  "cors",
5719
5969
  "responseFormat",