@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 +465 -209
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +29 -9
- package/dist/index.js +117 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
* - `
|
|
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
|
-
* 统一读取 `<
|
|
978
|
-
* - dev 模式:`faapi dev` 启动时由 `compileConfig` 生成 `.faapi/
|
|
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
|
|
1008
|
+
* @param dist 产物目录(如 'dist' 或 '.faapi')
|
|
989
1009
|
* @returns 配置对象,无配置文件时返回 null
|
|
990
1010
|
*/
|
|
991
|
-
declare function loadConfig(rootDir: string,
|
|
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
|
-
/**
|
|
1063
|
-
|
|
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,
|
|
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"
|
|
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
|
|
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({
|
|
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,
|
|
999
|
-
const configProductPath = path2.resolve(rootDir,
|
|
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] ${
|
|
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,
|
|
2459
|
+
function getSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
2404
2460
|
let rel = sourceFile.replace(/\\/g, "/");
|
|
2405
|
-
if (
|
|
2406
|
-
rel = rel.slice(
|
|
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,
|
|
2466
|
+
return path4.resolve(rootDir, dist, relDir, "zod.js");
|
|
2411
2467
|
}
|
|
2412
|
-
function getRuntimeSchemaPath(filePath,
|
|
2468
|
+
function getRuntimeSchemaPath(filePath, dist, rootDir) {
|
|
2413
2469
|
let rel = filePath.replace(/\\/g, "/");
|
|
2414
|
-
if (
|
|
2415
|
-
rel = rel.slice(
|
|
2416
|
-
} else if (rel.startsWith(`${
|
|
2417
|
-
rel = rel.slice(
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
2528
|
+
const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
|
|
2471
2529
|
const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
|
|
2472
2530
|
let relForDir = relFile;
|
|
2473
|
-
if (
|
|
2474
|
-
relForDir = relForDir.slice(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
2887
|
-
const routesPath = path7.resolve(rootDir,
|
|
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] ${
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3075
|
-
|
|
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
|
|
3143
|
-
const withoutPrefix = filePath.startsWith(
|
|
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
|
-
|
|
3210
|
+
var APP_DIR = "src";
|
|
3211
|
+
function toProdAbsPath(sourceAbsPath, rootDir, dist) {
|
|
3155
3212
|
let rel = path8.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
3156
|
-
if (
|
|
3157
|
-
rel = rel.slice(
|
|
3213
|
+
if (rel.startsWith(`${APP_DIR}/`)) {
|
|
3214
|
+
rel = rel.slice(APP_DIR.length + 1);
|
|
3158
3215
|
}
|
|
3159
|
-
const prodRel = `${
|
|
3216
|
+
const prodRel = `${dist}/${rel.replace(/\.ts$/, ".js")}`;
|
|
3160
3217
|
return path8.resolve(rootDir, prodRel);
|
|
3161
3218
|
}
|
|
3162
|
-
async function findMergedMiddlewares(routeFilePath, rootDir,
|
|
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 (
|
|
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,
|
|
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,
|
|
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 =
|
|
3251
|
-
const urlPath = filePathToUrlPath(normalizedFile
|
|
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,
|
|
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.
|
|
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.
|
|
3354
|
+
await generateSchemaFiles(sorted, ctx.rootDir, ctx.dist);
|
|
3299
3355
|
ctx.updateRoutes(sorted, reScanned.wsRoutes);
|
|
3300
3356
|
};
|
|
3301
3357
|
return devApp;
|