@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.
@@ -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 ToolRegistry 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 SseWriter as G, type HelmetOptions as H, type InjectorMap as I, type TaskContext as J, type TaskDriverJob as K, type LogConfig as L, type TaskDriverProcess as M, type TaskDriverRecord as N, type TaskFailedInfo as O, type TaskJob as P, type TaskJobStatus as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type TaskMetadata as U, type TaskModule as V, type WsRouteManifest as W, type TaskRegistriesSnapshot as X, type TaskRegistriesView as Y, type ToolCore as Z, type ToolPathMeta as _, type FaapiContext as a, cors as a0, createAppRegistries as a1, createTaskRegistriesView as a2, createTaskRegistry as a3, helmet as a4, 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 };
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-gyz4PMh7.js';
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(path16) {
289
- super("ROUTE_NOT_FOUND", `Route not found: ${path16}`, 404);
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, path16, allowedMethods) {
295
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path16}`, 405);
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(request, new URL(request.url), params, config, ip, registries);
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: path16,
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${path16}`);
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 path15 from "path";
1342
+ import path16 from "path";
1299
1343
  import os from "os";
1300
- import fs12 from "fs/promises";
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(path16) {
1313
- if (!path16) return "";
1314
- let result = path16.replace(/\\/g, "/");
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: ["**/*.test.ts", "**/*.e2e.test.ts", "**/*.d.ts"]
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 path16 = issue.path.map(String).join(".") || "";
3837
+ const path17 = issue.path.map(String).join(".") || "";
3793
3838
  return {
3794
- path: path16,
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 path14 from "path";
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, path16) {
3931
+ function matchRoute(routes, method, path17) {
3887
3932
  const index = getHttpIndex(routes);
3888
3933
  const upper = method.toUpperCase();
3889
- const hit = matchByMethod(index, upper, path16);
3934
+ const hit = matchByMethod(index, upper, path17);
3890
3935
  if (hit) return hit;
3891
3936
  if (upper === "HEAD") {
3892
- return matchByMethod(index, "GET", path16);
3937
+ return matchByMethod(index, "GET", path17);
3893
3938
  }
3894
3939
  return null;
3895
3940
  }
3896
- function matchByMethod(index, method, path16) {
3897
- const staticHit = index.static.get(`${method}|${path16}`);
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 = path16.split("/").filter(Boolean);
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, path16) {
3962
+ function matchWsRoute(wsRoutes, path17) {
3918
3963
  const index = getWsIndex(wsRoutes);
3919
- const staticHit = index.static.get(path16);
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, path16, route.paramNames, route.isCatchAll);
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, path16) {
3976
+ function findAllowedMethods(routes, path17) {
3932
3977
  const index = getHttpIndex(routes);
3933
3978
  const methods = /* @__PURE__ */ new Set();
3934
- const staticMethods = index.methodsByStaticPath.get(path16);
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 = path16.split("/").filter(Boolean);
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, path16, paramNames, isCatchAll) {
4004
+ function matchDynamicPath(pattern, path17, paramNames, isCatchAll) {
3960
4005
  return matchSegments(
3961
4006
  pattern.split("/").filter(Boolean),
3962
- path16.split("/").filter(Boolean),
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 fs11 from "fs";
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 && fs11.existsSync(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(request, params, config, getClientIp(req, trustedProxy), registries);
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 (fs11.existsSync(schemaPath)) {
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 = path13.resolve(rootDir, route.filePath);
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: path14.resolve(rootDir, route.filePath),
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 ? path15.isAbsolute(dist) ? dist : path15.resolve(rootDir, dist) : await fs12.mkdtemp(path15.join(os.tmpdir(), "faapi-test-schema-"));
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 fs12.rm(schemaDist, { recursive: true, force: true }).catch(() => {
5381
+ await fs13.rm(schemaDist, { recursive: true, force: true }).catch(() => {
5317
5382
  });
5318
5383
  invalidateSchemaCache();
5319
5384
  }