@faapi/faapi 0.0.0-canary.a3f7014 → 0.0.0-canary.b2b338c

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/index.d.ts CHANGED
@@ -307,6 +307,8 @@ interface LoggerOptions {
307
307
  * before/after 一体,闭包变量共享开始时间,无需污染 ctx。
308
308
  * 错误用 try/catch 捕获,记录后重新抛出(让上层处理)。
309
309
  * 成功时从 next() 返回的 Response 读取状态码。
310
+ *
311
+ * log 函数每次请求时读取(options.log ?? console.log),运行时替换 console.log 会生效。
310
312
  */
311
313
  declare function logger(options?: LoggerOptions): FaapiMiddleware;
312
314
 
@@ -561,9 +563,8 @@ interface LifecycleContext {
561
563
  * 环境覆盖通过 faapi.config.{NODE_ENV}.ts 实现(如 faapi.config.production.ts)
562
564
  *
563
565
  * 框架元信息通过环境变量配置(不放在 config 内):
564
- * - `FAAPI_APP_DIR`:源码目录前缀,默认 'src',设为 '.' 表示源码在项目根目录
565
566
  * - `PORT`:服务端口,默认 3000
566
- * - `FAAPI_OUT_DIR`:产物输出目录,dev 固定为 '.faapi/dev',prod 默认 'dist'
567
+ * - `FAAPI_DIST`:产物输出目录,dev 固定为 `.faapi`(不可修改),prod 默认为 `dist`(可通过 `--dist` 修改)
567
568
  */
568
569
  interface FaapiConfig {
569
570
  /** CORS 配置,false 禁用 */
@@ -810,6 +811,13 @@ type RuntimeType = {
810
811
  kind: 'record';
811
812
  key: RuntimeType;
812
813
  value: RuntimeType;
814
+ } | {
815
+ kind: 'map';
816
+ key: RuntimeType;
817
+ value: RuntimeType;
818
+ } | {
819
+ kind: 'set';
820
+ element: RuntimeType;
813
821
  } | {
814
822
  kind: 'ref';
815
823
  name: string;
@@ -949,6 +957,18 @@ interface RouteSchemaSource {
949
957
  filePath: string;
950
958
  schemaName: string;
951
959
  typeInfo: HandlerTypeInfo | null;
960
+ /**
961
+ * 是否对 number/boolean 字段生成 z.preprocess 字符串转换(coerce)。
962
+ *
963
+ * - query/params:始终 coerce=true(URL 来源均为 string)
964
+ * - body:始终 coerce=false(JSON 解析已是天然 JS 类型)
965
+ * - form:coerce=true(form-urlencoded 来源均为 string),由本函数在提取时
966
+ * 检测到 handler 声明 `form` 参数时显式设置。schema 名仍为 `POSTBody`
967
+ * (与 body 共享运行时 schema key),运行时 validateInput 无需感知 form/body 差异。
968
+ *
969
+ * 未设置时由 generateSchemaFileSource 回退到 schemaName 后缀正则推断(Query/Params → true)。
970
+ */
971
+ coerce?: boolean;
952
972
  }
953
973
  /**
954
974
  * 从路由清单收集 schema 提取所需的原始数据
@@ -974,8 +994,8 @@ declare function collectRouteSchemaSources(routes: RouteManifest, rootDir?: stri
974
994
  /**
975
995
  * 加载 faapi 配置文件
976
996
  *
977
- * 统一读取 `<outDir>/faapi-config.js` 产物:
978
- * - dev 模式:`faapi dev` 启动时由 `compileConfig` 生成 `.faapi/dev/faapi-config.js`
997
+ * 统一读取 `<dist>/faapi-config.js` 产物:
998
+ * - dev 模式:`faapi dev` 启动时由 `compileConfig` 生成 `.faapi/faapi-config.js`
979
999
  * - prod 模式:`faapi build` 时由 `compileConfig` 生成 `dist/faapi-config.js`
980
1000
  *
981
1001
  * 产物由 `compileConfig` 在构建阶段合并 env 后固化,运行时不读源码、不现场编译、不按 env 合并。
@@ -985,10 +1005,10 @@ declare function collectRouteSchemaSources(routes: RouteManifest, rootDir?: stri
985
1005
  * - 源码也无配置文件 → 返回 `null`(配置可选)
986
1006
  *
987
1007
  * @param rootDir 项目根目录
988
- * @param outDir 产物目录(如 'dist' 或 '.faapi/dev')
1008
+ * @param dist 产物目录(如 'dist' 或 '.faapi')
989
1009
  * @returns 配置对象,无配置文件时返回 null
990
1010
  */
991
- declare function loadConfig(rootDir: string, outDir: string): Promise<Partial<FaapiConfig> | null>;
1011
+ declare function loadConfig(rootDir: string, dist: string): Promise<Partial<FaapiConfig> | null>;
992
1012
 
993
1013
  declare const VALIDATION_ERROR = "VALIDATION_ERROR";
994
1014
  declare const ROUTE_NOT_FOUND = "ROUTE_NOT_FOUND";
@@ -1059,8 +1079,8 @@ interface InjectResponse {
1059
1079
  interface CreateAppOptions {
1060
1080
  /** 项目根目录,默认 process.cwd() */
1061
1081
  rootDir?: string;
1062
- /** 源码目录前缀,覆盖环境变量 FAAPI_APP_DIR,默认 'src' */
1063
- appDir?: string;
1082
+ /** 产物输出目录(如 dist 或 .faapi),覆盖环境变量 FAAPI_DIST,默认 'dist' */
1083
+ dist?: string;
1064
1084
  /** 端口号,也可在 listen() 时传入;默认环境变量 PORT 或 3000 */
1065
1085
  port?: number;
1066
1086
  }
@@ -1108,7 +1128,7 @@ interface DevApp extends AppBase {
1108
1128
  * // devCommand 内部
1109
1129
  * const app = await createDevApp();
1110
1130
  * await app.listen();
1111
- * startWatcher({ rootDir, appDir, app });
1131
+ * startWatcher({ rootDir, app, devDist });
1112
1132
  * ```
1113
1133
  */
1114
1134
  declare function createDevApp(options?: CreateAppOptions): Promise<DevApp>;
package/dist/index.js CHANGED
@@ -290,10 +290,35 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
290
290
  const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
291
291
  return { kind: "object", properties };
292
292
  }
293
- if (typeName === "Map" || typeName === "Set" || typeName === "WeakMap" || typeName === "WeakSet") {
293
+ if (typeName === "Map") {
294
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
295
+ throw new SchemaExtractionError(
296
+ typeNode.getText(),
297
+ "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
298
+ );
299
+ }
300
+ return {
301
+ kind: "map",
302
+ key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
303
+ value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
304
+ };
305
+ }
306
+ if (typeName === "Set") {
307
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
308
+ throw new SchemaExtractionError(
309
+ typeNode.getText(),
310
+ "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
311
+ );
312
+ }
313
+ return {
314
+ kind: "set",
315
+ element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
316
+ };
317
+ }
318
+ if (typeName === "WeakMap" || typeName === "WeakSet") {
294
319
  throw new SchemaExtractionError(
295
320
  typeNode.getText(),
296
- `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528\u5BF9\u8C61\u6216\u6570\u7EC4`
321
+ `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
297
322
  );
298
323
  }
299
324
  if (typeName === "Promise") {
@@ -650,6 +675,7 @@ import ts4 from "typescript";
650
675
  var PARAM_TYPE_MAP = {
651
676
  query: "query",
652
677
  body: "body",
678
+ form: "form",
653
679
  headers: "headers",
654
680
  params: "params",
655
681
  context: "context",
@@ -807,9 +833,16 @@ function collectRouteSchemaSources(routes, rootDir) {
807
833
  const inputType = getInputTypeForMethod(method);
808
834
  const schemaName = getSchemaName(method, inputType);
809
835
  const meta = analyzeInjection(code, method);
810
- const param = meta.params.find((p) => p.type === inputType);
836
+ const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
837
+ const isForm = param?.type === "form";
811
838
  const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
812
- sources.push({ urlPath: entry.urlPath, filePath, schemaName, typeInfo });
839
+ sources.push({
840
+ urlPath: entry.urlPath,
841
+ filePath,
842
+ schemaName,
843
+ typeInfo,
844
+ coerce: isForm || void 0
845
+ });
813
846
  }
814
847
  }
815
848
  return { sources, allTypesByFile, mergedAllTypes };
@@ -882,8 +915,8 @@ function cors(options = {}) {
882
915
 
883
916
  // src/middleware/logger.ts
884
917
  function logger(options = {}) {
885
- const { log = console.log } = options;
886
918
  return async (ctx, next) => {
919
+ const log = options.log ?? console.log;
887
920
  const start = Date.now();
888
921
  try {
889
922
  const response = await next();
@@ -995,8 +1028,8 @@ async function importWithCacheBust(filePath) {
995
1028
 
996
1029
  // src/config/loadConfig.ts
997
1030
  var CONFIG_PRODUCT_FILE = "faapi-config.js";
998
- async function loadConfig(rootDir, outDir) {
999
- const configProductPath = path2.resolve(rootDir, outDir, CONFIG_PRODUCT_FILE);
1031
+ async function loadConfig(rootDir, dist) {
1032
+ const configProductPath = path2.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
1000
1033
  if (fs.existsSync(configProductPath)) {
1001
1034
  const module = await importWithCacheBust(configProductPath);
1002
1035
  return module.default ?? {};
@@ -1004,7 +1037,7 @@ async function loadConfig(rootDir, outDir) {
1004
1037
  const hasSourceConfig = fs.existsSync(path2.join(rootDir, "faapi.config.ts")) || fs.existsSync(path2.join(rootDir, "faapi.config.js"));
1005
1038
  if (hasSourceConfig) {
1006
1039
  throw new Error(
1007
- `[faapi] ${outDir}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
1040
+ `[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
1008
1041
  );
1009
1042
  }
1010
1043
  return null;
@@ -1649,6 +1682,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
1649
1682
  return ctx.ip;
1650
1683
  case "body":
1651
1684
  return body;
1685
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
1686
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
1687
+ case "form":
1688
+ return body;
1652
1689
  case "files":
1653
1690
  if (body && typeof body === "object" && "files" in body) {
1654
1691
  return body.files;
@@ -2175,6 +2212,13 @@ function collectNamedTypes(type, ctx) {
2175
2212
  collectNamedTypes(type.key, ctx);
2176
2213
  collectNamedTypes(type.value, ctx);
2177
2214
  return;
2215
+ case "map":
2216
+ collectNamedTypes(type.key, ctx);
2217
+ collectNamedTypes(type.value, ctx);
2218
+ return;
2219
+ case "set":
2220
+ collectNamedTypes(type.element, ctx);
2221
+ return;
2178
2222
  case "ref": {
2179
2223
  if (ctx.namedTypes.has(type.name)) return;
2180
2224
  ctx.namedTypes.set(type.name, { kind: "any" });
@@ -2264,6 +2308,10 @@ function baseExpression(type, ctx) {
2264
2308
  return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
2265
2309
  case "record":
2266
2310
  return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
2311
+ case "map":
2312
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
2313
+ case "set":
2314
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
2267
2315
  case "ref":
2268
2316
  if (type.name === ctx.entryTypeName) {
2269
2317
  return `${ctx.entryExportName}Schema`;
@@ -2273,17 +2321,21 @@ function baseExpression(type, ctx) {
2273
2321
  }
2274
2322
  var COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
2275
2323
  var COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
2324
+ var COERCE_MAP_HELPER = 'export const coerceMap = (v) => Array.isArray(v) ? new Map(v) : v instanceof Map ? v : (v && typeof v === "object" ? new Map(Object.entries(v)) : v);';
2325
+ var COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
2276
2326
  var HELPERS_FILENAME = "faapi-helpers.js";
2277
2327
  function generateHelpersFileSource() {
2278
2328
  return [
2279
2329
  "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
2280
2330
  COERCE_NUMBER_HELPER,
2281
2331
  COERCE_BOOLEAN_HELPER,
2332
+ COERCE_MAP_HELPER,
2333
+ COERCE_SET_HELPER,
2282
2334
  ""
2283
2335
  ].join("\n");
2284
2336
  }
2285
2337
  function usesCoerceHelpers(code) {
2286
- return code.includes("coerceNumber") || code.includes("coerceBoolean");
2338
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
2287
2339
  }
2288
2340
  function wrapCoercePreprocess(kind, inner) {
2289
2341
  if (kind === "number") {
@@ -2370,6 +2422,10 @@ function containsRef(type, visited) {
2370
2422
  return type.members.some((m) => containsRef(m, visited));
2371
2423
  case "record":
2372
2424
  return containsRef(type.key, visited) || containsRef(type.value, visited);
2425
+ case "map":
2426
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
2427
+ case "set":
2428
+ return containsRef(type.element, visited);
2373
2429
  default:
2374
2430
  return false;
2375
2431
  }
@@ -2400,25 +2456,25 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
2400
2456
  }
2401
2457
 
2402
2458
  // src/cli/generateSchemaFiles.ts
2403
- function getSchemaOutputPath(sourceFile, appDir, outDir, rootDir) {
2459
+ function getSchemaOutputPath(sourceFile, dist, rootDir) {
2404
2460
  let rel = sourceFile.replace(/\\/g, "/");
2405
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
2406
- rel = rel.slice(appDir.length + 1);
2461
+ if (rel.startsWith("src/")) {
2462
+ rel = rel.slice(4);
2407
2463
  }
2408
2464
  const idx = rel.lastIndexOf("/");
2409
2465
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2410
- return path4.resolve(rootDir, outDir, relDir, "zod.js");
2466
+ return path4.resolve(rootDir, dist, relDir, "zod.js");
2411
2467
  }
2412
- function getRuntimeSchemaPath(filePath, appDir, outDir, rootDir) {
2468
+ function getRuntimeSchemaPath(filePath, dist, rootDir) {
2413
2469
  let rel = filePath.replace(/\\/g, "/");
2414
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
2415
- rel = rel.slice(appDir.length + 1);
2416
- } else if (rel.startsWith(`${outDir}/`)) {
2417
- rel = rel.slice(outDir.length + 1);
2470
+ if (rel.startsWith("src/")) {
2471
+ rel = rel.slice(4);
2472
+ } else if (rel.startsWith(`${dist}/`)) {
2473
+ rel = rel.slice(dist.length + 1);
2418
2474
  }
2419
2475
  const idx = rel.lastIndexOf("/");
2420
2476
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2421
- return path4.resolve(rootDir, outDir, relDir, "zod.js");
2477
+ return path4.resolve(rootDir, dist, relDir, "zod.js");
2422
2478
  }
2423
2479
  function getHelpersImportPath(relDir) {
2424
2480
  if (!relDir) return `./${HELPERS_FILENAME}`;
@@ -2434,7 +2490,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2434
2490
  if (!typeInfo) {
2435
2491
  continue;
2436
2492
  }
2437
- const coerce = /(?:Query|Params)$/.test(schemaName);
2493
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
2438
2494
  const block = [`// ${schemaName}`];
2439
2495
  const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
2440
2496
  /^import \{ z \} from 'zod';\s*\n\s*\n/,
@@ -2446,13 +2502,15 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
2446
2502
  }
2447
2503
  const allSchemaCode = schemaBlocks.join("\n");
2448
2504
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
2449
- lines.push(`import { coerceNumber, coerceBoolean } from '${helpersImportPath}';`);
2505
+ lines.push(
2506
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
2507
+ );
2450
2508
  }
2451
2509
  lines.push("");
2452
2510
  lines.push(...schemaBlocks);
2453
2511
  return lines.join("\n").replace(/\n+$/, "\n");
2454
2512
  }
2455
- async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
2513
+ async function generateSchemaFiles(routes, rootDir, dist) {
2456
2514
  if (routes.length === 0) return;
2457
2515
  const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
2458
2516
  const sourcesByFile = /* @__PURE__ */ new Map();
@@ -2467,11 +2525,11 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
2467
2525
  const fileEntries = [];
2468
2526
  for (const [filePath, fileSources] of sourcesByFile) {
2469
2527
  const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
2470
- const outputPath = getSchemaOutputPath(relFile, appDir, outDir, rootDir);
2528
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
2471
2529
  const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
2472
2530
  let relForDir = relFile;
2473
- if (appDir !== "." && relForDir.startsWith(`${appDir}/`)) {
2474
- relForDir = relForDir.slice(appDir.length + 1);
2531
+ if (relForDir.startsWith("src/")) {
2532
+ relForDir = relForDir.slice(4);
2475
2533
  }
2476
2534
  const dirIdx = relForDir.lastIndexOf("/");
2477
2535
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
@@ -2481,7 +2539,7 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
2481
2539
  }
2482
2540
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2483
2541
  if (usesCoerceHelpers(allSourceCode)) {
2484
- const helpersPath = path4.resolve(rootDir, outDir, HELPERS_FILENAME);
2542
+ const helpersPath = path4.resolve(rootDir, dist, HELPERS_FILENAME);
2485
2543
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
2486
2544
  }
2487
2545
  await Promise.all(
@@ -2558,8 +2616,7 @@ function createServer(options) {
2558
2616
  const {
2559
2617
  routes,
2560
2618
  rootDir,
2561
- appDir,
2562
- outDir,
2619
+ dist,
2563
2620
  cors: corsOption,
2564
2621
  onError,
2565
2622
  config,
@@ -2567,6 +2624,7 @@ function createServer(options) {
2567
2624
  middlewares: globalMiddlewares,
2568
2625
  injectors: globalInjectors,
2569
2626
  helmet: helmetOption,
2627
+ logger: loggerOption,
2570
2628
  bodyLimit = DEFAULT_BODY_LIMIT,
2571
2629
  http2: http2Option
2572
2630
  } = options;
@@ -2578,6 +2636,8 @@ function createServer(options) {
2578
2636
  const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
2579
2637
  configMiddlewares.push(helmet(helmOpts));
2580
2638
  }
2639
+ const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
2640
+ if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
2581
2641
  const server = (() => {
2582
2642
  if (http2Option) {
2583
2643
  const h2Opts = typeof http2Option === "object" ? http2Option : {};
@@ -2594,8 +2654,7 @@ function createServer(options) {
2594
2654
  handleRequest(
2595
2655
  currentRoutes,
2596
2656
  rootDir,
2597
- appDir,
2598
- outDir,
2657
+ dist,
2599
2658
  req,
2600
2659
  res,
2601
2660
  configMiddlewares,
@@ -2614,7 +2673,7 @@ function createServer(options) {
2614
2673
  }
2615
2674
  return { server, routesRef };
2616
2675
  }
2617
- async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
2676
+ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
2618
2677
  const request = toWebRequest(req, bodyLimit);
2619
2678
  const method = request.method.toUpperCase();
2620
2679
  const urlPath = new URL(request.url).pathname;
@@ -2635,7 +2694,7 @@ async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMi
2635
2694
  const routeModule = await loadRouteModule(absoluteFilePath, route.method);
2636
2695
  const input = await resolveInput(route.method, request);
2637
2696
  const inputType = getInputTypeForMethod(route.method);
2638
- const schemaPath = getRuntimeSchemaPath(route.filePath, appDir, outDir, rootDir);
2697
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
2639
2698
  const result = await validateInput(schemaPath, route.method, inputType, input);
2640
2699
  if (!result.valid) {
2641
2700
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
@@ -2862,10 +2921,10 @@ function resolveDeclaration(decl) {
2862
2921
  }
2863
2922
 
2864
2923
  // src/cli/createAppCore.ts
2865
- var DEFAULT_OUT_DIR = "dist";
2866
- var DEFAULT_APP_DIR = "src";
2924
+ var DEFAULT_DIST = "dist";
2867
2925
  var DEFAULT_PORT = 3e3;
2868
2926
  var ROUTES_FILE = "faapi-routes.js";
2927
+ var PATTERNS = ["src/api/**/*.ts"];
2869
2928
  var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
2870
2929
  "cors",
2871
2930
  "lifecycle",
@@ -2883,16 +2942,14 @@ function isFaapiConfigKey(key) {
2883
2942
  }
2884
2943
  async function createAppBase(options) {
2885
2944
  const rootDir = options?.rootDir ?? process.cwd();
2886
- const outDir = process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
2887
- const routesPath = path7.resolve(rootDir, outDir, ROUTES_FILE);
2945
+ const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
2946
+ const routesPath = path7.resolve(rootDir, dist, ROUTES_FILE);
2888
2947
  if (!fs4.existsSync(routesPath)) {
2889
2948
  throw new Error(
2890
- `[faapi] ${outDir}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
2949
+ `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
2891
2950
  );
2892
2951
  }
2893
- const config = await loadConfig(rootDir, outDir);
2894
- const appDir = options?.appDir ?? process.env.FAAPI_APP_DIR ?? DEFAULT_APP_DIR;
2895
- const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
2952
+ const config = await loadConfig(rootDir, dist);
2896
2953
  const serialized = await importWithCacheBust(routesPath);
2897
2954
  const hydrated = await hydrateRoutes(serialized);
2898
2955
  let sorted = sortRoutes(hydrated.routes);
@@ -2910,8 +2967,7 @@ async function createAppBase(options) {
2910
2967
  const { server, routesRef } = createServer({
2911
2968
  routes: sorted,
2912
2969
  rootDir,
2913
- appDir,
2914
- outDir,
2970
+ dist,
2915
2971
  cors: config?.cors ?? true,
2916
2972
  onError: config?.lifecycle?.onError,
2917
2973
  config: config ?? void 0,
@@ -2919,6 +2975,7 @@ async function createAppBase(options) {
2919
2975
  middlewares: config?.middlewares,
2920
2976
  injectors: config?.injectors,
2921
2977
  helmet: config?.helmet,
2978
+ logger: config?.logger,
2922
2979
  bodyLimit: config?.bodyLimit,
2923
2980
  http2: config?.http2
2924
2981
  });
@@ -3071,9 +3128,8 @@ async function createAppBase(options) {
3071
3128
  };
3072
3129
  const ctx = {
3073
3130
  rootDir,
3074
- appDir,
3075
- outDir,
3076
- patterns,
3131
+ dist,
3132
+ patterns: PATTERNS,
3077
3133
  server,
3078
3134
  routesRef,
3079
3135
  config,
@@ -3139,8 +3195,8 @@ function isCatchAllSegment(segment) {
3139
3195
  function isRouteGroup(segment) {
3140
3196
  return /^\(.+\)$/.test(segment);
3141
3197
  }
3142
- function filePathToUrlPath(filePath, appDir = ".") {
3143
- const withoutPrefix = filePath.startsWith(appDir + "/") ? filePath.slice(appDir.length + 1) : filePath;
3198
+ function filePathToUrlPath(filePath) {
3199
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
3144
3200
  const lastSlashIndex = withoutPrefix.lastIndexOf("/");
3145
3201
  const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
3146
3202
  if (!dirPath) {
@@ -3151,24 +3207,25 @@ function filePathToUrlPath(filePath, appDir = ".") {
3151
3207
  }
3152
3208
 
3153
3209
  // src/router/scanRoutes.ts
3154
- function toProdAbsPath(sourceAbsPath, rootDir, appDir, prodDir) {
3210
+ var APP_DIR = "src";
3211
+ function toProdAbsPath(sourceAbsPath, rootDir, dist) {
3155
3212
  let rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3156
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
3157
- rel = rel.slice(appDir.length + 1);
3213
+ if (rel.startsWith(`${APP_DIR}/`)) {
3214
+ rel = rel.slice(APP_DIR.length + 1);
3158
3215
  }
3159
- const prodRel = `${prodDir}/${rel.replace(/\.ts$/, ".js")}`;
3216
+ const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
3160
3217
  return path8.resolve(rootDir, prodRel);
3161
3218
  }
3162
- async function findMergedMiddlewares(routeFilePath, rootDir, appDir, prodDir) {
3219
+ async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
3163
3220
  const routeDir = path8.dirname(routeFilePath);
3164
3221
  const resolvedRoot = path8.resolve(rootDir);
3165
3222
  const mwPaths = [];
3166
3223
  let currentDir = path8.resolve(rootDir, routeDir);
3167
3224
  while (true) {
3168
- if (prodDir) {
3225
+ if (dist) {
3169
3226
  const mwPath = path8.join(currentDir, "middlewares.js");
3170
3227
  const absMwPath = path8.resolve(rootDir, mwPath);
3171
- const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, appDir, prodDir);
3228
+ const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
3172
3229
  if (fs5.existsSync(prodAbsMwPath)) {
3173
3230
  mwPaths.push(prodAbsMwPath);
3174
3231
  }
@@ -3233,8 +3290,7 @@ async function hasWsExport(absPath) {
3233
3290
  return false;
3234
3291
  }
3235
3292
  }
3236
- async function scanRoutes(rootDir, patterns, appDir, prodDir) {
3237
- const dir = appDir ?? ".";
3293
+ async function scanRoutes(rootDir, patterns, dist) {
3238
3294
  const files = await fg(patterns, {
3239
3295
  cwd: rootDir,
3240
3296
  onlyFiles: true,
@@ -3247,12 +3303,12 @@ async function scanRoutes(rootDir, patterns, appDir, prodDir) {
3247
3303
  const fileName = normalizedFile.split("/").pop();
3248
3304
  if (fileName === "handler.ts" || fileName === "handler.js") {
3249
3305
  const absPath = path8.resolve(rootDir, normalizedFile);
3250
- const importPath = prodDir ? toProdAbsPath(absPath, rootDir, dir, prodDir) : absPath;
3251
- const urlPath = filePathToUrlPath(normalizedFile, dir);
3306
+ const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
3307
+ const urlPath = filePathToUrlPath(normalizedFile);
3252
3308
  const paramNames = extractParamNames(urlPath);
3253
3309
  const isDynamic = paramNames.length > 0;
3254
3310
  const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
3255
- const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dir, prodDir);
3311
+ const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
3256
3312
  const methods = await extractMethodsFromHandler(importPath);
3257
3313
  for (const method of methods) {
3258
3314
  routes.push({
@@ -3293,9 +3349,9 @@ async function createDevApp(options) {
3293
3349
  invalidateMiddlewareCache();
3294
3350
  invalidateProgramCache();
3295
3351
  invalidateSchemaCache();
3296
- const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.appDir, ctx.outDir);
3352
+ const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
3297
3353
  const sorted = sortRoutes(reScanned.routes);
3298
- await generateSchemaFiles(sorted, ctx.rootDir, ctx.appDir, ctx.outDir);
3354
+ await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
3299
3355
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
3300
3356
  };
3301
3357
  return devApp;