@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 +5 -0
- package/dist/cli/index.js +197 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +221 -85
- package/dist/index.js.map +1 -1
- package/dist/testing.js +42 -15
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
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}${
|
|
2097
|
-
|
|
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}${
|
|
2114
|
-
|
|
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}${
|
|
2127
|
-
|
|
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(
|
|
3100
|
-
super("ROUTE_NOT_FOUND", `Route not found: ${
|
|
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,
|
|
3106
|
-
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${
|
|
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
|
|
3133
|
-
import
|
|
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,
|
|
3240
|
+
function matchRoute(routes, method, path24) {
|
|
3229
3241
|
const index = getHttpIndex(routes);
|
|
3230
3242
|
const upper = method.toUpperCase();
|
|
3231
|
-
const hit = matchByMethod(index, upper,
|
|
3243
|
+
const hit = matchByMethod(index, upper, path24);
|
|
3232
3244
|
if (hit) return hit;
|
|
3233
3245
|
if (upper === "HEAD") {
|
|
3234
|
-
return matchByMethod(index, "GET",
|
|
3246
|
+
return matchByMethod(index, "GET", path24);
|
|
3235
3247
|
}
|
|
3236
3248
|
return null;
|
|
3237
3249
|
}
|
|
3238
|
-
function matchByMethod(index, method,
|
|
3239
|
-
const staticHit = index.static.get(`${method}|${
|
|
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,
|
|
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,
|
|
3267
|
+
function matchWsRoute(wsRoutes, path24) {
|
|
3256
3268
|
const index = getWsIndex(wsRoutes);
|
|
3257
|
-
const staticHit = index.static.get(
|
|
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,
|
|
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,
|
|
3281
|
+
function findAllowedMethods(routes, path24) {
|
|
3270
3282
|
const index = getHttpIndex(routes);
|
|
3271
3283
|
const methods = /* @__PURE__ */ new Set();
|
|
3272
|
-
const staticMethods = index.methodsByStaticPath.get(
|
|
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
|
-
|
|
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,
|
|
3295
|
-
return matchSegments(pattern.split("/").filter(Boolean),
|
|
3306
|
+
function matchDynamicPath(pattern, path24, paramNames, isCatchAll) {
|
|
3307
|
+
return matchSegments(pattern.split("/").filter(Boolean), path24, paramNames, isCatchAll);
|
|
3296
3308
|
}
|
|
3297
|
-
function matchSegments(patternSegments,
|
|
3298
|
-
const pathSegments =
|
|
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
|
|
4198
|
+
const path24 = issue.path.map(String).join(".") || "";
|
|
4187
4199
|
return {
|
|
4188
|
-
path:
|
|
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 {
|
|
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(
|
|
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
|
|
5301
|
+
import path19 from "path";
|
|
5302
|
+
import fs16 from "fs";
|
|
5275
5303
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5276
|
-
|
|
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
|
|
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
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
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
|
-
|
|
5342
|
-
|
|
5412
|
+
for (const indexExt of ["/index.ts", "/index.js"]) {
|
|
5413
|
+
if (fs16.existsSync(base + indexExt)) return base + indexExt;
|
|
5343
5414
|
}
|
|
5344
|
-
return
|
|
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 =
|
|
5371
|
-
if (!
|
|
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 =
|
|
5381
|
-
if (!
|
|
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 =
|
|
5449
|
-
if (!
|
|
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
|
|
5697
|
-
import
|
|
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(
|
|
5705
|
-
if (!
|
|
5706
|
-
let result =
|
|
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 =
|
|
5768
|
-
const resolvedRoot =
|
|
5903
|
+
const routeDir = path21.dirname(routeFilePath);
|
|
5904
|
+
const resolvedRoot = path21.resolve(rootDir);
|
|
5769
5905
|
const paths = [];
|
|
5770
|
-
let currentDir =
|
|
5906
|
+
let currentDir = path21.resolve(rootDir, routeDir);
|
|
5771
5907
|
while (true) {
|
|
5772
5908
|
if (dist) {
|
|
5773
|
-
const mwTsPath =
|
|
5774
|
-
const mwJsPath =
|
|
5775
|
-
const absTsPath =
|
|
5776
|
-
const absJsPath =
|
|
5777
|
-
const absMwPath =
|
|
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 =
|
|
5780
|
-
const prodAbsPath =
|
|
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 =
|
|
5786
|
-
const absMwPath =
|
|
5787
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
5863
|
-
import
|
|
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 =
|
|
5914
|
-
const source = await
|
|
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
|
|
5939
|
-
import
|
|
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 =
|
|
5972
|
-
const source = await
|
|
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);
|