@faapi/faapi 0.0.0-canary.2a7b6a3 → 0.0.0-canary.9a79cdf

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, appDir) {
93
+ const appDirAbs = toRealPath(path2.resolve(rootDir, appDir));
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 && options?.appDir ? toRealPath(path2.resolve(options.rootDir, options.appDir)) : null;
92
122
  return {
93
123
  name: "faapi-alias",
94
124
  setup(build) {
@@ -100,17 +130,44 @@ 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
+ options.appDir
151
+ )}${quote}`;
152
+ }
153
+ modified = true;
154
+ return `${prefix}${quote}${toProdImportPath(resolved, importer)}${quote}`;
155
+ }
106
156
  return full;
107
157
  }
108
158
  const candidates = resolveAlias(specifier, config);
109
159
  for (const candidate of candidates) {
110
- for (const ext of EXTS) {
160
+ for (const ext of SOURCE_EXTS) {
111
161
  const file = candidate + ext;
112
162
  if (fs2.existsSync(file)) {
113
163
  modified = true;
164
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
165
+ return `${prefix}${quote}${toStrippedProdImportPath(
166
+ file,
167
+ options.rootDir,
168
+ options.appDir
169
+ )}${quote}`;
170
+ }
114
171
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
115
172
  }
116
173
  }
@@ -118,6 +175,13 @@ function createAliasPlugin(config) {
118
175
  const file = candidate + indexExt;
119
176
  if (fs2.existsSync(file)) {
120
177
  modified = true;
178
+ if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
179
+ return `${prefix}${quote}${toStrippedProdImportPath(
180
+ file,
181
+ options.rootDir,
182
+ options.appDir
183
+ )}${quote}`;
184
+ }
121
185
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
122
186
  }
123
187
  }
@@ -130,15 +194,31 @@ function createAliasPlugin(config) {
130
194
  }
131
195
  };
132
196
  }
133
- function buildAliasPlugins(rootDir) {
197
+ function buildAliasPlugins(rootDir, appDir) {
134
198
  const tsconfig = readTsconfig(rootDir);
135
- return tsconfig ? [createAliasPlugin(tsconfig)] : [];
199
+ return [
200
+ createAliasPlugin(
201
+ tsconfig ?? { baseUrl: ".", paths: {} },
202
+ appDir ? { rootDir, appDir } : void 0
203
+ )
204
+ ];
136
205
  }
206
+ var PROD_EXTS, SOURCE_EXTS, INDEX_EXTS;
137
207
  var init_aliasPlugin = __esm({
138
208
  "src/cli/aliasPlugin.ts"() {
139
209
  "use strict";
140
210
  init_resolveAlias();
141
211
  init_readTsconfig();
212
+ PROD_EXTS = [".js", ".mjs", ".cjs"];
213
+ SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
214
+ INDEX_EXTS = [
215
+ "/index.ts",
216
+ "/index.tsx",
217
+ "/index.js",
218
+ "/index.jsx",
219
+ "/index.mjs",
220
+ "/index.cjs"
221
+ ];
142
222
  }
143
223
  });
144
224
 
@@ -215,13 +295,24 @@ var init_deepMerge = __esm({
215
295
  // src/cli/compileConfig.ts
216
296
  import path4 from "path";
217
297
  import fs4 from "fs";
298
+ function toRealPath2(p) {
299
+ try {
300
+ return fs4.realpathSync(p);
301
+ } catch {
302
+ return p;
303
+ }
304
+ }
305
+ function isInsideDir2(filePath, dir) {
306
+ const rel = path4.relative(dir, filePath);
307
+ return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
308
+ }
218
309
  function getEnv() {
219
310
  return process.env.FAAPI_ENV || process.env.NODE_ENV || "development";
220
311
  }
221
312
  function findBaseConfig(rootDir) {
222
313
  for (const f of BASE_CONFIG_FILES) {
223
314
  if (fs4.existsSync(path4.join(rootDir, f))) {
224
- return f.replace(/\.(ts|js)$/, "");
315
+ return f;
225
316
  }
226
317
  }
227
318
  return null;
@@ -230,33 +321,141 @@ function findEnvConfig(rootDir, env) {
230
321
  for (const ext of ENV_CONFIG_EXTS) {
231
322
  const f = `faapi.config.${env}${ext}`;
232
323
  if (fs4.existsSync(path4.join(rootDir, f))) {
233
- return `faapi.config.${env}`;
324
+ return f;
234
325
  }
235
326
  }
236
327
  return null;
237
328
  }
329
+ function toProdExtension2(filePath) {
330
+ if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
331
+ if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
332
+ if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
333
+ return filePath;
334
+ }
335
+ function toProdImport(filename) {
336
+ return toProdExtension2(filename);
337
+ }
338
+ function extractRelativeSpecifiers(source) {
339
+ const specifiers = [];
340
+ let match;
341
+ SPEC_RE.lastIndex = 0;
342
+ while ((match = SPEC_RE.exec(source)) !== null) {
343
+ const specifier = match[3];
344
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
345
+ specifiers.push(specifier);
346
+ }
347
+ }
348
+ return specifiers;
349
+ }
350
+ async function collectRelativeImports(entryFiles, rootDir, appDir) {
351
+ const appDirAbs = toRealPath2(path4.resolve(rootDir, appDir));
352
+ const visited = /* @__PURE__ */ new Set();
353
+ const appDirFiles = /* @__PURE__ */ new Set();
354
+ const nonAppDirFiles = /* @__PURE__ */ new Set();
355
+ async function collect(filePath) {
356
+ if (visited.has(filePath)) return;
357
+ visited.add(filePath);
358
+ let source;
359
+ try {
360
+ source = await fs4.promises.readFile(filePath, "utf8");
361
+ } catch {
362
+ return;
363
+ }
364
+ const specifiers = extractRelativeSpecifiers(source);
365
+ for (const specifier of specifiers) {
366
+ const resolved = resolveRelativeSpecifier(filePath, specifier);
367
+ if (!resolved) continue;
368
+ if (/\.(js|mjs|cjs)$/.test(specifier)) continue;
369
+ if (isInsideDir2(resolved, appDirAbs)) {
370
+ appDirFiles.add(resolved);
371
+ } else {
372
+ nonAppDirFiles.add(resolved);
373
+ }
374
+ await collect(resolved);
375
+ }
376
+ }
377
+ for (const entry of entryFiles) {
378
+ await collect(entry);
379
+ }
380
+ return {
381
+ appDirFiles: Array.from(appDirFiles),
382
+ nonAppDirFiles: Array.from(nonAppDirFiles)
383
+ };
384
+ }
385
+ function createExternalRelativePlugin() {
386
+ return {
387
+ name: "faapi-external-relative",
388
+ setup(build) {
389
+ build.onResolve({ filter: /^\.{1,2}\// }, (args) => ({
390
+ path: args.path,
391
+ external: true
392
+ }));
393
+ }
394
+ };
395
+ }
238
396
  async function compileConfig(options) {
239
- const { rootDir, outDir } = options;
240
- const baseConfig = findBaseConfig(rootDir);
241
- if (!baseConfig) {
397
+ const { rootDir, outDir, appDir = "src" } = options;
398
+ const baseConfigName = findBaseConfig(rootDir);
399
+ if (!baseConfigName) {
242
400
  return { generated: false, outputFile: "" };
243
401
  }
244
402
  const env = getEnv();
245
- const envConfig = findEnvConfig(rootDir, env);
246
- const imports = [`import base from './${baseConfig}';`];
403
+ const envConfigName = findEnvConfig(rootDir, env);
404
+ const absOutDir = path4.resolve(rootDir, outDir);
405
+ await fs4.promises.mkdir(absOutDir, { recursive: true });
406
+ const configEntryPoints = [path4.resolve(rootDir, baseConfigName)];
407
+ if (envConfigName) {
408
+ configEntryPoints.push(path4.resolve(rootDir, envConfigName));
409
+ }
410
+ const { appDirFiles, nonAppDirFiles } = await collectRelativeImports(
411
+ configEntryPoints,
412
+ rootDir,
413
+ appDir
414
+ );
415
+ const esbuild = await import("esbuild");
416
+ const aliasPlugins = buildAliasPlugins(rootDir, appDir);
417
+ const step1aEntries = [...configEntryPoints, ...nonAppDirFiles];
418
+ await esbuild.build({
419
+ entryPoints: step1aEntries,
420
+ outdir: absOutDir,
421
+ outbase: rootDir,
422
+ bundle: false,
423
+ platform: "node",
424
+ format: "esm",
425
+ sourcemap: true,
426
+ packages: "external",
427
+ plugins: aliasPlugins,
428
+ logLevel: "silent"
429
+ });
430
+ if (appDirFiles.length > 0) {
431
+ const appOutbase = appDir === "." ? rootDir : path4.resolve(rootDir, appDir);
432
+ await esbuild.build({
433
+ entryPoints: appDirFiles,
434
+ outdir: absOutDir,
435
+ outbase: appOutbase,
436
+ bundle: false,
437
+ platform: "node",
438
+ format: "esm",
439
+ sourcemap: true,
440
+ packages: "external",
441
+ plugins: aliasPlugins,
442
+ logLevel: "silent"
443
+ });
444
+ }
445
+ const baseImport = `import base from './${toProdImport(baseConfigName)}';`;
446
+ const imports = [baseImport];
247
447
  let exportDefault;
248
- if (envConfig) {
249
- imports.push(`import env from './${envConfig}';`);
448
+ if (envConfigName) {
449
+ const envImport = `import env from './${toProdImport(envConfigName)}';`;
450
+ imports.push(envImport);
250
451
  exportDefault = "export default deepMerge(base, env);";
251
452
  } else {
252
453
  exportDefault = "export default base;";
253
454
  }
254
455
  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");
456
+ const outputFile = path4.resolve(absOutDir, "faapi-config.js");
258
457
  await esbuild.build({
259
- stdin: { contents: entryCode, resolveDir: rootDir, loader: "ts" },
458
+ stdin: { contents: entryCode, resolveDir: absOutDir, loader: "ts" },
260
459
  outfile: outputFile,
261
460
  bundle: true,
262
461
  format: "esm",
@@ -264,17 +463,20 @@ async function compileConfig(options) {
264
463
  target: "node20",
265
464
  sourcemap: true,
266
465
  packages: "external",
466
+ plugins: [createExternalRelativePlugin()],
267
467
  logLevel: "silent"
268
468
  });
269
469
  return { generated: true, outputFile };
270
470
  }
271
- var BASE_CONFIG_FILES, ENV_CONFIG_EXTS;
471
+ var BASE_CONFIG_FILES, ENV_CONFIG_EXTS, SPEC_RE;
272
472
  var init_compileConfig = __esm({
273
473
  "src/cli/compileConfig.ts"() {
274
474
  "use strict";
275
475
  init_deepMerge();
476
+ init_aliasPlugin();
276
477
  BASE_CONFIG_FILES = ["faapi.config.ts", "faapi.config.js"];
277
478
  ENV_CONFIG_EXTS = [".ts", ".js"];
479
+ SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
278
480
  }
279
481
  });
280
482
 
@@ -563,10 +765,35 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
563
765
  const properties = typeName === "Pick" ? innerType.properties.filter((p) => keySet.has(p.name)) : innerType.properties.filter((p) => !keySet.has(p.name));
564
766
  return { kind: "object", properties };
565
767
  }
566
- if (typeName === "Map" || typeName === "Set" || typeName === "WeakMap" || typeName === "WeakSet") {
768
+ if (typeName === "Map") {
769
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 2) {
770
+ throw new SchemaExtractionError(
771
+ typeNode.getText(),
772
+ "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
773
+ );
774
+ }
775
+ return {
776
+ kind: "map",
777
+ key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
778
+ value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
779
+ };
780
+ }
781
+ if (typeName === "Set") {
782
+ if (!typeNode.typeArguments || typeNode.typeArguments.length !== 1) {
783
+ throw new SchemaExtractionError(
784
+ typeNode.getText(),
785
+ "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
786
+ );
787
+ }
788
+ return {
789
+ kind: "set",
790
+ element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
791
+ };
792
+ }
793
+ if (typeName === "WeakMap" || typeName === "WeakSet") {
567
794
  throw new SchemaExtractionError(
568
795
  typeNode.getText(),
569
- `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528\u5BF9\u8C61\u6216\u6570\u7EC4`
796
+ `${typeName} \u8FD0\u884C\u65F6\u65E0\u6CD5\u679A\u4E3E\u6821\u9A8C\uFF0C\u8BF7\u6539\u7528 Map / Set \u6216\u5BF9\u8C61`
570
797
  );
571
798
  }
572
799
  if (typeName === "Promise") {
@@ -1025,6 +1252,7 @@ var init_resolveInjection = __esm({
1025
1252
  PARAM_TYPE_MAP = {
1026
1253
  query: "query",
1027
1254
  body: "body",
1255
+ form: "form",
1028
1256
  headers: "headers",
1029
1257
  params: "params",
1030
1258
  context: "context",
@@ -1123,9 +1351,16 @@ function collectRouteSchemaSources(routes, rootDir) {
1123
1351
  const inputType = getInputTypeForMethod(method);
1124
1352
  const schemaName = getSchemaName(method, inputType);
1125
1353
  const meta = analyzeInjection(code, method);
1126
- const param = meta.params.find((p) => p.type === inputType);
1354
+ const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1355
+ const isForm = param?.type === "form";
1127
1356
  const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
1128
- sources.push({ urlPath: entry.urlPath, filePath, schemaName, typeInfo });
1357
+ sources.push({
1358
+ urlPath: entry.urlPath,
1359
+ filePath,
1360
+ schemaName,
1361
+ typeInfo,
1362
+ coerce: isForm || void 0
1363
+ });
1129
1364
  }
1130
1365
  }
1131
1366
  return { sources, allTypesByFile, mergedAllTypes };
@@ -1177,6 +1412,13 @@ function collectNamedTypes(type, ctx) {
1177
1412
  collectNamedTypes(type.key, ctx);
1178
1413
  collectNamedTypes(type.value, ctx);
1179
1414
  return;
1415
+ case "map":
1416
+ collectNamedTypes(type.key, ctx);
1417
+ collectNamedTypes(type.value, ctx);
1418
+ return;
1419
+ case "set":
1420
+ collectNamedTypes(type.element, ctx);
1421
+ return;
1180
1422
  case "ref": {
1181
1423
  if (ctx.namedTypes.has(type.name)) return;
1182
1424
  ctx.namedTypes.set(type.name, { kind: "any" });
@@ -1266,6 +1508,10 @@ function baseExpression(type, ctx) {
1266
1508
  return 'z.preprocess((v) => (typeof v === "string" ? new Date(v) : v), z.date())';
1267
1509
  case "record":
1268
1510
  return `z.record(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)})`;
1511
+ case "map":
1512
+ return `z.preprocess(coerceMap, z.map(${runtimeTypeToZodExpression(type.key, ctx)}, ${runtimeTypeToZodExpression(type.value, ctx)}))`;
1513
+ case "set":
1514
+ return `z.preprocess(coerceSet, z.set(${runtimeTypeToZodExpression(type.element, ctx)}))`;
1269
1515
  case "ref":
1270
1516
  if (type.name === ctx.entryTypeName) {
1271
1517
  return `${ctx.entryExportName}Schema`;
@@ -1278,11 +1524,13 @@ function generateHelpersFileSource() {
1278
1524
  "// faapi-helpers.js \u2014 faapi \u81EA\u52A8\u751F\u6210\u7684\u516C\u7528\u51FD\u6570\uFF08\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91\uFF09",
1279
1525
  COERCE_NUMBER_HELPER,
1280
1526
  COERCE_BOOLEAN_HELPER,
1527
+ COERCE_MAP_HELPER,
1528
+ COERCE_SET_HELPER,
1281
1529
  ""
1282
1530
  ].join("\n");
1283
1531
  }
1284
1532
  function usesCoerceHelpers(code) {
1285
- return code.includes("coerceNumber") || code.includes("coerceBoolean");
1533
+ return code.includes("coerceNumber") || code.includes("coerceBoolean") || code.includes("coerceMap") || code.includes("coerceSet");
1286
1534
  }
1287
1535
  function wrapCoercePreprocess(kind, inner) {
1288
1536
  if (kind === "number") {
@@ -1369,6 +1617,10 @@ function containsRef(type, visited) {
1369
1617
  return type.members.some((m) => containsRef(m, visited));
1370
1618
  case "record":
1371
1619
  return containsRef(type.key, visited) || containsRef(type.value, visited);
1620
+ case "map":
1621
+ return containsRef(type.key, visited) || containsRef(type.value, visited);
1622
+ case "set":
1623
+ return containsRef(type.element, visited);
1372
1624
  default:
1373
1625
  return false;
1374
1626
  }
@@ -1397,7 +1649,7 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
1397
1649
  }
1398
1650
  return lines.join("\n");
1399
1651
  }
1400
- var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, HELPERS_FILENAME;
1652
+ var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, COERCE_MAP_HELPER, COERCE_SET_HELPER, HELPERS_FILENAME;
1401
1653
  var init_generateZodSchema = __esm({
1402
1654
  "src/ast/generateZodSchema.ts"() {
1403
1655
  "use strict";
@@ -1423,6 +1675,8 @@ var init_generateZodSchema = __esm({
1423
1675
  };
1424
1676
  COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
1425
1677
  COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
1678
+ 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);';
1679
+ COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
1426
1680
  HELPERS_FILENAME = "faapi-helpers.js";
1427
1681
  }
1428
1682
  });
@@ -1464,7 +1718,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1464
1718
  if (!typeInfo) {
1465
1719
  continue;
1466
1720
  }
1467
- const coerce = /(?:Query|Params)$/.test(schemaName);
1721
+ const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
1468
1722
  const block = [`// ${schemaName}`];
1469
1723
  const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1470
1724
  /^import \{ z \} from 'zod';\s*\n\s*\n/,
@@ -1476,7 +1730,9 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1476
1730
  }
1477
1731
  const allSchemaCode = schemaBlocks.join("\n");
1478
1732
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
1479
- lines.push(`import { coerceNumber, coerceBoolean } from '${helpersImportPath}';`);
1733
+ lines.push(
1734
+ `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
1735
+ );
1480
1736
  }
1481
1737
  lines.push("");
1482
1738
  lines.push(...schemaBlocks);
@@ -4473,6 +4729,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
4473
4729
  return ctx.ip;
4474
4730
  case "body":
4475
4731
  return body;
4732
+ // form 与 body 共享解析结果(resolveInput 已按 Content-Type 解析 form-urlencoded)
4733
+ // 差异仅在 schema 校验(form coerce=true,由 collectRouteSchemaSources 标记)
4734
+ case "form":
4735
+ return body;
4476
4736
  case "files":
4477
4737
  if (body && typeof body === "object" && "files" in body) {
4478
4738
  return body.files;
@@ -4891,6 +5151,44 @@ var init_helmet = __esm({
4891
5151
  }
4892
5152
  });
4893
5153
 
5154
+ // src/middleware/logger.ts
5155
+ function logger(options = {}) {
5156
+ return async (ctx, next) => {
5157
+ const log = options.log ?? console.log;
5158
+ const start = Date.now();
5159
+ try {
5160
+ const response = await next();
5161
+ const duration = Date.now() - start;
5162
+ const entry = {
5163
+ method: ctx.method,
5164
+ path: ctx.path,
5165
+ status: response.status,
5166
+ durationMs: duration
5167
+ };
5168
+ log(entry, `${ctx.method} ${ctx.path} ${response.status} ${duration}ms`);
5169
+ return response;
5170
+ } catch (err) {
5171
+ const duration = Date.now() - start;
5172
+ const message = err instanceof Error ? err.message : String(err);
5173
+ const status = err?.statusCode ?? 500;
5174
+ const entry = {
5175
+ method: ctx.method,
5176
+ path: ctx.path,
5177
+ status,
5178
+ durationMs: duration,
5179
+ error: message
5180
+ };
5181
+ log(entry, `${ctx.method} ${ctx.path} ${status} ${duration}ms - ${message}`);
5182
+ throw err;
5183
+ }
5184
+ };
5185
+ }
5186
+ var init_logger = __esm({
5187
+ "src/middleware/logger.ts"() {
5188
+ "use strict";
5189
+ }
5190
+ });
5191
+
4894
5192
  // src/errors/formatErrorResponse.ts
4895
5193
  function formatErrorResponse(error) {
4896
5194
  if (error instanceof ValidationError) {
@@ -5219,6 +5517,7 @@ function createServer(options) {
5219
5517
  middlewares: globalMiddlewares,
5220
5518
  injectors: globalInjectors,
5221
5519
  helmet: helmetOption,
5520
+ logger: loggerOption,
5222
5521
  bodyLimit = DEFAULT_BODY_LIMIT,
5223
5522
  http2: http2Option
5224
5523
  } = options;
@@ -5230,6 +5529,8 @@ function createServer(options) {
5230
5529
  const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
5231
5530
  configMiddlewares.push(helmet(helmOpts));
5232
5531
  }
5532
+ const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
5533
+ if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
5233
5534
  const server = (() => {
5234
5535
  if (http2Option) {
5235
5536
  const h2Opts = typeof http2Option === "object" ? http2Option : {};
@@ -5343,6 +5644,7 @@ var init_createServer = __esm({
5343
5644
  init_getClientIp();
5344
5645
  init_cors();
5345
5646
  init_helmet();
5647
+ init_logger();
5346
5648
  init_handleWsUpgrade();
5347
5649
  init_serverUtils();
5348
5650
  init_generateSchemaFiles();
@@ -5456,7 +5758,7 @@ function isFaapiConfigKey(key) {
5456
5758
  }
5457
5759
  async function createAppBase(options) {
5458
5760
  const rootDir = options?.rootDir ?? process.cwd();
5459
- const outDir = process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
5761
+ const outDir = options?.outDir ?? process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
5460
5762
  const routesPath = path13.resolve(rootDir, outDir, ROUTES_FILE);
5461
5763
  if (!fs9.existsSync(routesPath)) {
5462
5764
  throw new Error(
@@ -5492,6 +5794,7 @@ async function createAppBase(options) {
5492
5794
  middlewares: config?.middlewares,
5493
5795
  injectors: config?.injectors,
5494
5796
  helmet: config?.helmet,
5797
+ logger: config?.logger,
5495
5798
  bodyLimit: config?.bodyLimit,
5496
5799
  http2: config?.http2
5497
5800
  });
@@ -5729,7 +6032,7 @@ __export(devCommand_exports, {
5729
6032
  generateRouteArtifacts: () => generateRouteArtifacts
5730
6033
  });
5731
6034
  import path14 from "path";
5732
- async function devCommand() {
6035
+ async function devCommand(options) {
5733
6036
  const rootDir = process.cwd();
5734
6037
  process.env.FAAPI_OUT_DIR = DEV_OUT_DIR2;
5735
6038
  if (!process.env.NODE_ENV) process.env.NODE_ENV = "development";
@@ -5737,14 +6040,14 @@ async function devCommand() {
5737
6040
  console.log("- Compiling config...");
5738
6041
  await compileConfig({ rootDir, outDir: DEV_OUT_DIR2 });
5739
6042
  const _config = await loadConfig(rootDir, DEV_OUT_DIR2);
5740
- const appDir = process.env.FAAPI_APP_DIR ?? "src";
6043
+ const appDir = options?.appDir ?? process.env.FAAPI_APP_DIR ?? "src";
5741
6044
  const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
5742
6045
  console.log("- Compiling TypeScript...");
5743
6046
  await compileDevRoutes({ rootDir, appDir, outDir: DEV_OUT_DIR2 });
5744
6047
  console.log("- Generating route manifest and schema...");
5745
6048
  await generateRouteArtifacts(rootDir, appDir, patterns);
5746
6049
  console.log("- Starting dev app...");
5747
- const app = await createDevApp({ rootDir });
6050
+ const app = await createDevApp({ rootDir, port: options?.port });
5748
6051
  await app.listen();
5749
6052
  startWatcher({ rootDir, appDir, app });
5750
6053
  }
@@ -5777,18 +6080,16 @@ var init_devCommand = __esm({
5777
6080
  // src/cli/compileBuildRoutes.ts
5778
6081
  import path15 from "path";
5779
6082
  import fs10 from "fs";
6083
+ import fg3 from "fast-glob";
5780
6084
  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) {
6085
+ const { rootDir, appDir, outDir, files, logLevel = "silent" } = options;
6086
+ const entryPoints = files ?? await fg3([`${appDir}/**/*.ts`], {
6087
+ cwd: rootDir,
6088
+ onlyFiles: true,
6089
+ absolute: true,
6090
+ ignore: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
6091
+ });
6092
+ if (entryPoints.length === 0) {
5792
6093
  return { compiledFiles: [] };
5793
6094
  }
5794
6095
  const absOutDir = path15.resolve(rootDir, outDir);
@@ -5797,21 +6098,20 @@ async function compileBuildRoutes(options) {
5797
6098
  const esbuild = await import("esbuild");
5798
6099
  const outbase = appDir === "." ? rootDir : path15.resolve(rootDir, appDir);
5799
6100
  await esbuild.build({
5800
- entryPoints: entries,
6101
+ entryPoints,
5801
6102
  outdir: absOutDir,
5802
6103
  outbase,
5803
- bundle: true,
5804
- splitting,
6104
+ bundle: false,
5805
6105
  platform: "node",
5806
6106
  format: "esm",
5807
6107
  sourcemap: true,
5808
6108
  packages: "external",
5809
6109
  plugins,
5810
- define,
5811
- minifySyntax,
6110
+ define: { "process.env.NODE_ENV": '"production"' },
6111
+ minifySyntax: true,
5812
6112
  logLevel
5813
6113
  });
5814
- return { compiledFiles: entries };
6114
+ return { compiledFiles: entryPoints };
5815
6115
  }
5816
6116
  var init_compileBuildRoutes = __esm({
5817
6117
  "src/cli/compileBuildRoutes.ts"() {
@@ -5827,65 +6127,37 @@ __export(buildCommand_exports, {
5827
6127
  });
5828
6128
  import path16 from "path";
5829
6129
  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
6130
  async function buildCommand(options) {
5852
6131
  const rootDir = options?.rootDir ?? process.cwd();
5853
- const outdir = PROD_OUT_DIR;
6132
+ const outdir = options?.outDir ?? DEFAULT_PROD_OUT_DIR;
5854
6133
  await compileConfig({ rootDir, outDir: outdir });
5855
6134
  const _config = await loadConfig(rootDir, outdir);
5856
- const appDir = process.env.FAAPI_APP_DIR ?? "src";
6135
+ const appDir = options?.appDir ?? process.env.FAAPI_APP_DIR ?? "src";
5857
6136
  const patterns = appDir === "." ? ["api/**/*.ts"] : [`${appDir}/api/**/*.ts`];
5858
6137
  console.log("faapi build started");
5859
6138
  console.log(`- Root: ${rootDir}`);
5860
6139
  console.log(`- AppDir: ${appDir}`);
5861
6140
  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)...");
6141
+ console.log("\n[1/6] Compiling TypeScript (bundle: false)...");
5870
6142
  const result = await compileBuildRoutes({
5871
6143
  rootDir,
5872
6144
  appDir,
5873
6145
  outDir: outdir,
5874
- entries,
5875
- splitting: true,
5876
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
5877
- minifySyntax: true,
5878
6146
  logLevel: "silent"
5879
6147
  });
5880
- console.log(` Compiled ${result.compiledFiles.length} entry file(s)`);
5881
- console.log("\n[3/7] Compiling config...");
6148
+ console.log(` Compiled ${result.compiledFiles.length} file(s)`);
6149
+ if (result.compiledFiles.length === 0) {
6150
+ console.warn(" ! No source files found, nothing to build");
6151
+ return;
6152
+ }
6153
+ console.log("\n[2/6] Compiling config...");
5882
6154
  const configResult = await compileConfig({ rootDir, outDir: outdir });
5883
6155
  if (configResult.generated) {
5884
6156
  console.log(` Written to ${configResult.outputFile}`);
5885
6157
  } else {
5886
6158
  console.log(" No config file found, skipped");
5887
6159
  }
5888
- console.log("\n[4/7] Scanning routes...");
6160
+ console.log("\n[3/6] Scanning routes...");
5889
6161
  const { routes, wsRoutes } = await scanRoutes(rootDir, patterns, appDir, outdir);
5890
6162
  const sorted = sortRoutes(routes);
5891
6163
  console.log(` Found ${sorted.length} routes, ${wsRoutes.length} WS routes`);
@@ -5899,27 +6171,29 @@ async function buildCommand(options) {
5899
6171
  }
5900
6172
  }
5901
6173
  }
5902
- console.log("\n[5/7] Generating schema...");
6174
+ console.log("\n[4/6] Generating schema...");
5903
6175
  await generateSchemaFiles(sorted, rootDir, appDir, outdir);
5904
6176
  console.log(` Schema: zod.js files under ${path16.resolve(rootDir, outdir)}`);
5905
- console.log("\n[6/7] Generating routes manifest...");
6177
+ console.log("\n[5/6] Generating routes manifest...");
5906
6178
  const routesPath = path16.resolve(rootDir, outdir, "faapi-routes.js");
5907
6179
  const serialized = serializeRoutes(sorted, wsRoutes, rootDir, appDir, outdir);
5908
6180
  await writeRoutesModule(serialized, routesPath);
5909
6181
  console.log(` Written to ${routesPath}`);
5910
- console.log("\n[7/7] Generating entry file...");
6182
+ console.log("\n[6/6] Generating entry file...");
5911
6183
  const mainPath = path16.resolve(rootDir, outdir, "main.js");
6184
+ const createProdAppArgs = options?.outDir && options.outDir !== DEFAULT_PROD_OUT_DIR ? `{ outDir: '${options.outDir}' }` : "";
6185
+ const listenArgs = options?.port ? String(options.port) : "";
5912
6186
  const mainContent = `// \u7531 faapi build \u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91
5913
6187
  import { createProdApp } from '@faapi/faapi';
5914
6188
 
5915
- const app = await createProdApp();
5916
- await app.listen();
6189
+ const app = await createProdApp(${createProdAppArgs});
6190
+ await app.listen(${listenArgs});
5917
6191
  `;
5918
6192
  await fs11.promises.writeFile(mainPath, mainContent, "utf-8");
5919
6193
  console.log(` Written to ${mainPath}`);
5920
6194
  console.log("\nfaapi build completed");
5921
6195
  }
5922
- var PROD_OUT_DIR;
6196
+ var DEFAULT_PROD_OUT_DIR;
5923
6197
  var init_buildCommand = __esm({
5924
6198
  "src/cli/buildCommand.ts"() {
5925
6199
  "use strict";
@@ -5931,7 +6205,7 @@ var init_buildCommand = __esm({
5931
6205
  init_compileBuildRoutes();
5932
6206
  init_compileConfig();
5933
6207
  init_loadConfig();
5934
- PROD_OUT_DIR = "dist";
6208
+ DEFAULT_PROD_OUT_DIR = "dist";
5935
6209
  }
5936
6210
  });
5937
6211
 
@@ -6522,13 +6796,13 @@ var cac = (name = "") => new CAC(name);
6522
6796
 
6523
6797
  // src/cli/index.ts
6524
6798
  var cli = cac("faapi");
6525
- cli.command("").alias("dev").action(async () => {
6799
+ cli.command("").alias("dev").option("--port <number>", "\u670D\u52A1\u7AEF\u53E3\uFF08\u9ED8\u8BA4 3000\uFF09").option("--appDir <dir>", "\u6E90\u7801\u76EE\u5F55\u524D\u7F00\uFF08\u9ED8\u8BA4 src\uFF09").action(async (options) => {
6526
6800
  const { devCommand: devCommand2 } = await Promise.resolve().then(() => (init_devCommand(), devCommand_exports));
6527
- await devCommand2();
6801
+ await devCommand2(options);
6528
6802
  });
6529
- cli.command("build", "Build for production").action(async () => {
6803
+ cli.command("build", "Build for production").option("--port <number>", "prod \u670D\u52A1\u7AEF\u53E3\uFF0C\u5199\u5165 dist/main.js").option("--appDir <dir>", "\u6E90\u7801\u76EE\u5F55\u524D\u7F00\uFF08\u9ED8\u8BA4 src\uFF09").option("--outDir <dir>", "\u4EA7\u7269\u8F93\u51FA\u76EE\u5F55\uFF08\u9ED8\u8BA4 dist\uFF09").action(async (options) => {
6530
6804
  const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_buildCommand(), buildCommand_exports));
6531
- await buildCommand2();
6805
+ await buildCommand2(options);
6532
6806
  });
6533
6807
  cli.help();
6534
6808
  cli.parse();