@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 +382 -108
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.js +71 -9
- 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
|
|
|
@@ -810,6 +812,13 @@ type RuntimeType = {
|
|
|
810
812
|
kind: 'record';
|
|
811
813
|
key: RuntimeType;
|
|
812
814
|
value: RuntimeType;
|
|
815
|
+
} | {
|
|
816
|
+
kind: 'map';
|
|
817
|
+
key: RuntimeType;
|
|
818
|
+
value: RuntimeType;
|
|
819
|
+
} | {
|
|
820
|
+
kind: 'set';
|
|
821
|
+
element: RuntimeType;
|
|
813
822
|
} | {
|
|
814
823
|
kind: 'ref';
|
|
815
824
|
name: string;
|
|
@@ -949,6 +958,18 @@ interface RouteSchemaSource {
|
|
|
949
958
|
filePath: string;
|
|
950
959
|
schemaName: string;
|
|
951
960
|
typeInfo: HandlerTypeInfo | null;
|
|
961
|
+
/**
|
|
962
|
+
* 是否对 number/boolean 字段生成 z.preprocess 字符串转换(coerce)。
|
|
963
|
+
*
|
|
964
|
+
* - query/params:始终 coerce=true(URL 来源均为 string)
|
|
965
|
+
* - body:始终 coerce=false(JSON 解析已是天然 JS 类型)
|
|
966
|
+
* - form:coerce=true(form-urlencoded 来源均为 string),由本函数在提取时
|
|
967
|
+
* 检测到 handler 声明 `form` 参数时显式设置。schema 名仍为 `POSTBody`
|
|
968
|
+
* (与 body 共享运行时 schema key),运行时 validateInput 无需感知 form/body 差异。
|
|
969
|
+
*
|
|
970
|
+
* 未设置时由 generateSchemaFileSource 回退到 schemaName 后缀正则推断(Query/Params → true)。
|
|
971
|
+
*/
|
|
972
|
+
coerce?: boolean;
|
|
952
973
|
}
|
|
953
974
|
/**
|
|
954
975
|
* 从路由清单收集 schema 提取所需的原始数据
|
|
@@ -1061,6 +1082,8 @@ interface CreateAppOptions {
|
|
|
1061
1082
|
rootDir?: string;
|
|
1062
1083
|
/** 源码目录前缀,覆盖环境变量 FAAPI_APP_DIR,默认 'src' */
|
|
1063
1084
|
appDir?: string;
|
|
1085
|
+
/** 产物输出目录,覆盖环境变量 FAAPI_OUT_DIR,默认 'dist' */
|
|
1086
|
+
outDir?: string;
|
|
1064
1087
|
/** 端口号,也可在 listen() 时传入;默认环境变量 PORT 或 3000 */
|
|
1065
1088
|
port?: number;
|
|
1066
1089
|
}
|
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();
|
|
@@ -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
|
}
|
|
@@ -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,7 +2502,9 @@ 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);
|
|
@@ -2567,6 +2625,7 @@ function createServer(options) {
|
|
|
2567
2625
|
middlewares: globalMiddlewares,
|
|
2568
2626
|
injectors: globalInjectors,
|
|
2569
2627
|
helmet: helmetOption,
|
|
2628
|
+
logger: loggerOption,
|
|
2570
2629
|
bodyLimit = DEFAULT_BODY_LIMIT,
|
|
2571
2630
|
http2: http2Option
|
|
2572
2631
|
} = options;
|
|
@@ -2578,6 +2637,8 @@ function createServer(options) {
|
|
|
2578
2637
|
const helmOpts = typeof helmetOption === "object" ? helmetOption : {};
|
|
2579
2638
|
configMiddlewares.push(helmet(helmOpts));
|
|
2580
2639
|
}
|
|
2640
|
+
const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
|
|
2641
|
+
if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
|
|
2581
2642
|
const server = (() => {
|
|
2582
2643
|
if (http2Option) {
|
|
2583
2644
|
const h2Opts = typeof http2Option === "object" ? http2Option : {};
|
|
@@ -2883,7 +2944,7 @@ function isFaapiConfigKey(key) {
|
|
|
2883
2944
|
}
|
|
2884
2945
|
async function createAppBase(options) {
|
|
2885
2946
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
2886
|
-
const outDir = process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
|
|
2947
|
+
const outDir = options?.outDir ?? process.env.FAAPI_OUT_DIR ?? DEFAULT_OUT_DIR;
|
|
2887
2948
|
const routesPath = path7.resolve(rootDir, outDir, ROUTES_FILE);
|
|
2888
2949
|
if (!fs4.existsSync(routesPath)) {
|
|
2889
2950
|
throw new Error(
|
|
@@ -2919,6 +2980,7 @@ async function createAppBase(options) {
|
|
|
2919
2980
|
middlewares: config?.middlewares,
|
|
2920
2981
|
injectors: config?.injectors,
|
|
2921
2982
|
helmet: config?.helmet,
|
|
2983
|
+
logger: config?.logger,
|
|
2922
2984
|
bodyLimit: config?.bodyLimit,
|
|
2923
2985
|
http2: config?.http2
|
|
2924
2986
|
});
|