@faapi/faapi 4.1.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/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,
@@ -7870,8 +7888,9 @@ var init_startServer = __esm({
7870
7888
 
7871
7889
  // src/cli/loadPlugins.ts
7872
7890
  import path23 from "path";
7891
+ import fs17 from "fs";
7873
7892
  import { pathToFileURL as pathToFileURL2 } from "url";
7874
- async function loadPlugins(declarations, ctx, rootDir) {
7893
+ async function loadPlugins(declarations, ctx, rootDir, dist) {
7875
7894
  const handlerWrappers = [];
7876
7895
  const upgradeWrappers = [];
7877
7896
  const failures = [];
@@ -7908,7 +7927,7 @@ async function loadPlugins(declarations, ctx, rootDir) {
7908
7927
  }
7909
7928
  loaded.add(specifier);
7910
7929
  try {
7911
- const mod = await import(resolveSpecifier(specifier, rootDir));
7930
+ const mod = await importPluginModule(specifier, rootDir ?? process.cwd(), dist);
7912
7931
  const plugin = mod.default ?? mod;
7913
7932
  if (typeof plugin.setup !== "function") {
7914
7933
  failures.push({ specifier, reason: "plugin has no setup function" });
@@ -7931,15 +7950,70 @@ async function loadPlugins(declarations, ctx, rootDir) {
7931
7950
  }
7932
7951
  return { handlerWrappers, upgradeWrappers, failures };
7933
7952
  }
7934
- function resolveSpecifier(specifier, rootDir) {
7935
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
7936
- const base = rootDir ?? process.cwd();
7937
- 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;
7938
7958
  }
7939
- if (path23.isAbsolute(specifier)) {
7940
- return pathToFileURL2(specifier).href;
7959
+ for (const indexExt of ["/index.ts", "/index.js"]) {
7960
+ if (fs17.existsSync(base + indexExt)) return base + indexExt;
7941
7961
  }
7942
- return specifier;
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
+ }
8002
+ }
8003
+ if (sourcePath && JS_EXTS.test(sourcePath)) {
8004
+ return import(pathToFileURL2(sourcePath).href);
8005
+ }
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
+ );
7943
8017
  }
7944
8018
  function resolveDeclaration(decl) {
7945
8019
  if (typeof decl === "string") {
@@ -7957,19 +8031,24 @@ function resolveDeclaration(decl) {
7957
8031
  }
7958
8032
  throw new Error(`Invalid plugin declaration: ${JSON.stringify(decl)}`);
7959
8033
  }
8034
+ var TS_EXTS, JS_EXTS;
7960
8035
  var init_loadPlugins = __esm({
7961
8036
  "src/cli/loadPlugins.ts"() {
7962
8037
  "use strict";
8038
+ init_compileOnDemand();
8039
+ init_compileConfig();
8040
+ TS_EXTS = /\.(ts|tsx|mts|cts)$/;
8041
+ JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
7963
8042
  }
7964
8043
  });
7965
8044
 
7966
8045
  // src/cli/createAppCore.ts
7967
- import fs17 from "fs";
8046
+ import fs18 from "fs";
7968
8047
  import path24 from "path";
7969
8048
  import { PassThrough, Readable as Readable4 } from "stream";
7970
8049
  async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries) {
7971
8050
  const toolsPath = path24.resolve(rootDir, dist, TOOLS_FILE2);
7972
- if (!fs17.existsSync(toolsPath)) {
8051
+ if (!fs18.existsSync(toolsPath)) {
7973
8052
  return [];
7974
8053
  }
7975
8054
  const serialized = await importWithCacheBust(toolsPath);
@@ -7979,7 +8058,7 @@ async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries
7979
8058
  }
7980
8059
  async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistries) {
7981
8060
  const agentsPath = path24.resolve(rootDir, dist, AGENTS_FILE2);
7982
- if (!fs17.existsSync(agentsPath)) {
8061
+ if (!fs18.existsSync(agentsPath)) {
7983
8062
  return [];
7984
8063
  }
7985
8064
  const serialized = await importWithCacheBust(agentsPath);
@@ -8020,7 +8099,7 @@ async function createAppBase(options) {
8020
8099
  const rootDir = options?.rootDir ?? process.cwd();
8021
8100
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
8022
8101
  const routesPath = path24.resolve(rootDir, dist, ROUTES_FILE);
8023
- if (!fs17.existsSync(routesPath)) {
8102
+ if (!fs18.existsSync(routesPath)) {
8024
8103
  throw new Error(
8025
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`
8026
8105
  );
@@ -8072,7 +8151,8 @@ async function createAppBase(options) {
8072
8151
  server,
8073
8152
  config: pluginConfig
8074
8153
  },
8075
- rootDir
8154
+ rootDir,
8155
+ dist
8076
8156
  );
8077
8157
  applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
8078
8158
  let closed = false;
@@ -8085,6 +8165,16 @@ async function createAppBase(options) {
8085
8165
  async listen(listenPort) {
8086
8166
  const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
8087
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
+ }
8088
8178
  return new Promise((resolve3, reject) => {
8089
8179
  const onListenError = (err) => {
8090
8180
  if (err.code === "EADDRINUSE") {
@@ -8462,14 +8552,14 @@ __export(buildCommand_exports, {
8462
8552
  buildCommand: () => buildCommand
8463
8553
  });
8464
8554
  import path26 from "path";
8465
- import fs18 from "fs";
8555
+ import fs19 from "fs";
8466
8556
  async function buildCommand(options) {
8467
8557
  const rootDir = options?.rootDir ?? process.cwd();
8468
8558
  const outdir = options?.dist ?? DEFAULT_DIST2;
8469
8559
  const absOut = path26.resolve(rootDir, outdir);
8470
8560
  const realRoot = toRealPath(path26.resolve(rootDir));
8471
8561
  if (isInsideDir(toRealPath(absOut), realRoot)) {
8472
- fs18.rmSync(absOut, { recursive: true, force: true });
8562
+ fs19.rmSync(absOut, { recursive: true, force: true });
8473
8563
  } else {
8474
8564
  console.warn(
8475
8565
  `! Output directory "${outdir}" is outside the project root, skipping clean (stale artifacts may remain)`
@@ -8478,9 +8568,9 @@ async function buildCommand(options) {
8478
8568
  await compileConfig({ rootDir, dist: outdir });
8479
8569
  const _config = await loadConfig(rootDir, outdir);
8480
8570
  const pkgPath = path26.resolve(rootDir, "package.json");
8481
- if (fs18.existsSync(pkgPath)) {
8571
+ if (fs19.existsSync(pkgPath)) {
8482
8572
  try {
8483
- const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
8573
+ const pkg = JSON.parse(fs19.readFileSync(pkgPath, "utf-8"));
8484
8574
  if (pkg.type !== "module") {
8485
8575
  console.warn(
8486
8576
  '! package.json is missing "type": "module" \u2014 build output is ESM and `node dist/main` will fail without it'
@@ -8504,6 +8594,18 @@ async function buildCommand(options) {
8504
8594
  console.warn(" ! No source files found, nothing to build");
8505
8595
  return;
8506
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
+ }
8507
8609
  console.log("\n[3/8] Scanning routes...");
8508
8610
  const { routes, wsRoutes } = await scanRoutes(rootDir, ROUTE_PATTERNS, outdir);
8509
8611
  const sorted = sortRoutes(routes);
@@ -8549,10 +8651,31 @@ loadEnv(process.cwd());
8549
8651
  const app = await createProdApp(${createProdAppArgs});
8550
8652
  await app.listen();
8551
8653
  `;
8552
- await fs18.promises.writeFile(mainPath, mainContent, "utf-8");
8654
+ await fs19.promises.writeFile(mainPath, mainContent, "utf-8");
8553
8655
  console.log(` Written to ${mainPath}`);
8554
8656
  console.log("\nfaapi build completed");
8555
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
+ }
8556
8679
  var DEFAULT_DIST2;
8557
8680
  var init_buildCommand = __esm({
8558
8681
  "src/cli/buildCommand.ts"() {
@@ -8572,6 +8695,7 @@ var init_buildCommand = __esm({
8572
8695
  init_prodPaths();
8573
8696
  init_compileConfig();
8574
8697
  init_loadConfig();
8698
+ init_loadPlugins();
8575
8699
  init_prodPaths();
8576
8700
  DEFAULT_DIST2 = "dist";
8577
8701
  }