@faapi/faapi 6.21.1 → 6.23.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 +404 -259
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +31 -2
- package/dist/index.js +184 -114
- package/dist/index.js.map +1 -1
- package/dist/{routeTypes-gyz4PMh7.d.ts → routeTypes-CJV5M90s.d.ts} +39 -3
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +115 -50
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -762,7 +762,7 @@ type TaskFailedHandler = (info: TaskFailedInfo) => Promise<void> | void;
|
|
|
762
762
|
*
|
|
763
763
|
* 核心导出:
|
|
764
764
|
* - `encodeSseEvent(event)`:把 SSE 事件对象编码为符合 HTML5 SSE 规范的字符串
|
|
765
|
-
* - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/sendError API
|
|
765
|
+
* - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/onClose/sendError API
|
|
766
766
|
* - `SseWriter`:writer 类型,ctx.sse() 返回此类型
|
|
767
767
|
*
|
|
768
768
|
* 设计要点:
|
|
@@ -803,6 +803,20 @@ interface SseEvent {
|
|
|
803
803
|
/** 注释行(以 : 开头),用于 keep-alive 心跳 */
|
|
804
804
|
comment?: string;
|
|
805
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* ctx.sse() 的选项
|
|
808
|
+
*/
|
|
809
|
+
interface SseOptions {
|
|
810
|
+
/**
|
|
811
|
+
* 声明流生命周期独立于 handler 返回(长连接订阅模式)
|
|
812
|
+
*
|
|
813
|
+
* `true` 时 handler 返回后框架不自动 close——handler 只负责把连接挂到推送源
|
|
814
|
+
* (事件总线、change stream),注册完立即返回,连接长存直到显式 close()、
|
|
815
|
+
* 客户端断开或 handler 抛错兜底。清理推送源用 `onClose(callback)`。
|
|
816
|
+
* 默认 false(handler 返回时框架自动 close 兜底)。
|
|
817
|
+
*/
|
|
818
|
+
keepOpen?: boolean;
|
|
819
|
+
}
|
|
806
820
|
/**
|
|
807
821
|
* SSE writer:封装流式推送 API
|
|
808
822
|
*
|
|
@@ -827,10 +841,20 @@ interface SseWriter {
|
|
|
827
841
|
sendError(error: unknown): void;
|
|
828
842
|
/** 关闭流(多次调用安全) */
|
|
829
843
|
close(): void;
|
|
844
|
+
/**
|
|
845
|
+
* 注册流结束回调
|
|
846
|
+
*
|
|
847
|
+
* 显式 close、框架兜底 close、客户端断开、sendError 任一路径结束流时恰好触发
|
|
848
|
+
* 一次(多个回调按注册顺序执行,回调自身抛错被忽略);注册时流已结束则立即触发。
|
|
849
|
+
* keepOpen 模式下用于退订/清理推送源,防止客户端断开后订阅随重连累积泄漏。
|
|
850
|
+
*/
|
|
851
|
+
onClose(callback: () => void): void;
|
|
830
852
|
/** 流是否已关闭(handler 主动 close 或框架自动 close) */
|
|
831
853
|
readonly closed: boolean;
|
|
832
854
|
/** 客户端是否已断开(ReadableStream 被 cancel) */
|
|
833
855
|
readonly aborted: boolean;
|
|
856
|
+
/** 创建时是否声明了 `{ keepOpen: true }`(handler 返回后框架不自动 close) */
|
|
857
|
+
readonly keepOpen: boolean;
|
|
834
858
|
/**
|
|
835
859
|
* 流缓冲背压状态(透传 controller.desiredSize)
|
|
836
860
|
*
|
|
@@ -1001,6 +1025,14 @@ interface FaapiContext {
|
|
|
1001
1025
|
cookies: Record<string, string>;
|
|
1002
1026
|
/** 配置文件中的自定义业务配置(类型可通过 declare module '@faapi/faapi' 增强 FaapiContextConfig) */
|
|
1003
1027
|
config: FaapiContextConfig;
|
|
1028
|
+
/**
|
|
1029
|
+
* 运行时资源根目录绝对路径(<rootDir>/<dist>/resources)
|
|
1030
|
+
*
|
|
1031
|
+
* `src/resources/` 经 dev/build 复制进产物后的位置,handler 读静态文件用:
|
|
1032
|
+
* `await fs.readFile(path.join(ctx.resourcesDir, 'prompts/foo.md'))`。
|
|
1033
|
+
* HTTP/WS 请求链路恒有值;testing 直调(createTestContext)未传时为 undefined。
|
|
1034
|
+
*/
|
|
1035
|
+
resourcesDir?: string;
|
|
1004
1036
|
/**
|
|
1005
1037
|
* 设置响应状态码
|
|
1006
1038
|
*/
|
|
@@ -1061,8 +1093,12 @@ interface FaapiContext {
|
|
|
1061
1093
|
* sse.close();
|
|
1062
1094
|
* }
|
|
1063
1095
|
* ```
|
|
1096
|
+
*
|
|
1097
|
+
* @param options `{ keepOpen: true }` 声明流生命周期独立于 handler 返回(长连接
|
|
1098
|
+
* 订阅模式):handler 返回后框架不自动 close,连接长存直到显式 close()、客户端
|
|
1099
|
+
* 断开或 handler 抛错兜底;清理推送源用 `writer.onClose(callback)`。默认 false。
|
|
1064
1100
|
*/
|
|
1065
|
-
sse(): SseWriter;
|
|
1101
|
+
sse(options?: SseOptions): SseWriter;
|
|
1066
1102
|
/**
|
|
1067
1103
|
* 显式包装成功响应(返回 Response 对象)
|
|
1068
1104
|
*
|
|
@@ -1313,4 +1349,4 @@ interface RouteInfo {
|
|
|
1313
1349
|
output: RouteOutputSchema | null;
|
|
1314
1350
|
}
|
|
1315
1351
|
|
|
1316
|
-
export { type
|
|
1352
|
+
export { type ToolPathMeta as $, type AppRegistries as A, type RouteOutputSchema as B, type CorsOptions as C, type RouteParamSchema as D, type SseEvent as E, type FaapiMiddleware as F, type SseOptions as G, type HelmetOptions as H, type InjectorMap as I, type SseWriter as J, type TaskContext as K, type LogConfig as L, type TaskDriverJob as M, type TaskDriverProcess as N, type TaskDriverRecord as O, type TaskFailedInfo as P, type TaskJob as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type TaskJobStatus as U, type TaskMetadata as V, type WsRouteManifest as W, type TaskModule as X, type TaskRegistriesSnapshot as Y, type TaskRegistriesView as Z, type ToolCore as _, type FaapiContext as a, type ToolRegistry as a0, cors as a1, createAppRegistries as a2, createTaskRegistriesView as a3, createTaskRegistry as a4, helmet as a5, type TaskFailedHandler as b, type TaskQueueDeps as c, type TaskQueue as d, type TaskDriver as e, type TaskRegistry as f, type TaskManifest as g, type ToolMetadata as h, type AgentCore as i, type AgentMetadata as j, type CreateLoggerOptions as k, type Logger as l, type AgentHandleFactory as m, type AgentHandleStore as n, type AgentPathMeta as o, type AgentRegistry as p, type AgentToolDescriptor as q, type FaapiContextConfig as r, type FaapiTaskMeta as s, type FailOptions as t, type Injector as u, type LogEntry as v, type LogLevel as w, type LogSink as x, type RouteInfo as y, type RouteInputSchema as z };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as FaapiContext, F as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions } from './routeTypes-
|
|
1
|
+
import { a as FaapiContext, F as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions } from './routeTypes-CJV5M90s.js';
|
|
2
2
|
import { Server } from 'node:http';
|
|
3
3
|
import { WebSocket } from 'ws';
|
|
4
4
|
|
|
@@ -25,6 +25,8 @@ declare function createTestContext(options: CreateTestContextOptions): FaapiCont
|
|
|
25
25
|
interface CreateTestContextOptions {
|
|
26
26
|
/** app 级注册表(测试 handler 声明 agent/agents 参数时注入用,可选) */
|
|
27
27
|
registries?: FaapiContext['registries'];
|
|
28
|
+
/** 运行时资源根目录(测试 handler 读 ctx.resourcesDir 时传入,可选) */
|
|
29
|
+
resourcesDir?: string;
|
|
28
30
|
/** 请求方法,默认 'GET' */
|
|
29
31
|
method?: string;
|
|
30
32
|
/** 请求路径,必填,如 '/api/user'(无需写 host) */
|
package/dist/testing.js
CHANGED
|
@@ -155,13 +155,28 @@ function encodeSseEvent(event) {
|
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
157
|
function createSseWriter(options = {}) {
|
|
158
|
-
const { onFirstWrite } = options;
|
|
158
|
+
const { onFirstWrite, keepOpen = false } = options;
|
|
159
159
|
const encoder = new TextEncoder();
|
|
160
160
|
let controller = null;
|
|
161
161
|
let closed = false;
|
|
162
162
|
let aborted = false;
|
|
163
163
|
let firstWriteTriggered = false;
|
|
164
164
|
let drainWaiters = [];
|
|
165
|
+
let closeCallbacks = [];
|
|
166
|
+
let closeCallbacksFired = false;
|
|
167
|
+
const safeRunCallback = (callback) => {
|
|
168
|
+
try {
|
|
169
|
+
callback();
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const runCloseCallbacks = () => {
|
|
174
|
+
if (closeCallbacksFired) return;
|
|
175
|
+
closeCallbacksFired = true;
|
|
176
|
+
const callbacks = closeCallbacks;
|
|
177
|
+
closeCallbacks = [];
|
|
178
|
+
for (const callback of callbacks) safeRunCallback(callback);
|
|
179
|
+
};
|
|
165
180
|
const resolveDrainWaiters = () => {
|
|
166
181
|
const waiters = drainWaiters;
|
|
167
182
|
drainWaiters = [];
|
|
@@ -185,6 +200,7 @@ function createSseWriter(options = {}) {
|
|
|
185
200
|
aborted = true;
|
|
186
201
|
closed = true;
|
|
187
202
|
controller = null;
|
|
203
|
+
runCloseCallbacks();
|
|
188
204
|
resolveDrainWaiters();
|
|
189
205
|
}
|
|
190
206
|
},
|
|
@@ -227,6 +243,7 @@ function createSseWriter(options = {}) {
|
|
|
227
243
|
close() {
|
|
228
244
|
if (closed) return;
|
|
229
245
|
closed = true;
|
|
246
|
+
runCloseCallbacks();
|
|
230
247
|
resolveDrainWaiters();
|
|
231
248
|
if (controller) {
|
|
232
249
|
try {
|
|
@@ -236,6 +253,13 @@ function createSseWriter(options = {}) {
|
|
|
236
253
|
controller = null;
|
|
237
254
|
}
|
|
238
255
|
},
|
|
256
|
+
onClose(callback) {
|
|
257
|
+
if (closeCallbacksFired) {
|
|
258
|
+
safeRunCallback(callback);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
closeCallbacks.push(callback);
|
|
262
|
+
},
|
|
239
263
|
get closed() {
|
|
240
264
|
return closed;
|
|
241
265
|
},
|
|
@@ -252,6 +276,9 @@ function createSseWriter(options = {}) {
|
|
|
252
276
|
get aborted() {
|
|
253
277
|
return aborted;
|
|
254
278
|
},
|
|
279
|
+
get keepOpen() {
|
|
280
|
+
return keepOpen;
|
|
281
|
+
},
|
|
255
282
|
get response() {
|
|
256
283
|
return response;
|
|
257
284
|
}
|
|
@@ -285,14 +312,14 @@ var ValidationError = class extends FaapiError {
|
|
|
285
312
|
issues;
|
|
286
313
|
};
|
|
287
314
|
var RouteNotFoundError = class extends FaapiError {
|
|
288
|
-
constructor(
|
|
289
|
-
super("ROUTE_NOT_FOUND", `Route not found: ${
|
|
315
|
+
constructor(path17) {
|
|
316
|
+
super("ROUTE_NOT_FOUND", `Route not found: ${path17}`, 404);
|
|
290
317
|
this.name = "RouteNotFoundError";
|
|
291
318
|
}
|
|
292
319
|
};
|
|
293
320
|
var MethodNotAllowedError = class extends FaapiError {
|
|
294
|
-
constructor(method,
|
|
295
|
-
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${
|
|
321
|
+
constructor(method, path17, allowedMethods) {
|
|
322
|
+
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path17}`, 405);
|
|
296
323
|
this.allowedMethods = allowedMethods;
|
|
297
324
|
this.name = "MethodNotAllowedError";
|
|
298
325
|
}
|
|
@@ -557,10 +584,18 @@ function formatSetCookie(name, value, options) {
|
|
|
557
584
|
if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
558
585
|
return cookie;
|
|
559
586
|
}
|
|
560
|
-
function createContext(request, params, config = {}, ip = "", registries) {
|
|
561
|
-
return createContextFromUrl(
|
|
587
|
+
function createContext(request, params, config = {}, ip = "", registries, resourcesDir) {
|
|
588
|
+
return createContextFromUrl(
|
|
589
|
+
request,
|
|
590
|
+
new URL(request.url),
|
|
591
|
+
params,
|
|
592
|
+
config,
|
|
593
|
+
ip,
|
|
594
|
+
registries,
|
|
595
|
+
resourcesDir
|
|
596
|
+
);
|
|
562
597
|
}
|
|
563
|
-
function createContextFromUrl(request, url, params, config = {}, ip = "", registries) {
|
|
598
|
+
function createContextFromUrl(request, url, params, config = {}, ip = "", registries, resourcesDir) {
|
|
564
599
|
const meta = { headers: {}, setCookies: [] };
|
|
565
600
|
const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
|
|
566
601
|
const cookiesObj = {};
|
|
@@ -588,6 +623,9 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
588
623
|
ua: request.headers.get("user-agent") ?? "",
|
|
589
624
|
cookies: cookiesObj,
|
|
590
625
|
config,
|
|
626
|
+
// 运行时资源根目录(HTTP/WS 链路由 createServer/handleWsUpgrade 传入;
|
|
627
|
+
// testing 直调未传时为 undefined,见 contextTypes.resourcesDir 注释)
|
|
628
|
+
resourcesDir,
|
|
591
629
|
meta,
|
|
592
630
|
setStatus(status) {
|
|
593
631
|
meta.status = status;
|
|
@@ -633,10 +671,15 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
633
671
|
* 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
|
|
634
672
|
* 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
|
|
635
673
|
*
|
|
674
|
+
* `{ keepOpen: true }` 声明流生命周期独立于 handler 返回:handler 返回后框架
|
|
675
|
+
* 不自动 close(长连接订阅模式,清理推送源用 writer.onClose);handler 抛错
|
|
676
|
+
* 的兜底 close 不受影响。详见 runtime/sse.md「长连接订阅模式」。
|
|
677
|
+
*
|
|
636
678
|
* 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
|
|
637
679
|
*/
|
|
638
|
-
sse() {
|
|
680
|
+
sse(options) {
|
|
639
681
|
const writer = createSseWriter({
|
|
682
|
+
keepOpen: options?.keepOpen,
|
|
640
683
|
onFirstWrite: (response) => {
|
|
641
684
|
ctx.__earlyRespond?.(response);
|
|
642
685
|
}
|
|
@@ -688,15 +731,16 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
688
731
|
function createTestContext(options) {
|
|
689
732
|
const {
|
|
690
733
|
method = "GET",
|
|
691
|
-
path:
|
|
734
|
+
path: path17,
|
|
692
735
|
query,
|
|
693
736
|
headers,
|
|
694
737
|
params = {},
|
|
695
738
|
config = {},
|
|
696
739
|
ip = "",
|
|
697
|
-
registries
|
|
740
|
+
registries,
|
|
741
|
+
resourcesDir
|
|
698
742
|
} = options;
|
|
699
|
-
const url = new URL(`http://localhost${
|
|
743
|
+
const url = new URL(`http://localhost${path17}`);
|
|
700
744
|
if (query) {
|
|
701
745
|
for (const [key, value] of Object.entries(query)) {
|
|
702
746
|
if (Array.isArray(value)) {
|
|
@@ -712,7 +756,7 @@ function createTestContext(options) {
|
|
|
712
756
|
method,
|
|
713
757
|
headers
|
|
714
758
|
});
|
|
715
|
-
return createContext(request, params, config, ip, registries);
|
|
759
|
+
return createContext(request, params, config, ip, registries, resourcesDir);
|
|
716
760
|
}
|
|
717
761
|
|
|
718
762
|
// src/response/pendingMeta.ts
|
|
@@ -1265,7 +1309,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
1265
1309
|
const pickSseAndAutoClose = () => {
|
|
1266
1310
|
const sseWriter = ctx.__sseWriter;
|
|
1267
1311
|
if (!sseWriter) return null;
|
|
1268
|
-
if (!sseWriter.closed && !sseWriter.aborted) {
|
|
1312
|
+
if (!sseWriter.keepOpen && !sseWriter.closed && !sseWriter.aborted) {
|
|
1269
1313
|
sseWriter.close();
|
|
1270
1314
|
}
|
|
1271
1315
|
return mergeMeta(sseWriter.response, meta);
|
|
@@ -1295,9 +1339,9 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
1295
1339
|
}
|
|
1296
1340
|
|
|
1297
1341
|
// src/testServer.ts
|
|
1298
|
-
import
|
|
1342
|
+
import path16 from "path";
|
|
1299
1343
|
import os from "os";
|
|
1300
|
-
import
|
|
1344
|
+
import fs13 from "fs/promises";
|
|
1301
1345
|
|
|
1302
1346
|
// src/router/scanRoutes.ts
|
|
1303
1347
|
import fg from "fast-glob";
|
|
@@ -1309,9 +1353,9 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
|
1309
1353
|
var HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
1310
1354
|
|
|
1311
1355
|
// src/utils/normalizePath.ts
|
|
1312
|
-
function normalizePath(
|
|
1313
|
-
if (!
|
|
1314
|
-
let result =
|
|
1356
|
+
function normalizePath(path17) {
|
|
1357
|
+
if (!path17) return "";
|
|
1358
|
+
let result = path17.replace(/\\/g, "/");
|
|
1315
1359
|
result = result.replace(/\/+/g, "/");
|
|
1316
1360
|
result = result.replace(/\/+$/, "");
|
|
1317
1361
|
if (result && !result.startsWith("/")) {
|
|
@@ -3479,13 +3523,14 @@ function buildAliasPlugins(rootDir) {
|
|
|
3479
3523
|
}
|
|
3480
3524
|
|
|
3481
3525
|
// src/cli/compileSourceFiles.ts
|
|
3526
|
+
var SOURCE_IGNORE = ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts", `${APP_DIR}/resources/**`];
|
|
3482
3527
|
async function compileSourceFiles(options) {
|
|
3483
3528
|
const { rootDir, dist, files, logLevel = "silent", production, atomicWrite } = options;
|
|
3484
3529
|
const entryPoints = files ?? await fg2([`${APP_DIR}/**/*.ts`], {
|
|
3485
3530
|
cwd: rootDir,
|
|
3486
3531
|
onlyFiles: true,
|
|
3487
3532
|
absolute: true,
|
|
3488
|
-
ignore:
|
|
3533
|
+
ignore: SOURCE_IGNORE
|
|
3489
3534
|
});
|
|
3490
3535
|
if (entryPoints.length === 0) {
|
|
3491
3536
|
return { compiledFiles: [] };
|
|
@@ -3789,9 +3834,9 @@ async function validateInput(schemaPath, method, inputType, input) {
|
|
|
3789
3834
|
function mapZodIssues(error) {
|
|
3790
3835
|
return error.issues.map((issue) => {
|
|
3791
3836
|
const code = mapZodCode(issue);
|
|
3792
|
-
const
|
|
3837
|
+
const path17 = issue.path.map(String).join(".") || "";
|
|
3793
3838
|
return {
|
|
3794
|
-
path:
|
|
3839
|
+
path: path17,
|
|
3795
3840
|
code,
|
|
3796
3841
|
expected: issue.expected ?? mapExpectedFromMessage(issue.message),
|
|
3797
3842
|
received: issue.received ?? mapReceivedFromMessage(issue.message),
|
|
@@ -3841,7 +3886,7 @@ import {
|
|
|
3841
3886
|
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
3842
3887
|
import { readFileSync } from "fs";
|
|
3843
3888
|
import { Readable as Readable2 } from "stream";
|
|
3844
|
-
import
|
|
3889
|
+
import path15 from "path";
|
|
3845
3890
|
|
|
3846
3891
|
// src/router/matchRoute.ts
|
|
3847
3892
|
var httpIndexCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -3883,25 +3928,25 @@ function getWsIndex(routes) {
|
|
|
3883
3928
|
wsIndexCache.set(routes, index);
|
|
3884
3929
|
return index;
|
|
3885
3930
|
}
|
|
3886
|
-
function matchRoute(routes, method,
|
|
3931
|
+
function matchRoute(routes, method, path17) {
|
|
3887
3932
|
const index = getHttpIndex(routes);
|
|
3888
3933
|
const upper = method.toUpperCase();
|
|
3889
|
-
const hit = matchByMethod(index, upper,
|
|
3934
|
+
const hit = matchByMethod(index, upper, path17);
|
|
3890
3935
|
if (hit) return hit;
|
|
3891
3936
|
if (upper === "HEAD") {
|
|
3892
|
-
return matchByMethod(index, "GET",
|
|
3937
|
+
return matchByMethod(index, "GET", path17);
|
|
3893
3938
|
}
|
|
3894
3939
|
return null;
|
|
3895
3940
|
}
|
|
3896
|
-
function matchByMethod(index, method,
|
|
3897
|
-
const staticHit = index.static.get(`${method}|${
|
|
3941
|
+
function matchByMethod(index, method, path17) {
|
|
3942
|
+
const staticHit = index.static.get(`${method}|${path17}`);
|
|
3898
3943
|
if (staticHit) {
|
|
3899
3944
|
return { route: staticHit, params: {} };
|
|
3900
3945
|
}
|
|
3901
3946
|
if (index.dynamics.length === 0) {
|
|
3902
3947
|
return null;
|
|
3903
3948
|
}
|
|
3904
|
-
const pathSegments =
|
|
3949
|
+
const pathSegments = path17.split("/").filter(Boolean);
|
|
3905
3950
|
for (const entry of index.dynamics) {
|
|
3906
3951
|
const route = entry.route;
|
|
3907
3952
|
if (route.method !== method) {
|
|
@@ -3914,31 +3959,31 @@ function matchByMethod(index, method, path16) {
|
|
|
3914
3959
|
}
|
|
3915
3960
|
return null;
|
|
3916
3961
|
}
|
|
3917
|
-
function matchWsRoute(wsRoutes,
|
|
3962
|
+
function matchWsRoute(wsRoutes, path17) {
|
|
3918
3963
|
const index = getWsIndex(wsRoutes);
|
|
3919
|
-
const staticHit = index.static.get(
|
|
3964
|
+
const staticHit = index.static.get(path17);
|
|
3920
3965
|
if (staticHit) {
|
|
3921
3966
|
return { route: staticHit, params: {} };
|
|
3922
3967
|
}
|
|
3923
3968
|
for (const route of index.dynamics) {
|
|
3924
|
-
const params = matchDynamicPath(route.urlPath,
|
|
3969
|
+
const params = matchDynamicPath(route.urlPath, path17, route.paramNames, route.isCatchAll);
|
|
3925
3970
|
if (params !== null) {
|
|
3926
3971
|
return { route, params };
|
|
3927
3972
|
}
|
|
3928
3973
|
}
|
|
3929
3974
|
return null;
|
|
3930
3975
|
}
|
|
3931
|
-
function findAllowedMethods(routes,
|
|
3976
|
+
function findAllowedMethods(routes, path17) {
|
|
3932
3977
|
const index = getHttpIndex(routes);
|
|
3933
3978
|
const methods = /* @__PURE__ */ new Set();
|
|
3934
|
-
const staticMethods = index.methodsByStaticPath.get(
|
|
3979
|
+
const staticMethods = index.methodsByStaticPath.get(path17);
|
|
3935
3980
|
if (staticMethods) {
|
|
3936
3981
|
for (const method of staticMethods) {
|
|
3937
3982
|
methods.add(method);
|
|
3938
3983
|
}
|
|
3939
3984
|
}
|
|
3940
3985
|
if (index.dynamics.length > 0) {
|
|
3941
|
-
const pathSegments =
|
|
3986
|
+
const pathSegments = path17.split("/").filter(Boolean);
|
|
3942
3987
|
for (const entry of index.dynamics) {
|
|
3943
3988
|
const params = matchSegments(
|
|
3944
3989
|
entry.segments,
|
|
@@ -3956,10 +4001,10 @@ function findAllowedMethods(routes, path16) {
|
|
|
3956
4001
|
}
|
|
3957
4002
|
return Array.from(methods);
|
|
3958
4003
|
}
|
|
3959
|
-
function matchDynamicPath(pattern,
|
|
4004
|
+
function matchDynamicPath(pattern, path17, paramNames, isCatchAll) {
|
|
3960
4005
|
return matchSegments(
|
|
3961
4006
|
pattern.split("/").filter(Boolean),
|
|
3962
|
-
|
|
4007
|
+
path17.split("/").filter(Boolean),
|
|
3963
4008
|
paramNames,
|
|
3964
4009
|
isCatchAll
|
|
3965
4010
|
);
|
|
@@ -4581,9 +4626,17 @@ function logger(options = {}) {
|
|
|
4581
4626
|
}
|
|
4582
4627
|
|
|
4583
4628
|
// src/server/handleWsUpgrade.ts
|
|
4584
|
-
import
|
|
4629
|
+
import fs12 from "fs";
|
|
4585
4630
|
import { WebSocketServer, WebSocket } from "ws";
|
|
4631
|
+
import path14 from "path";
|
|
4632
|
+
|
|
4633
|
+
// src/cli/copyResources.ts
|
|
4586
4634
|
import path13 from "path";
|
|
4635
|
+
import fs11 from "fs";
|
|
4636
|
+
var RESOURCES_OUTPUT_DIR = "resources";
|
|
4637
|
+
function resolveResourcesDir(rootDir, dist) {
|
|
4638
|
+
return path13.resolve(rootDir, dist, RESOURCES_OUTPUT_DIR);
|
|
4639
|
+
}
|
|
4587
4640
|
|
|
4588
4641
|
// src/server/serverUtils.ts
|
|
4589
4642
|
function nodeHttpToWebHeaders(req) {
|
|
@@ -4641,7 +4694,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
|
|
|
4641
4694
|
const dist = getDevDist();
|
|
4642
4695
|
if (dist) {
|
|
4643
4696
|
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
4644
|
-
if (sourcePath &&
|
|
4697
|
+
if (sourcePath && fs12.existsSync(sourcePath)) {
|
|
4645
4698
|
await ensureCompiled(sourcePath, rootDir, dist);
|
|
4646
4699
|
}
|
|
4647
4700
|
}
|
|
@@ -4739,6 +4792,7 @@ function attachWebSocket(options) {
|
|
|
4739
4792
|
trustedProxy = false,
|
|
4740
4793
|
registries
|
|
4741
4794
|
} = options;
|
|
4795
|
+
const resourcesDir = resolveResourcesDir(rootDir, dist);
|
|
4742
4796
|
const wss = new WebSocketServer({ noServer: true });
|
|
4743
4797
|
server.on("upgrade", async (req, socket, head) => {
|
|
4744
4798
|
try {
|
|
@@ -4770,12 +4824,19 @@ function attachWebSocket(options) {
|
|
|
4770
4824
|
const host = req.headers.host ?? "localhost";
|
|
4771
4825
|
const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
|
|
4772
4826
|
const request = new Request(fullUrl.toString(), { method: "GET", headers });
|
|
4773
|
-
const ctx = createContext(
|
|
4827
|
+
const ctx = createContext(
|
|
4828
|
+
request,
|
|
4829
|
+
params,
|
|
4830
|
+
config,
|
|
4831
|
+
getClientIp(req, trustedProxy),
|
|
4832
|
+
registries,
|
|
4833
|
+
resourcesDir
|
|
4834
|
+
);
|
|
4774
4835
|
const meta = ctx.meta;
|
|
4775
4836
|
let upgraded = false;
|
|
4776
4837
|
const finalHandler = async () => {
|
|
4777
4838
|
const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
|
|
4778
|
-
if (
|
|
4839
|
+
if (fs12.existsSync(schemaPath)) {
|
|
4779
4840
|
if (isDevOnDemandEnabled()) {
|
|
4780
4841
|
const devDist = getDevDist();
|
|
4781
4842
|
if (devDist) {
|
|
@@ -4814,7 +4875,7 @@ function attachWebSocket(options) {
|
|
|
4814
4875
|
}
|
|
4815
4876
|
let handlers;
|
|
4816
4877
|
try {
|
|
4817
|
-
const absoluteFilePath =
|
|
4878
|
+
const absoluteFilePath = path14.resolve(rootDir, route.filePath);
|
|
4818
4879
|
handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
|
|
4819
4880
|
} catch (err) {
|
|
4820
4881
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -4973,6 +5034,7 @@ function createServer(options) {
|
|
|
4973
5034
|
trustedProxy = false
|
|
4974
5035
|
} = options;
|
|
4975
5036
|
const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
|
|
5037
|
+
const resourcesDir = resolveResourcesDir(rootDir, dist);
|
|
4976
5038
|
const configMiddlewares = [];
|
|
4977
5039
|
if (compressionOption) {
|
|
4978
5040
|
const compOpts = typeof compressionOption === "object" ? compressionOption : {};
|
|
@@ -5019,7 +5081,8 @@ function createServer(options) {
|
|
|
5019
5081
|
bodyLimit,
|
|
5020
5082
|
trustedProxy,
|
|
5021
5083
|
registries,
|
|
5022
|
-
routesRef.wsCurrent
|
|
5084
|
+
routesRef.wsCurrent,
|
|
5085
|
+
resourcesDir
|
|
5023
5086
|
).catch((err) => {
|
|
5024
5087
|
console.error("[faapi] Request pipeline failed after error response attempt:", err);
|
|
5025
5088
|
try {
|
|
@@ -5045,7 +5108,7 @@ function createServer(options) {
|
|
|
5045
5108
|
});
|
|
5046
5109
|
return { server, routesRef };
|
|
5047
5110
|
}
|
|
5048
|
-
function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
|
|
5111
|
+
function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched, resourcesDir) {
|
|
5049
5112
|
const method = request.method.toUpperCase();
|
|
5050
5113
|
const urlPath = url.pathname;
|
|
5051
5114
|
const ctx = createContextFromUrl(
|
|
@@ -5054,7 +5117,8 @@ function prepareRequest(req, request, url, config, trustedProxy, registries, pre
|
|
|
5054
5117
|
preMatched?.params ?? {},
|
|
5055
5118
|
config,
|
|
5056
5119
|
getClientIp(req, trustedProxy),
|
|
5057
|
-
registries
|
|
5120
|
+
registries,
|
|
5121
|
+
resourcesDir
|
|
5058
5122
|
);
|
|
5059
5123
|
const meta = ctx.meta;
|
|
5060
5124
|
return { request, url, ctx, meta, method, urlPath };
|
|
@@ -5073,7 +5137,7 @@ function getRoutePaths(route, rootDir, dist) {
|
|
|
5073
5137
|
let cached = routePathCache.get(route);
|
|
5074
5138
|
if (!cached) {
|
|
5075
5139
|
cached = {
|
|
5076
|
-
absFilePath:
|
|
5140
|
+
absFilePath: path15.resolve(rootDir, route.filePath),
|
|
5077
5141
|
schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
|
|
5078
5142
|
};
|
|
5079
5143
|
routePathCache.set(route, cached);
|
|
@@ -5196,7 +5260,7 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
5196
5260
|
}
|
|
5197
5261
|
}
|
|
5198
5262
|
}
|
|
5199
|
-
async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes) {
|
|
5263
|
+
async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes, resourcesDir) {
|
|
5200
5264
|
let meta = { headers: {}, setCookies: [] };
|
|
5201
5265
|
let ctx;
|
|
5202
5266
|
let sseEarlySent = false;
|
|
@@ -5216,7 +5280,8 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
|
|
|
5216
5280
|
config,
|
|
5217
5281
|
trustedProxy,
|
|
5218
5282
|
registries,
|
|
5219
|
-
preMatched
|
|
5283
|
+
preMatched,
|
|
5284
|
+
resourcesDir
|
|
5220
5285
|
);
|
|
5221
5286
|
ctx = prepared.ctx;
|
|
5222
5287
|
meta = prepared.meta;
|
|
@@ -5281,7 +5346,7 @@ async function createTestServer(options) {
|
|
|
5281
5346
|
} = options;
|
|
5282
5347
|
const { routes, wsRoutes } = await scanRoutes(rootDir, patterns);
|
|
5283
5348
|
const sorted = sortRoutes(routes);
|
|
5284
|
-
const schemaDist = dist ?
|
|
5349
|
+
const schemaDist = dist ? path16.isAbsolute(dist) ? dist : path16.resolve(rootDir, dist) : await fs13.mkdtemp(path16.join(os.tmpdir(), "faapi-test-schema-"));
|
|
5285
5350
|
await generateSchemaFiles(sorted, rootDir, schemaDist);
|
|
5286
5351
|
const { server } = createServer({
|
|
5287
5352
|
routes: sorted,
|
|
@@ -5313,7 +5378,7 @@ async function createTestServer(options) {
|
|
|
5313
5378
|
await new Promise((resolve) => {
|
|
5314
5379
|
server.close(() => resolve());
|
|
5315
5380
|
});
|
|
5316
|
-
await
|
|
5381
|
+
await fs13.rm(schemaDist, { recursive: true, force: true }).catch(() => {
|
|
5317
5382
|
});
|
|
5318
5383
|
invalidateSchemaCache();
|
|
5319
5384
|
}
|