@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/README.md CHANGED
@@ -48,6 +48,11 @@ node dist/main # 启动生产服务器
48
48
  - 洋葱模型中间件:`(ctx, next) => {}`
49
49
  - 依赖注入:注入器按参数名匹配 handler 参数
50
50
  - WebSocket 路由:导出 `WS` 函数声明
51
+ - SSE 流式响应:`ctx.sse()` 流式推送(LLM 输出、通知推送)
52
+ - 响应压缩:`compression: true`,gzip/deflate/br 协商,SSE 自动跳过
53
+ - ETag/304 协商:`etag: true`,GET/HEAD 弱 ETag 自动生成与协商
54
+ - 优雅停机:SIGTERM/SIGINT 默认注册,drain 在途请求(`FAAPI_SHUTDOWN_TIMEOUT_MS` 可调)
55
+ - app 级注册表:tool/agent/skill 注册表随 app 实例创建与销毁,多 app 同进程隔离
51
56
  - SSE 流式响应:`ctx.sse()` 推送
52
57
  - 动态路由:`[id]` / `[...slug]` / `(group)`
53
58
  - MCP 集成:LLM 可查询路由 schema
package/dist/cli/index.js CHANGED
@@ -132,6 +132,15 @@ function toStrippedProdImportPath(sourceFile, rootDir) {
132
132
  if (!rel.startsWith(".")) rel = "./" + rel;
133
133
  return toProdExtension(rel);
134
134
  }
135
+ function toProdImportFromImporter(importer, rootDir, relFromDist) {
136
+ const importerRel = path3.relative(toRealPath(path3.resolve(rootDir)), toRealPath(importer)).split(path3.sep).join("/");
137
+ const importerProdDir = path3.posix.dirname(toProdExtension(importerRel));
138
+ if (importerProdDir === ".") return relFromDist;
139
+ const target = relFromDist.replace(/^\.\//, "");
140
+ let rel = path3.posix.relative(importerProdDir, target);
141
+ if (!rel.startsWith(".")) rel = "./" + rel;
142
+ return rel;
143
+ }
135
144
  function resolveRelativeSpecifier(importer, specifier) {
136
145
  const importerDir = path3.dirname(importer);
137
146
  const base = path3.resolve(importerDir, specifier);
@@ -179,9 +188,10 @@ function createAliasPlugin(config, options) {
179
188
  }
180
189
  if (appDirAbs && importerOutsideAppDir && isInsideDir(resolved, appDirAbs)) {
181
190
  modified = true;
182
- return `${prefix}${quote}${toStrippedProdImportPath(
183
- resolved,
184
- options.rootDir
191
+ return `${prefix}${quote}${toProdImportFromImporter(
192
+ importer,
193
+ options.rootDir,
194
+ toStrippedProdImportPath(resolved, options.rootDir)
185
195
  )}${quote}`;
186
196
  }
187
197
  modified = true;
@@ -196,9 +206,10 @@ function createAliasPlugin(config, options) {
196
206
  if (fs3.existsSync(file)) {
197
207
  modified = true;
198
208
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
199
- return `${prefix}${quote}${toStrippedProdImportPath(
200
- file,
201
- options.rootDir
209
+ return `${prefix}${quote}${toProdImportFromImporter(
210
+ importer,
211
+ options.rootDir,
212
+ toStrippedProdImportPath(file, options.rootDir)
202
213
  )}${quote}`;
203
214
  }
204
215
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
@@ -209,9 +220,10 @@ function createAliasPlugin(config, options) {
209
220
  if (fs3.existsSync(file)) {
210
221
  modified = true;
211
222
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
212
- return `${prefix}${quote}${toStrippedProdImportPath(
213
- file,
214
- options.rootDir
223
+ return `${prefix}${quote}${toProdImportFromImporter(
224
+ importer,
225
+ options.rootDir,
226
+ toStrippedProdImportPath(file, options.rootDir)
215
227
  )}${quote}`;
216
228
  }
217
229
  return `${prefix}${quote}${toProdImportPath(file, importer)}${quote}`;
@@ -368,31 +380,13 @@ function createExternalRelativePlugin() {
368
380
  }
369
381
  };
370
382
  }
371
- async function compileConfig(options) {
372
- const { rootDir, dist } = options;
373
- const baseConfigName = findBaseConfig(rootDir);
374
- if (!baseConfigName) {
375
- return { generated: false, outputFile: "", skipped: false };
376
- }
377
- const absDist = path5.resolve(rootDir, dist);
378
- await fs5.promises.mkdir(absDist, { recursive: true });
379
- const outputFile = path5.resolve(absDist, "faapi-config.js");
380
- const cacheKey = `${toRealPath(rootDir)}::${dist}`;
381
- const cached = compileConfigCache.get(cacheKey);
382
- if (cached && await isCacheFresh(cached)) {
383
- return { generated: true, outputFile: cached.outputFile, skipped: true };
384
- }
385
- compileConfigCache.delete(cacheKey);
386
- const configEntryPoints = [path5.resolve(rootDir, baseConfigName)];
387
- const { insideFiles: appDirFiles, outsideFiles: nonAppDirFiles } = await collectRelativeImports(
388
- configEntryPoints,
389
- rootDir
390
- );
383
+ async function compileProjectModules(entryPoints, rootDir, dist) {
384
+ const { insideFiles, outsideFiles } = await collectRelativeImports(entryPoints, rootDir);
391
385
  const esbuild = await import("esbuild");
392
386
  const aliasPlugins = buildAliasPlugins(rootDir);
393
- const step1aEntries = [...configEntryPoints, ...nonAppDirFiles];
387
+ const absDist = path5.resolve(rootDir, dist);
394
388
  await esbuild.build({
395
- entryPoints: step1aEntries,
389
+ entryPoints: [...entryPoints, ...outsideFiles],
396
390
  outdir: absDist,
397
391
  outbase: rootDir,
398
392
  bundle: false,
@@ -403,10 +397,10 @@ async function compileConfig(options) {
403
397
  plugins: aliasPlugins,
404
398
  logLevel: "silent"
405
399
  });
406
- if (appDirFiles.length > 0) {
400
+ if (insideFiles.length > 0) {
407
401
  const appOutbase = path5.resolve(rootDir, "src");
408
402
  await esbuild.build({
409
- entryPoints: appDirFiles,
403
+ entryPoints: insideFiles,
410
404
  outdir: absDist,
411
405
  outbase: appOutbase,
412
406
  bundle: false,
@@ -418,9 +412,33 @@ async function compileConfig(options) {
418
412
  logLevel: "silent"
419
413
  });
420
414
  }
415
+ return { insideFiles, outsideFiles };
416
+ }
417
+ async function compileConfig(options) {
418
+ const { rootDir, dist } = options;
419
+ const baseConfigName = findBaseConfig(rootDir);
420
+ if (!baseConfigName) {
421
+ return { generated: false, outputFile: "", skipped: false };
422
+ }
423
+ const absDist = path5.resolve(rootDir, dist);
424
+ await fs5.promises.mkdir(absDist, { recursive: true });
425
+ const outputFile = path5.resolve(absDist, "faapi-config.js");
426
+ const cacheKey = `${toRealPath(rootDir)}::${dist}`;
427
+ const cached = compileConfigCache.get(cacheKey);
428
+ if (cached && await isCacheFresh(cached)) {
429
+ return { generated: true, outputFile: cached.outputFile, skipped: true };
430
+ }
431
+ compileConfigCache.delete(cacheKey);
432
+ const configEntryPoints = [path5.resolve(rootDir, baseConfigName)];
433
+ const { insideFiles: appDirFiles, outsideFiles: nonAppDirFiles } = await compileProjectModules(
434
+ configEntryPoints,
435
+ rootDir,
436
+ dist
437
+ );
421
438
  const baseImport = `import base from './${toProdImport(baseConfigName)}';`;
422
439
  const exportDefault = "export default base;";
423
440
  const entryCode = [baseImport, exportDefault].join("\n");
441
+ const esbuild = await import("esbuild");
424
442
  await esbuild.build({
425
443
  stdin: { contents: entryCode, resolveDir: absDist, loader: "ts" },
426
444
  outfile: outputFile,
@@ -7519,7 +7537,7 @@ import { createSecureServer as createHttp2SecureServer } from "http2";
7519
7537
  import { readFileSync } from "fs";
7520
7538
  import { Readable as Readable3 } from "stream";
7521
7539
  import path22 from "path";
7522
- function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
7540
+ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT, requestSignal) {
7523
7541
  const forwardedProto = req.headers["x-forwarded-proto"];
7524
7542
  const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
7525
7543
  const host = req.headers.host ?? "localhost";
@@ -7527,7 +7545,10 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
7527
7545
  const headers = nodeHttpToWebHeaders(req);
7528
7546
  const method = req.method ?? "GET";
7529
7547
  if (method === "GET" || method === "HEAD") {
7530
- return { request: new Request(url.toString(), { method, headers }), url };
7548
+ return {
7549
+ request: new Request(url.toString(), { method, headers, signal: requestSignal }),
7550
+ url
7551
+ };
7531
7552
  }
7532
7553
  const contentLength = req.headers["content-length"];
7533
7554
  if (contentLength !== void 0) {
@@ -7543,7 +7564,8 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
7543
7564
  method,
7544
7565
  headers,
7545
7566
  body: limitedStream,
7546
- duplex: "half"
7567
+ duplex: "half",
7568
+ signal: requestSignal
7547
7569
  }),
7548
7570
  url
7549
7571
  };
@@ -7682,8 +7704,8 @@ function createServer(options) {
7682
7704
  });
7683
7705
  return { server, routesRef };
7684
7706
  }
7685
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries) {
7686
- const { request, url } = toWebRequest(req, bodyLimit);
7707
+ function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
7708
+ const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
7687
7709
  const method = request.method.toUpperCase();
7688
7710
  const urlPath = url.pathname;
7689
7711
  const ctx = createContextFromUrl(
@@ -7768,8 +7790,19 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
7768
7790
  async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
7769
7791
  let meta = { headers: {}, setCookies: [] };
7770
7792
  let ctx;
7793
+ const abortController = new AbortController();
7794
+ res.on("close", () => {
7795
+ if (!res.writableEnded) abortController.abort();
7796
+ });
7771
7797
  try {
7772
- const prepared = prepareRequest(req, config, bodyLimit, trustedProxy, registries);
7798
+ const prepared = prepareRequest(
7799
+ req,
7800
+ config,
7801
+ bodyLimit,
7802
+ trustedProxy,
7803
+ registries,
7804
+ abortController.signal
7805
+ );
7773
7806
  ctx = prepared.ctx;
7774
7807
  meta = prepared.meta;
7775
7808
  const { request, url, method, urlPath } = prepared;
@@ -7855,8 +7888,9 @@ var init_startServer = __esm({
7855
7888
 
7856
7889
  // src/cli/loadPlugins.ts
7857
7890
  import path23 from "path";
7891
+ import fs17 from "fs";
7858
7892
  import { pathToFileURL as pathToFileURL2 } from "url";
7859
- async function loadPlugins(declarations, ctx, rootDir) {
7893
+ async function loadPlugins(declarations, ctx, rootDir, dist) {
7860
7894
  const handlerWrappers = [];
7861
7895
  const upgradeWrappers = [];
7862
7896
  const failures = [];
@@ -7893,7 +7927,7 @@ async function loadPlugins(declarations, ctx, rootDir) {
7893
7927
  }
7894
7928
  loaded.add(specifier);
7895
7929
  try {
7896
- const mod = await import(resolveSpecifier(specifier, rootDir));
7930
+ const mod = await importPluginModule(specifier, rootDir ?? process.cwd(), dist);
7897
7931
  const plugin = mod.default ?? mod;
7898
7932
  if (typeof plugin.setup !== "function") {
7899
7933
  failures.push({ specifier, reason: "plugin has no setup function" });
@@ -7916,15 +7950,70 @@ async function loadPlugins(declarations, ctx, rootDir) {
7916
7950
  }
7917
7951
  return { handlerWrappers, upgradeWrappers, failures };
7918
7952
  }
7919
- function resolveSpecifier(specifier, rootDir) {
7920
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
7921
- const base = rootDir ?? process.cwd();
7922
- return pathToFileURL2(path23.resolve(base, specifier)).href;
7953
+ function resolveLocalPluginSource(specifier, baseDir) {
7954
+ const base = path23.isAbsolute(specifier) ? specifier : path23.resolve(baseDir, specifier);
7955
+ if ((TS_EXTS.test(base) || JS_EXTS.test(base)) && fs17.existsSync(base)) return base;
7956
+ for (const ext of [".ts", ".js"]) {
7957
+ if (fs17.existsSync(base + ext)) return base + ext;
7958
+ }
7959
+ for (const indexExt of ["/index.ts", "/index.js"]) {
7960
+ if (fs17.existsSync(base + indexExt)) return base + indexExt;
7961
+ }
7962
+ return null;
7963
+ }
7964
+ function mirrorProductPath(sourcePath, baseDir, distDir) {
7965
+ let rel = path23.relative(baseDir, sourcePath).replace(/\\/g, "/");
7966
+ if (rel.startsWith("src/")) rel = rel.slice(4);
7967
+ return path23.resolve(distDir, rel.replace(TS_EXTS, ".js"));
7968
+ }
7969
+ function isSourceNewer(sourcePath, productPath) {
7970
+ try {
7971
+ return fs17.statSync(sourcePath).mtimeMs > fs17.statSync(productPath).mtimeMs;
7972
+ } catch {
7973
+ return true;
7974
+ }
7975
+ }
7976
+ async function importPluginModule(specifier, baseDir, dist) {
7977
+ const isRelative = specifier.startsWith("./") || specifier.startsWith("../");
7978
+ if (!isRelative && !path23.isAbsolute(specifier)) {
7979
+ return import(specifier);
7980
+ }
7981
+ const distDir = dist ? path23.resolve(baseDir, dist) : null;
7982
+ const sourcePath = resolveLocalPluginSource(specifier, baseDir);
7983
+ const productPath = sourcePath && distDir ? mirrorProductPath(sourcePath, baseDir, distDir) : null;
7984
+ if (productPath !== null && fs17.existsSync(productPath) && (sourcePath === null || !isSourceNewer(sourcePath, productPath))) {
7985
+ return import(pathToFileURL2(productPath).href);
7986
+ }
7987
+ if (sourcePath && TS_EXTS.test(sourcePath)) {
7988
+ if (!isDevOnDemandEnabled() || !distDir) {
7989
+ throw new Error(
7990
+ `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.`
7991
+ );
7992
+ }
7993
+ const relFromRoot = path23.relative(baseDir, sourcePath).replace(/\\/g, "/");
7994
+ if (relFromRoot.startsWith("src/")) {
7995
+ await ensureCompiled(sourcePath, baseDir, dist);
7996
+ } else {
7997
+ await compileProjectModules([sourcePath], baseDir, dist);
7998
+ }
7999
+ if (productPath && fs17.existsSync(productPath)) {
8000
+ return import(pathToFileURL2(productPath).href);
8001
+ }
7923
8002
  }
7924
- if (path23.isAbsolute(specifier)) {
7925
- return pathToFileURL2(specifier).href;
8003
+ if (sourcePath && JS_EXTS.test(sourcePath)) {
8004
+ return import(pathToFileURL2(sourcePath).href);
7926
8005
  }
7927
- return specifier;
8006
+ const candidates = [
8007
+ `${specifier}.ts`,
8008
+ `${specifier}/index.ts`,
8009
+ `${specifier}.js`,
8010
+ `${specifier}/index.js`
8011
+ ].map((c) => path23.isAbsolute(c) ? c : path23.join(baseDir, c));
8012
+ throw new Error(
8013
+ `Cannot find local plugin "${specifier}" (resolved from ${baseDir}). Expected one of:
8014
+ ${candidates.join("\n ")}
8015
+ Create the plugin file (export default { name, setup(ctx) {...} }) or fix the path.`
8016
+ );
7928
8017
  }
7929
8018
  function resolveDeclaration(decl) {
7930
8019
  if (typeof decl === "string") {
@@ -7942,19 +8031,24 @@ function resolveDeclaration(decl) {
7942
8031
  }
7943
8032
  throw new Error(`Invalid plugin declaration: ${JSON.stringify(decl)}`);
7944
8033
  }
8034
+ var TS_EXTS, JS_EXTS;
7945
8035
  var init_loadPlugins = __esm({
7946
8036
  "src/cli/loadPlugins.ts"() {
7947
8037
  "use strict";
8038
+ init_compileOnDemand();
8039
+ init_compileConfig();
8040
+ TS_EXTS = /\.(ts|tsx|mts|cts)$/;
8041
+ JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
7948
8042
  }
7949
8043
  });
7950
8044
 
7951
8045
  // src/cli/createAppCore.ts
7952
- import fs17 from "fs";
8046
+ import fs18 from "fs";
7953
8047
  import path24 from "path";
7954
8048
  import { PassThrough, Readable as Readable4 } from "stream";
7955
8049
  async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries) {
7956
8050
  const toolsPath = path24.resolve(rootDir, dist, TOOLS_FILE2);
7957
- if (!fs17.existsSync(toolsPath)) {
8051
+ if (!fs18.existsSync(toolsPath)) {
7958
8052
  return [];
7959
8053
  }
7960
8054
  const serialized = await importWithCacheBust(toolsPath);
@@ -7964,7 +8058,7 @@ async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries
7964
8058
  }
7965
8059
  async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistries) {
7966
8060
  const agentsPath = path24.resolve(rootDir, dist, AGENTS_FILE2);
7967
- if (!fs17.existsSync(agentsPath)) {
8061
+ if (!fs18.existsSync(agentsPath)) {
7968
8062
  return [];
7969
8063
  }
7970
8064
  const serialized = await importWithCacheBust(agentsPath);
@@ -8005,7 +8099,7 @@ async function createAppBase(options) {
8005
8099
  const rootDir = options?.rootDir ?? process.cwd();
8006
8100
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
8007
8101
  const routesPath = path24.resolve(rootDir, dist, ROUTES_FILE);
8008
- if (!fs17.existsSync(routesPath)) {
8102
+ if (!fs18.existsSync(routesPath)) {
8009
8103
  throw new Error(
8010
8104
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
8011
8105
  );
@@ -8057,7 +8151,8 @@ async function createAppBase(options) {
8057
8151
  server,
8058
8152
  config: pluginConfig
8059
8153
  },
8060
- rootDir
8154
+ rootDir,
8155
+ dist
8061
8156
  );
8062
8157
  applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
8063
8158
  let closed = false;
@@ -8070,6 +8165,16 @@ async function createAppBase(options) {
8070
8165
  async listen(listenPort) {
8071
8166
  const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
8072
8167
  const actualPort = listenPort ?? options?.port ?? envPort ?? DEFAULT_PORT;
8168
+ if (config?.lifecycle?.onBoot) {
8169
+ try {
8170
+ await config.lifecycle.onBoot({ rootDir, routes: sorted, server, registries });
8171
+ console.log("- onBoot hook executed");
8172
+ } catch (err) {
8173
+ const message = err instanceof Error ? err.message : String(err);
8174
+ console.error(`[faapi] onBoot hook failed: ${message}`);
8175
+ throw err;
8176
+ }
8177
+ }
8073
8178
  return new Promise((resolve3, reject) => {
8074
8179
  const onListenError = (err) => {
8075
8180
  if (err.code === "EADDRINUSE") {
@@ -8447,14 +8552,14 @@ __export(buildCommand_exports, {
8447
8552
  buildCommand: () => buildCommand
8448
8553
  });
8449
8554
  import path26 from "path";
8450
- import fs18 from "fs";
8555
+ import fs19 from "fs";
8451
8556
  async function buildCommand(options) {
8452
8557
  const rootDir = options?.rootDir ?? process.cwd();
8453
8558
  const outdir = options?.dist ?? DEFAULT_DIST2;
8454
8559
  const absOut = path26.resolve(rootDir, outdir);
8455
8560
  const realRoot = toRealPath(path26.resolve(rootDir));
8456
8561
  if (isInsideDir(toRealPath(absOut), realRoot)) {
8457
- fs18.rmSync(absOut, { recursive: true, force: true });
8562
+ fs19.rmSync(absOut, { recursive: true, force: true });
8458
8563
  } else {
8459
8564
  console.warn(
8460
8565
  `! Output directory "${outdir}" is outside the project root, skipping clean (stale artifacts may remain)`
@@ -8463,9 +8568,9 @@ async function buildCommand(options) {
8463
8568
  await compileConfig({ rootDir, dist: outdir });
8464
8569
  const _config = await loadConfig(rootDir, outdir);
8465
8570
  const pkgPath = path26.resolve(rootDir, "package.json");
8466
- if (fs18.existsSync(pkgPath)) {
8571
+ if (fs19.existsSync(pkgPath)) {
8467
8572
  try {
8468
- const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
8573
+ const pkg = JSON.parse(fs19.readFileSync(pkgPath, "utf-8"));
8469
8574
  if (pkg.type !== "module") {
8470
8575
  console.warn(
8471
8576
  '! package.json is missing "type": "module" \u2014 build output is ESM and `node dist/main` will fail without it'
@@ -8489,6 +8594,18 @@ async function buildCommand(options) {
8489
8594
  console.warn(" ! No source files found, nothing to build");
8490
8595
  return;
8491
8596
  }
8597
+ const localPluginSources = extractLocalPluginSources(_config?.plugins, rootDir);
8598
+ if (localPluginSources.length > 0) {
8599
+ console.log("\n[2.5/8] Compiling local plugins...");
8600
+ const outside = localPluginSources.filter((p) => {
8601
+ const rel = path26.relative(rootDir, p).replace(/\\/g, "/");
8602
+ return !rel.startsWith("src/");
8603
+ });
8604
+ if (outside.length > 0) {
8605
+ await compileProjectModules(outside, rootDir, outdir);
8606
+ }
8607
+ console.log(` Compiled ${localPluginSources.length} local plugin(s)`);
8608
+ }
8492
8609
  console.log("\n[3/8] Scanning routes...");
8493
8610
  const { routes, wsRoutes } = await scanRoutes(rootDir, ROUTE_PATTERNS, outdir);
8494
8611
  const sorted = sortRoutes(routes);
@@ -8534,10 +8651,31 @@ loadEnv(process.cwd());
8534
8651
  const app = await createProdApp(${createProdAppArgs});
8535
8652
  await app.listen();
8536
8653
  `;
8537
- await fs18.promises.writeFile(mainPath, mainContent, "utf-8");
8654
+ await fs19.promises.writeFile(mainPath, mainContent, "utf-8");
8538
8655
  console.log(` Written to ${mainPath}`);
8539
8656
  console.log("\nfaapi build completed");
8540
8657
  }
8658
+ function extractLocalPluginSources(declarations, rootDir) {
8659
+ if (!declarations || declarations.length === 0) return [];
8660
+ const sources = [];
8661
+ const seen = /* @__PURE__ */ new Set();
8662
+ for (const decl of declarations) {
8663
+ let specifier;
8664
+ if (typeof decl === "string") specifier = decl;
8665
+ else if (Array.isArray(decl)) specifier = decl[0];
8666
+ else if ("path" in decl) specifier = decl.path;
8667
+ if (!specifier) continue;
8668
+ if (!specifier.startsWith("./") && !specifier.startsWith("../") && !path26.isAbsolute(specifier)) {
8669
+ continue;
8670
+ }
8671
+ const source = resolveLocalPluginSource(specifier, rootDir);
8672
+ if (source && !seen.has(source)) {
8673
+ seen.add(source);
8674
+ sources.push(source);
8675
+ }
8676
+ }
8677
+ return sources;
8678
+ }
8541
8679
  var DEFAULT_DIST2;
8542
8680
  var init_buildCommand = __esm({
8543
8681
  "src/cli/buildCommand.ts"() {
@@ -8557,6 +8695,7 @@ var init_buildCommand = __esm({
8557
8695
  init_prodPaths();
8558
8696
  init_compileConfig();
8559
8697
  init_loadConfig();
8698
+ init_loadPlugins();
8560
8699
  init_prodPaths();
8561
8700
  DEFAULT_DIST2 = "dist";
8562
8701
  }