@faapi/faapi 1.2.0 → 1.3.0
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 +117 -76
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +128 -1
- package/dist/index.js +318 -274
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1505,9 +1505,14 @@ function getVitestImportActual() {
|
|
|
1505
1505
|
if (typeof vi?.importActual !== "function") return void 0;
|
|
1506
1506
|
return vi.importActual.bind(vi);
|
|
1507
1507
|
}
|
|
1508
|
-
async function importWithCacheBust(filePath) {
|
|
1508
|
+
async function importWithCacheBust(filePath, bustViteCache = false) {
|
|
1509
1509
|
const importActual = getVitestImportActual();
|
|
1510
1510
|
if (importActual) {
|
|
1511
|
+
if (bustViteCache) {
|
|
1512
|
+
let url2 = pathToFileURL(filePath).href;
|
|
1513
|
+
url2 += `?t=${Date.now()}`;
|
|
1514
|
+
return await import(url2);
|
|
1515
|
+
}
|
|
1511
1516
|
return await importActual(filePath);
|
|
1512
1517
|
}
|
|
1513
1518
|
let url = pathToFileURL(filePath).href;
|
|
@@ -1868,6 +1873,41 @@ function createContext(request, params, config = {}, ip = "") {
|
|
|
1868
1873
|
ctxWithSse.__sseResponse = writer.response;
|
|
1869
1874
|
ctxWithSse.__sseWriter = writer;
|
|
1870
1875
|
return writer;
|
|
1876
|
+
},
|
|
1877
|
+
/**
|
|
1878
|
+
* 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
|
|
1879
|
+
*
|
|
1880
|
+
* 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
|
|
1881
|
+
* handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
|
|
1882
|
+
*/
|
|
1883
|
+
ok(data) {
|
|
1884
|
+
const responseConfig = config.response;
|
|
1885
|
+
const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
|
|
1886
|
+
const body = okFn(data);
|
|
1887
|
+
return ctx.json(body);
|
|
1888
|
+
},
|
|
1889
|
+
/**
|
|
1890
|
+
* 返回错误响应(对象形式参数,status 和 code 均可省略)
|
|
1891
|
+
*
|
|
1892
|
+
* - status 省略时 HTTP 状态码默认 500
|
|
1893
|
+
* - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
|
|
1894
|
+
* - status 和 code 独立无关联
|
|
1895
|
+
*
|
|
1896
|
+
* body 用 config.response.fail(或默认实现)包装。
|
|
1897
|
+
*/
|
|
1898
|
+
fail(options) {
|
|
1899
|
+
const responseConfig = config.response;
|
|
1900
|
+
const failFn = responseConfig?.fail ?? ((e) => {
|
|
1901
|
+
const error = { message: e.message };
|
|
1902
|
+
if (e.code !== void 0) error.code = e.code;
|
|
1903
|
+
return { error };
|
|
1904
|
+
});
|
|
1905
|
+
const body = failFn({
|
|
1906
|
+
status: options.status,
|
|
1907
|
+
code: options.code,
|
|
1908
|
+
message: options.message
|
|
1909
|
+
});
|
|
1910
|
+
return ctx.json(body, options.status ?? 500);
|
|
1871
1911
|
}
|
|
1872
1912
|
};
|
|
1873
1913
|
const extend = config?.extendContext;
|
|
@@ -2047,6 +2087,12 @@ async function injectParamsAsync(handler, ctx, body, injectors) {
|
|
|
2047
2087
|
}
|
|
2048
2088
|
|
|
2049
2089
|
// src/runtime/invokeHandler.ts
|
|
2090
|
+
function wrapResult(result, ctx) {
|
|
2091
|
+
if (result instanceof Response) return result;
|
|
2092
|
+
const responseConfig = ctx.config.response;
|
|
2093
|
+
const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
|
|
2094
|
+
return okFn(result);
|
|
2095
|
+
}
|
|
2050
2096
|
function mergeMeta(response, meta) {
|
|
2051
2097
|
const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
|
|
2052
2098
|
if (!hasMeta) return response;
|
|
@@ -2111,7 +2157,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
2111
2157
|
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
2112
2158
|
const sseResponse = pickSseAndAutoClose();
|
|
2113
2159
|
if (sseResponse) return sseResponse;
|
|
2114
|
-
return toResponse(result, meta);
|
|
2160
|
+
return toResponse(wrapResult(result, ctx), meta);
|
|
2115
2161
|
} catch (err) {
|
|
2116
2162
|
autoCloseSseOnError();
|
|
2117
2163
|
throw err;
|
|
@@ -2122,7 +2168,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
2122
2168
|
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
2123
2169
|
const sseResponse = pickSseAndAutoClose();
|
|
2124
2170
|
if (sseResponse) return sseResponse;
|
|
2125
|
-
return toResponse(result, meta);
|
|
2171
|
+
return toResponse(wrapResult(result, ctx), meta);
|
|
2126
2172
|
} catch (err) {
|
|
2127
2173
|
autoCloseSseOnError();
|
|
2128
2174
|
throw err;
|
|
@@ -2134,7 +2180,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
2134
2180
|
// src/testServer.ts
|
|
2135
2181
|
import path12 from "path";
|
|
2136
2182
|
import os from "os";
|
|
2137
|
-
import
|
|
2183
|
+
import fs11 from "fs/promises";
|
|
2138
2184
|
|
|
2139
2185
|
// src/router/scanRoutes.ts
|
|
2140
2186
|
import fg from "fast-glob";
|
|
@@ -2398,205 +2444,6 @@ init_generateSchemaFiles();
|
|
|
2398
2444
|
|
|
2399
2445
|
// src/validator/validateInput.ts
|
|
2400
2446
|
init_schemaName();
|
|
2401
|
-
var moduleCache = /* @__PURE__ */ new Map();
|
|
2402
|
-
function invalidateSchemaCache() {
|
|
2403
|
-
moduleCache.clear();
|
|
2404
|
-
}
|
|
2405
|
-
async function loadSchemaModule(schemaPath) {
|
|
2406
|
-
let mod = moduleCache.get(schemaPath);
|
|
2407
|
-
if (!mod) {
|
|
2408
|
-
mod = await importWithCacheBust(schemaPath);
|
|
2409
|
-
moduleCache.set(schemaPath, mod);
|
|
2410
|
-
}
|
|
2411
|
-
return mod;
|
|
2412
|
-
}
|
|
2413
|
-
async function validateInput(schemaPath, method, inputType, input) {
|
|
2414
|
-
const schemaName = getSchemaName(method, inputType);
|
|
2415
|
-
const schemaKey = `${schemaName}Schema`;
|
|
2416
|
-
let mod;
|
|
2417
|
-
try {
|
|
2418
|
-
mod = await loadSchemaModule(schemaPath);
|
|
2419
|
-
} catch (err) {
|
|
2420
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
2421
|
-
throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
|
|
2422
|
-
}
|
|
2423
|
-
const schema = mod[schemaKey];
|
|
2424
|
-
if (schema === void 0 || schema === null) {
|
|
2425
|
-
const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
2426
|
-
return { valid: true, issues: [], data };
|
|
2427
|
-
}
|
|
2428
|
-
if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
|
|
2429
|
-
throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
|
|
2430
|
-
}
|
|
2431
|
-
const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
2432
|
-
const zodSchema = schema;
|
|
2433
|
-
const result = zodSchema.safeParse(inputObj);
|
|
2434
|
-
if (result.success) {
|
|
2435
|
-
const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
|
|
2436
|
-
return { valid: true, issues: [], data };
|
|
2437
|
-
}
|
|
2438
|
-
const issues = mapZodIssues(result.error);
|
|
2439
|
-
return { valid: false, issues, data: inputObj };
|
|
2440
|
-
}
|
|
2441
|
-
function mapZodIssues(error) {
|
|
2442
|
-
return error.issues.map((issue) => {
|
|
2443
|
-
const code = mapZodCode(issue.code, issue.message);
|
|
2444
|
-
const path15 = issue.path.map(String).join(".") || "";
|
|
2445
|
-
return {
|
|
2446
|
-
path: path15,
|
|
2447
|
-
code,
|
|
2448
|
-
expected: issue.expected ?? mapExpectedFromMessage(issue.message),
|
|
2449
|
-
received: issue.received ?? mapReceivedFromMessage(issue.message),
|
|
2450
|
-
message: issue.message
|
|
2451
|
-
};
|
|
2452
|
-
});
|
|
2453
|
-
}
|
|
2454
|
-
function mapZodCode(zodCode, message) {
|
|
2455
|
-
switch (zodCode) {
|
|
2456
|
-
case "invalid_type":
|
|
2457
|
-
case "invalid_union":
|
|
2458
|
-
case "invalid_union_discriminator":
|
|
2459
|
-
return "TYPE_MISMATCH";
|
|
2460
|
-
case "unrecognized_keys":
|
|
2461
|
-
return "INVALID_FORMAT";
|
|
2462
|
-
case "invalid_value":
|
|
2463
|
-
case "invalid_string":
|
|
2464
|
-
case "too_small":
|
|
2465
|
-
case "too_big":
|
|
2466
|
-
case "invalid_intersection_types":
|
|
2467
|
-
case "not_multiple_of":
|
|
2468
|
-
return "INVALID_VALUE";
|
|
2469
|
-
case "custom":
|
|
2470
|
-
return "INVALID_VALUE";
|
|
2471
|
-
default:
|
|
2472
|
-
if (message.includes("Required") || message.includes("required")) {
|
|
2473
|
-
return "MISSING_FIELD";
|
|
2474
|
-
}
|
|
2475
|
-
return "INVALID_VALUE";
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
function mapExpectedFromMessage(message) {
|
|
2479
|
-
const match = message.match(/Expected\s+(\w+)/i);
|
|
2480
|
-
return match ? match[1].toLowerCase() : "unknown";
|
|
2481
|
-
}
|
|
2482
|
-
function mapReceivedFromMessage(message) {
|
|
2483
|
-
const match = message.match(/received\s+(\w+)/i);
|
|
2484
|
-
return match ? match[1].toLowerCase() : "unknown";
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
// src/server/createServer.ts
|
|
2488
|
-
import {
|
|
2489
|
-
createServer as createHttpServer
|
|
2490
|
-
} from "http";
|
|
2491
|
-
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
2492
|
-
import { readFileSync } from "fs";
|
|
2493
|
-
import { Readable as Readable2 } from "stream";
|
|
2494
|
-
import path11 from "path";
|
|
2495
|
-
|
|
2496
|
-
// src/router/matchRoute.ts
|
|
2497
|
-
function matchRoute(routes, method, path15) {
|
|
2498
|
-
for (const route of routes) {
|
|
2499
|
-
if (route.method !== method) {
|
|
2500
|
-
continue;
|
|
2501
|
-
}
|
|
2502
|
-
if (!route.isDynamic) {
|
|
2503
|
-
if (route.urlPath === path15) {
|
|
2504
|
-
return { route, params: {} };
|
|
2505
|
-
}
|
|
2506
|
-
continue;
|
|
2507
|
-
}
|
|
2508
|
-
const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
|
|
2509
|
-
if (params !== null) {
|
|
2510
|
-
return { route, params };
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
return null;
|
|
2514
|
-
}
|
|
2515
|
-
function matchWsRoute(wsRoutes, path15) {
|
|
2516
|
-
for (const route of wsRoutes) {
|
|
2517
|
-
if (!route.isDynamic) {
|
|
2518
|
-
if (route.urlPath === path15) {
|
|
2519
|
-
return { route, params: {} };
|
|
2520
|
-
}
|
|
2521
|
-
continue;
|
|
2522
|
-
}
|
|
2523
|
-
const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
|
|
2524
|
-
if (params !== null) {
|
|
2525
|
-
return { route, params };
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return null;
|
|
2529
|
-
}
|
|
2530
|
-
function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
|
|
2531
|
-
const patternSegments = pattern.split("/").filter(Boolean);
|
|
2532
|
-
const pathSegments = path15.split("/").filter(Boolean);
|
|
2533
|
-
if (isCatchAll) {
|
|
2534
|
-
const nonCatchAllCount = patternSegments.length - 1;
|
|
2535
|
-
if (pathSegments.length <= nonCatchAllCount) {
|
|
2536
|
-
return null;
|
|
2537
|
-
}
|
|
2538
|
-
const params2 = {};
|
|
2539
|
-
for (let i = 0; i < nonCatchAllCount; i++) {
|
|
2540
|
-
const patternSeg = patternSegments[i];
|
|
2541
|
-
const pathSeg = pathSegments[i];
|
|
2542
|
-
if (patternSeg.startsWith(":")) {
|
|
2543
|
-
const paramName = patternSeg.slice(1);
|
|
2544
|
-
params2[paramName] = pathSeg;
|
|
2545
|
-
} else if (patternSeg !== pathSeg) {
|
|
2546
|
-
return null;
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
|
|
2550
|
-
const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
|
|
2551
|
-
params2[catchAllParamName] = catchAllValue;
|
|
2552
|
-
if (Object.keys(params2).length !== paramNames.length) {
|
|
2553
|
-
return null;
|
|
2554
|
-
}
|
|
2555
|
-
return params2;
|
|
2556
|
-
}
|
|
2557
|
-
if (patternSegments.length !== pathSegments.length) {
|
|
2558
|
-
return null;
|
|
2559
|
-
}
|
|
2560
|
-
const params = {};
|
|
2561
|
-
for (let i = 0; i < patternSegments.length; i++) {
|
|
2562
|
-
const patternSeg = patternSegments[i];
|
|
2563
|
-
const pathSeg = pathSegments[i];
|
|
2564
|
-
if (patternSeg.startsWith(":")) {
|
|
2565
|
-
const paramName = patternSeg.slice(1);
|
|
2566
|
-
params[paramName] = pathSeg;
|
|
2567
|
-
} else if (patternSeg !== pathSeg) {
|
|
2568
|
-
return null;
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
if (Object.keys(params).length !== paramNames.length) {
|
|
2572
|
-
return null;
|
|
2573
|
-
}
|
|
2574
|
-
return params;
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
// src/loader/resolveExports.ts
|
|
2578
|
-
function resolveExport(module, exportName) {
|
|
2579
|
-
if (exportName in module && typeof module[exportName] !== "undefined") {
|
|
2580
|
-
return module[exportName];
|
|
2581
|
-
}
|
|
2582
|
-
const defaultExport = module.default;
|
|
2583
|
-
if (defaultExport !== null && typeof defaultExport === "object") {
|
|
2584
|
-
const value = defaultExport[exportName];
|
|
2585
|
-
if (value !== void 0) {
|
|
2586
|
-
return value;
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
return void 0;
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
// src/loader/validateRouteModule.ts
|
|
2593
|
-
function validateRouteModule(value, method, filePath) {
|
|
2594
|
-
if (typeof value !== "function") {
|
|
2595
|
-
throw new Error(
|
|
2596
|
-
`Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
|
|
2597
|
-
);
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
2447
|
|
|
2601
2448
|
// src/cli/compileOnDemand.ts
|
|
2602
2449
|
import path9 from "path";
|
|
@@ -2945,32 +2792,232 @@ function getDevDist() {
|
|
|
2945
2792
|
return devDistDir;
|
|
2946
2793
|
}
|
|
2947
2794
|
|
|
2948
|
-
// src/
|
|
2949
|
-
|
|
2950
|
-
|
|
2795
|
+
// src/validator/validateInput.ts
|
|
2796
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
2797
|
+
function invalidateSchemaCache() {
|
|
2798
|
+
moduleCache.clear();
|
|
2799
|
+
}
|
|
2800
|
+
async function loadSchemaModule(schemaPath) {
|
|
2801
|
+
let mod = moduleCache.get(schemaPath);
|
|
2802
|
+
if (!mod) {
|
|
2803
|
+
mod = await importWithCacheBust(schemaPath, isDevOnDemandEnabled());
|
|
2804
|
+
moduleCache.set(schemaPath, mod);
|
|
2805
|
+
}
|
|
2806
|
+
return mod;
|
|
2807
|
+
}
|
|
2808
|
+
async function validateInput(schemaPath, method, inputType, input) {
|
|
2809
|
+
const schemaName = getSchemaName(method, inputType);
|
|
2810
|
+
const schemaKey = `${schemaName}Schema`;
|
|
2811
|
+
let mod;
|
|
2951
2812
|
try {
|
|
2952
|
-
|
|
2813
|
+
mod = await loadSchemaModule(schemaPath);
|
|
2953
2814
|
} catch (err) {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2815
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2816
|
+
throw new InternalError(`Schema \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: ${schemaPath}: ${reason}`);
|
|
2817
|
+
}
|
|
2818
|
+
const schema = mod[schemaKey];
|
|
2819
|
+
if (schema === void 0 || schema === null) {
|
|
2820
|
+
const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
2821
|
+
return { valid: true, issues: [], data };
|
|
2822
|
+
}
|
|
2823
|
+
if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
|
|
2824
|
+
throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
|
|
2825
|
+
}
|
|
2826
|
+
const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
2827
|
+
const zodSchema = schema;
|
|
2828
|
+
const result = zodSchema.safeParse(inputObj);
|
|
2829
|
+
if (result.success) {
|
|
2830
|
+
const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
|
|
2831
|
+
return { valid: true, issues: [], data };
|
|
2832
|
+
}
|
|
2833
|
+
const issues = mapZodIssues(result.error);
|
|
2834
|
+
return { valid: false, issues, data: inputObj };
|
|
2835
|
+
}
|
|
2836
|
+
function mapZodIssues(error) {
|
|
2837
|
+
return error.issues.map((issue) => {
|
|
2838
|
+
const code = mapZodCode(issue.code, issue.message);
|
|
2839
|
+
const path15 = issue.path.map(String).join(".") || "";
|
|
2840
|
+
return {
|
|
2841
|
+
path: path15,
|
|
2842
|
+
code,
|
|
2843
|
+
expected: issue.expected ?? mapExpectedFromMessage(issue.message),
|
|
2844
|
+
received: issue.received ?? mapReceivedFromMessage(issue.message),
|
|
2845
|
+
message: issue.message
|
|
2846
|
+
};
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
function mapZodCode(zodCode, message) {
|
|
2850
|
+
switch (zodCode) {
|
|
2851
|
+
case "invalid_type":
|
|
2852
|
+
case "invalid_union":
|
|
2853
|
+
case "invalid_union_discriminator":
|
|
2854
|
+
return "TYPE_MISMATCH";
|
|
2855
|
+
case "unrecognized_keys":
|
|
2856
|
+
return "INVALID_FORMAT";
|
|
2857
|
+
case "invalid_value":
|
|
2858
|
+
case "invalid_string":
|
|
2859
|
+
case "too_small":
|
|
2860
|
+
case "too_big":
|
|
2861
|
+
case "invalid_intersection_types":
|
|
2862
|
+
case "not_multiple_of":
|
|
2863
|
+
return "INVALID_VALUE";
|
|
2864
|
+
case "custom":
|
|
2865
|
+
return "INVALID_VALUE";
|
|
2866
|
+
default:
|
|
2867
|
+
if (message.includes("Required") || message.includes("required")) {
|
|
2868
|
+
return "MISSING_FIELD";
|
|
2869
|
+
}
|
|
2870
|
+
return "INVALID_VALUE";
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function mapExpectedFromMessage(message) {
|
|
2874
|
+
const match = message.match(/Expected\s+(\w+)/i);
|
|
2875
|
+
return match ? match[1].toLowerCase() : "unknown";
|
|
2876
|
+
}
|
|
2877
|
+
function mapReceivedFromMessage(message) {
|
|
2878
|
+
const match = message.match(/received\s+(\w+)/i);
|
|
2879
|
+
return match ? match[1].toLowerCase() : "unknown";
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// src/server/createServer.ts
|
|
2883
|
+
import {
|
|
2884
|
+
createServer as createHttpServer
|
|
2885
|
+
} from "http";
|
|
2886
|
+
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
2887
|
+
import { readFileSync } from "fs";
|
|
2888
|
+
import { Readable as Readable2 } from "stream";
|
|
2889
|
+
import path11 from "path";
|
|
2890
|
+
|
|
2891
|
+
// src/router/matchRoute.ts
|
|
2892
|
+
function matchRoute(routes, method, path15) {
|
|
2893
|
+
for (const route of routes) {
|
|
2894
|
+
if (route.method !== method) {
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
if (!route.isDynamic) {
|
|
2898
|
+
if (route.urlPath === path15) {
|
|
2899
|
+
return { route, params: {} };
|
|
2900
|
+
}
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
|
|
2904
|
+
if (params !== null) {
|
|
2905
|
+
return { route, params };
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
function matchWsRoute(wsRoutes, path15) {
|
|
2911
|
+
for (const route of wsRoutes) {
|
|
2912
|
+
if (!route.isDynamic) {
|
|
2913
|
+
if (route.urlPath === path15) {
|
|
2914
|
+
return { route, params: {} };
|
|
2915
|
+
}
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const params = matchDynamicPath(route.urlPath, path15, route.paramNames, route.isCatchAll);
|
|
2919
|
+
if (params !== null) {
|
|
2920
|
+
return { route, params };
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return null;
|
|
2924
|
+
}
|
|
2925
|
+
function matchDynamicPath(pattern, path15, paramNames, isCatchAll) {
|
|
2926
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
2927
|
+
const pathSegments = path15.split("/").filter(Boolean);
|
|
2928
|
+
if (isCatchAll) {
|
|
2929
|
+
const nonCatchAllCount = patternSegments.length - 1;
|
|
2930
|
+
if (pathSegments.length <= nonCatchAllCount) {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
const params2 = {};
|
|
2934
|
+
for (let i = 0; i < nonCatchAllCount; i++) {
|
|
2935
|
+
const patternSeg = patternSegments[i];
|
|
2936
|
+
const pathSeg = pathSegments[i];
|
|
2937
|
+
if (patternSeg.startsWith(":")) {
|
|
2938
|
+
const paramName = patternSeg.slice(1);
|
|
2939
|
+
params2[paramName] = pathSeg;
|
|
2940
|
+
} else if (patternSeg !== pathSeg) {
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const catchAllValue = pathSegments.slice(nonCatchAllCount).join("/");
|
|
2945
|
+
const catchAllParamName = patternSegments[nonCatchAllCount].slice(4);
|
|
2946
|
+
params2[catchAllParamName] = catchAllValue;
|
|
2947
|
+
if (Object.keys(params2).length !== paramNames.length) {
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
return params2;
|
|
2951
|
+
}
|
|
2952
|
+
if (patternSegments.length !== pathSegments.length) {
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
const params = {};
|
|
2956
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
2957
|
+
const patternSeg = patternSegments[i];
|
|
2958
|
+
const pathSeg = pathSegments[i];
|
|
2959
|
+
if (patternSeg.startsWith(":")) {
|
|
2960
|
+
const paramName = patternSeg.slice(1);
|
|
2961
|
+
params[paramName] = pathSeg;
|
|
2962
|
+
} else if (patternSeg !== pathSeg) {
|
|
2963
|
+
return null;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (Object.keys(params).length !== paramNames.length) {
|
|
2967
|
+
return null;
|
|
2968
|
+
}
|
|
2969
|
+
return params;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// src/loader/loadRouteModule.ts
|
|
2973
|
+
import fs9 from "fs";
|
|
2974
|
+
|
|
2975
|
+
// src/loader/resolveExports.ts
|
|
2976
|
+
function resolveExport(module, exportName) {
|
|
2977
|
+
if (exportName in module && typeof module[exportName] !== "undefined") {
|
|
2978
|
+
return module[exportName];
|
|
2979
|
+
}
|
|
2980
|
+
const defaultExport = module.default;
|
|
2981
|
+
if (defaultExport !== null && typeof defaultExport === "object") {
|
|
2982
|
+
const value = defaultExport[exportName];
|
|
2983
|
+
if (value !== void 0) {
|
|
2984
|
+
return value;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return void 0;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/loader/validateRouteModule.ts
|
|
2991
|
+
function validateRouteModule(value, method, filePath) {
|
|
2992
|
+
if (typeof value !== "function") {
|
|
2993
|
+
throw new Error(
|
|
2994
|
+
`Route module "${filePath}" does not export a valid handler for method "${method}". Expected a function, got ${typeof value}.`
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// src/loader/loadRouteModule.ts
|
|
3000
|
+
async function loadRouteModule(filePath, method, rootDir) {
|
|
3001
|
+
if (isDevOnDemandEnabled() && rootDir) {
|
|
3002
|
+
const dist = getDevDist();
|
|
3003
|
+
if (dist) {
|
|
3004
|
+
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
3005
|
+
if (sourcePath && fs9.existsSync(sourcePath)) {
|
|
2958
3006
|
try {
|
|
2959
|
-
|
|
2960
|
-
if (compiled) {
|
|
2961
|
-
module = await importWithCacheBust(filePath);
|
|
2962
|
-
const handler2 = resolveExport(module, method);
|
|
2963
|
-
validateRouteModule(handler2, method, filePath);
|
|
2964
|
-
return { handler: handler2, method };
|
|
2965
|
-
}
|
|
3007
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
2966
3008
|
} catch (compileErr) {
|
|
2967
|
-
const
|
|
2968
|
-
throw new Error(`Failed to compile route module "${sourcePath}": ${
|
|
3009
|
+
const reason = compileErr instanceof Error ? compileErr.message : String(compileErr);
|
|
3010
|
+
throw new Error(`Failed to compile route module "${sourcePath}": ${reason}`, {
|
|
2969
3011
|
cause: compileErr
|
|
2970
3012
|
});
|
|
2971
3013
|
}
|
|
2972
3014
|
}
|
|
2973
3015
|
}
|
|
3016
|
+
}
|
|
3017
|
+
let module;
|
|
3018
|
+
try {
|
|
3019
|
+
module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
|
|
3020
|
+
} catch (err) {
|
|
2974
3021
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2975
3022
|
throw new Error(`Failed to load route module "${filePath}": ${reason}`, { cause: err });
|
|
2976
3023
|
}
|
|
@@ -3105,6 +3152,7 @@ function getClientIp(req) {
|
|
|
3105
3152
|
}
|
|
3106
3153
|
|
|
3107
3154
|
// src/server/handleWsUpgrade.ts
|
|
3155
|
+
import fs10 from "fs";
|
|
3108
3156
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3109
3157
|
import path10 from "path";
|
|
3110
3158
|
|
|
@@ -3204,24 +3252,16 @@ function getPathname(req) {
|
|
|
3204
3252
|
return idx >= 0 ? url.slice(0, idx) : url;
|
|
3205
3253
|
}
|
|
3206
3254
|
async function loadWsHandler(filePath, ctx, rootDir) {
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
if (dist) {
|
|
3214
|
-
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
3215
|
-
const compiled = await ensureCompiled(sourcePath, rootDir, dist);
|
|
3216
|
-
if (compiled) {
|
|
3217
|
-
module = await importWithCacheBust(filePath);
|
|
3218
|
-
}
|
|
3255
|
+
if (isDevOnDemandEnabled() && rootDir) {
|
|
3256
|
+
const dist = getDevDist();
|
|
3257
|
+
if (dist) {
|
|
3258
|
+
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
3259
|
+
if (sourcePath && fs10.existsSync(sourcePath)) {
|
|
3260
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3219
3261
|
}
|
|
3220
3262
|
}
|
|
3221
|
-
if (!module) {
|
|
3222
|
-
throw err;
|
|
3223
|
-
}
|
|
3224
3263
|
}
|
|
3264
|
+
const module = await importWithCacheBust(filePath, isDevOnDemandEnabled());
|
|
3225
3265
|
const handler = module["WS"];
|
|
3226
3266
|
if (typeof handler !== "function") {
|
|
3227
3267
|
throw new Error(`WS export not found in ${filePath}`);
|
|
@@ -3569,7 +3609,7 @@ async function createTestServer(options) {
|
|
|
3569
3609
|
} = options;
|
|
3570
3610
|
const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
|
|
3571
3611
|
const sorted = sortRoutes(routes);
|
|
3572
|
-
const schemaDist = dist ? path12.isAbsolute(dist) ? dist : path12.resolve(rootDir, dist) : await
|
|
3612
|
+
const schemaDist = dist ? path12.isAbsolute(dist) ? dist : path12.resolve(rootDir, dist) : await fs11.mkdtemp(path12.join(os.tmpdir(), "faapi-test-schema-"));
|
|
3573
3613
|
await generateSchemaFiles(sorted, rootDir, schemaDist);
|
|
3574
3614
|
const { server } = createServer({
|
|
3575
3615
|
routes: sorted,
|
|
@@ -3602,7 +3642,7 @@ async function createTestServer(options) {
|
|
|
3602
3642
|
await new Promise((resolve) => {
|
|
3603
3643
|
server.close(() => resolve());
|
|
3604
3644
|
});
|
|
3605
|
-
await
|
|
3645
|
+
await fs11.rm(schemaDist, { recursive: true, force: true }).catch(() => {
|
|
3606
3646
|
});
|
|
3607
3647
|
invalidateSchemaCache();
|
|
3608
3648
|
}
|
|
@@ -3744,7 +3784,7 @@ async function connectWs(baseUrl, pathname, options = {}) {
|
|
|
3744
3784
|
}
|
|
3745
3785
|
|
|
3746
3786
|
// src/cli/createAppCore.ts
|
|
3747
|
-
import
|
|
3787
|
+
import fs13 from "fs";
|
|
3748
3788
|
import path14 from "path";
|
|
3749
3789
|
import { PassThrough } from "stream";
|
|
3750
3790
|
|
|
@@ -3802,7 +3842,7 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
|
|
|
3802
3842
|
}
|
|
3803
3843
|
|
|
3804
3844
|
// src/cli/generateRoutes.ts
|
|
3805
|
-
import
|
|
3845
|
+
import fs12 from "fs";
|
|
3806
3846
|
import path13 from "path";
|
|
3807
3847
|
async function hydrateRoutes(manifest) {
|
|
3808
3848
|
const hydrateRoute = (serialized) => ({
|
|
@@ -3901,7 +3941,8 @@ var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
3901
3941
|
"helmet",
|
|
3902
3942
|
"bodyLimit",
|
|
3903
3943
|
"logger",
|
|
3904
|
-
"http2"
|
|
3944
|
+
"http2",
|
|
3945
|
+
"response"
|
|
3905
3946
|
]);
|
|
3906
3947
|
function isFaapiConfigKey(key) {
|
|
3907
3948
|
return FAAPI_CONFIG_KEYS.has(key);
|
|
@@ -3910,7 +3951,7 @@ async function createAppBase(options) {
|
|
|
3910
3951
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
3911
3952
|
const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
|
|
3912
3953
|
const routesPath = path14.resolve(rootDir, dist, ROUTES_FILE);
|
|
3913
|
-
if (!
|
|
3954
|
+
if (!fs13.existsSync(routesPath)) {
|
|
3914
3955
|
throw new Error(
|
|
3915
3956
|
`[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
3916
3957
|
);
|
|
@@ -4007,41 +4048,40 @@ async function createAppBase(options) {
|
|
|
4007
4048
|
} = injectOpts ?? {};
|
|
4008
4049
|
const queryStr = query ? "?" + new URLSearchParams(Object.entries(query).map(([k, v]) => [k, String(v)])).toString() : "";
|
|
4009
4050
|
return new Promise((resolve, reject) => {
|
|
4010
|
-
const
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
}
|
|
4027
|
-
},
|
|
4028
|
-
end(data) {
|
|
4029
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data ?? "");
|
|
4030
|
-
this._body = buf;
|
|
4031
|
-
resolve({
|
|
4032
|
-
status: this.statusCode,
|
|
4033
|
-
headers: new Headers(this._headers),
|
|
4034
|
-
body: this.parseBody()
|
|
4035
|
-
});
|
|
4036
|
-
},
|
|
4037
|
-
parseBody() {
|
|
4038
|
-
try {
|
|
4039
|
-
return JSON.parse(this._body.toString());
|
|
4040
|
-
} catch {
|
|
4041
|
-
return this._body.toString();
|
|
4042
|
-
}
|
|
4051
|
+
const chunks = [];
|
|
4052
|
+
const mockRes = new PassThrough();
|
|
4053
|
+
mockRes.statusCode = 200;
|
|
4054
|
+
mockRes._headers = {};
|
|
4055
|
+
mockRes.setHeader = function(name, value) {
|
|
4056
|
+
this._headers[name.toLowerCase()] = value;
|
|
4057
|
+
};
|
|
4058
|
+
mockRes.appendHeader = function(name, value) {
|
|
4059
|
+
const key = name.toLowerCase();
|
|
4060
|
+
const existing = this._headers[key];
|
|
4061
|
+
this._headers[key] = existing ? `${existing}, ${value}` : value;
|
|
4062
|
+
};
|
|
4063
|
+
mockRes.writeHead = function(status, headers) {
|
|
4064
|
+
this.statusCode = status;
|
|
4065
|
+
if (headers) {
|
|
4066
|
+
Object.assign(this._headers, headers);
|
|
4043
4067
|
}
|
|
4044
4068
|
};
|
|
4069
|
+
mockRes.on("data", (chunk) => chunks.push(chunk));
|
|
4070
|
+
mockRes.on("error", reject);
|
|
4071
|
+
mockRes.on("finish", () => {
|
|
4072
|
+
const body2 = Buffer.concat(chunks);
|
|
4073
|
+
let parsed;
|
|
4074
|
+
try {
|
|
4075
|
+
parsed = JSON.parse(body2.toString());
|
|
4076
|
+
} catch {
|
|
4077
|
+
parsed = body2.toString();
|
|
4078
|
+
}
|
|
4079
|
+
resolve({
|
|
4080
|
+
status: mockRes.statusCode,
|
|
4081
|
+
headers: new Headers(mockRes._headers),
|
|
4082
|
+
body: parsed
|
|
4083
|
+
});
|
|
4084
|
+
});
|
|
4045
4085
|
const listeners = server.listeners("request");
|
|
4046
4086
|
const handler = listeners[listeners.length - 1];
|
|
4047
4087
|
if (typeof handler !== "function") {
|
|
@@ -4083,6 +4123,10 @@ async function createAppBase(options) {
|
|
|
4083
4123
|
if (config?.lifecycle?.onClose) {
|
|
4084
4124
|
await config.lifecycle.onClose({ rootDir, routes: sorted, server });
|
|
4085
4125
|
}
|
|
4126
|
+
if (!server.listening) {
|
|
4127
|
+
app.server = null;
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4086
4130
|
return new Promise((resolve) => {
|
|
4087
4131
|
server.close((err) => {
|
|
4088
4132
|
if (err) console.error("Error closing server:", err);
|