@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/cli/index.js CHANGED
@@ -78,17 +78,47 @@ function toProdImportPath(sourceFile, importer) {
78
78
  if (!rel.startsWith(".")) rel = "./" + rel;
79
79
  return toProdExtension(rel);
80
80
  }
81
- function createAliasPlugin(config) {
82
- const EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
83
- const INDEX_EXTS = [
84
- "/index.ts",
85
- "/index.tsx",
86
- "/index.js",
87
- "/index.jsx",
88
- "/index.mjs",
89
- "/index.cjs"
90
- ];
91
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
81
+ function toRealPath(p) {
82
+ try {
83
+ return fs2.realpathSync(p);
84
+ } catch {
85
+ return p;
86
+ }
87
+ }
88
+ function isInsideDir(filePath, dir) {
89
+ const rel = path2.relative(dir, filePath);
90
+ return rel !== "" && !rel.startsWith("..") && !path2.isAbsolute(rel);
91
+ }
92
+ function toStrippedProdImportPath(sourceFile, rootDir) {
93
+ const appDirAbs = toRealPath(path2.resolve(rootDir, APP_DIR));
94
+ const sourceReal = toRealPath(sourceFile);
95
+ let rel = path2.relative(appDirAbs, sourceReal);
96
+ rel = rel.split(path2.sep).join("/");
97
+ if (!rel.startsWith(".")) rel = "./" + rel;
98
+ return toProdExtension(rel);
99
+ }
100
+ function resolveRelativeSpecifier(importer, specifier) {
101
+ const importerDir = path2.dirname(importer);
102
+ const base = path2.resolve(importerDir, specifier);
103
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
104
+ return fs2.existsSync(base) ? base : null;
105
+ }
106
+ if (/\.(ts|tsx|jsx)$/.test(specifier)) {
107
+ return fs2.existsSync(base) ? base : null;
108
+ }
109
+ for (const ext of SOURCE_EXTS) {
110
+ const file = base + ext;
111
+ if (fs2.existsSync(file)) return file;
112
+ }
113
+ for (const indexExt of INDEX_EXTS) {
114
+ const file = base + indexExt;
115
+ if (fs2.existsSync(file)) return file;
116
+ }
117
+ return null;
118
+ }
119
+ function createAliasPlugin(config, options) {
120
+ const SPEC_RE2 = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
121
+ const appDirAbs = options?.rootDir ? toRealPath(path2.resolve(options.rootDir, APP_DIR)) : null;
92
122
  return {
93
123
  name: "faapi-alias",
94
124
  setup(build) {
@@ -100,17 +130,42 @@ function createAliasPlugin(config) {
100
130
  return void 0;
101
131
  }
102
132
  const importer = args.path;
133
+ const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
103
134
  let modified = false;
104
- const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
105
- if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
135
+ const newSource = source.replace(SPEC_RE2, (full, prefix, quote, specifier) => {
136
+ if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
137
+ return full;
138
+ }
139
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
140
+ const resolved = resolveRelativeSpecifier(importer, specifier);
141
+ if (resolved) {
142
+ if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
143
+ return full;
144
+ }
145
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
146
+ modified = true;
147
+ return `${prefix}${quote}${toStrippedProdImportPath(
148
+ resolved,
149
+ options.rootDir
150
+ )}${quote}`;
151
+ }
152
+ modified = true;
153
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
154
+ }
106
155
  return full;
107
156
  }
108
157
  const candidates = resolveAlias(specifier, config);
109
158
  for (const candidate of candidates) {
110
- for (const ext of EXTS) {
159
+ for (const ext of SOURCE_EXTS) {
111
160
  const file = candidate + ext;
112
161
  if (fs2.existsSync(file)) {
113
162
  modified = true;
163
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
164
+ return `${prefix}${quote}${toStrippedProdImportPath(
165
+ file,
166
+ options.rootDir
167
+ )}${quote}`;
168
+ }
114
169
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
115
170
  }
116
171
  }
@@ -118,6 +173,12 @@ function createAliasPlugin(config) {
118
173
  const file = candidate + indexExt;
119
174
  if (fs2.existsSync(file)) {
120
175
  modified = true;
176
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
177
+ return `${prefix}${quote}${toStrippedProdImportPath(
178
+ file,
179
+ options.rootDir
180
+ )}${quote}`;
181
+ }
121
182
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
122
183
  }
123
184
  }
@@ -132,13 +193,25 @@ function createAliasPlugin(config) {
132
193
  }
133
194
  function buildAliasPlugins(rootDir) {
134
195
  const tsconfig = readTsconfig(rootDir);
135
- return tsconfig ? [createAliasPlugin(tsconfig)] : [];
196
+ return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
136
197
  }
198
+ var APP_DIR, PROD_EXTS, SOURCE_EXTS, INDEX_EXTS;
137
199
  var init_aliasPlugin = __esm({
138
200
  "src/cli/aliasPlugin.ts"() {
139
201
  "use strict";
140
202
  init_resolveAlias();
141
203
  init_readTsconfig();
204
+ APP_DIR = "src";
205
+ PROD_EXTS = [".js", ".mjs", ".cjs"];
206
+ SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
207
+ INDEX_EXTS = [
208
+ "/index.ts",
209
+ "/index.tsx",
210
+ "/index.js",
211
+ "/index.jsx",
212
+ "/index.mjs",
213
+ "/index.cjs"
214
+ ];
142
215
  }
143
216
  });
144
217
 
@@ -147,8 +220,8 @@ import path3 from "path";
147
220
  import fs3 from "fs";
148
221
  import fg from "fast-glob";
149
222
  async function compileDevRoutes(options) {
150
- const { rootDir, appDir, outDir, files, logLevel = "silent" } = options;
151
- const entryPoints = files ?? await fg([`${appDir}/**/*.ts`], {
223
+ const { rootDir, dist, files, logLevel = "silent" } = options;
224
+ const entryPoints = files ?? await fg([`${APP_DIR2}/**/*.ts`], {
152
225
  cwd: rootDir,
153
226
  onlyFiles: true,
154
227
  absolute: true,
@@ -157,14 +230,14 @@ async function compileDevRoutes(options) {
157
230
  if (entryPoints.length === 0) {
158
231
  return { compiledFiles: [] };
159
232
  }
160
- const absOutDir = path3.resolve(rootDir, outDir);
161
- await fs3.promises.mkdir(absOutDir, { recursive: true });
233
+ const absDist = path3.resolve(rootDir, dist);
234
+ await fs3.promises.mkdir(absDist, { recursive: true });
162
235
  const plugins = buildAliasPlugins(rootDir);
163
236
  const esbuild = await import("esbuild");
164
- const outbase = appDir === "." ? rootDir : path3.resolve(rootDir, appDir);
237
+ const outbase = path3.resolve(rootDir, APP_DIR2);
165
238
  await esbuild.build({
166
239
  entryPoints,
167
- outdir: absOutDir,
240
+ outdir: absDist,
168
241
  outbase,
169
242
  bundle: false,
170
243
  platform: "node",
@@ -176,10 +249,12 @@ async function compileDevRoutes(options) {
176
249
  });
177
250
  return { compiledFiles: entryPoints };
178
251
  }
252
+ var APP_DIR2;
179
253
  var init_compileDevRoutes = __esm({
180
254
  "src/cli/compileDevRoutes.ts"() {
181
255
  "use strict";
182
256
  init_aliasPlugin();
257
+ APP_DIR2 = "src";
183
258
  }
184
259
  });
185
260
 
@@ -215,13 +290,24 @@ var init_deepMerge = __esm({
215
290
  // src/cli/compileConfig.ts
216
291
  import path4 from "path";
217
292
  import fs4 from "fs";
293
+ function toRealPath2(p) {
294
+ try {
295
+ return fs4.realpathSync(p);
296
+ } catch {
297
+ return p;
298
+ }
299
+ }
300
+ function isInsideDir2(filePath, dir) {
301
+ const rel = path4.relative(dir, filePath);
302
+ return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
303
+ }
218
304
  function getEnv() {
219
305
  return process.env.FAAPI_ENV || process.env.NODE_ENV || "development";
220
306
  }
221
307
  function findBaseConfig(rootDir) {
222
308
  for (const f of BASE_CONFIG_FILES) {
223
309
  if (fs4.existsSync(path4.join(rootDir, f))) {
224
- return f.replace(/\.(ts|js)$/, "");
310
+ return f;
225
311
  }
226
312
  }
227
313
  return null;
@@ -230,33 +316,137 @@ function findEnvConfig(rootDir, env) {
230
316
  for (const ext of ENV_CONFIG_EXTS) {
231
317
  const f = `faapi.config.${env}${ext}`;
232
318
  if (fs4.existsSync(path4.join(rootDir, f))) {
233
- return `faapi.config.${env}`;
319
+ return f;
234
320
  }
235
321
  }
236
322
  return null;
237
323
  }
324
+ function toProdExtension2(filePath) {
325
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
326
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
327
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
328
+ return filePath;
329
+ }
330
+ function toProdImport(filename) {
331
+ return toProdExtension2(filename);
332
+ }
333
+ function extractRelativeSpecifiers(source) {
334
+ const specifiers = [];
335
+ let match;
336
+ SPEC_RE.lastIndex = 0;
337
+ while ((match = SPEC_RE.exec(source)) !== null) {
338
+ const specifier = match[3];
339
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
340
+ specifiers.push(specifier);
341
+ }
342
+ }
343
+ return specifiers;
344
+ }
345
+ async function collectRelativeImports(entryFiles, rootDir) {
346
+ const appDirAbs = toRealPath2(path4.resolve(rootDir, "src"));
347
+ const visited = /* @__PURE__ */ new Set();
348
+ const appDirFiles = /* @__PURE__ */ new Set();
349
+ const nonAppDirFiles = /* @__PURE__ */ new Set();
350
+ async function collect(filePath) {
351
+ if (visited.has(filePath)) return;
352
+ visited.add(filePath);
353
+ let source;
354
+ try {
355
+ source = await fs4.promises.readFile(filePath, "utf8");
356
+ } catch {
357
+ return;
358
+ }
359
+ const specifiers = extractRelativeSpecifiers(source);
360
+ for (const specifier of specifiers) {
361
+ const resolved = resolveRelativeSpecifier(filePath, specifier);
362
+ if (!resolved) continue;
363
+ if (/\.(js|mjs|cjs)$/.test(specifier)) continue;
364
+ if (isInsideDir2(resolved, appDirAbs)) {
365
+ appDirFiles.add(resolved);
366
+ } else {
367
+ nonAppDirFiles.add(resolved);
368
+ }
369
+ await collect(resolved);
370
+ }
371
+ }
372
+ for (const entry of entryFiles) {
373
+ await collect(entry);
374
+ }
375
+ return {
376
+ appDirFiles: Array.from(appDirFiles),
377
+ nonAppDirFiles: Array.from(nonAppDirFiles)
378
+ };
379
+ }
380
+ function createExternalRelativePlugin() {
381
+ return {
382
+ name: "faapi-external-relative",
383
+ setup(build) {
384
+ build.onResolve({ filter: /^\.{1,2}\// }, (args) => ({
385
+ path: args.path,
386
+ external: true
387
+ }));
388
+ }
389
+ };
390
+ }
238
391
  async function compileConfig(options) {
239
- const { rootDir, outDir } = options;
240
- const baseConfig = findBaseConfig(rootDir);
241
- if (!baseConfig) {
392
+ const { rootDir, dist } = options;
393
+ const baseConfigName = findBaseConfig(rootDir);
394
+ if (!baseConfigName) {
242
395
  return { generated: false, outputFile: "" };
243
396
  }
244
397
  const env = getEnv();
245
- const envConfig = findEnvConfig(rootDir, env);
246
- const imports = [`import base from './${baseConfig}';`];
398
+ const envConfigName = findEnvConfig(rootDir, env);
399
+ const absDist = path4.resolve(rootDir, dist);
400
+ await fs4.promises.mkdir(absDist, { recursive: true });
401
+ const configEntryPoints = [path4.resolve(rootDir, baseConfigName)];
402
+ if (envConfigName) {
403
+ configEntryPoints.push(path4.resolve(rootDir, envConfigName));
404
+ }
405
+ const { appDirFiles, nonAppDirFiles } = await collectRelativeImports(configEntryPoints, rootDir);
406
+ const esbuild = await import("esbuild");
407
+ const aliasPlugins = buildAliasPlugins(rootDir);
408
+ const step1aEntries = [...configEntryPoints, ...nonAppDirFiles];
409
+ await esbuild.build({
410
+ entryPoints: step1aEntries,
411
+ outdir: absDist,
412
+ outbase: rootDir,
413
+ bundle: false,
414
+ platform: "node",
415
+ format: "esm",
416
+ sourcemap: true,
417
+ packages: "external",
418
+ plugins: aliasPlugins,
419
+ logLevel: "silent"
420
+ });
421
+ if (appDirFiles.length > 0) {
422
+ const appOutbase = path4.resolve(rootDir, "src");
423
+ await esbuild.build({
424
+ entryPoints: appDirFiles,
425
+ outdir: absDist,
426
+ outbase: appOutbase,
427
+ bundle: false,
428
+ platform: "node",
429
+ format: "esm",
430
+ sourcemap: true,
431
+ packages: "external",
432
+ plugins: aliasPlugins,
433
+ logLevel: "silent"
434
+ });
435
+ }
436
+ const baseImport = `import base from './${toProdImport(baseConfigName)}';`;
437
+ const imports = [baseImport];
247
438
  let exportDefault;
248
- if (envConfig) {
249
- imports.push(`import env from './${envConfig}';`);
439
+ if (envConfigName) {
440
+ const envImport = `import env from './${toProdImport(envConfigName)}';`;
441
+ imports.push(envImport);
250
442
  exportDefault = "export default deepMerge(base, env);";
251
443
  } else {
252
444
  exportDefault = "export default base;";
253
445
  }
254
446
  const entryCode = [...imports, DEEP_MERGE_SOURCE, exportDefault].join("\n");
255
- const outputFile = path4.resolve(rootDir, outDir, "faapi-config.js");
256
- await fs4.promises.mkdir(path4.dirname(outputFile), { recursive: true });
257
- const esbuild = await import("esbuild");
447
+ const outputFile = path4.resolve(absDist, "faapi-config.js");
258
448
  await esbuild.build({
259
- stdin: { contents: entryCode, resolveDir: rootDir, loader: "ts" },
449
+ stdin: { contents: entryCode, resolveDir: absDist, loader: "ts" },
260
450
  outfile: outputFile,
261
451
  bundle: true,
262
452
  format: "esm",
@@ -264,17 +454,20 @@ async function compileConfig(options) {
264
454
  target: "node20",
265
455
  sourcemap: true,
266
456
  packages: "external",
457
+ plugins: [createExternalRelativePlugin()],
267
458
  logLevel: "silent"
268
459
  });
269
460
  return { generated: true, outputFile };
270
461
  }
271
- var BASE_CONFIG_FILES, ENV_CONFIG_EXTS;
462
+ var BASE_CONFIG_FILES, ENV_CONFIG_EXTS, SPEC_RE;
272
463
  var init_compileConfig = __esm({
273
464
  "src/cli/compileConfig.ts"() {
274
465
  "use strict";
275
466
  init_deepMerge();
467
+ init_aliasPlugin();
276
468
  BASE_CONFIG_FILES = ["faapi.config.ts", "faapi.config.js"];
277
469
  ENV_CONFIG_EXTS = [".ts", ".js"];
470
+ SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
278
471
  }
279
472
  });
280
473
 
@@ -563,10 +756,35 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
563
756
  const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
564
757
  return { kind: "object", properties };
565
758
  }
566
- if (typeName === "Map" || typeName === "Set" || typeName === "WeakMap" || typeName === "WeakSet") {
759
+ if (typeName === "Map") {
760
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
761
+ throw new SchemaExtractionError(
762
+ typeNode.getText(),
763
+ "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
764
+ );
765
+ }
766
+ return {
767
+ kind: "map",
768
+ key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
769
+ value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
770
+ };
771
+ }
772
+ if (typeName === "Set") {
773
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
774
+ throw new SchemaExtractionError(
775
+ typeNode.getText(),
776
+ "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
777
+ );
778
+ }
779
+ return {
780
+ kind: "set",
781
+ element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
782
+ };
783
+ }
784
+ if (typeName === "WeakMap" || typeName === "WeakSet") {
567
785
  throw new SchemaExtractionError(
568
786
  typeNode.getText(),
569
- `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528\u5BF9\u8C61\u6216\u6570\u7EC4`
787
+ `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
570
788
  );
571
789
  }
572
790
  if (typeName === "Promise") {
@@ -1025,6 +1243,7 @@ var init_resolveInjection = __esm({
1025
1243
  PARAM_TYPE_MAP = {
1026
1244
  query: "query",
1027
1245
  body: "body",
1246
+ form: "form",
1028
1247
  headers: "headers",
1029
1248
  params: "params",
1030
1249
  context: "context",
@@ -1123,9 +1342,16 @@ function collectRouteSchemaSources(routes, rootDir) {
1123
1342
  const inputType = getInputTypeForMethod(method);
1124
1343
  const schemaName = getSchemaName(method, inputType);
1125
1344
  const meta = analyzeInjection(code, method);
1126
- const param = meta.params.find((p) => p.type === inputType);
1345
+ const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1346
+ const isForm = param?.type === "form";
1127
1347
  const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
1128
- sources.push({ urlPath: entry.urlPath, filePath, schemaName, typeInfo });
1348
+ sources.push({
1349
+ urlPath: entry.urlPath,
1350
+ filePath,
1351
+ schemaName,
1352
+ typeInfo,
1353
+ coerce: isForm || void 0
1354
+ });
1129
1355
  }
1130
1356
  }
1131
1357
  return { sources, allTypesByFile, mergedAllTypes };
@@ -1177,6 +1403,13 @@ function collectNamedTypes(type, ctx) {
1177
1403
  collectNamedTypes(type.key, ctx);
1178
1404
  collectNamedTypes(type.value, ctx);
1179
1405
  return;
1406
+ case "map":
1407
+ collectNamedTypes(type.key, ctx);
1408
+ collectNamedTypes(type.value, ctx);
1409
+ return;
1410
+ case "set":
1411
+ collectNamedTypes(type.element, ctx);
1412
+ return;
1180
1413
  case "ref": {
1181
1414
  if (ctx.namedTypes.has(type.name)) return;
1182
1415
  ctx.namedTypes.set(type.name, { kind: "any" });
@@ -1266,6 +1499,10 @@ function baseExpression(type, ctx) {
1266
1499
  return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
1267
1500
  case "record":
1268
1501
  return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
1502
+ case "map":
1503
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
1504
+ case "set":
1505
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
1269
1506
  case "ref":
1270
1507
  if (type.name === ctx.entryTypeName) {
1271
1508
  return `${ctx.entryExportName}Schema`;
@@ -1278,11 +1515,13 @@ function generateHelpersFileSource() {
1278
1515
  "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
1279
1516
  COERCE_NUMBER_HELPER,
1280
1517
  COERCE_BOOLEAN_HELPER,
1518
+ COERCE_MAP_HELPER,
1519
+ COERCE_SET_HELPER,
1281
1520
  ""
1282
1521
  ].join("\n");
1283
1522
  }
1284
1523
  function usesCoerceHelpers(code) {
1285
- return code.includes("coerceNumber") || code.includes("coerceBoolean");
1524
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
1286
1525
  }
1287
1526
  function wrapCoercePreprocess(kind, inner) {
1288
1527
  if (kind === "number") {
@@ -1369,6 +1608,10 @@ function containsRef(type, visited) {
1369
1608
  return type.members.some((m) => containsRef(m, visited));
1370
1609
  case "record":
1371
1610
  return containsRef(type.key, visited) || containsRef(type.value, visited);
1611
+ case "map":
1612
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1613
+ case "set":
1614
+ return containsRef(type.element, visited);
1372
1615
  default:
1373
1616
  return false;
1374
1617
  }
@@ -1397,7 +1640,7 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
1397
1640
  }
1398
1641
  return lines.join("\n");
1399
1642
  }
1400
- var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, HELPERS_FILENAME;
1643
+ var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, COERCE_MAP_HELPER, COERCE_SET_HELPER, HELPERS_FILENAME;
1401
1644
  var init_generateZodSchema = __esm({
1402
1645
  "src/ast/generateZodSchema.ts"() {
1403
1646
  "use strict";
@@ -1423,6 +1666,8 @@ var init_generateZodSchema = __esm({
1423
1666
  };
1424
1667
  COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
1425
1668
  COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
1669
+ 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);';
1670
+ COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
1426
1671
  HELPERS_FILENAME = "faapi-helpers.js";
1427
1672
  }
1428
1673
  });
@@ -1430,25 +1675,25 @@ var init_generateZodSchema = __esm({
1430
1675
  // src/cli/generateSchemaFiles.ts
1431
1676
  import path6 from "path";
1432
1677
  import fs5 from "fs/promises";
1433
- function getSchemaOutputPath(sourceFile, appDir, outDir, rootDir) {
1678
+ function getSchemaOutputPath(sourceFile, dist, rootDir) {
1434
1679
  let rel = sourceFile.replace(/\\/g, "/");
1435
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
1436
- rel = rel.slice(appDir.length + 1);
1680
+ if (rel.startsWith("src/")) {
1681
+ rel = rel.slice(4);
1437
1682
  }
1438
1683
  const idx = rel.lastIndexOf("/");
1439
1684
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1440
- return path6.resolve(rootDir, outDir, relDir, "zod.js");
1685
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
1441
1686
  }
1442
- function getRuntimeSchemaPath(filePath, appDir, outDir, rootDir) {
1687
+ function getRuntimeSchemaPath(filePath, dist, rootDir) {
1443
1688
  let rel = filePath.replace(/\\/g, "/");
1444
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
1445
- rel = rel.slice(appDir.length + 1);
1446
- } else if (rel.startsWith(`${outDir}/`)) {
1447
- rel = rel.slice(outDir.length + 1);
1689
+ if (rel.startsWith("src/")) {
1690
+ rel = rel.slice(4);
1691
+ } else if (rel.startsWith(`${dist}/`)) {
1692
+ rel = rel.slice(dist.length + 1);
1448
1693
  }
1449
1694
  const idx = rel.lastIndexOf("/");
1450
1695
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1451
- return path6.resolve(rootDir, outDir, relDir, "zod.js");
1696
+ return path6.resolve(rootDir, dist, relDir, "zod.js");
1452
1697
  }
1453
1698
  function getHelpersImportPath(relDir) {
1454
1699
  if (!relDir) return `./${HELPERS_FILENAME}`;
@@ -1464,7 +1709,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1464
1709
  if (!typeInfo) {
1465
1710
  continue;
1466
1711
  }
1467
- const coerce = /(?:Query|Params)$/.test(schemaName);
1712
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
1468
1713
  const block = [`// ${schemaName}`];
1469
1714
  const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1470
1715
  /^import \{ z \} from 'zod';\s*\n\s*\n/,
@@ -1476,13 +1721,15 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1476
1721
  }
1477
1722
  const allSchemaCode = schemaBlocks.join("\n");
1478
1723
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
1479
- lines.push(`import { coerceNumber, coerceBoolean } from '${helpersImportPath}';`);
1724
+ lines.push(
1725
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
1726
+ );
1480
1727
  }
1481
1728
  lines.push("");
1482
1729
  lines.push(...schemaBlocks);
1483
1730
  return lines.join("\n").replace(/\n+$/, "\n");
1484
1731
  }
1485
- async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
1732
+ async function generateSchemaFiles(routes, rootDir, dist) {
1486
1733
  if (routes.length === 0) return;
1487
1734
  const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
1488
1735
  const sourcesByFile = /* @__PURE__ */ new Map();
@@ -1497,11 +1744,11 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
1497
1744
  const fileEntries = [];
1498
1745
  for (const [filePath, fileSources] of sourcesByFile) {
1499
1746
  const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
1500
- const outputPath = getSchemaOutputPath(relFile, appDir, outDir, rootDir);
1747
+ const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
1501
1748
  const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
1502
1749
  let relForDir = relFile;
1503
- if (appDir !== "." && relForDir.startsWith(`${appDir}/`)) {
1504
- relForDir = relForDir.slice(appDir.length + 1);
1750
+ if (relForDir.startsWith("src/")) {
1751
+ relForDir = relForDir.slice(4);
1505
1752
  }
1506
1753
  const dirIdx = relForDir.lastIndexOf("/");
1507
1754
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
@@ -1511,7 +1758,7 @@ async function generateSchemaFiles(routes, rootDir, appDir, outDir) {
1511
1758
  }
1512
1759
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
1513
1760
  if (usesCoerceHelpers(allSourceCode)) {
1514
- const helpersPath = path6.resolve(rootDir, outDir, HELPERS_FILENAME);
1761
+ const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
1515
1762
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
1516
1763
  }
1517
1764
  await Promise.all(
@@ -1604,20 +1851,20 @@ var init_loadMiddlewares = __esm({
1604
1851
  // src/cli/generateRoutes.ts
1605
1852
  import fs6 from "fs";
1606
1853
  import path7 from "path";
1607
- function toProdFilePath(filePath, appDir, prodDir) {
1854
+ function toProdFilePath(filePath, dist) {
1608
1855
  let rel = filePath.replace(/\\/g, "/");
1609
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
1610
- rel = rel.slice(appDir.length + 1);
1856
+ if (rel.startsWith("src/")) {
1857
+ rel = rel.slice(4);
1611
1858
  }
1612
1859
  const jsPath = rel.replace(/\.ts$/, ".js");
1613
- return jsPath.startsWith(`${prodDir}/`) ? jsPath : `${prodDir}/${jsPath}`;
1860
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
1614
1861
  }
1615
- function serializeRoutes(routes, wsRoutes, rootDir, appDir = "src", prodDir = "dist") {
1862
+ function serializeRoutes(routes, wsRoutes, rootDir, dist = "dist") {
1616
1863
  const serialize = (route) => {
1617
- const middlewarePaths = extractMiddlewarePaths(route.filePath, rootDir, appDir, prodDir);
1864
+ const middlewarePaths = extractMiddlewarePaths(route.filePath, rootDir, dist);
1618
1865
  const serialized = {
1619
1866
  urlPath: route.urlPath,
1620
- filePath: toProdFilePath(route.filePath, appDir, prodDir),
1867
+ filePath: toProdFilePath(route.filePath, dist),
1621
1868
  paramNames: route.paramNames,
1622
1869
  isDynamic: route.isDynamic,
1623
1870
  isCatchAll: route.isCatchAll,
@@ -1633,7 +1880,7 @@ function serializeRoutes(routes, wsRoutes, rootDir, appDir = "src", prodDir = "d
1633
1880
  wsRoutes: wsRoutes.map(serialize)
1634
1881
  };
1635
1882
  }
1636
- function extractMiddlewarePaths(routeFilePath, rootDir, appDir, prodDir) {
1883
+ function extractMiddlewarePaths(routeFilePath, rootDir, dist) {
1637
1884
  const routeDir = path7.dirname(routeFilePath);
1638
1885
  const resolvedRoot = path7.resolve(rootDir);
1639
1886
  const paths = [];
@@ -1646,7 +1893,7 @@ function extractMiddlewarePaths(routeFilePath, rootDir, appDir, prodDir) {
1646
1893
  const absMwPath = fs6.existsSync(absTsPath) ? absTsPath : fs6.existsSync(absJsPath) ? absJsPath : null;
1647
1894
  if (absMwPath) {
1648
1895
  const relMwPath = path7.relative(rootDir, absMwPath);
1649
- const prodAbsPath = path7.resolve(rootDir, toProdFilePath(relMwPath, appDir, prodDir));
1896
+ const prodAbsPath = path7.resolve(rootDir, toProdFilePath(relMwPath, dist));
1650
1897
  paths.push(prodAbsPath);
1651
1898
  }
1652
1899
  if (currentDir === resolvedRoot) break;
@@ -1776,8 +2023,8 @@ function isCatchAllSegment(segment) {
1776
2023
  function isRouteGroup(segment) {
1777
2024
  return /^\(.+\)$/.test(segment);
1778
2025
  }
1779
- function filePathToUrlPath(filePath, appDir = ".") {
1780
- const withoutPrefix = filePath.startsWith(appDir + "/") ? filePath.slice(appDir.length + 1) : filePath;
2026
+ function filePathToUrlPath(filePath) {
2027
+ const withoutPrefix = filePath.startsWith("src/") ? filePath.slice(4) : filePath;
1781
2028
  const lastSlashIndex = withoutPrefix.lastIndexOf("/");
1782
2029
  const dirPath = lastSlashIndex === -1 ? "" : withoutPrefix.slice(0, lastSlashIndex);
1783
2030
  if (!dirPath) {
@@ -1797,24 +2044,24 @@ var init_parseRouteFile = __esm({
1797
2044
  import fg2 from "fast-glob";
1798
2045
  import path8 from "path";
1799
2046
  import fs7 from "fs";
1800
- function toProdAbsPath(sourceAbsPath, rootDir, appDir, prodDir) {
2047
+ function toProdAbsPath(sourceAbsPath, rootDir, dist) {
1801
2048
  let rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
1802
- if (appDir !== "." && rel.startsWith(`${appDir}/`)) {
1803
- rel = rel.slice(appDir.length + 1);
2049
+ if (rel.startsWith(`${APP_DIR3}/`)) {
2050
+ rel = rel.slice(APP_DIR3.length + 1);
1804
2051
  }
1805
- const prodRel = `${prodDir}/${rel.replace(/\.ts$/, ".js")}`;
2052
+ const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
1806
2053
  return path8.resolve(rootDir, prodRel);
1807
2054
  }
1808
- async function findMergedMiddlewares(routeFilePath, rootDir, appDir, prodDir) {
2055
+ async function findMergedMiddlewares(routeFilePath, rootDir, dist) {
1809
2056
  const routeDir = path8.dirname(routeFilePath);
1810
2057
  const resolvedRoot = path8.resolve(rootDir);
1811
2058
  const mwPaths = [];
1812
2059
  let currentDir = path8.resolve(rootDir, routeDir);
1813
2060
  while (true) {
1814
- if (prodDir) {
2061
+ if (dist) {
1815
2062
  const mwPath = path8.join(currentDir, "middlewares.js");
1816
2063
  const absMwPath = path8.resolve(rootDir, mwPath);
1817
- const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, appDir, prodDir);
2064
+ const prodAbsMwPath = toProdAbsPath(absMwPath, rootDir, dist);
1818
2065
  if (fs7.existsSync(prodAbsMwPath)) {
1819
2066
  mwPaths.push(prodAbsMwPath);
1820
2067
  }
@@ -1879,8 +2126,7 @@ async function hasWsExport(absPath) {
1879
2126
  return false;
1880
2127
  }
1881
2128
  }
1882
- async function scanRoutes(rootDir, patterns, appDir, prodDir) {
1883
- const dir = appDir ?? ".";
2129
+ async function scanRoutes(rootDir, patterns, dist) {
1884
2130
  const files = await fg2(patterns, {
1885
2131
  cwd: rootDir,
1886
2132
  onlyFiles: true,
@@ -1893,12 +2139,12 @@ async function scanRoutes(rootDir, patterns, appDir, prodDir) {
1893
2139
  const fileName = normalizedFile.split("/").pop();
1894
2140
  if (fileName === "handler.ts" || fileName === "handler.js") {
1895
2141
  const absPath = path8.resolve(rootDir, normalizedFile);
1896
- const importPath = prodDir ? toProdAbsPath(absPath, rootDir, dir, prodDir) : absPath;
1897
- const urlPath = filePathToUrlPath(normalizedFile, dir);
2142
+ const importPath = dist ? toProdAbsPath(absPath, rootDir, dist) : absPath;
2143
+ const urlPath = filePathToUrlPath(normalizedFile);
1898
2144
  const paramNames = extractParamNames(urlPath);
1899
2145
  const isDynamic = paramNames.length > 0;
1900
2146
  const isCatchAll = normalizedFile.split("/").some(isCatchAllSegment);
1901
- const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dir, prodDir);
2147
+ const middlewareBundle = await findMergedMiddlewares(normalizedFile, rootDir, dist);
1902
2148
  const methods = await extractMethodsFromHandler(importPath);
1903
2149
  for (const method of methods) {
1904
2150
  routes.push({
@@ -1929,6 +2175,7 @@ async function scanRoutes(rootDir, patterns, appDir, prodDir) {
1929
2175
  }
1930
2176
  return { routes, wsRoutes };
1931
2177
  }
2178
+ var APP_DIR3;
1932
2179
  var init_scanRoutes = __esm({
1933
2180
  "src/router/scanRoutes.ts"() {
1934
2181
  "use strict";
@@ -1936,6 +2183,7 @@ var init_scanRoutes = __esm({
1936
2183
  init_parseRouteFile();
1937
2184
  init_loadMiddlewares();
1938
2185
  init_importWithCacheBust();
2186
+ APP_DIR3 = "src";
1939
2187
  }
1940
2188
  });
1941
2189
 
@@ -1965,8 +2213,8 @@ var init_sortRoutes = __esm({
1965
2213
  // src/config/loadConfig.ts
1966
2214
  import path9 from "path";
1967
2215
  import fs8 from "fs";
1968
- async function loadConfig(rootDir, outDir) {
1969
- const configProductPath = path9.resolve(rootDir, outDir, CONFIG_PRODUCT_FILE);
2216
+ async function loadConfig(rootDir, dist) {
2217
+ const configProductPath = path9.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
1970
2218
  if (fs8.existsSync(configProductPath)) {
1971
2219
  const module = await importWithCacheBust(configProductPath);
1972
2220
  return module.default ?? {};
@@ -1974,7 +2222,7 @@ async function loadConfig(rootDir, outDir) {
1974
2222
  const hasSourceConfig = fs8.existsSync(path9.join(rootDir, "faapi.config.ts")) || fs8.existsSync(path9.join(rootDir, "faapi.config.js"));
1975
2223
  if (hasSourceConfig) {
1976
2224
  throw new Error(
1977
- `[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`
2225
+ `[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`
1978
2226
  );
1979
2227
  }
1980
2228
  return null;
@@ -3700,7 +3948,7 @@ var init_esm2 = __esm({
3700
3948
  // src/cli/watcher.ts
3701
3949
  import path10 from "path";
3702
3950
  function startWatcher(options) {
3703
- const { rootDir, appDir, app } = options;
3951
+ const { rootDir, app, devDist } = options;
3704
3952
  let rebuildTimer = null;
3705
3953
  let pendingFiles = /* @__PURE__ */ new Set();
3706
3954
  async function rebuildRoutes() {
@@ -3710,12 +3958,11 @@ function startWatcher(options) {
3710
3958
  if (filesToCompile.length > 0) {
3711
3959
  await compileDevRoutes({
3712
3960
  rootDir,
3713
- appDir,
3714
- outDir: DEV_OUT_DIR,
3961
+ dist: devDist,
3715
3962
  files: filesToCompile
3716
3963
  });
3717
3964
  }
3718
- await compileConfig({ rootDir, outDir: DEV_OUT_DIR });
3965
+ await compileConfig({ rootDir, dist: devDist });
3719
3966
  await app.reloadRoutes();
3720
3967
  const recompiledCount = filesToCompile.length;
3721
3968
  console.log(
@@ -3738,12 +3985,12 @@ function startWatcher(options) {
3738
3985
  "faapi.config.production.ts",
3739
3986
  "faapi.config.production.js"
3740
3987
  ];
3741
- const watchPaths = [appDir, ...CONFIG_FILES];
3988
+ const watchPaths = ["src", ...CONFIG_FILES];
3742
3989
  const watcher = esm_default.watch(watchPaths, {
3743
3990
  cwd: rootDir,
3744
3991
  ignoreInitial: true,
3745
3992
  ignored: (filePath, stats) => {
3746
- if (filePath.includes("node_modules") || filePath.includes(".faapi") || filePath.includes("dist") || filePath.includes(".git")) {
3993
+ if (filePath.includes("node_modules") || filePath.includes(".faapi") || filePath.includes(devDist) || filePath.includes(".git")) {
3747
3994
  return true;
3748
3995
  }
3749
3996
  if (!stats) return false;
@@ -3773,14 +4020,12 @@ function startWatcher(options) {
3773
4020
  });
3774
4021
  console.log("- Watch mode enabled");
3775
4022
  }
3776
- var DEV_OUT_DIR;
3777
4023
  var init_watcher = __esm({
3778
4024
  "src/cli/watcher.ts"() {
3779
4025
  "use strict";
3780
4026
  init_esm2();
3781
4027
  init_compileDevRoutes();
3782
4028
  init_compileConfig();
3783
- DEV_OUT_DIR = ".faapi/dev";
3784
4029
  }
3785
4030
  });
3786
4031
 
@@ -4473,6 +4718,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
4473
4718
  return ctx.ip;
4474
4719
  case "body":
4475
4720
  return body;
4721
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
4722
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
4723
+ case "form":
4724
+ return body;
4476
4725
  case "files":
4477
4726
  if (body && typeof body === "object" && "files" in body) {
4478
4727
  return body.files;
@@ -4891,6 +5140,44 @@ var init_helmet = __esm({
4891
5140
  }
4892
5141
  });
4893
5142
 
5143
+ // src/middleware/logger.ts
5144
+ function logger(options = {}) {
5145
+ return async (ctx, next) => {
5146
+ const log = options.log ?? console.log;
5147
+ const start = Date.now();
5148
+ try {
5149
+ const response = await next();
5150
+ const duration = Date.now() - start;
5151
+ const entry = {
5152
+ method: ctx.method,
5153
+ path: ctx.path,
5154
+ status: response.status,
5155
+ durationMs: duration
5156
+ };
5157
+ log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
5158
+ return response;
5159
+ } catch (err) {
5160
+ const duration = Date.now() - start;
5161
+ const message = err instanceof Error ? err.message : String(err);
5162
+ const status = err?.statusCode ?? 500;
5163
+ const entry = {
5164
+ method: ctx.method,
5165
+ path: ctx.path,
5166
+ status,
5167
+ durationMs: duration,
5168
+ error: message
5169
+ };
5170
+ log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
5171
+ throw err;
5172
+ }
5173
+ };
5174
+ }
5175
+ var init_logger = __esm({
5176
+ "src/middleware/logger.ts"() {
5177
+ "use strict";
5178
+ }
5179
+ });
5180
+
4894
5181
  // src/errors/formatErrorResponse.ts
4895
5182
  function formatErrorResponse(error) {
4896
5183
  if (error instanceof ValidationError) {
@@ -5210,8 +5497,7 @@ function createServer(options) {
5210
5497
  const {
5211
5498
  routes,
5212
5499
  rootDir,
5213
- appDir,
5214
- outDir,
5500
+ dist,
5215
5501
  cors: corsOption,
5216
5502
  onError,
5217
5503
  config,
@@ -5219,6 +5505,7 @@ function createServer(options) {
5219
5505
  middlewares: globalMiddlewares,
5220
5506
  injectors: globalInjectors,
5221
5507
  helmet: helmetOption,
5508
+ logger: loggerOption,
5222
5509
  bodyLimit = DEFAULT_BODY_LIMIT,
5223
5510
  http2: http2Option
5224
5511
  } = options;
@@ -5230,6 +5517,8 @@ function createServer(options) {
5230
5517
  const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
5231
5518
  configMiddlewares.push(helmet(helmOpts));
5232
5519
  }
5520
+ const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
5521
+ if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
5233
5522
  const server = (() => {
5234
5523
  if (http2Option) {
5235
5524
  const h2Opts = typeof http2Option === "object" ? http2Option : {};
@@ -5246,8 +5535,7 @@ function createServer(options) {
5246
5535
  handleRequest(
5247
5536
  currentRoutes,
5248
5537
  rootDir,
5249
- appDir,
5250
- outDir,
5538
+ dist,
5251
5539
  req,
5252
5540
  res,
5253
5541
  configMiddlewares,
@@ -5266,7 +5554,7 @@ function createServer(options) {
5266
5554
  }
5267
5555
  return { server, routesRef };
5268
5556
  }
5269
- async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
5557
+ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
5270
5558
  const request = toWebRequest(req, bodyLimit);
5271
5559
  const method = request.method.toUpperCase();
5272
5560
  const urlPath = new URL(request.url).pathname;
@@ -5287,7 +5575,7 @@ async function handleRequest(routes, rootDir, appDir, outDir, req, res, configMi
5287
5575
  const routeModule = await loadRouteModule(absoluteFilePath, route.method);
5288
5576
  const input = await resolveInput(route.method, request);
5289
5577
  const inputType = getInputTypeForMethod(route.method);
5290
- const schemaPath = getRuntimeSchemaPath(route.filePath, appDir, outDir, rootDir);
5578
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
5291
5579
  const result = await validateInput(schemaPath, route.method, inputType, input);
5292
5580
  if (!result.valid) {
5293
5581
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
@@ -5343,6 +5631,7 @@ var init_createServer = __esm({
5343
5631
  init_getClientIp();
5344
5632
  init_cors();
5345
5633
  init_helmet();
5634
+ init_logger();
5346
5635
  init_handleWsUpgrade();
5347
5636
  init_serverUtils();
5348
5637
  init_generateSchemaFiles();
@@ -5456,16 +5745,14 @@ function isFaapiConfigKey(key) {
5456
5745
  }
5457
5746
  async function createAppBase(options) {
5458
5747
  const rootDir = options?.rootDir ?? process.cwd();
5459
- const outDir = process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
5460
- const routesPath = path13.resolve(rootDir, outDir, ROUTES_FILE);
5748
+ const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
5749
+ const routesPath = path13.resolve(rootDir, dist, ROUTES_FILE);
5461
5750
  if (!fs9.existsSync(routesPath)) {
5462
5751
  throw new Error(
5463
- `[faapi] ${outDir}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
5752
+ `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
5464
5753
  );
5465
5754
  }
5466
- const config = await loadConfig(rootDir, outDir);
5467
- const appDir = options?.appDir ?? process.env.FAAPI_APP_DIR ?? DEFAULT_APP_DIR;
5468
- const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
5755
+ const config = await loadConfig(rootDir, dist);
5469
5756
  const serialized = await importWithCacheBust(routesPath);
5470
5757
  const hydrated = await hydrateRoutes(serialized);
5471
5758
  let sorted = sortRoutes(hydrated.routes);
@@ -5483,8 +5770,7 @@ async function createAppBase(options) {
5483
5770
  const { server, routesRef } = createServer({
5484
5771
  routes: sorted,
5485
5772
  rootDir,
5486
- appDir,
5487
- outDir,
5773
+ dist,
5488
5774
  cors: config?.cors ?? true,
5489
5775
  onError: config?.lifecycle?.onError,
5490
5776
  config: config ?? void 0,
@@ -5492,6 +5778,7 @@ async function createAppBase(options) {
5492
5778
  middlewares: config?.middlewares,
5493
5779
  injectors: config?.injectors,
5494
5780
  helmet: config?.helmet,
5781
+ logger: config?.logger,
5495
5782
  bodyLimit: config?.bodyLimit,
5496
5783
  http2: config?.http2
5497
5784
  });
@@ -5644,9 +5931,8 @@ async function createAppBase(options) {
5644
5931
  };
5645
5932
  const ctx = {
5646
5933
  rootDir,
5647
- appDir,
5648
- outDir,
5649
- patterns,
5934
+ dist,
5935
+ patterns: PATTERNS,
5650
5936
  server,
5651
5937
  routesRef,
5652
5938
  config,
@@ -5661,7 +5947,7 @@ async function createAppBase(options) {
5661
5947
  };
5662
5948
  return { app, ctx };
5663
5949
  }
5664
- var DEFAULT_OUT_DIR, DEFAULT_APP_DIR, DEFAULT_PORT, ROUTES_FILE, FAAPI_CONFIG_KEYS;
5950
+ var DEFAULT_DIST, DEFAULT_PORT, ROUTES_FILE, PATTERNS, FAAPI_CONFIG_KEYS;
5665
5951
  var init_createAppCore = __esm({
5666
5952
  "src/cli/createAppCore.ts"() {
5667
5953
  "use strict";
@@ -5673,10 +5959,10 @@ var init_createAppCore = __esm({
5673
5959
  init_generateRoutes();
5674
5960
  init_loadPlugins();
5675
5961
  init_importWithCacheBust();
5676
- DEFAULT_OUT_DIR = "dist";
5677
- DEFAULT_APP_DIR = "src";
5962
+ DEFAULT_DIST = "dist";
5678
5963
  DEFAULT_PORT = 3e3;
5679
5964
  ROUTES_FILE = "faapi-routes.js";
5965
+ PATTERNS = ["src/api/**/*.ts"];
5680
5966
  FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
5681
5967
  "cors",
5682
5968
  "lifecycle",
@@ -5701,9 +5987,9 @@ async function createDevApp(options) {
5701
5987
  invalidateMiddlewareCache();
5702
5988
  invalidateProgramCache();
5703
5989
  invalidateSchemaCache();
5704
- const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.appDir, ctx.outDir);
5990
+ const reScanned = await scanRoutes(ctx.rootDir, ctx.patterns, ctx.dist);
5705
5991
  const sorted = sortRoutes(reScanned.routes);
5706
- await generateSchemaFiles(sorted, ctx.rootDir, ctx.appDir, ctx.outDir);
5992
+ await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
5707
5993
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
5708
5994
  };
5709
5995
  return devApp;
@@ -5729,34 +6015,33 @@ __export(devCommand_exports, {
5729
6015
  generateRouteArtifacts: () => generateRouteArtifacts
5730
6016
  });
5731
6017
  import path14 from "path";
5732
- async function devCommand() {
6018
+ async function devCommand(options) {
5733
6019
  const rootDir = process.cwd();
5734
- process.env.FAAPI_OUT_DIR = DEV_OUT_DIR2;
6020
+ const devDist = DEV_DIST;
6021
+ process.env.FAAPI_DIST = devDist;
5735
6022
  if (!process.env.NODE_ENV) process.env.NODE_ENV = "development";
5736
6023
  console.log("- Development mode");
5737
6024
  console.log("- Compiling config...");
5738
- await compileConfig({ rootDir, outDir: DEV_OUT_DIR2 });
5739
- const _config = await loadConfig(rootDir, DEV_OUT_DIR2);
5740
- const appDir = process.env.FAAPI_APP_DIR ?? "src";
5741
- const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
6025
+ await compileConfig({ rootDir, dist: devDist });
6026
+ const _config = await loadConfig(rootDir, devDist);
5742
6027
  console.log("- Compiling TypeScript...");
5743
- await compileDevRoutes({ rootDir, appDir, outDir: DEV_OUT_DIR2 });
6028
+ await compileDevRoutes({ rootDir, dist: devDist });
5744
6029
  console.log("- Generating route manifest and schema...");
5745
- await generateRouteArtifacts(rootDir, appDir, patterns);
6030
+ await generateRouteArtifacts(rootDir, PATTERNS2, devDist);
5746
6031
  console.log("- Starting dev app...");
5747
- const app = await createDevApp({ rootDir });
6032
+ const app = await createDevApp({ rootDir, port: options?.port });
5748
6033
  await app.listen();
5749
- startWatcher({ rootDir, appDir, app });
6034
+ startWatcher({ rootDir, app, devDist });
5750
6035
  }
5751
- async function generateRouteArtifacts(rootDir, appDir, patterns) {
5752
- const { routes, wsRoutes } = await scanRoutes(rootDir, patterns, appDir, DEV_OUT_DIR2);
6036
+ async function generateRouteArtifacts(rootDir, patterns, dist) {
6037
+ const { routes, wsRoutes } = await scanRoutes(rootDir, patterns, dist);
5753
6038
  const sorted = sortRoutes(routes);
5754
- const routesPath = path14.resolve(rootDir, DEV_OUT_DIR2, ROUTES_FILE2);
5755
- const serialized = serializeRoutes(sorted, wsRoutes, rootDir, appDir, DEV_OUT_DIR2);
6039
+ const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE2);
6040
+ const serialized = serializeRoutes(sorted, wsRoutes, rootDir, dist);
5756
6041
  await writeRoutesModule(serialized, routesPath);
5757
- await generateSchemaFiles(sorted, rootDir, appDir, DEV_OUT_DIR2);
6042
+ await generateSchemaFiles(sorted, rootDir, dist);
5758
6043
  }
5759
- var DEV_OUT_DIR2, ROUTES_FILE2;
6044
+ var DEV_DIST, ROUTES_FILE2, PATTERNS2;
5760
6045
  var init_devCommand = __esm({
5761
6046
  "src/cli/devCommand.ts"() {
5762
6047
  "use strict";
@@ -5769,54 +6054,54 @@ var init_devCommand = __esm({
5769
6054
  init_loadConfig();
5770
6055
  init_watcher();
5771
6056
  init_createDevApp();
5772
- DEV_OUT_DIR2 = ".faapi/dev";
6057
+ DEV_DIST = ".faapi";
5773
6058
  ROUTES_FILE2 = "faapi-routes.js";
6059
+ PATTERNS2 = ["src/api/**/*.ts"];
5774
6060
  }
5775
6061
  });
5776
6062
 
5777
6063
  // src/cli/compileBuildRoutes.ts
5778
6064
  import path15 from "path";
5779
6065
  import fs10 from "fs";
6066
+ import fg3 from "fast-glob";
5780
6067
  async function compileBuildRoutes(options) {
5781
- const {
5782
- rootDir,
5783
- appDir,
5784
- outDir,
5785
- entries,
5786
- splitting = true,
5787
- define,
5788
- minifySyntax = true,
5789
- logLevel = "silent"
5790
- } = options;
5791
- if (entries.length === 0) {
6068
+ const { rootDir, dist, files, logLevel = "silent" } = options;
6069
+ const entryPoints = files ?? await fg3([`${APP_DIR4}/**/*.ts`], {
6070
+ cwd: rootDir,
6071
+ onlyFiles: true,
6072
+ absolute: true,
6073
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
6074
+ });
6075
+ if (entryPoints.length === 0) {
5792
6076
  return { compiledFiles: [] };
5793
6077
  }
5794
- const absOutDir = path15.resolve(rootDir, outDir);
5795
- await fs10.promises.mkdir(absOutDir, { recursive: true });
6078
+ const absDist = path15.resolve(rootDir, dist);
6079
+ await fs10.promises.mkdir(absDist, { recursive: true });
5796
6080
  const plugins = buildAliasPlugins(rootDir);
5797
6081
  const esbuild = await import("esbuild");
5798
- const outbase = appDir === "." ? rootDir : path15.resolve(rootDir, appDir);
6082
+ const outbase = path15.resolve(rootDir, APP_DIR4);
5799
6083
  await esbuild.build({
5800
- entryPoints: entries,
5801
- outdir: absOutDir,
6084
+ entryPoints,
6085
+ outdir: absDist,
5802
6086
  outbase,
5803
- bundle: true,
5804
- splitting,
6087
+ bundle: false,
5805
6088
  platform: "node",
5806
6089
  format: "esm",
5807
6090
  sourcemap: true,
5808
6091
  packages: "external",
5809
6092
  plugins,
5810
- define,
5811
- minifySyntax,
6093
+ define: { "process.env.NODE_ENV": '"production"' },
6094
+ minifySyntax: true,
5812
6095
  logLevel
5813
6096
  });
5814
- return { compiledFiles: entries };
6097
+ return { compiledFiles: entryPoints };
5815
6098
  }
6099
+ var APP_DIR4;
5816
6100
  var init_compileBuildRoutes = __esm({
5817
6101
  "src/cli/compileBuildRoutes.ts"() {
5818
6102
  "use strict";
5819
6103
  init_aliasPlugin();
6104
+ APP_DIR4 = "src";
5820
6105
  }
5821
6106
  });
5822
6107
 
@@ -5827,66 +6112,35 @@ __export(buildCommand_exports, {
5827
6112
  });
5828
6113
  import path16 from "path";
5829
6114
  import fs11 from "fs";
5830
- import fg3 from "fast-glob";
5831
- async function collectBundleEntries(rootDir, patterns, appDir) {
5832
- const entries = /* @__PURE__ */ new Set();
5833
- const handlerFiles = await fg3(patterns, {
5834
- cwd: rootDir,
5835
- onlyFiles: true,
5836
- absolute: true
5837
- });
5838
- for (const f of handlerFiles) {
5839
- if (f.endsWith("handler.ts")) entries.add(f);
5840
- }
5841
- const mwGlob = appDir === "." ? "**/middlewares.ts" : appDir + "/**/middlewares.ts";
5842
- const mwFiles = await fg3([mwGlob], {
5843
- cwd: rootDir,
5844
- onlyFiles: true,
5845
- absolute: true,
5846
- ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
5847
- });
5848
- for (const f of mwFiles) entries.add(f);
5849
- return Array.from(entries);
5850
- }
5851
6115
  async function buildCommand(options) {
5852
6116
  const rootDir = options?.rootDir ?? process.cwd();
5853
- const outdir = PROD_OUT_DIR;
5854
- await compileConfig({ rootDir, outDir: outdir });
6117
+ const outdir = options?.dist ?? DEFAULT_DIST2;
6118
+ await compileConfig({ rootDir, dist: outdir });
5855
6119
  const _config = await loadConfig(rootDir, outdir);
5856
- const appDir = process.env.FAAPI_APP_DIR ?? "src";
5857
- const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
5858
6120
  console.log("faapi build started");
5859
6121
  console.log(`- Root: ${rootDir}`);
5860
- console.log(`- AppDir: ${appDir}`);
6122
+ console.log(`- Source: src/`);
5861
6123
  console.log(`- Output: ${outdir}`);
5862
- console.log("\n[1/7] Collecting bundle entries...");
5863
- const entries = await collectBundleEntries(rootDir, patterns, appDir);
5864
- console.log(` ${entries.length} entry file(s)`);
5865
- if (entries.length === 0) {
5866
- console.warn(" ! No entry files found, nothing to build");
5867
- return;
5868
- }
5869
- console.log("\n[2/7] Compiling TypeScript (bundle mode)...");
6124
+ console.log("\n[1/6] Compiling TypeScript (bundle: false)...");
5870
6125
  const result = await compileBuildRoutes({
5871
6126
  rootDir,
5872
- appDir,
5873
- outDir: outdir,
5874
- entries,
5875
- splitting: true,
5876
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
5877
- minifySyntax: true,
6127
+ dist: outdir,
5878
6128
  logLevel: "silent"
5879
6129
  });
5880
- console.log(` Compiled ${result.compiledFiles.length} entry file(s)`);
5881
- console.log("\n[3/7] Compiling config...");
5882
- const configResult = await compileConfig({ rootDir, outDir: outdir });
6130
+ console.log(` Compiled ${result.compiledFiles.length} file(s)`);
6131
+ if (result.compiledFiles.length === 0) {
6132
+ console.warn(" ! No source files found, nothing to build");
6133
+ return;
6134
+ }
6135
+ console.log("\n[2/6] Compiling config...");
6136
+ const configResult = await compileConfig({ rootDir, dist: outdir });
5883
6137
  if (configResult.generated) {
5884
6138
  console.log(` Written to ${configResult.outputFile}`);
5885
6139
  } else {
5886
6140
  console.log(" No config file found, skipped");
5887
6141
  }
5888
- console.log("\n[4/7] Scanning routes...");
5889
- const { routes, wsRoutes } = await scanRoutes(rootDir, patterns, appDir, outdir);
6142
+ console.log("\n[3/6] Scanning routes...");
6143
+ const { routes, wsRoutes } = await scanRoutes(rootDir, PATTERNS3, outdir);
5890
6144
  const sorted = sortRoutes(routes);
5891
6145
  console.log(` Found ${sorted.length} routes, ${wsRoutes.length} WS routes`);
5892
6146
  const conflicts = detectRouteConflicts(sorted);
@@ -5899,27 +6153,28 @@ async function buildCommand(options) {
5899
6153
  }
5900
6154
  }
5901
6155
  }
5902
- console.log("\n[5/7] Generating schema...");
5903
- await generateSchemaFiles(sorted, rootDir, appDir, outdir);
6156
+ console.log("\n[4/6] Generating schema...");
6157
+ await generateSchemaFiles(sorted, rootDir, outdir);
5904
6158
  console.log(` Schema: zod.js files under ${path16.resolve(rootDir, outdir)}`);
5905
- console.log("\n[6/7] Generating routes manifest...");
6159
+ console.log("\n[5/6] Generating routes manifest...");
5906
6160
  const routesPath = path16.resolve(rootDir, outdir, "faapi-routes.js");
5907
- const serialized = serializeRoutes(sorted, wsRoutes, rootDir, appDir, outdir);
6161
+ const serialized = serializeRoutes(sorted, wsRoutes, rootDir, outdir);
5908
6162
  await writeRoutesModule(serialized, routesPath);
5909
6163
  console.log(` Written to ${routesPath}`);
5910
- console.log("\n[7/7] Generating entry file...");
6164
+ console.log("\n[6/6] Generating entry file...");
5911
6165
  const mainPath = path16.resolve(rootDir, outdir, "main.js");
6166
+ const createProdAppArgs = options?.dist && options.dist !== DEFAULT_DIST2 ? `{ dist: '${outdir}' }` : "";
5912
6167
  const mainContent = `// \u7531 faapi build \u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91
5913
6168
  import { createProdApp } from '@faapi/faapi';
5914
6169
 
5915
- const app = await createProdApp();
6170
+ const app = await createProdApp(${createProdAppArgs});
5916
6171
  await app.listen();
5917
6172
  `;
5918
6173
  await fs11.promises.writeFile(mainPath, mainContent, "utf-8");
5919
6174
  console.log(` Written to ${mainPath}`);
5920
6175
  console.log("\nfaapi build completed");
5921
6176
  }
5922
- var PROD_OUT_DIR;
6177
+ var DEFAULT_DIST2, PATTERNS3;
5923
6178
  var init_buildCommand = __esm({
5924
6179
  "src/cli/buildCommand.ts"() {
5925
6180
  "use strict";
@@ -5931,7 +6186,8 @@ var init_buildCommand = __esm({
5931
6186
  init_compileBuildRoutes();
5932
6187
  init_compileConfig();
5933
6188
  init_loadConfig();
5934
- PROD_OUT_DIR = "dist";
6189
+ DEFAULT_DIST2 = "dist";
6190
+ PATTERNS3 = ["src/api/**/*.ts"];
5935
6191
  }
5936
6192
  });
5937
6193
 
@@ -6522,13 +6778,13 @@ var cac = (name = "") => new CAC(name);
6522
6778
 
6523
6779
  // src/cli/index.ts
6524
6780
  var cli = cac("faapi");
6525
- cli.command("").alias("dev").action(async () => {
6781
+ cli.command("").alias("dev").option("--port <number>", "\u670D\u52A1\u7AEF\u53E3\uFF08\u9ED8\u8BA4 3000\uFF09").action(async (options) => {
6526
6782
  const { devCommand: devCommand2 } = await Promise.resolve().then(() => (init_devCommand(), devCommand_exports));
6527
- await devCommand2();
6783
+ await devCommand2(options);
6528
6784
  });
6529
- cli.command("build", "Build for production").action(async () => {
6785
+ cli.command("build", "Build for production").option("--dist <dir>", "\u4EA7\u7269\u8F93\u51FA\u76EE\u5F55\uFF0C\u9ED8\u8BA4 dist").action(async (options) => {
6530
6786
  const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_buildCommand(), buildCommand_exports));
6531
- await buildCommand2();
6787
+ await buildCommand2(options);
6532
6788
  });
6533
6789
  cli.help();
6534
6790
  cli.parse();