@faapi/faapi 4.0.0 → 4.2.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/index.d.ts CHANGED
@@ -120,8 +120,23 @@ interface Http2Options {
120
120
 
121
121
  /**
122
122
  * 生命周期钩子
123
+ *
124
+ * 执行时序:onBoot(.listen 调用前)→ server.listen → listen 回调内 onReady → 运行期 onError → 关闭时 onClose
123
125
  */
124
126
  interface LifecycleHooks {
127
+ /**
128
+ * 服务器 listen 之前调用(启动校验钩子)
129
+ *
130
+ * 时机:`app.listen()` 内、`server.listen` 调用之前——server 已创建但未监听
131
+ * (`server.listening === false`),路由/tool/agent 清单已水合、插件已加载。
132
+ *
133
+ * 适合启动校验(环境变量、下游依赖可达性)、DB 迁移等**失败即不该暴露端口**的逻辑:
134
+ * 钩子抛错 → `listen()` 以原始错误 reject,`server.listen` 不会被调用,端口不暴露。
135
+ *
136
+ * 与 onReady 的差异:onReady 在 listen 回调内执行,失败时端口已开,
137
+ * 存在"接受连接但不服务"的窗口——启动校验请用 onBoot,资源初始化用 onReady。
138
+ */
139
+ onBoot?: (ctx: LifecycleContext) => Promise<void> | void;
125
140
  /** 服务器启动后调用(适合初始化数据库连接等) */
126
141
  onReady?: (ctx: LifecycleContext) => Promise<void> | void;
127
142
  /** 服务器关闭时调用(适合清理资源、优雅关闭) */
@@ -149,7 +164,7 @@ interface LifecycleContext {
149
164
  rootDir: string;
150
165
  /** 当前路由清单 */
151
166
  routes: RouteManifest;
152
- /** 服务器实例 */
167
+ /** 服务器实例(onBoot 钩子触发时已创建但未监听,`listening === false`) */
153
168
  server: node_http.Server;
154
169
  /** app 级注册表——skill 等运行时动态注册路径(`registries.skill.upsert(...)`) */
155
170
  registries: AppRegistries;
package/dist/index.js CHANGED
@@ -2036,6 +2036,15 @@ function toStrippedProdImportPath(sourceFile, rootDir) {
2036
2036
  if (!rel.startsWith(".")) rel = "./" + rel;
2037
2037
  return toProdExtension(rel);
2038
2038
  }
2039
+ function toProdImportFromImporter(importer, rootDir, relFromDist) {
2040
+ const importerRel = path5.relative(toRealPath(path5.resolve(rootDir)), toRealPath(importer)).split(path5.sep).join("/");
2041
+ const importerProdDir = path5.posix.dirname(toProdExtension(importerRel));
2042
+ if (importerProdDir === ".") return relFromDist;
2043
+ const target = relFromDist.replace(/^\.\//, "");
2044
+ let rel = path5.posix.relative(importerProdDir, target);
2045
+ if (!rel.startsWith(".")) rel = "./" + rel;
2046
+ return rel;
2047
+ }
2039
2048
  var PROD_EXTS = [".js", ".mjs", ".cjs"];
2040
2049
  var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2041
2050
  var INDEX_EXTS = [
@@ -2093,9 +2102,10 @@ function createAliasPlugin(config, options) {
2093
2102
  }
2094
2103
  if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
2095
2104
  modified = true;
2096
- return `${prefix}${quote}${toStrippedProdImportPath(
2097
- resolved,
2098
- options.rootDir
2105
+ return `${prefix}${quote}${toProdImportFromImporter(
2106
+ importer,
2107
+ options.rootDir,
2108
+ toStrippedProdImportPath(resolved, options.rootDir)
2099
2109
  )}${quote}`;
2100
2110
  }
2101
2111
  modified = true;
@@ -2110,9 +2120,10 @@ function createAliasPlugin(config, options) {
2110
2120
  if (fs4.existsSync(file)) {
2111
2121
  modified = true;
2112
2122
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2113
- return `${prefix}${quote}${toStrippedProdImportPath(
2114
- file,
2115
- options.rootDir
2123
+ return `${prefix}${quote}${toProdImportFromImporter(
2124
+ importer,
2125
+ options.rootDir,
2126
+ toStrippedProdImportPath(file, options.rootDir)
2116
2127
  )}${quote}`;
2117
2128
  }
2118
2129
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
@@ -2123,9 +2134,10 @@ function createAliasPlugin(config, options) {
2123
2134
  if (fs4.existsSync(file)) {
2124
2135
  modified = true;
2125
2136
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
2126
- return `${prefix}${quote}${toStrippedProdImportPath(
2127
- file,
2128
- options.rootDir
2137
+ return `${prefix}${quote}${toProdImportFromImporter(
2138
+ importer,
2139
+ options.rootDir,
2140
+ toStrippedProdImportPath(file, options.rootDir)
2129
2141
  )}${quote}`;
2130
2142
  }
2131
2143
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
@@ -3096,14 +3108,14 @@ var ValidationError = class extends FaapiError {
3096
3108
  issues;
3097
3109
  };
3098
3110
  var RouteNotFoundError = class extends FaapiError {
3099
- constructor(path23) {
3100
- super("ROUTE_NOT_FOUND", `Route not found: ${path23}`, 404);
3111
+ constructor(path24) {
3112
+ super("ROUTE_NOT_FOUND", `Route not found: ${path24}`, 404);
3101
3113
  this.name = "RouteNotFoundError";
3102
3114
  }
3103
3115
  };
3104
3116
  var MethodNotAllowedError = class extends FaapiError {
3105
- constructor(method, path23, allowedMethods) {
3106
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path23}`, 405);
3117
+ constructor(method, path24, allowedMethods) {
3118
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path24}`, 405);
3107
3119
  this.allowedMethods = allowedMethods;
3108
3120
  this.name = "MethodNotAllowedError";
3109
3121
  }
@@ -3129,8 +3141,8 @@ var PayloadTooLargeError = class extends FaapiError {
3129
3141
  };
3130
3142
 
3131
3143
  // src/cli/createAppCore.ts
3132
- import fs15 from "fs";
3133
- import path19 from "path";
3144
+ import fs17 from "fs";
3145
+ import path20 from "path";
3134
3146
  import { PassThrough, Readable as Readable3 } from "stream";
3135
3147
 
3136
3148
  // src/router/sortRoutes.ts
@@ -3225,18 +3237,18 @@ function getWsIndex(routes) {
3225
3237
  wsIndexCache.set(routes, index);
3226
3238
  return index;
3227
3239
  }
3228
- function matchRoute(routes, method, path23) {
3240
+ function matchRoute(routes, method, path24) {
3229
3241
  const index = getHttpIndex(routes);
3230
3242
  const upper = method.toUpperCase();
3231
- const hit = matchByMethod(index, upper, path23);
3243
+ const hit = matchByMethod(index, upper, path24);
3232
3244
  if (hit) return hit;
3233
3245
  if (upper === "HEAD") {
3234
- return matchByMethod(index, "GET", path23);
3246
+ return matchByMethod(index, "GET", path24);
3235
3247
  }
3236
3248
  return null;
3237
3249
  }
3238
- function matchByMethod(index, method, path23) {
3239
- const staticHit = index.static.get(`${method}|${path23}`);
3250
+ function matchByMethod(index, method, path24) {
3251
+ const staticHit = index.static.get(`${method}|${path24}`);
3240
3252
  if (staticHit) {
3241
3253
  return { route: staticHit, params: {} };
3242
3254
  }
@@ -3245,31 +3257,31 @@ function matchByMethod(index, method, path23) {
3245
3257
  if (route.method !== method) {
3246
3258
  continue;
3247
3259
  }
3248
- const params = matchSegments(entry.segments, path23, route.paramNames, route.isCatchAll);
3260
+ const params = matchSegments(entry.segments, path24, route.paramNames, route.isCatchAll);
3249
3261
  if (params !== null) {
3250
3262
  return { route, params };
3251
3263
  }
3252
3264
  }
3253
3265
  return null;
3254
3266
  }
3255
- function matchWsRoute(wsRoutes, path23) {
3267
+ function matchWsRoute(wsRoutes, path24) {
3256
3268
  const index = getWsIndex(wsRoutes);
3257
- const staticHit = index.static.get(path23);
3269
+ const staticHit = index.static.get(path24);
3258
3270
  if (staticHit) {
3259
3271
  return { route: staticHit, params: {} };
3260
3272
  }
3261
3273
  for (const route of index.dynamics) {
3262
- const params = matchDynamicPath(route.urlPath, path23, route.paramNames, route.isCatchAll);
3274
+ const params = matchDynamicPath(route.urlPath, path24, route.paramNames, route.isCatchAll);
3263
3275
  if (params !== null) {
3264
3276
  return { route, params };
3265
3277
  }
3266
3278
  }
3267
3279
  return null;
3268
3280
  }
3269
- function findAllowedMethods(routes, path23) {
3281
+ function findAllowedMethods(routes, path24) {
3270
3282
  const index = getHttpIndex(routes);
3271
3283
  const methods = /* @__PURE__ */ new Set();
3272
- const staticMethods = index.methodsByStaticPath.get(path23);
3284
+ const staticMethods = index.methodsByStaticPath.get(path24);
3273
3285
  if (staticMethods) {
3274
3286
  for (const method of staticMethods) {
3275
3287
  methods.add(method);
@@ -3278,7 +3290,7 @@ function findAllowedMethods(routes, path23) {
3278
3290
  for (const entry of index.dynamics) {
3279
3291
  const params = matchSegments(
3280
3292
  entry.segments,
3281
- path23,
3293
+ path24,
3282
3294
  entry.route.paramNames,
3283
3295
  entry.route.isCatchAll
3284
3296
  );
@@ -3291,11 +3303,11 @@ function findAllowedMethods(routes, path23) {
3291
3303
  }
3292
3304
  return Array.from(methods);
3293
3305
  }
3294
- function matchDynamicPath(pattern, path23, paramNames, isCatchAll) {
3295
- return matchSegments(pattern.split("/").filter(Boolean), path23, paramNames, isCatchAll);
3306
+ function matchDynamicPath(pattern, path24, paramNames, isCatchAll) {
3307
+ return matchSegments(pattern.split("/").filter(Boolean), path24, paramNames, isCatchAll);
3296
3308
  }
3297
- function matchSegments(patternSegments, path23, paramNames, isCatchAll) {
3298
- const pathSegments = path23.split("/").filter(Boolean);
3309
+ function matchSegments(patternSegments, path24, paramNames, isCatchAll) {
3310
+ const pathSegments = path24.split("/").filter(Boolean);
3299
3311
  if (isCatchAll) {
3300
3312
  const nonCatchAllCount = patternSegments.length - 1;
3301
3313
  if (pathSegments.length <= nonCatchAllCount) {
@@ -4183,9 +4195,9 @@ async function validateInput(schemaPath, method, inputType, input) {
4183
4195
  function mapZodIssues(error) {
4184
4196
  return error.issues.map((issue) => {
4185
4197
  const code = mapZodCode(issue);
4186
- const path23 = issue.path.map(String).join(".") || "";
4198
+ const path24 = issue.path.map(String).join(".") || "";
4187
4199
  return {
4188
- path: path23,
4200
+ path: path24,
4189
4201
  code,
4190
4202
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
4191
4203
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -4725,7 +4737,7 @@ function attachWebSocket(options) {
4725
4737
  // src/server/createServer.ts
4726
4738
  init_generateSchemaFiles();
4727
4739
  var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
4728
- function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4740
+ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT, requestSignal) {
4729
4741
  const forwardedProto = req.headers["x-forwarded-proto"];
4730
4742
  const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
4731
4743
  const host = req.headers.host ?? "localhost";
@@ -4733,7 +4745,10 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4733
4745
  const headers = nodeHttpToWebHeaders(req);
4734
4746
  const method = req.method ?? "GET";
4735
4747
  if (method === "GET" || method === "HEAD") {
4736
- return { request: new Request(url.toString(), { method, headers }), url };
4748
+ return {
4749
+ request: new Request(url.toString(), { method, headers, signal: requestSignal }),
4750
+ url
4751
+ };
4737
4752
  }
4738
4753
  const contentLength = req.headers["content-length"];
4739
4754
  if (contentLength !== void 0) {
@@ -4749,7 +4764,8 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4749
4764
  method,
4750
4765
  headers,
4751
4766
  body: limitedStream,
4752
- duplex: "half"
4767
+ duplex: "half",
4768
+ signal: requestSignal
4753
4769
  }),
4754
4770
  url
4755
4771
  };
@@ -4888,8 +4904,8 @@ function createServer(options) {
4888
4904
  });
4889
4905
  return { server, routesRef };
4890
4906
  }
4891
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries) {
4892
- const { request, url } = toWebRequest(req, bodyLimit);
4907
+ function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
4908
+ const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
4893
4909
  const method = request.method.toUpperCase();
4894
4910
  const urlPath = url.pathname;
4895
4911
  const ctx = createContextFromUrl(
@@ -4975,8 +4991,19 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
4975
4991
  async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
4976
4992
  let meta = { headers: {}, setCookies: [] };
4977
4993
  let ctx;
4994
+ const abortController = new AbortController();
4995
+ res.on("close", () => {
4996
+ if (!res.writableEnded) abortController.abort();
4997
+ });
4978
4998
  try {
4979
- const prepared = prepareRequest(req, config, bodyLimit, trustedProxy, registries);
4999
+ const prepared = prepareRequest(
5000
+ req,
5001
+ config,
5002
+ bodyLimit,
5003
+ trustedProxy,
5004
+ registries,
5005
+ abortController.signal
5006
+ );
4980
5007
  ctx = prepared.ctx;
4981
5008
  meta = prepared.meta;
4982
5009
  const { request, url, method, urlPath } = prepared;
@@ -5271,9 +5298,50 @@ async function generateAgentArtifacts(agents, rootDir, dist) {
5271
5298
  }
5272
5299
 
5273
5300
  // src/cli/loadPlugins.ts
5274
- import path18 from "path";
5301
+ import path19 from "path";
5302
+ import fs16 from "fs";
5275
5303
  import { pathToFileURL as pathToFileURL2 } from "url";
5276
- async function loadPlugins(declarations, ctx, rootDir) {
5304
+
5305
+ // src/cli/compileConfig.ts
5306
+ import path18 from "path";
5307
+ import fs15 from "fs";
5308
+ async function compileProjectModules(entryPoints, rootDir, dist) {
5309
+ const { insideFiles, outsideFiles } = await collectRelativeImports(entryPoints, rootDir);
5310
+ const esbuild = await import("esbuild");
5311
+ const aliasPlugins = buildAliasPlugins(rootDir);
5312
+ const absDist = path18.resolve(rootDir, dist);
5313
+ await esbuild.build({
5314
+ entryPoints: [...entryPoints, ...outsideFiles],
5315
+ outdir: absDist,
5316
+ outbase: rootDir,
5317
+ bundle: false,
5318
+ platform: "node",
5319
+ format: "esm",
5320
+ sourcemap: true,
5321
+ packages: "external",
5322
+ plugins: aliasPlugins,
5323
+ logLevel: "silent"
5324
+ });
5325
+ if (insideFiles.length > 0) {
5326
+ const appOutbase = path18.resolve(rootDir, "src");
5327
+ await esbuild.build({
5328
+ entryPoints: insideFiles,
5329
+ outdir: absDist,
5330
+ outbase: appOutbase,
5331
+ bundle: false,
5332
+ platform: "node",
5333
+ format: "esm",
5334
+ sourcemap: true,
5335
+ packages: "external",
5336
+ plugins: aliasPlugins,
5337
+ logLevel: "silent"
5338
+ });
5339
+ }
5340
+ return { insideFiles, outsideFiles };
5341
+ }
5342
+
5343
+ // src/cli/loadPlugins.ts
5344
+ async function loadPlugins(declarations, ctx, rootDir, dist) {
5277
5345
  const handlerWrappers = [];
5278
5346
  const upgradeWrappers = [];
5279
5347
  const failures = [];
@@ -5310,7 +5378,7 @@ async function loadPlugins(declarations, ctx, rootDir) {
5310
5378
  }
5311
5379
  loaded.add(specifier);
5312
5380
  try {
5313
- const mod = await import(resolveSpecifier(specifier, rootDir));
5381
+ const mod = await importPluginModule(specifier, rootDir ?? process.cwd(), dist);
5314
5382
  const plugin = mod.default ?? mod;
5315
5383
  if (typeof plugin.setup !== "function") {
5316
5384
  failures.push({ specifier, reason: "plugin has no setup function" });
@@ -5333,15 +5401,72 @@ async function loadPlugins(declarations, ctx, rootDir) {
5333
5401
  }
5334
5402
  return { handlerWrappers, upgradeWrappers, failures };
5335
5403
  }
5336
- function resolveSpecifier(specifier, rootDir) {
5337
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
5338
- const base = rootDir ?? process.cwd();
5339
- return pathToFileURL2(path18.resolve(base, specifier)).href;
5404
+ var TS_EXTS = /\.(ts|tsx|mts|cts)$/;
5405
+ var JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
5406
+ function resolveLocalPluginSource(specifier, baseDir) {
5407
+ const base = path19.isAbsolute(specifier) ? specifier : path19.resolve(baseDir, specifier);
5408
+ if ((TS_EXTS.test(base) || JS_EXTS.test(base)) && fs16.existsSync(base)) return base;
5409
+ for (const ext of [".ts", ".js"]) {
5410
+ if (fs16.existsSync(base + ext)) return base + ext;
5340
5411
  }
5341
- if (path18.isAbsolute(specifier)) {
5342
- return pathToFileURL2(specifier).href;
5412
+ for (const indexExt of ["/index.ts", "/index.js"]) {
5413
+ if (fs16.existsSync(base + indexExt)) return base + indexExt;
5343
5414
  }
5344
- return specifier;
5415
+ return null;
5416
+ }
5417
+ function mirrorProductPath(sourcePath, baseDir, distDir) {
5418
+ let rel = path19.relative(baseDir, sourcePath).replace(/\\/g, "/");
5419
+ if (rel.startsWith("src/")) rel = rel.slice(4);
5420
+ return path19.resolve(distDir, rel.replace(TS_EXTS, ".js"));
5421
+ }
5422
+ function isSourceNewer(sourcePath, productPath) {
5423
+ try {
5424
+ return fs16.statSync(sourcePath).mtimeMs > fs16.statSync(productPath).mtimeMs;
5425
+ } catch {
5426
+ return true;
5427
+ }
5428
+ }
5429
+ async function importPluginModule(specifier, baseDir, dist) {
5430
+ const isRelative = specifier.startsWith("./") || specifier.startsWith("../");
5431
+ if (!isRelative && !path19.isAbsolute(specifier)) {
5432
+ return import(specifier);
5433
+ }
5434
+ const distDir = dist ? path19.resolve(baseDir, dist) : null;
5435
+ const sourcePath = resolveLocalPluginSource(specifier, baseDir);
5436
+ const productPath = sourcePath && distDir ? mirrorProductPath(sourcePath, baseDir, distDir) : null;
5437
+ if (productPath !== null && fs16.existsSync(productPath) && (sourcePath === null || !isSourceNewer(sourcePath, productPath))) {
5438
+ return import(pathToFileURL2(productPath).href);
5439
+ }
5440
+ if (sourcePath && TS_EXTS.test(sourcePath)) {
5441
+ if (!isDevOnDemandEnabled() || !distDir) {
5442
+ throw new Error(
5443
+ `Local plugin "${specifier}" has no up-to-date build artifact (dist is stale or missing). Run "faapi build" first, or use "faapi dev" for on-demand compilation.`
5444
+ );
5445
+ }
5446
+ const relFromRoot = path19.relative(baseDir, sourcePath).replace(/\\/g, "/");
5447
+ if (relFromRoot.startsWith("src/")) {
5448
+ await ensureCompiled(sourcePath, baseDir, dist);
5449
+ } else {
5450
+ await compileProjectModules([sourcePath], baseDir, dist);
5451
+ }
5452
+ if (productPath && fs16.existsSync(productPath)) {
5453
+ return import(pathToFileURL2(productPath).href);
5454
+ }
5455
+ }
5456
+ if (sourcePath && JS_EXTS.test(sourcePath)) {
5457
+ return import(pathToFileURL2(sourcePath).href);
5458
+ }
5459
+ const candidates = [
5460
+ `${specifier}.ts`,
5461
+ `${specifier}/index.ts`,
5462
+ `${specifier}.js`,
5463
+ `${specifier}/index.js`
5464
+ ].map((c) => path19.isAbsolute(c) ? c : path19.join(baseDir, c));
5465
+ throw new Error(
5466
+ `Cannot find local plugin "${specifier}" (resolved from ${baseDir}). Expected one of:
5467
+ ${candidates.join("\n ")}
5468
+ Create the plugin file (export default { name, setup(ctx) {...} }) or fix the path.`
5469
+ );
5345
5470
  }
5346
5471
  function resolveDeclaration(decl) {
5347
5472
  if (typeof decl === "string") {
@@ -5367,8 +5492,8 @@ var ROUTES_FILE = "faapi-routes.js";
5367
5492
  var TOOLS_FILE2 = "faapi-tools.js";
5368
5493
  var AGENTS_FILE2 = "faapi-agents.js";
5369
5494
  async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries) {
5370
- const toolsPath = path19.resolve(rootDir, dist, TOOLS_FILE2);
5371
- if (!fs15.existsSync(toolsPath)) {
5495
+ const toolsPath = path20.resolve(rootDir, dist, TOOLS_FILE2);
5496
+ if (!fs17.existsSync(toolsPath)) {
5372
5497
  return [];
5373
5498
  }
5374
5499
  const serialized = await importWithCacheBust(toolsPath);
@@ -5377,8 +5502,8 @@ async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries
5377
5502
  return hydrated;
5378
5503
  }
5379
5504
  async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistries) {
5380
- const agentsPath = path19.resolve(rootDir, dist, AGENTS_FILE2);
5381
- if (!fs15.existsSync(agentsPath)) {
5505
+ const agentsPath = path20.resolve(rootDir, dist, AGENTS_FILE2);
5506
+ if (!fs17.existsSync(agentsPath)) {
5382
5507
  return [];
5383
5508
  }
5384
5509
  const serialized = await importWithCacheBust(agentsPath);
@@ -5445,8 +5570,8 @@ function isFaapiConfigKey(key) {
5445
5570
  async function createAppBase(options) {
5446
5571
  const rootDir = options?.rootDir ?? process.cwd();
5447
5572
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
5448
- const routesPath = path19.resolve(rootDir, dist, ROUTES_FILE);
5449
- if (!fs15.existsSync(routesPath)) {
5573
+ const routesPath = path20.resolve(rootDir, dist, ROUTES_FILE);
5574
+ if (!fs17.existsSync(routesPath)) {
5450
5575
  throw new Error(
5451
5576
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
5452
5577
  );
@@ -5498,7 +5623,8 @@ async function createAppBase(options) {
5498
5623
  server,
5499
5624
  config: pluginConfig
5500
5625
  },
5501
- rootDir
5626
+ rootDir,
5627
+ dist
5502
5628
  );
5503
5629
  applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
5504
5630
  let closed = false;
@@ -5511,6 +5637,16 @@ async function createAppBase(options) {
5511
5637
  async listen(listenPort) {
5512
5638
  const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
5513
5639
  const actualPort = listenPort ?? options?.port ?? envPort ?? DEFAULT_PORT;
5640
+ if (config?.lifecycle?.onBoot) {
5641
+ try {
5642
+ await config.lifecycle.onBoot({ rootDir, routes: sorted, server, registries });
5643
+ console.log("- onBoot hook executed");
5644
+ } catch (err) {
5645
+ const message = err instanceof Error ? err.message : String(err);
5646
+ console.error(`[faapi] onBoot hook failed: ${message}`);
5647
+ throw err;
5648
+ }
5649
+ }
5514
5650
  return new Promise((resolve, reject) => {
5515
5651
  const onListenError = (err) => {
5516
5652
  if (err.code === "EADDRINUSE") {
@@ -5693,17 +5829,17 @@ async function createAppBase(options) {
5693
5829
 
5694
5830
  // src/router/scanRoutes.ts
5695
5831
  import fg2 from "fast-glob";
5696
- import path20 from "path";
5697
- import fs16 from "fs";
5832
+ import path21 from "path";
5833
+ import fs18 from "fs";
5698
5834
 
5699
5835
  // src/router/constants.ts
5700
5836
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
5701
5837
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
5702
5838
 
5703
5839
  // src/utils/normalizePath.ts
5704
- function normalizePath(path23) {
5705
- if (!path23) return "";
5706
- let result = path23.replace(/\\/g, "/");
5840
+ function normalizePath(path24) {
5841
+ if (!path24) return "";
5842
+ let result = path24.replace(/\\/g, "/");
5707
5843
  result = result.replace(/\/+/g, "/");
5708
5844
  result = result.replace(/\/+$/, "");
5709
5845
  if (result && !result.startsWith("/")) {
@@ -5764,34 +5900,34 @@ function extractExportsFromSource(source) {
5764
5900
  return names;
5765
5901
  }
5766
5902
  function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
5767
- const routeDir = path20.dirname(routeFilePath);
5768
- const resolvedRoot = path20.resolve(rootDir);
5903
+ const routeDir = path21.dirname(routeFilePath);
5904
+ const resolvedRoot = path21.resolve(rootDir);
5769
5905
  const paths = [];
5770
- let currentDir = path20.resolve(rootDir, routeDir);
5906
+ let currentDir = path21.resolve(rootDir, routeDir);
5771
5907
  while (true) {
5772
5908
  if (dist) {
5773
- const mwTsPath = path20.join(currentDir, "middlewares.ts");
5774
- const mwJsPath = path20.join(currentDir, "middlewares.js");
5775
- const absTsPath = path20.resolve(rootDir, mwTsPath);
5776
- const absJsPath = path20.resolve(rootDir, mwJsPath);
5777
- const absMwPath = fs16.existsSync(absTsPath) ? absTsPath : fs16.existsSync(absJsPath) ? absJsPath : null;
5909
+ const mwTsPath = path21.join(currentDir, "middlewares.ts");
5910
+ const mwJsPath = path21.join(currentDir, "middlewares.js");
5911
+ const absTsPath = path21.resolve(rootDir, mwTsPath);
5912
+ const absJsPath = path21.resolve(rootDir, mwJsPath);
5913
+ const absMwPath = fs18.existsSync(absTsPath) ? absTsPath : fs18.existsSync(absJsPath) ? absJsPath : null;
5778
5914
  if (absMwPath) {
5779
- const relMwPath = path20.relative(rootDir, absMwPath);
5780
- const prodAbsPath = path20.resolve(rootDir, toProdFilePath(relMwPath, dist));
5915
+ const relMwPath = path21.relative(rootDir, absMwPath);
5916
+ const prodAbsPath = path21.resolve(rootDir, toProdFilePath(relMwPath, dist));
5781
5917
  paths.push(prodAbsPath);
5782
5918
  }
5783
5919
  } else {
5784
5920
  for (const ext of [".ts", ".js"]) {
5785
- const mwPath = path20.join(currentDir, `middlewares${ext}`);
5786
- const absMwPath = path20.resolve(rootDir, mwPath);
5787
- if (fs16.existsSync(absMwPath)) {
5921
+ const mwPath = path21.join(currentDir, `middlewares${ext}`);
5922
+ const absMwPath = path21.resolve(rootDir, mwPath);
5923
+ if (fs18.existsSync(absMwPath)) {
5788
5924
  paths.push(absMwPath);
5789
5925
  break;
5790
5926
  }
5791
5927
  }
5792
5928
  }
5793
5929
  if (currentDir === resolvedRoot) break;
5794
- const parentDir = path20.dirname(currentDir);
5930
+ const parentDir = path21.dirname(currentDir);
5795
5931
  if (parentDir === currentDir) break;
5796
5932
  currentDir = parentDir;
5797
5933
  }
@@ -5810,7 +5946,7 @@ async function scanRoutes(rootDir, patterns, dist) {
5810
5946
  const normalizedFile = file.replace(/\\/g, "/");
5811
5947
  const fileName = normalizedFile.split("/").pop();
5812
5948
  if (fileName === "handler.ts" || fileName === "handler.js") {
5813
- const absPath = path20.resolve(rootDir, normalizedFile);
5949
+ const absPath = path21.resolve(rootDir, normalizedFile);
5814
5950
  const urlPath = filePathToUrlPath(normalizedFile);
5815
5951
  const paramNames = extractParamNames(urlPath);
5816
5952
  const isDynamic = paramNames.length > 0;
@@ -5823,7 +5959,7 @@ async function scanRoutes(rootDir, patterns, dist) {
5823
5959
  const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
5824
5960
  middlewareBundle = await loadMergedMiddlewares(mwPaths);
5825
5961
  }
5826
- const source = await fs16.promises.readFile(absPath, "utf8").catch(() => "");
5962
+ const source = await fs18.promises.readFile(absPath, "utf8").catch(() => "");
5827
5963
  const exportNames = extractExportsFromSource(source);
5828
5964
  const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
5829
5965
  for (const method of methods) {
@@ -5859,8 +5995,8 @@ async function scanRoutes(rootDir, patterns, dist) {
5859
5995
 
5860
5996
  // src/tools/scanTools.ts
5861
5997
  import fg3 from "fast-glob";
5862
- import path21 from "path";
5863
- import fs17 from "fs";
5998
+ import path22 from "path";
5999
+ import fs19 from "fs";
5864
6000
  var TOOL_PATTERNS = ["src/tools/**/*.ts"];
5865
6001
  var TOOL_EXPORT_RE = new RegExp(
5866
6002
  String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)([A-Za-z_$][\w$]*)\s*(?:\(|=)`,
@@ -5910,8 +6046,8 @@ async function scanTools(rootDir, patterns) {
5910
6046
  if (fileName !== "handler.ts" && fileName !== "handler.js") {
5911
6047
  continue;
5912
6048
  }
5913
- const absPath = path21.resolve(rootDir, normalizedFile);
5914
- const source = await fs17.promises.readFile(absPath, "utf8").catch(() => "");
6049
+ const absPath = path22.resolve(rootDir, normalizedFile);
6050
+ const source = await fs19.promises.readFile(absPath, "utf8").catch(() => "");
5915
6051
  const exportNames = extractToolExportsFromSource(source);
5916
6052
  const namespace = filePathToToolNamespace(normalizedFile);
5917
6053
  for (const fnName of exportNames) {
@@ -5935,8 +6071,8 @@ async function scanTools(rootDir, patterns) {
5935
6071
 
5936
6072
  // src/agents/scanAgents.ts
5937
6073
  import fg4 from "fast-glob";
5938
- import path22 from "path";
5939
- import fs18 from "fs";
6074
+ import path23 from "path";
6075
+ import fs20 from "fs";
5940
6076
  var DEFAULT_AGENT_PATTERNS = ["src/agents/*/handler.ts"];
5941
6077
  var RUN_EXPORT_RE = /export\s+(?:async\s+)?(?:function\s+|const\s+)run\b/;
5942
6078
  function extractAgentNameFromPath(filePath) {
@@ -5968,8 +6104,8 @@ async function scanAgents(rootDir, patterns) {
5968
6104
  if (fileName !== "handler.ts" && fileName !== "handler.js") {
5969
6105
  continue;
5970
6106
  }
5971
- const absPath = path22.resolve(rootDir, normalizedFile);
5972
- const source = await fs18.promises.readFile(absPath, "utf8").catch(() => "");
6107
+ const absPath = path23.resolve(rootDir, normalizedFile);
6108
+ const source = await fs20.promises.readFile(absPath, "utf8").catch(() => "");
5973
6109
  const { hasRun } = detectAgentExports(source);
5974
6110
  const name = extractAgentNameFromPath(normalizedFile);
5975
6111
  const prevFile = seen.get(name);