@faapi/faapi 3.2.1 → 4.0.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 +2416 -1436
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +258 -289
- package/dist/index.js +1599 -736
- package/dist/index.js.map +1 -1
- package/dist/{routeTypes-FtbRkpVF.d.ts → routeTypes-DnJuuvq-.d.ts} +219 -1
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +1304 -446
- package/dist/testing.js.map +1 -1
- package/package.json +10 -7
package/dist/index.js
CHANGED
|
@@ -67,6 +67,46 @@ function createProgram(filePath) {
|
|
|
67
67
|
if (cached) {
|
|
68
68
|
return cached;
|
|
69
69
|
}
|
|
70
|
+
const program = buildProgram([filePath], findTsConfig(filePath));
|
|
71
|
+
programCache.set(filePath, program);
|
|
72
|
+
return program;
|
|
73
|
+
}
|
|
74
|
+
function createPrograms(filePaths) {
|
|
75
|
+
const unique = [...new Set(filePaths)];
|
|
76
|
+
const result = /* @__PURE__ */ new Map();
|
|
77
|
+
const groups = /* @__PURE__ */ new Map();
|
|
78
|
+
const noTsconfigFiles = [];
|
|
79
|
+
for (const filePath of unique) {
|
|
80
|
+
const tsconfigPath = findTsConfig(filePath);
|
|
81
|
+
if (!tsconfigPath) {
|
|
82
|
+
noTsconfigFiles.push(filePath);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const group = groups.get(tsconfigPath);
|
|
86
|
+
if (group) {
|
|
87
|
+
group.files.push(filePath);
|
|
88
|
+
} else {
|
|
89
|
+
groups.set(tsconfigPath, { tsconfigPath, files: [filePath] });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const { tsconfigPath, files } of groups.values()) {
|
|
93
|
+
const cacheKey = `shared::${tsconfigPath}`;
|
|
94
|
+
let program = programCache.get(cacheKey);
|
|
95
|
+
const coversAll = program !== void 0 && files.every((f) => program.getSourceFile(f) !== void 0);
|
|
96
|
+
if (!program || !coversAll) {
|
|
97
|
+
program = buildProgram(files, tsconfigPath);
|
|
98
|
+
programCache.set(cacheKey, program);
|
|
99
|
+
}
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
result.set(filePath, program);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const filePath of noTsconfigFiles) {
|
|
105
|
+
result.set(filePath, createProgram(filePath));
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function buildProgram(entryFiles, tsconfigPath) {
|
|
70
110
|
const options = {
|
|
71
111
|
strict: true,
|
|
72
112
|
target: ts.ScriptTarget.ES2022,
|
|
@@ -75,8 +115,7 @@ function createProgram(filePath) {
|
|
|
75
115
|
skipLibCheck: true,
|
|
76
116
|
noEmit: true
|
|
77
117
|
};
|
|
78
|
-
|
|
79
|
-
const tsconfigPath = findTsConfig(filePath);
|
|
118
|
+
const rootNames = [...entryFiles];
|
|
80
119
|
if (tsconfigPath) {
|
|
81
120
|
const tsOptions = parseTsConfig(tsconfigPath);
|
|
82
121
|
if (tsOptions.module !== void 0) {
|
|
@@ -86,16 +125,14 @@ function createProgram(filePath) {
|
|
|
86
125
|
options.moduleResolution = tsOptions.moduleResolution;
|
|
87
126
|
}
|
|
88
127
|
if (tsOptions.fileNames.length > 0) {
|
|
89
|
-
|
|
90
|
-
rootNames
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
for (const fileName of tsOptions.fileNames) {
|
|
129
|
+
if (!rootNames.includes(fileName)) {
|
|
130
|
+
rootNames.push(fileName);
|
|
131
|
+
}
|
|
93
132
|
}
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
|
-
|
|
97
|
-
programCache.set(filePath, program);
|
|
98
|
-
return program;
|
|
135
|
+
return ts.createProgram(rootNames, options);
|
|
99
136
|
}
|
|
100
137
|
var programCache, tsConfigCache;
|
|
101
138
|
var init_createProgram = __esm({
|
|
@@ -111,7 +148,7 @@ import ts2 from "typescript";
|
|
|
111
148
|
function setProgramContext(program) {
|
|
112
149
|
currentProgram = program;
|
|
113
150
|
}
|
|
114
|
-
function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
|
|
151
|
+
function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
|
|
115
152
|
const kind = typeNode.kind;
|
|
116
153
|
switch (kind) {
|
|
117
154
|
case ts2.SyntaxKind.StringKeyword:
|
|
@@ -167,13 +204,13 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
167
204
|
if (ts2.isArrayTypeNode(typeNode)) {
|
|
168
205
|
return {
|
|
169
206
|
kind: "array",
|
|
170
|
-
element: resolveTypeNode(typeNode.elementType, checker, visited)
|
|
207
|
+
element: resolveTypeNode(typeNode.elementType, checker, visited, bindings)
|
|
171
208
|
};
|
|
172
209
|
}
|
|
173
210
|
if (ts2.isTupleTypeNode(typeNode)) {
|
|
174
211
|
const elements = typeNode.elements.map((e) => {
|
|
175
212
|
if (ts2.isRestTypeNode(e)) {
|
|
176
|
-
const inner = resolveTypeNode(e.type, checker, visited);
|
|
213
|
+
const inner = resolveTypeNode(e.type, checker, visited, bindings);
|
|
177
214
|
if (inner.kind === "array") {
|
|
178
215
|
return { type: inner.element, optional: false, rest: true };
|
|
179
216
|
}
|
|
@@ -181,20 +218,20 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
181
218
|
}
|
|
182
219
|
if (ts2.isNamedTupleMember(e)) {
|
|
183
220
|
return {
|
|
184
|
-
type: resolveTypeNode(e.type, checker, visited),
|
|
221
|
+
type: resolveTypeNode(e.type, checker, visited, bindings),
|
|
185
222
|
optional: !!e.questionToken,
|
|
186
223
|
rest: false
|
|
187
224
|
};
|
|
188
225
|
}
|
|
189
226
|
if (ts2.isOptionalTypeNode(e)) {
|
|
190
227
|
return {
|
|
191
|
-
type: resolveTypeNode(e.type, checker, visited),
|
|
228
|
+
type: resolveTypeNode(e.type, checker, visited, bindings),
|
|
192
229
|
optional: true,
|
|
193
230
|
rest: false
|
|
194
231
|
};
|
|
195
232
|
}
|
|
196
233
|
return {
|
|
197
|
-
type: resolveTypeNode(e, checker, visited),
|
|
234
|
+
type: resolveTypeNode(e, checker, visited, bindings),
|
|
198
235
|
optional: false,
|
|
199
236
|
rest: false
|
|
200
237
|
};
|
|
@@ -202,40 +239,80 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
|
|
|
202
239
|
return { kind: "tuple", elements };
|
|
203
240
|
}
|
|
204
241
|
if (ts2.isUnionTypeNode(typeNode)) {
|
|
205
|
-
const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
|
|
242
|
+
const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited, bindings));
|
|
206
243
|
return { kind: "union", members };
|
|
207
244
|
}
|
|
208
245
|
if (ts2.isIntersectionTypeNode(typeNode)) {
|
|
209
|
-
const
|
|
246
|
+
const propMap = /* @__PURE__ */ new Map();
|
|
210
247
|
for (const t of typeNode.types) {
|
|
211
|
-
const resolved = resolveTypeNode(t, checker, visited);
|
|
212
|
-
if (resolved.kind
|
|
213
|
-
|
|
248
|
+
const resolved = resolveTypeNode(t, checker, visited, bindings);
|
|
249
|
+
if (resolved.kind !== "object") {
|
|
250
|
+
throw new SchemaExtractionError(
|
|
251
|
+
typeNode.getText(),
|
|
252
|
+
`\u4EA4\u53C9\u7C7B\u578B\u5305\u542B\u975E object \u6210\u5458\uFF08${resolved.kind}\uFF09,\u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C\u2014\u2014branded \u7C7B\u578B\u5EFA\u8BAE\u6539\u7528\u5177\u4F53\u7C7B\u578B\u6216 unknown`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const prop of resolved.properties) {
|
|
256
|
+
const existing = propMap.get(prop.name);
|
|
257
|
+
if (!existing) {
|
|
258
|
+
propMap.set(prop.name, prop);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (JSON.stringify(existing.type) !== JSON.stringify(prop.type) || existing.optional !== prop.optional) {
|
|
262
|
+
throw SchemaExtractionError.at(
|
|
263
|
+
typeNode,
|
|
264
|
+
typeNode.getText(),
|
|
265
|
+
`\u4EA4\u53C9\u7C7B\u578B\u6210\u5458\u7684\u540C\u540D\u5B57\u6BB5 "${prop.name}" \u7C7B\u578B\u51B2\u7A81\uFF08TS \u4E2D\u4E3A never\uFF09,\u8FD0\u884C\u65F6\u65E0\u6CD5\u6821\u9A8C`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (!existing.constraints?.length && prop.constraints?.length) {
|
|
269
|
+
propMap.set(prop.name, prop);
|
|
270
|
+
}
|
|
214
271
|
}
|
|
215
272
|
}
|
|
216
|
-
return { kind: "object", properties };
|
|
273
|
+
return { kind: "object", properties: [...propMap.values()] };
|
|
217
274
|
}
|
|
218
275
|
if (ts2.isTypeLiteralNode(typeNode)) {
|
|
219
|
-
return resolveTypeLiteral(typeNode, checker, visited);
|
|
276
|
+
return resolveTypeLiteral(typeNode, checker, visited, bindings);
|
|
220
277
|
}
|
|
221
278
|
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.KeyOfKeyword) {
|
|
222
279
|
return resolveKeyOf(typeNode, checker);
|
|
223
280
|
}
|
|
224
281
|
if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.ReadonlyKeyword) {
|
|
225
|
-
return resolveTypeNode(typeNode.type, checker, visited);
|
|
282
|
+
return resolveTypeNode(typeNode.type, checker, visited, bindings);
|
|
226
283
|
}
|
|
227
284
|
if (ts2.isTypeReferenceNode(typeNode)) {
|
|
228
|
-
return resolveTypeReference(typeNode, checker, visited);
|
|
285
|
+
return resolveTypeReference(typeNode, checker, visited, bindings);
|
|
286
|
+
}
|
|
287
|
+
if (ts2.isExpressionWithTypeArguments(typeNode)) {
|
|
288
|
+
return resolveTypeReference(
|
|
289
|
+
{
|
|
290
|
+
getText: () => typeNode.getText(),
|
|
291
|
+
typeName: typeNode.expression,
|
|
292
|
+
typeArguments: typeNode.typeArguments
|
|
293
|
+
},
|
|
294
|
+
checker,
|
|
295
|
+
visited,
|
|
296
|
+
bindings
|
|
297
|
+
);
|
|
229
298
|
}
|
|
230
|
-
throw
|
|
299
|
+
throw SchemaExtractionError.at(typeNode, typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
|
|
231
300
|
}
|
|
232
|
-
function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
|
|
301
|
+
function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
|
|
233
302
|
const properties = [];
|
|
303
|
+
let catchall;
|
|
234
304
|
for (const member of typeNode.members) {
|
|
305
|
+
if (ts2.isMethodSignature(member) || ts2.isGetAccessorDeclaration(member) || ts2.isSetAccessorDeclaration(member)) {
|
|
306
|
+
throw SchemaExtractionError.at(
|
|
307
|
+
member,
|
|
308
|
+
member.getText(),
|
|
309
|
+
"\u5BF9\u8C61\u7C7B\u578B\u542B\u65B9\u6CD5\u7B7E\u540D\u6216\u5B58\u53D6\u5668,\u8FD0\u884C\u65F6 JSON \u6570\u636E\u65E0\u6CD5\u6821\u9A8C\u65B9\u6CD5\u2014\u2014\u8BF7\u6539\u7528\u5177\u4F53\u5C5E\u6027\u7C7B\u578B"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
235
312
|
if (ts2.isPropertySignature(member) && member.name) {
|
|
236
313
|
const name = member.name.getText();
|
|
237
314
|
const optional = !!member.questionToken;
|
|
238
|
-
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
315
|
+
const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
|
|
239
316
|
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
240
317
|
validateConstraints(constraints, type, name);
|
|
241
318
|
properties.push(
|
|
@@ -243,12 +320,10 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
|
|
|
243
320
|
);
|
|
244
321
|
}
|
|
245
322
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
246
|
-
|
|
247
|
-
const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
248
|
-
return { kind: "record", key: keyType, value: valueType };
|
|
323
|
+
catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
|
|
249
324
|
}
|
|
250
325
|
}
|
|
251
|
-
return { kind: "object", properties };
|
|
326
|
+
return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
|
|
252
327
|
}
|
|
253
328
|
function extractLiteralKeys(type) {
|
|
254
329
|
if (type.kind === "literal" && typeof type.value === "string") {
|
|
@@ -317,36 +392,48 @@ function resolveKeyOf(typeNode, checker) {
|
|
|
317
392
|
}
|
|
318
393
|
throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u8054\u5408");
|
|
319
394
|
}
|
|
320
|
-
function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
|
|
395
|
+
function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
|
|
321
396
|
const typeName = typeNode.typeName.getText();
|
|
397
|
+
const bound = bindings.get(typeName);
|
|
398
|
+
if (bound) {
|
|
399
|
+
return bound;
|
|
400
|
+
}
|
|
322
401
|
if (typeName === "Date") {
|
|
323
402
|
return { kind: "date" };
|
|
324
403
|
}
|
|
325
404
|
if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
|
|
405
|
+
const [arg] = typeNode.typeArguments;
|
|
326
406
|
return {
|
|
327
407
|
kind: "array",
|
|
328
|
-
element: resolveTypeNode(
|
|
408
|
+
element: resolveTypeNode(arg, checker, visited, bindings)
|
|
329
409
|
};
|
|
330
410
|
}
|
|
331
411
|
if (typeName === "Record" && typeNode.typeArguments?.length === 2) {
|
|
412
|
+
const [keyArg, valueArg] = typeNode.typeArguments;
|
|
332
413
|
return {
|
|
333
414
|
kind: "record",
|
|
334
|
-
key: resolveTypeNode(
|
|
335
|
-
value: resolveTypeNode(
|
|
415
|
+
key: resolveTypeNode(keyArg, checker, visited, bindings),
|
|
416
|
+
value: resolveTypeNode(valueArg, checker, visited, bindings)
|
|
336
417
|
};
|
|
337
418
|
}
|
|
338
419
|
if ((typeName === "Partial" || typeName === "Required" || typeName === "Readonly") && typeNode.typeArguments?.length === 1) {
|
|
339
|
-
const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
|
|
420
|
+
const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
|
|
340
421
|
if (inner.kind === "object" && typeName === "Partial") {
|
|
341
422
|
return {
|
|
342
423
|
kind: "object",
|
|
343
424
|
properties: inner.properties.map((p) => ({ ...p, optional: true }))
|
|
344
425
|
};
|
|
345
426
|
}
|
|
427
|
+
if (inner.kind === "object" && typeName === "Required") {
|
|
428
|
+
return {
|
|
429
|
+
kind: "object",
|
|
430
|
+
properties: inner.properties.map((p) => ({ ...p, optional: false }))
|
|
431
|
+
};
|
|
432
|
+
}
|
|
346
433
|
return inner;
|
|
347
434
|
}
|
|
348
435
|
if ((typeName === "Pick" || typeName === "Omit") && typeNode.typeArguments?.length === 2) {
|
|
349
|
-
const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
|
|
436
|
+
const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
|
|
350
437
|
if (innerType.kind !== "object") {
|
|
351
438
|
throw new SchemaExtractionError(
|
|
352
439
|
typeNode.getText(),
|
|
@@ -354,7 +441,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
354
441
|
);
|
|
355
442
|
}
|
|
356
443
|
const keyTypeNode = typeNode.typeArguments[1];
|
|
357
|
-
let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited));
|
|
444
|
+
let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited, bindings));
|
|
358
445
|
if (keys === null) {
|
|
359
446
|
keys = extractKeysFromChecker(keyTypeNode, checker);
|
|
360
447
|
}
|
|
@@ -372,10 +459,11 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
372
459
|
"Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
|
|
373
460
|
);
|
|
374
461
|
}
|
|
462
|
+
const [mapKey, mapValue] = typeNode.typeArguments;
|
|
375
463
|
return {
|
|
376
464
|
kind: "map",
|
|
377
|
-
key: resolveTypeNode(
|
|
378
|
-
value: resolveTypeNode(
|
|
465
|
+
key: resolveTypeNode(mapKey, checker, visited, bindings),
|
|
466
|
+
value: resolveTypeNode(mapValue, checker, visited, bindings)
|
|
379
467
|
};
|
|
380
468
|
}
|
|
381
469
|
if (typeName === "Set") {
|
|
@@ -385,9 +473,10 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
385
473
|
"Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
|
|
386
474
|
);
|
|
387
475
|
}
|
|
476
|
+
const [setArg] = typeNode.typeArguments;
|
|
388
477
|
return {
|
|
389
478
|
kind: "set",
|
|
390
|
-
element: resolveTypeNode(
|
|
479
|
+
element: resolveTypeNode(setArg, checker, visited, bindings)
|
|
391
480
|
};
|
|
392
481
|
}
|
|
393
482
|
if (typeName === "WeakMap" || typeName === "WeakSet") {
|
|
@@ -410,21 +499,42 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
410
499
|
}
|
|
411
500
|
visited.add(typeName);
|
|
412
501
|
if (checker) {
|
|
413
|
-
const symbol = typeNode.typeName
|
|
502
|
+
const symbol = ts2.isIdentifier(typeNode.typeName) || ts2.isQualifiedName(typeNode.typeName) ? checker.getSymbolAtLocation(typeNode.typeName) : void 0;
|
|
414
503
|
if (symbol) {
|
|
415
504
|
const declaration = symbol.declarations?.[0];
|
|
416
505
|
if (declaration) {
|
|
417
506
|
if (ts2.isInterfaceDeclaration(declaration)) {
|
|
418
|
-
return resolveInterfaceDeclaration(
|
|
507
|
+
return resolveInterfaceDeclaration(
|
|
508
|
+
declaration,
|
|
509
|
+
checker,
|
|
510
|
+
visited,
|
|
511
|
+
bindings,
|
|
512
|
+
typeNode.typeArguments
|
|
513
|
+
);
|
|
419
514
|
}
|
|
420
515
|
if (ts2.isTypeAliasDeclaration(declaration)) {
|
|
421
|
-
|
|
516
|
+
const declBindings = bindTypeParameters(
|
|
517
|
+
declaration.typeParameters,
|
|
518
|
+
typeNode.typeArguments,
|
|
519
|
+
bindings,
|
|
520
|
+
checker,
|
|
521
|
+
visited,
|
|
522
|
+
typeNode
|
|
523
|
+
);
|
|
524
|
+
return resolveTypeNode(declaration.type, checker, visited, declBindings);
|
|
422
525
|
}
|
|
423
526
|
if (ts2.isEnumDeclaration(declaration)) {
|
|
424
527
|
return resolveEnumDeclaration(declaration);
|
|
425
528
|
}
|
|
426
529
|
if (ts2.isImportSpecifier(declaration) || ts2.isImportClause(declaration)) {
|
|
427
|
-
const resolved = resolveImportAlias(
|
|
530
|
+
const resolved = resolveImportAlias(
|
|
531
|
+
typeNode,
|
|
532
|
+
symbol,
|
|
533
|
+
checker,
|
|
534
|
+
visited,
|
|
535
|
+
bindings,
|
|
536
|
+
typeNode.typeArguments
|
|
537
|
+
);
|
|
428
538
|
if (resolved) return resolved;
|
|
429
539
|
}
|
|
430
540
|
}
|
|
@@ -432,17 +542,45 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
|
|
|
432
542
|
}
|
|
433
543
|
throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
|
|
434
544
|
}
|
|
435
|
-
function
|
|
545
|
+
function bindTypeParameters(typeParameters, typeArguments, outerBindings, checker, visited, errorNode) {
|
|
546
|
+
if (!typeParameters || typeParameters.length === 0) return outerBindings;
|
|
547
|
+
const bindings = new Map(outerBindings);
|
|
548
|
+
for (let i = 0; i < typeParameters.length; i++) {
|
|
549
|
+
const param = typeParameters[i];
|
|
550
|
+
if (!param) continue;
|
|
551
|
+
const arg = typeArguments?.[i];
|
|
552
|
+
if (arg) {
|
|
553
|
+
bindings.set(param.name.text, resolveTypeNode(arg, checker, visited, outerBindings));
|
|
554
|
+
} else if (param.default) {
|
|
555
|
+
bindings.set(param.name.text, resolveTypeNode(param.default, checker, visited, bindings));
|
|
556
|
+
} else {
|
|
557
|
+
throw new SchemaExtractionError(
|
|
558
|
+
errorNode.getText(),
|
|
559
|
+
`\u6CDB\u578B\u53C2\u6570 "${param.name.text}" \u7F3A\u5C11\u7C7B\u578B\u5B9E\u53C2\uFF08\u4E14\u65E0\u9ED8\u8BA4\u7C7B\u578B\uFF09`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return bindings;
|
|
564
|
+
}
|
|
565
|
+
function resolveImportAlias(typeNode, symbol, checker, visited, bindings = /* @__PURE__ */ new Map(), typeArguments) {
|
|
436
566
|
const typeName = typeNode.typeName.getText();
|
|
437
567
|
try {
|
|
438
568
|
const aliased = checker.getAliasedSymbol(symbol);
|
|
439
569
|
if (aliased && aliased.declarations && aliased.declarations.length > 0) {
|
|
440
570
|
const decl = aliased.declarations[0];
|
|
441
571
|
if (ts2.isInterfaceDeclaration(decl)) {
|
|
442
|
-
return resolveInterfaceDeclaration(decl, checker, visited);
|
|
572
|
+
return resolveInterfaceDeclaration(decl, checker, visited, bindings, typeArguments);
|
|
443
573
|
}
|
|
444
574
|
if (ts2.isTypeAliasDeclaration(decl)) {
|
|
445
|
-
|
|
575
|
+
const declBindings = bindTypeParameters(
|
|
576
|
+
decl.typeParameters,
|
|
577
|
+
typeArguments,
|
|
578
|
+
bindings,
|
|
579
|
+
checker,
|
|
580
|
+
visited,
|
|
581
|
+
typeNode
|
|
582
|
+
);
|
|
583
|
+
return resolveTypeNode(decl.type, checker, visited, declBindings);
|
|
446
584
|
}
|
|
447
585
|
if (ts2.isEnumDeclaration(decl)) {
|
|
448
586
|
return resolveEnumDeclaration(decl);
|
|
@@ -460,10 +598,18 @@ function resolveImportAlias(typeNode, symbol, checker, visited) {
|
|
|
460
598
|
const found = findTopLevelDecl(sourceFile, typeName);
|
|
461
599
|
if (found) {
|
|
462
600
|
if (found.kind === "interface") {
|
|
463
|
-
return resolveInterfaceDeclaration(found.node, checker, visited);
|
|
601
|
+
return resolveInterfaceDeclaration(found.node, checker, visited, bindings, typeArguments);
|
|
464
602
|
}
|
|
465
603
|
if (found.kind === "typeAlias") {
|
|
466
|
-
|
|
604
|
+
const declBindings = bindTypeParameters(
|
|
605
|
+
found.node.typeParameters,
|
|
606
|
+
typeArguments,
|
|
607
|
+
bindings,
|
|
608
|
+
checker,
|
|
609
|
+
visited,
|
|
610
|
+
typeNode
|
|
611
|
+
);
|
|
612
|
+
return resolveTypeNode(found.node.type, checker, visited, declBindings);
|
|
467
613
|
}
|
|
468
614
|
if (found.kind === "enum") {
|
|
469
615
|
return resolveEnumDeclaration(found.node);
|
|
@@ -510,13 +656,22 @@ function resolveEnumDeclaration(node) {
|
|
|
510
656
|
}
|
|
511
657
|
return { kind: "union", members };
|
|
512
658
|
}
|
|
513
|
-
function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
|
|
659
|
+
function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set(), outerBindings = /* @__PURE__ */ new Map(), typeArguments) {
|
|
514
660
|
const properties = [];
|
|
515
661
|
const propMap = /* @__PURE__ */ new Map();
|
|
662
|
+
let catchall;
|
|
663
|
+
const bindings = bindTypeParameters(
|
|
664
|
+
node.typeParameters,
|
|
665
|
+
typeArguments,
|
|
666
|
+
outerBindings,
|
|
667
|
+
checker,
|
|
668
|
+
visited,
|
|
669
|
+
node
|
|
670
|
+
);
|
|
516
671
|
for (const heritageClause of node.heritageClauses ?? []) {
|
|
517
672
|
if (heritageClause.token === ts2.SyntaxKind.ExtendsKeyword) {
|
|
518
673
|
for (const expr of heritageClause.types) {
|
|
519
|
-
const parentType = resolveTypeNode(expr, checker, visited);
|
|
674
|
+
const parentType = resolveTypeNode(expr, checker, visited, bindings);
|
|
520
675
|
if (parentType.kind === "object") {
|
|
521
676
|
for (const prop of parentType.properties) {
|
|
522
677
|
propMap.set(prop.name, prop);
|
|
@@ -526,10 +681,17 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
526
681
|
}
|
|
527
682
|
}
|
|
528
683
|
for (const member of node.members) {
|
|
684
|
+
if (ts2.isMethodSignature(member) || ts2.isGetAccessorDeclaration(member) || ts2.isSetAccessorDeclaration(member)) {
|
|
685
|
+
throw SchemaExtractionError.at(
|
|
686
|
+
member,
|
|
687
|
+
member.getText(),
|
|
688
|
+
"\u63A5\u53E3\u542B\u65B9\u6CD5\u7B7E\u540D\u6216\u5B58\u53D6\u5668,\u8FD0\u884C\u65F6 JSON \u6570\u636E\u65E0\u6CD5\u6821\u9A8C\u65B9\u6CD5\u2014\u2014\u8BF7\u6539\u7528\u5177\u4F53\u5C5E\u6027\u7C7B\u578B"
|
|
689
|
+
);
|
|
690
|
+
}
|
|
529
691
|
if (ts2.isPropertySignature(member) && member.name) {
|
|
530
692
|
const name = member.name.getText();
|
|
531
693
|
const optional = !!member.questionToken;
|
|
532
|
-
const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
694
|
+
const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
|
|
533
695
|
const constraints = extractConstraintsFromJsDoc(member, name);
|
|
534
696
|
validateConstraints(constraints, type, name);
|
|
535
697
|
propMap.set(
|
|
@@ -538,15 +700,13 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
|
|
|
538
700
|
);
|
|
539
701
|
}
|
|
540
702
|
if (ts2.isIndexSignatureDeclaration(member)) {
|
|
541
|
-
|
|
542
|
-
const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
|
|
543
|
-
return { kind: "record", key: keyType, value: valueType };
|
|
703
|
+
catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
|
|
544
704
|
}
|
|
545
705
|
}
|
|
546
706
|
for (const prop of propMap.values()) {
|
|
547
707
|
properties.push(prop);
|
|
548
708
|
}
|
|
549
|
-
return { kind: "object", properties };
|
|
709
|
+
return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
|
|
550
710
|
}
|
|
551
711
|
function extractConstraintsFromJsDoc(node, fieldName) {
|
|
552
712
|
const jsDocs = ts2.getJSDocCommentsAndTags(node).filter((entry) => ts2.isJSDoc(entry));
|
|
@@ -669,15 +829,38 @@ var init_resolveTypeNode = __esm({
|
|
|
669
829
|
"src/ast/resolveTypeNode.ts"() {
|
|
670
830
|
"use strict";
|
|
671
831
|
currentProgram = null;
|
|
672
|
-
SchemaExtractionError = class extends Error {
|
|
673
|
-
constructor(typeText, reason, options) {
|
|
674
|
-
super(
|
|
832
|
+
SchemaExtractionError = class _SchemaExtractionError extends Error {
|
|
833
|
+
constructor(typeText, reason, options, location) {
|
|
834
|
+
super(
|
|
835
|
+
`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}` + (location ? ` (${location.file}:${location.line}:${location.column})` : ""),
|
|
836
|
+
options
|
|
837
|
+
);
|
|
675
838
|
this.typeText = typeText;
|
|
676
839
|
this.reason = reason;
|
|
840
|
+
this.location = location;
|
|
677
841
|
this.name = "SchemaExtractionError";
|
|
678
842
|
}
|
|
679
843
|
typeText;
|
|
680
844
|
reason;
|
|
845
|
+
location;
|
|
846
|
+
/**
|
|
847
|
+
* 从 AST 节点构造错误(自动携带 file:line:column)
|
|
848
|
+
*
|
|
849
|
+
* 所有抛错点应优先使用此工厂——错误无行号时,几百行的类型文件只能靠
|
|
850
|
+
* 类型名肉眼定位;解析 lib.d.ts 类型别名时还会出现错误文本与文件上下文错位
|
|
851
|
+
*/
|
|
852
|
+
static at(node, typeText, reason) {
|
|
853
|
+
const sourceFile = node.getSourceFile();
|
|
854
|
+
if (!sourceFile) {
|
|
855
|
+
return new _SchemaExtractionError(typeText, reason);
|
|
856
|
+
}
|
|
857
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
858
|
+
return new _SchemaExtractionError(typeText, reason, void 0, {
|
|
859
|
+
file: sourceFile.fileName,
|
|
860
|
+
line: line + 1,
|
|
861
|
+
column: character + 1
|
|
862
|
+
});
|
|
863
|
+
}
|
|
681
864
|
};
|
|
682
865
|
NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
|
|
683
866
|
"max",
|
|
@@ -744,55 +927,47 @@ function extractTypeInfo(program, filePath, typeName) {
|
|
|
744
927
|
return;
|
|
745
928
|
}
|
|
746
929
|
});
|
|
747
|
-
return result;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
function extractAllTypes(program, filePath) {
|
|
753
|
-
const sourceFile = program.getSourceFile(filePath);
|
|
754
|
-
if (!sourceFile) return /* @__PURE__ */ new Map();
|
|
755
|
-
const checker = program.getTypeChecker();
|
|
756
|
-
setProgramContext(program);
|
|
757
|
-
try {
|
|
758
|
-
const result = /* @__PURE__ */ new Map();
|
|
759
|
-
ts3.forEachChild(sourceFile, (node) => {
|
|
760
|
-
if (ts3.isInterfaceDeclaration(node)) {
|
|
761
|
-
const visited = /* @__PURE__ */ new Set();
|
|
762
|
-
visited.add(node.name.text);
|
|
763
|
-
const runtimeType = withFileContext(
|
|
764
|
-
filePath,
|
|
765
|
-
node.name.text,
|
|
766
|
-
() => resolveInterfaceDeclaration(node, checker, visited)
|
|
767
|
-
);
|
|
768
|
-
result.set(node.name.text, {
|
|
769
|
-
name: node.name.text,
|
|
770
|
-
properties: runtimeType.kind === "object" ? runtimeType.properties : [],
|
|
771
|
-
runtimeType
|
|
772
|
-
});
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
if (ts3.isTypeAliasDeclaration(node)) {
|
|
776
|
-
const visited = /* @__PURE__ */ new Set();
|
|
777
|
-
visited.add(node.name.text);
|
|
778
|
-
const runtimeType = withFileContext(
|
|
779
|
-
filePath,
|
|
780
|
-
node.name.text,
|
|
781
|
-
() => resolveTypeNode(node.type, checker, visited)
|
|
782
|
-
);
|
|
783
|
-
result.set(node.name.text, {
|
|
784
|
-
name: node.name.text,
|
|
785
|
-
properties: runtimeType.kind === "object" ? runtimeType.properties : [],
|
|
786
|
-
runtimeType
|
|
787
|
-
});
|
|
788
|
-
return;
|
|
930
|
+
if (result) return result;
|
|
931
|
+
for (const sf of program.getSourceFiles()) {
|
|
932
|
+
if (sf === sourceFile) continue;
|
|
933
|
+
if (sf.fileName.includes("/node_modules/") || sf.fileName.includes("typescript/lib/")) {
|
|
934
|
+
continue;
|
|
789
935
|
}
|
|
790
|
-
|
|
791
|
-
|
|
936
|
+
const found = findTopLevelDecl(sf, typeName);
|
|
937
|
+
if (!found) continue;
|
|
938
|
+
const visited = /* @__PURE__ */ new Set();
|
|
939
|
+
visited.add(typeName);
|
|
940
|
+
const runtimeType = withFileContext(filePath, typeName, () => {
|
|
941
|
+
if (found.kind === "interface") {
|
|
942
|
+
return resolveInterfaceDeclaration(found.node, checker, visited);
|
|
943
|
+
}
|
|
944
|
+
if (found.kind === "typeAlias") {
|
|
945
|
+
return resolveTypeNode(found.node.type, checker, visited);
|
|
946
|
+
}
|
|
947
|
+
return resolveEnumDeclaration(found.node);
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
name: typeName,
|
|
951
|
+
properties: runtimeType.kind === "object" ? runtimeType.properties : [],
|
|
952
|
+
runtimeType
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
792
956
|
} finally {
|
|
793
957
|
setProgramContext(null);
|
|
794
958
|
}
|
|
795
959
|
}
|
|
960
|
+
function createLazyTypeResolver(program, filePath) {
|
|
961
|
+
const cache = /* @__PURE__ */ new Map();
|
|
962
|
+
return {
|
|
963
|
+
resolve(name) {
|
|
964
|
+
if (cache.has(name)) return cache.get(name);
|
|
965
|
+
const info = extractTypeInfo(program, filePath, name);
|
|
966
|
+
cache.set(name, info);
|
|
967
|
+
return info;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
796
971
|
function withFileContext(filePath, typeName, fn) {
|
|
797
972
|
try {
|
|
798
973
|
return fn();
|
|
@@ -802,7 +977,8 @@ function withFileContext(filePath, typeName, fn) {
|
|
|
802
977
|
const enriched = new SchemaExtractionError(
|
|
803
978
|
err.typeText,
|
|
804
979
|
`${err.reason}\uFF08\u6587\u4EF6: ${fileName}, \u7C7B\u578B: ${typeName}\uFF09`,
|
|
805
|
-
{ cause: err }
|
|
980
|
+
{ cause: err },
|
|
981
|
+
err.location
|
|
806
982
|
);
|
|
807
983
|
throw enriched;
|
|
808
984
|
}
|
|
@@ -847,9 +1023,13 @@ var init_schemaName = __esm({
|
|
|
847
1023
|
// src/injection/resolveInjection.ts
|
|
848
1024
|
import ts4 from "typescript";
|
|
849
1025
|
function resolveInjection(fn) {
|
|
1026
|
+
const cached = injectionCache.get(fn);
|
|
1027
|
+
if (cached) {
|
|
1028
|
+
return cached;
|
|
1029
|
+
}
|
|
850
1030
|
const fnStr = fn.toString();
|
|
851
1031
|
const params = extractParamsWithAst(fnStr);
|
|
852
|
-
|
|
1032
|
+
const items = params.map((param) => {
|
|
853
1033
|
const type = PARAM_TYPE_MAP[param.name] || "unknown";
|
|
854
1034
|
return {
|
|
855
1035
|
name: param.name,
|
|
@@ -858,6 +1038,8 @@ function resolveInjection(fn) {
|
|
|
858
1038
|
// 运行时类型已擦除
|
|
859
1039
|
};
|
|
860
1040
|
});
|
|
1041
|
+
injectionCache.set(fn, items);
|
|
1042
|
+
return items;
|
|
861
1043
|
}
|
|
862
1044
|
function extractParamsWithAst(fnStr) {
|
|
863
1045
|
const sourceFile = ts4.createSourceFile(
|
|
@@ -914,7 +1096,7 @@ function extractParamName(param, names) {
|
|
|
914
1096
|
return;
|
|
915
1097
|
}
|
|
916
1098
|
}
|
|
917
|
-
var PARAM_TYPE_MAP;
|
|
1099
|
+
var PARAM_TYPE_MAP, injectionCache;
|
|
918
1100
|
var init_resolveInjection = __esm({
|
|
919
1101
|
"src/injection/resolveInjection.ts"() {
|
|
920
1102
|
"use strict";
|
|
@@ -937,13 +1119,13 @@ var init_resolveInjection = __esm({
|
|
|
937
1119
|
agents: "agents"
|
|
938
1120
|
// Phase 2.3
|
|
939
1121
|
};
|
|
1122
|
+
injectionCache = /* @__PURE__ */ new WeakMap();
|
|
940
1123
|
}
|
|
941
1124
|
});
|
|
942
1125
|
|
|
943
1126
|
// src/injection/analyzeInjection.ts
|
|
944
1127
|
import ts5 from "typescript";
|
|
945
|
-
function
|
|
946
|
-
const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
|
|
1128
|
+
function analyzeInjectionInSourceFile(sourceFile, functionName) {
|
|
947
1129
|
const params = [];
|
|
948
1130
|
ts5.forEachChild(sourceFile, (node) => {
|
|
949
1131
|
if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
|
|
@@ -1004,30 +1186,24 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
1004
1186
|
}
|
|
1005
1187
|
entry.methods.add(route.method);
|
|
1006
1188
|
}
|
|
1007
|
-
const programByFile =
|
|
1008
|
-
const
|
|
1009
|
-
const mergedAllTypes = /* @__PURE__ */ new Map();
|
|
1189
|
+
const programByFile = createPrograms([...methodsByFile.keys()]);
|
|
1190
|
+
const resolversByFile = /* @__PURE__ */ new Map();
|
|
1010
1191
|
for (const filePath of methodsByFile.keys()) {
|
|
1011
|
-
|
|
1012
|
-
programByFile.set(filePath, program);
|
|
1013
|
-
const allTypes = extractAllTypes(program, filePath);
|
|
1014
|
-
allTypesByFile.set(filePath, allTypes);
|
|
1015
|
-
for (const [name, info] of allTypes) {
|
|
1016
|
-
mergedAllTypes.set(name, info);
|
|
1017
|
-
}
|
|
1192
|
+
resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
|
|
1018
1193
|
}
|
|
1019
1194
|
const sources = [];
|
|
1020
1195
|
for (const [filePath, entry] of methodsByFile) {
|
|
1021
1196
|
const program = programByFile.get(filePath);
|
|
1022
1197
|
const sourceFile = program.getSourceFile(filePath);
|
|
1023
|
-
|
|
1198
|
+
if (!sourceFile) continue;
|
|
1199
|
+
const resolver = resolversByFile.get(filePath);
|
|
1024
1200
|
for (const method of entry.methods) {
|
|
1025
1201
|
const inputType = getInputTypeForMethod(method);
|
|
1026
1202
|
const schemaName = getSchemaName(method, inputType);
|
|
1027
|
-
const meta =
|
|
1203
|
+
const meta = analyzeInjectionInSourceFile(sourceFile, method);
|
|
1028
1204
|
const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
|
|
1029
1205
|
const isForm = param?.type === "form";
|
|
1030
|
-
const typeInfo = param?.typeName ?
|
|
1206
|
+
const typeInfo = param?.typeName ? resolver.resolve(param.typeName) : null;
|
|
1031
1207
|
sources.push({
|
|
1032
1208
|
urlPath: entry.urlPath,
|
|
1033
1209
|
filePath,
|
|
@@ -1037,7 +1213,7 @@ function collectRouteSchemaSources(routes, rootDir) {
|
|
|
1037
1213
|
});
|
|
1038
1214
|
}
|
|
1039
1215
|
}
|
|
1040
|
-
return { sources,
|
|
1216
|
+
return { sources, resolversByFile };
|
|
1041
1217
|
}
|
|
1042
1218
|
var init_collectRouteSchemaSources = __esm({
|
|
1043
1219
|
"src/cli/collectRouteSchemaSources.ts"() {
|
|
@@ -1050,6 +1226,21 @@ var init_collectRouteSchemaSources = __esm({
|
|
|
1050
1226
|
}
|
|
1051
1227
|
});
|
|
1052
1228
|
|
|
1229
|
+
// src/utils/atomicWrite.ts
|
|
1230
|
+
import path7 from "path";
|
|
1231
|
+
import fs6 from "fs";
|
|
1232
|
+
async function atomicWriteFile(outputPath, content) {
|
|
1233
|
+
await fs6.promises.mkdir(path7.dirname(outputPath), { recursive: true });
|
|
1234
|
+
const tmp = `${outputPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1235
|
+
await fs6.promises.writeFile(tmp, content, "utf-8");
|
|
1236
|
+
await fs6.promises.rename(tmp, outputPath);
|
|
1237
|
+
}
|
|
1238
|
+
var init_atomicWrite = __esm({
|
|
1239
|
+
"src/utils/atomicWrite.ts"() {
|
|
1240
|
+
"use strict";
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1053
1244
|
// src/ast/generateZodSchema.ts
|
|
1054
1245
|
function collectNamedTypes(type, ctx) {
|
|
1055
1246
|
switch (type.kind) {
|
|
@@ -1100,8 +1291,12 @@ function collectNamedTypes(type, ctx) {
|
|
|
1100
1291
|
if (resolved) {
|
|
1101
1292
|
ctx.namedTypes.set(type.name, resolved);
|
|
1102
1293
|
collectNamedTypes(resolved, ctx);
|
|
1294
|
+
return;
|
|
1103
1295
|
}
|
|
1104
|
-
|
|
1296
|
+
throw new SchemaExtractionError(
|
|
1297
|
+
type.name,
|
|
1298
|
+
`\u65E0\u6CD5\u89E3\u6790\u547D\u540D\u7C7B\u578B\u5F15\u7528 "${type.name}"\uFF08handler \u6587\u4EF6\u4E0E program \u6E90\u6587\u4EF6\u4E2D\u5747\u672A\u627E\u5230\u540C\u540D\u9876\u5C42\u58F0\u660E\uFF09`
|
|
1299
|
+
);
|
|
1105
1300
|
}
|
|
1106
1301
|
}
|
|
1107
1302
|
}
|
|
@@ -1111,6 +1306,12 @@ function runtimeTypeToZodExpression(type, ctx, constraints) {
|
|
|
1111
1306
|
if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
|
|
1112
1307
|
return wrapCoercePreprocess(type.kind, withConstraints);
|
|
1113
1308
|
}
|
|
1309
|
+
if (ctx.coerce && type.kind === "literal" && (typeof type.value === "number" || typeof type.value === "boolean")) {
|
|
1310
|
+
return wrapCoercePreprocess(
|
|
1311
|
+
typeof type.value === "number" ? "number" : "boolean",
|
|
1312
|
+
withConstraints
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1114
1315
|
return withConstraints;
|
|
1115
1316
|
}
|
|
1116
1317
|
function applyConstraints(baseExpr, constraints, typeKind) {
|
|
@@ -1175,7 +1376,7 @@ function baseExpression(type, ctx) {
|
|
|
1175
1376
|
case "tuple":
|
|
1176
1377
|
return generateTupleExpression(type.elements, ctx);
|
|
1177
1378
|
case "object":
|
|
1178
|
-
return generateObjectExpression(type
|
|
1379
|
+
return generateObjectExpression(type, ctx);
|
|
1179
1380
|
case "union":
|
|
1180
1381
|
return generateUnionExpression(type.members, ctx);
|
|
1181
1382
|
case "date":
|
|
@@ -1227,7 +1428,10 @@ function generateTupleExpression(elements, ctx) {
|
|
|
1227
1428
|
}
|
|
1228
1429
|
}
|
|
1229
1430
|
if (restExpression) {
|
|
1230
|
-
|
|
1431
|
+
const fixedWithOptional = fixedExprs.map(
|
|
1432
|
+
(expr, i) => fixedOptional[i] ? `${expr}.optional()` : expr
|
|
1433
|
+
);
|
|
1434
|
+
return `z.tuple([${fixedWithOptional.join(", ")}]).rest(${restExpression})`;
|
|
1231
1435
|
}
|
|
1232
1436
|
const hasOptional = fixedOptional.some((o) => o);
|
|
1233
1437
|
if (!hasOptional) {
|
|
@@ -1248,13 +1452,14 @@ function generateTupleExpression(elements, ctx) {
|
|
|
1248
1452
|
}
|
|
1249
1453
|
return `z.union([${variants.join(", ")}])`;
|
|
1250
1454
|
}
|
|
1251
|
-
function generateObjectExpression(
|
|
1252
|
-
const fields = properties.map((prop) => {
|
|
1455
|
+
function generateObjectExpression(type, ctx) {
|
|
1456
|
+
const fields = type.properties.map((prop) => {
|
|
1253
1457
|
const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
|
|
1254
1458
|
const finalExpr = prop.optional ? `${expr}.optional()` : expr;
|
|
1255
1459
|
return `${JSON.stringify(prop.name)}: ${finalExpr}`;
|
|
1256
1460
|
});
|
|
1257
|
-
|
|
1461
|
+
const objectExpr = `z.object({ ${fields.join(", ")} })`;
|
|
1462
|
+
return type.catchall !== void 0 ? `${objectExpr}.catchall(${runtimeTypeToZodExpression(type.catchall, ctx)})` : objectExpr;
|
|
1258
1463
|
}
|
|
1259
1464
|
function generateUnionExpression(members, ctx) {
|
|
1260
1465
|
const hasNull = members.some((m) => m.kind === "null");
|
|
@@ -1300,6 +1505,23 @@ function containsRef(type, visited) {
|
|
|
1300
1505
|
}
|
|
1301
1506
|
}
|
|
1302
1507
|
function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = false) {
|
|
1508
|
+
const { namedTypeDeclarations, entryDeclaration } = generateZodSchemaSourceParts(
|
|
1509
|
+
typeInfo,
|
|
1510
|
+
resolveType,
|
|
1511
|
+
exportName,
|
|
1512
|
+
coerce
|
|
1513
|
+
);
|
|
1514
|
+
const lines = [];
|
|
1515
|
+
lines.push("import { z } from 'zod';");
|
|
1516
|
+
lines.push("");
|
|
1517
|
+
for (const { declaration } of namedTypeDeclarations) {
|
|
1518
|
+
lines.push(declaration);
|
|
1519
|
+
}
|
|
1520
|
+
if (namedTypeDeclarations.length > 0) lines.push("");
|
|
1521
|
+
lines.push(entryDeclaration);
|
|
1522
|
+
return lines.join("\n");
|
|
1523
|
+
}
|
|
1524
|
+
function generateZodSchemaSourceParts(typeInfo, resolveType, exportName, coerce = false) {
|
|
1303
1525
|
const ctx = new CodeGenContext(resolveType);
|
|
1304
1526
|
const name = exportName ?? typeInfo.name;
|
|
1305
1527
|
ctx.entryTypeName = typeInfo.name;
|
|
@@ -1307,26 +1529,20 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
|
|
|
1307
1529
|
ctx.coerce = coerce;
|
|
1308
1530
|
collectNamedTypes(typeInfo.runtimeType, ctx);
|
|
1309
1531
|
ctx.namedTypes.delete(typeInfo.name);
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
lines.push(generateNamedTypeDeclaration(n, type, ctx));
|
|
1315
|
-
}
|
|
1316
|
-
if (ctx.namedTypes.size > 0) lines.push("");
|
|
1532
|
+
const namedTypeDeclarations = [...ctx.namedTypes].map(([n, type]) => ({
|
|
1533
|
+
name: n,
|
|
1534
|
+
declaration: generateNamedTypeDeclaration(n, type, ctx)
|
|
1535
|
+
}));
|
|
1317
1536
|
const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
|
|
1318
1537
|
const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
} else {
|
|
1322
|
-
lines.push(`export const ${name}Schema = ${entryExpr};`);
|
|
1323
|
-
}
|
|
1324
|
-
return lines.join("\n");
|
|
1538
|
+
const entryDeclaration = hasSelfRef ? `export const ${name}Schema = z.lazy(() => ${entryExpr});` : `export const ${name}Schema = ${entryExpr};`;
|
|
1539
|
+
return { namedTypeDeclarations, entryDeclaration };
|
|
1325
1540
|
}
|
|
1326
1541
|
var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, COERCE_MAP_HELPER, COERCE_SET_HELPER, HELPERS_FILENAME;
|
|
1327
1542
|
var init_generateZodSchema = __esm({
|
|
1328
1543
|
"src/ast/generateZodSchema.ts"() {
|
|
1329
1544
|
"use strict";
|
|
1545
|
+
init_resolveTypeNode();
|
|
1330
1546
|
CodeGenContext = class {
|
|
1331
1547
|
/** 命名类型集合:name → RuntimeType */
|
|
1332
1548
|
namedTypes = /* @__PURE__ */ new Map();
|
|
@@ -1348,7 +1564,7 @@ var init_generateZodSchema = __esm({
|
|
|
1348
1564
|
}
|
|
1349
1565
|
};
|
|
1350
1566
|
COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
|
|
1351
|
-
COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => v === "true" ||
|
|
1567
|
+
COERCE_BOOLEAN_HELPER = 'export const coerceBoolean = (v) => {\n const lower = typeof v === "string" ? v.toLowerCase() : v;\n return lower === "true" || lower === "1" ? true : lower === "false" || lower === "0" ? false : v;\n};';
|
|
1352
1568
|
COERCE_MAP_HELPER = 'export const coerceMap = (v) => Array.isArray(v) ? new Map(v) : v instanceof Map ? v : (v && typeof v === "object" ? new Map(Object.entries(v)) : v);';
|
|
1353
1569
|
COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
|
|
1354
1570
|
HELPERS_FILENAME = "faapi-helpers.js";
|
|
@@ -1364,8 +1580,7 @@ __export(generateSchemaFiles_exports, {
|
|
|
1364
1580
|
getRuntimeSchemaPath: () => getRuntimeSchemaPath,
|
|
1365
1581
|
getSchemaOutputPath: () => getSchemaOutputPath
|
|
1366
1582
|
});
|
|
1367
|
-
import
|
|
1368
|
-
import fs5 from "fs/promises";
|
|
1583
|
+
import path8 from "path";
|
|
1369
1584
|
function getSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
1370
1585
|
let rel = sourceFile.replace(/\\/g, "/");
|
|
1371
1586
|
if (rel.startsWith("src/")) {
|
|
@@ -1373,7 +1588,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
|
1373
1588
|
}
|
|
1374
1589
|
const idx = rel.lastIndexOf("/");
|
|
1375
1590
|
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
1376
|
-
return
|
|
1591
|
+
return path8.resolve(rootDir, dist, relDir, "zod.js");
|
|
1377
1592
|
}
|
|
1378
1593
|
function getRuntimeSchemaPath(filePath, dist, rootDir) {
|
|
1379
1594
|
let rel = filePath.replace(/\\/g, "/");
|
|
@@ -1384,16 +1599,17 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
|
|
|
1384
1599
|
}
|
|
1385
1600
|
const idx = rel.lastIndexOf("/");
|
|
1386
1601
|
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
1387
|
-
return
|
|
1602
|
+
return path8.resolve(rootDir, dist, relDir, "zod.js");
|
|
1388
1603
|
}
|
|
1389
1604
|
function getHelpersImportPath(relDir) {
|
|
1390
1605
|
if (!relDir) return `./${HELPERS_FILENAME}`;
|
|
1391
1606
|
const depth = relDir.split("/").filter(Boolean).length;
|
|
1392
1607
|
return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
|
|
1393
1608
|
}
|
|
1394
|
-
function generateSchemaFileSource(sources,
|
|
1395
|
-
const resolveType = (name) => allTypes.get(name)?.runtimeType;
|
|
1609
|
+
function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
1396
1610
|
const lines = ["import { z } from 'zod';"];
|
|
1611
|
+
const namedTypeDeclarations = [];
|
|
1612
|
+
const seenNamedTypes = /* @__PURE__ */ new Set();
|
|
1397
1613
|
const schemaBlocks = [];
|
|
1398
1614
|
for (const source of sources) {
|
|
1399
1615
|
const { schemaName, typeInfo } = source;
|
|
@@ -1401,16 +1617,24 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
|
1401
1617
|
continue;
|
|
1402
1618
|
}
|
|
1403
1619
|
const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1620
|
+
const { namedTypeDeclarations: decls, entryDeclaration } = generateZodSchemaSourceParts(
|
|
1621
|
+
typeInfo,
|
|
1622
|
+
resolveType,
|
|
1623
|
+
schemaName,
|
|
1624
|
+
coerce
|
|
1408
1625
|
);
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1626
|
+
for (const { name, declaration } of decls) {
|
|
1627
|
+
if (seenNamedTypes.has(name)) continue;
|
|
1628
|
+
seenNamedTypes.add(name);
|
|
1629
|
+
namedTypeDeclarations.push(declaration);
|
|
1630
|
+
}
|
|
1631
|
+
schemaBlocks.push([`// ${schemaName}`, entryDeclaration, ""].join("\n"));
|
|
1412
1632
|
}
|
|
1413
|
-
|
|
1633
|
+
if (namedTypeDeclarations.length > 0) {
|
|
1634
|
+
lines.push(...namedTypeDeclarations);
|
|
1635
|
+
lines.push("");
|
|
1636
|
+
}
|
|
1637
|
+
const allSchemaCode = [...namedTypeDeclarations, ...schemaBlocks].join("\n");
|
|
1414
1638
|
if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
|
|
1415
1639
|
lines.push(
|
|
1416
1640
|
`import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
|
|
@@ -1422,7 +1646,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
|
1422
1646
|
}
|
|
1423
1647
|
async function generateSchemaFiles(routes, rootDir, dist) {
|
|
1424
1648
|
if (routes.length === 0) return;
|
|
1425
|
-
const { sources,
|
|
1649
|
+
const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
|
|
1426
1650
|
const sourcesByFile = /* @__PURE__ */ new Map();
|
|
1427
1651
|
for (const source of sources) {
|
|
1428
1652
|
let list = sourcesByFile.get(source.filePath);
|
|
@@ -1434,9 +1658,8 @@ async function generateSchemaFiles(routes, rootDir, dist) {
|
|
|
1434
1658
|
}
|
|
1435
1659
|
const fileEntries = [];
|
|
1436
1660
|
for (const [filePath, fileSources] of sourcesByFile) {
|
|
1437
|
-
const relFile =
|
|
1661
|
+
const relFile = path8.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1438
1662
|
const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
|
|
1439
|
-
const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
|
|
1440
1663
|
let relForDir = relFile;
|
|
1441
1664
|
if (relForDir.startsWith("src/")) {
|
|
1442
1665
|
relForDir = relForDir.slice(4);
|
|
@@ -1444,12 +1667,17 @@ async function generateSchemaFiles(routes, rootDir, dist) {
|
|
|
1444
1667
|
const dirIdx = relForDir.lastIndexOf("/");
|
|
1445
1668
|
const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
|
|
1446
1669
|
const helpersImportPath = getHelpersImportPath(zodRelDir);
|
|
1447
|
-
const
|
|
1670
|
+
const resolver = resolversByFile.get(filePath);
|
|
1671
|
+
const source = generateSchemaFileSource(
|
|
1672
|
+
fileSources,
|
|
1673
|
+
(name) => resolver?.resolve(name)?.runtimeType,
|
|
1674
|
+
helpersImportPath
|
|
1675
|
+
);
|
|
1448
1676
|
fileEntries.push({ outputPath, source });
|
|
1449
1677
|
}
|
|
1450
1678
|
const allSourceCode = fileEntries.map((e) => e.source).join("\n");
|
|
1451
1679
|
if (usesCoerceHelpers(allSourceCode)) {
|
|
1452
|
-
const helpersPath =
|
|
1680
|
+
const helpersPath = path8.resolve(rootDir, dist, HELPERS_FILENAME);
|
|
1453
1681
|
await writeSchemaFile(helpersPath, generateHelpersFileSource());
|
|
1454
1682
|
}
|
|
1455
1683
|
await Promise.all(
|
|
@@ -1457,12 +1685,12 @@ async function generateSchemaFiles(routes, rootDir, dist) {
|
|
|
1457
1685
|
);
|
|
1458
1686
|
}
|
|
1459
1687
|
async function writeSchemaFile(outputPath, source) {
|
|
1460
|
-
await
|
|
1461
|
-
await fs5.writeFile(outputPath, source, "utf-8");
|
|
1688
|
+
await atomicWriteFile(outputPath, source);
|
|
1462
1689
|
}
|
|
1463
1690
|
var init_generateSchemaFiles = __esm({
|
|
1464
1691
|
"src/cli/generateSchemaFiles.ts"() {
|
|
1465
1692
|
"use strict";
|
|
1693
|
+
init_atomicWrite();
|
|
1466
1694
|
init_collectRouteSchemaSources();
|
|
1467
1695
|
init_generateZodSchema();
|
|
1468
1696
|
}
|
|
@@ -1475,95 +1703,178 @@ init_resolveTypeNode();
|
|
|
1475
1703
|
init_inputType();
|
|
1476
1704
|
init_collectRouteSchemaSources();
|
|
1477
1705
|
|
|
1478
|
-
// src/injection/
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1706
|
+
// src/injection/registries.ts
|
|
1707
|
+
function createToolRegistry() {
|
|
1708
|
+
let registry = /* @__PURE__ */ new Map();
|
|
1709
|
+
return {
|
|
1710
|
+
hydrate(tools) {
|
|
1711
|
+
const next = /* @__PURE__ */ new Map();
|
|
1712
|
+
for (const tool of tools) {
|
|
1713
|
+
next.set(tool.name, tool);
|
|
1714
|
+
}
|
|
1715
|
+
registry = next;
|
|
1716
|
+
},
|
|
1717
|
+
get(name) {
|
|
1718
|
+
return registry.get(name);
|
|
1719
|
+
},
|
|
1720
|
+
list() {
|
|
1721
|
+
return Array.from(registry.values());
|
|
1722
|
+
},
|
|
1723
|
+
clear() {
|
|
1724
|
+
registry = /* @__PURE__ */ new Map();
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1486
1727
|
}
|
|
1487
|
-
function
|
|
1488
|
-
registry = /* @__PURE__ */ new Map();
|
|
1728
|
+
function createAgentRegistry(tool) {
|
|
1729
|
+
let registry = /* @__PURE__ */ new Map();
|
|
1730
|
+
const getAgent2 = (name) => registry.get(name);
|
|
1731
|
+
return {
|
|
1732
|
+
hydrate(agents) {
|
|
1733
|
+
const next = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const agent of agents) {
|
|
1735
|
+
next.set(agent.name, agent);
|
|
1736
|
+
}
|
|
1737
|
+
registry = next;
|
|
1738
|
+
},
|
|
1739
|
+
getAgent: getAgent2,
|
|
1740
|
+
getAgentEntry(name) {
|
|
1741
|
+
return registry.get(name);
|
|
1742
|
+
},
|
|
1743
|
+
listAgents() {
|
|
1744
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1745
|
+
for (const agent of registry.values()) merged.set(agent.name, agent);
|
|
1746
|
+
return Array.from(merged.values());
|
|
1747
|
+
},
|
|
1748
|
+
asTool(name) {
|
|
1749
|
+
const agent = getAgent2(name);
|
|
1750
|
+
if (!agent) return void 0;
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "agent",
|
|
1753
|
+
name: `agent.${agent.name}`,
|
|
1754
|
+
agentName: agent.name,
|
|
1755
|
+
description: agent.description,
|
|
1756
|
+
metadata: agent
|
|
1757
|
+
};
|
|
1758
|
+
},
|
|
1759
|
+
resolveAgentTools(name) {
|
|
1760
|
+
const agent = getAgent2(name);
|
|
1761
|
+
if (!agent) return [];
|
|
1762
|
+
const result = /* @__PURE__ */ new Map();
|
|
1763
|
+
if (agent.tools) {
|
|
1764
|
+
for (const toolName of agent.tools) {
|
|
1765
|
+
const resolved = tool.get(toolName);
|
|
1766
|
+
if (resolved) result.set(resolved.name, resolved);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return Array.from(result.values());
|
|
1770
|
+
},
|
|
1771
|
+
resolveSubAgents(name) {
|
|
1772
|
+
const agent = getAgent2(name);
|
|
1773
|
+
if (!agent || !agent.agents) return [];
|
|
1774
|
+
const result = [];
|
|
1775
|
+
for (const subName of agent.agents) {
|
|
1776
|
+
const sub = getAgent2(subName);
|
|
1777
|
+
if (sub) result.push(sub);
|
|
1778
|
+
}
|
|
1779
|
+
return result;
|
|
1780
|
+
},
|
|
1781
|
+
clear() {
|
|
1782
|
+
registry = /* @__PURE__ */ new Map();
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1489
1785
|
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1786
|
+
function createSkillRegistry() {
|
|
1787
|
+
let registry = /* @__PURE__ */ new Map();
|
|
1788
|
+
return {
|
|
1789
|
+
hydrate(skills) {
|
|
1790
|
+
const next = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const skill of skills) {
|
|
1792
|
+
next.set(skill.name, skill);
|
|
1793
|
+
}
|
|
1794
|
+
registry = next;
|
|
1795
|
+
},
|
|
1796
|
+
upsert(skill) {
|
|
1797
|
+
registry.set(skill.name, skill);
|
|
1798
|
+
},
|
|
1799
|
+
remove(name) {
|
|
1800
|
+
registry.delete(name);
|
|
1801
|
+
},
|
|
1802
|
+
get(name) {
|
|
1803
|
+
return registry.get(name);
|
|
1804
|
+
},
|
|
1805
|
+
list() {
|
|
1806
|
+
return Array.from(registry.values());
|
|
1807
|
+
},
|
|
1808
|
+
clear() {
|
|
1809
|
+
registry = /* @__PURE__ */ new Map();
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1492
1812
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1813
|
+
function createAgentHandleStore() {
|
|
1814
|
+
let currentFactory = null;
|
|
1815
|
+
return {
|
|
1816
|
+
register(factory) {
|
|
1817
|
+
currentFactory = factory;
|
|
1818
|
+
},
|
|
1819
|
+
get(ctx) {
|
|
1820
|
+
if (currentFactory === null) return void 0;
|
|
1821
|
+
return currentFactory(ctx);
|
|
1822
|
+
},
|
|
1823
|
+
clear() {
|
|
1824
|
+
currentFactory = null;
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1502
1827
|
}
|
|
1503
|
-
function
|
|
1504
|
-
|
|
1828
|
+
function createAppRegistries() {
|
|
1829
|
+
const tool = createToolRegistry();
|
|
1830
|
+
const agent = createAgentRegistry(tool);
|
|
1831
|
+
const skill = createSkillRegistry();
|
|
1832
|
+
const agentHandle = createAgentHandleStore();
|
|
1833
|
+
return { tool, agent, skill, agentHandle };
|
|
1505
1834
|
}
|
|
1835
|
+
var defaultRegistries = createAppRegistries();
|
|
1836
|
+
|
|
1837
|
+
// src/injection/agentRegistry.ts
|
|
1506
1838
|
function getAgent(name) {
|
|
1507
|
-
return
|
|
1839
|
+
return defaultRegistries.agent.getAgent(name);
|
|
1508
1840
|
}
|
|
1509
1841
|
function getAgentEntry(name) {
|
|
1510
|
-
return
|
|
1842
|
+
return defaultRegistries.agent.getAgentEntry(name);
|
|
1511
1843
|
}
|
|
1512
1844
|
function listAgents() {
|
|
1513
|
-
|
|
1514
|
-
for (const agent of registry2.values()) merged.set(agent.name, agent);
|
|
1515
|
-
return Array.from(merged.values());
|
|
1845
|
+
return defaultRegistries.agent.listAgents();
|
|
1516
1846
|
}
|
|
1517
1847
|
function resolveAgentTools(name) {
|
|
1518
|
-
|
|
1519
|
-
if (!agent) return [];
|
|
1520
|
-
const result = /* @__PURE__ */ new Map();
|
|
1521
|
-
if (agent.tools) {
|
|
1522
|
-
for (const toolName of agent.tools) {
|
|
1523
|
-
const tool = getTool(toolName);
|
|
1524
|
-
if (tool) result.set(tool.name, tool);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
return Array.from(result.values());
|
|
1848
|
+
return defaultRegistries.agent.resolveAgentTools(name);
|
|
1528
1849
|
}
|
|
1529
1850
|
function resolveSubAgents(name) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1537
|
-
return result;
|
|
1851
|
+
return defaultRegistries.agent.resolveSubAgents(name);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/injection/toolRegistry.ts
|
|
1855
|
+
function getTool(name) {
|
|
1856
|
+
return defaultRegistries.tool.get(name);
|
|
1538
1857
|
}
|
|
1539
1858
|
|
|
1540
1859
|
// src/injection/skillRegistry.ts
|
|
1541
|
-
var registry3 = /* @__PURE__ */ new Map();
|
|
1542
1860
|
function hydrateSkillRegistry(skills) {
|
|
1543
|
-
|
|
1544
|
-
for (const skill of skills) {
|
|
1545
|
-
next.set(skill.name, skill);
|
|
1546
|
-
}
|
|
1547
|
-
registry3 = next;
|
|
1548
|
-
}
|
|
1549
|
-
function clearSkillRegistry() {
|
|
1550
|
-
registry3 = /* @__PURE__ */ new Map();
|
|
1861
|
+
defaultRegistries.skill.hydrate(skills);
|
|
1551
1862
|
}
|
|
1552
1863
|
function upsertSkill(core) {
|
|
1553
|
-
|
|
1864
|
+
defaultRegistries.skill.upsert(core);
|
|
1554
1865
|
}
|
|
1555
1866
|
function removeSkill(name) {
|
|
1556
|
-
|
|
1867
|
+
defaultRegistries.skill.remove(name);
|
|
1557
1868
|
}
|
|
1558
1869
|
function getSkill(name) {
|
|
1559
|
-
return
|
|
1870
|
+
return defaultRegistries.skill.get(name);
|
|
1560
1871
|
}
|
|
1561
1872
|
function listSkills() {
|
|
1562
|
-
return
|
|
1873
|
+
return defaultRegistries.skill.list();
|
|
1563
1874
|
}
|
|
1564
1875
|
|
|
1565
1876
|
// src/loader/loadAgentModule.ts
|
|
1566
|
-
import
|
|
1877
|
+
import fs9 from "fs";
|
|
1567
1878
|
|
|
1568
1879
|
// src/loader/resolveExports.ts
|
|
1569
1880
|
function resolveExport(module, exportName) {
|
|
@@ -1583,8 +1894,8 @@ function resolveExport(module, exportName) {
|
|
|
1583
1894
|
// src/utils/importWithCacheBust.ts
|
|
1584
1895
|
import { pathToFileURL } from "url";
|
|
1585
1896
|
var loadTs;
|
|
1586
|
-
function setLoadTimestamp(
|
|
1587
|
-
loadTs =
|
|
1897
|
+
function setLoadTimestamp(ts10) {
|
|
1898
|
+
loadTs = ts10;
|
|
1588
1899
|
}
|
|
1589
1900
|
function getVitestImportActual() {
|
|
1590
1901
|
const vi = globalThis.vi;
|
|
@@ -1609,17 +1920,17 @@ async function importWithCacheBust(filePath, bustViteCache = false) {
|
|
|
1609
1920
|
}
|
|
1610
1921
|
|
|
1611
1922
|
// src/cli/compileOnDemand.ts
|
|
1612
|
-
import
|
|
1613
|
-
import
|
|
1923
|
+
import path10 from "path";
|
|
1924
|
+
import fs8 from "fs";
|
|
1614
1925
|
|
|
1615
|
-
// src/cli/
|
|
1616
|
-
import
|
|
1617
|
-
import
|
|
1926
|
+
// src/cli/compileSourceFiles.ts
|
|
1927
|
+
import path6 from "path";
|
|
1928
|
+
import fs5 from "fs";
|
|
1618
1929
|
import fg from "fast-glob";
|
|
1619
1930
|
|
|
1620
1931
|
// src/cli/aliasPlugin.ts
|
|
1621
|
-
import
|
|
1622
|
-
import
|
|
1932
|
+
import path5 from "path";
|
|
1933
|
+
import fs4 from "fs";
|
|
1623
1934
|
|
|
1624
1935
|
// src/utils/resolveAlias.ts
|
|
1625
1936
|
function resolveAlias(specifier, config) {
|
|
@@ -1644,57 +1955,84 @@ function resolveAlias(specifier, config) {
|
|
|
1644
1955
|
return candidates;
|
|
1645
1956
|
}
|
|
1646
1957
|
|
|
1647
|
-
// src/utils/
|
|
1648
|
-
import ts6 from "typescript";
|
|
1958
|
+
// src/utils/prodPaths.ts
|
|
1649
1959
|
import path3 from "path";
|
|
1650
1960
|
import fs2 from "fs";
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1657
|
-
const baseUrl = parsed.options.baseUrl ?? rootDir;
|
|
1658
|
-
const rawPaths = parsed.options.paths;
|
|
1659
|
-
if (!rawPaths) return null;
|
|
1660
|
-
const paths = {};
|
|
1661
|
-
for (const [pattern, targets] of Object.entries(rawPaths)) {
|
|
1662
|
-
paths[pattern] = targets.map((t) => path3.resolve(baseUrl, t));
|
|
1961
|
+
var APP_DIR = "src";
|
|
1962
|
+
var ROUTE_PATTERNS = ["src/api/**/*.ts"];
|
|
1963
|
+
function toProdFilePath(filePath, dist) {
|
|
1964
|
+
let rel = filePath.replace(/\\/g, "/");
|
|
1965
|
+
if (rel.startsWith("src/")) {
|
|
1966
|
+
rel = rel.slice(4);
|
|
1663
1967
|
}
|
|
1664
|
-
|
|
1968
|
+
const jsPath = rel.replace(/\.ts$/, ".js");
|
|
1969
|
+
return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
|
|
1665
1970
|
}
|
|
1666
|
-
|
|
1667
|
-
// src/cli/aliasPlugin.ts
|
|
1668
1971
|
function toProdExtension(filePath) {
|
|
1669
1972
|
if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
|
|
1670
1973
|
if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
|
|
1671
1974
|
if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
|
|
1672
1975
|
return filePath;
|
|
1673
1976
|
}
|
|
1674
|
-
function toProdImportPath(sourceFile, importer) {
|
|
1675
|
-
const importerDir = path4.dirname(importer);
|
|
1676
|
-
let rel = path4.relative(importerDir, sourceFile);
|
|
1677
|
-
rel = rel.split(path4.sep).join("/");
|
|
1678
|
-
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1679
|
-
return toProdExtension(rel);
|
|
1680
|
-
}
|
|
1681
1977
|
function toRealPath(p) {
|
|
1682
1978
|
try {
|
|
1683
|
-
return
|
|
1979
|
+
return fs2.realpathSync(p);
|
|
1684
1980
|
} catch {
|
|
1685
1981
|
return p;
|
|
1686
1982
|
}
|
|
1687
1983
|
}
|
|
1688
1984
|
function isInsideDir(filePath, dir) {
|
|
1689
|
-
const rel =
|
|
1690
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
1985
|
+
const rel = path3.relative(dir, filePath);
|
|
1986
|
+
return rel !== "" && !rel.startsWith("..") && !path3.isAbsolute(rel);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// src/utils/readTsconfig.ts
|
|
1990
|
+
import ts6 from "typescript";
|
|
1991
|
+
import path4 from "path";
|
|
1992
|
+
import fs3 from "fs";
|
|
1993
|
+
var tsconfigCache = /* @__PURE__ */ new Map();
|
|
1994
|
+
function readTsconfig(rootDir) {
|
|
1995
|
+
const tsconfigPath = path4.resolve(rootDir, "tsconfig.json");
|
|
1996
|
+
if (!fs3.existsSync(tsconfigPath)) return null;
|
|
1997
|
+
let mtimeMs;
|
|
1998
|
+
try {
|
|
1999
|
+
mtimeMs = fs3.statSync(tsconfigPath).mtimeMs;
|
|
2000
|
+
} catch {
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
const cached = tsconfigCache.get(tsconfigPath);
|
|
2004
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
2005
|
+
return cached.config;
|
|
2006
|
+
}
|
|
2007
|
+
const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
|
|
2008
|
+
if (configFile.error || !configFile.config) return null;
|
|
2009
|
+
const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
|
|
2010
|
+
const baseUrl = parsed.options.baseUrl ?? rootDir;
|
|
2011
|
+
const rawPaths = parsed.options.paths;
|
|
2012
|
+
const config = rawPaths ? (() => {
|
|
2013
|
+
const paths = {};
|
|
2014
|
+
for (const [pattern, targets] of Object.entries(rawPaths)) {
|
|
2015
|
+
paths[pattern] = targets.map((t) => path4.resolve(baseUrl, t));
|
|
2016
|
+
}
|
|
2017
|
+
return { baseUrl, paths };
|
|
2018
|
+
})() : null;
|
|
2019
|
+
tsconfigCache.set(tsconfigPath, { mtimeMs, config });
|
|
2020
|
+
return config;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/cli/aliasPlugin.ts
|
|
2024
|
+
function toProdImportPath(sourceFile, importer) {
|
|
2025
|
+
const importerDir = path5.dirname(importer);
|
|
2026
|
+
let rel = path5.relative(importerDir, sourceFile);
|
|
2027
|
+
rel = rel.split(path5.sep).join("/");
|
|
2028
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
2029
|
+
return toProdExtension(rel);
|
|
1691
2030
|
}
|
|
1692
|
-
var APP_DIR = "src";
|
|
1693
2031
|
function toStrippedProdImportPath(sourceFile, rootDir) {
|
|
1694
|
-
const appDirAbs = toRealPath(
|
|
2032
|
+
const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
|
|
1695
2033
|
const sourceReal = toRealPath(sourceFile);
|
|
1696
|
-
let rel =
|
|
1697
|
-
rel = rel.split(
|
|
2034
|
+
let rel = path5.relative(appDirAbs, sourceReal);
|
|
2035
|
+
rel = rel.split(path5.sep).join("/");
|
|
1698
2036
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1699
2037
|
return toProdExtension(rel);
|
|
1700
2038
|
}
|
|
@@ -1709,41 +2047,41 @@ var INDEX_EXTS = [
|
|
|
1709
2047
|
"/index.cjs"
|
|
1710
2048
|
];
|
|
1711
2049
|
function resolveRelativeSpecifier(importer, specifier) {
|
|
1712
|
-
const importerDir =
|
|
1713
|
-
const base =
|
|
2050
|
+
const importerDir = path5.dirname(importer);
|
|
2051
|
+
const base = path5.resolve(importerDir, specifier);
|
|
1714
2052
|
if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
|
|
1715
|
-
return
|
|
2053
|
+
return fs4.existsSync(base) ? base : null;
|
|
1716
2054
|
}
|
|
1717
2055
|
if (/\.(ts|tsx|jsx)$/.test(specifier)) {
|
|
1718
|
-
return
|
|
2056
|
+
return fs4.existsSync(base) ? base : null;
|
|
1719
2057
|
}
|
|
1720
2058
|
for (const ext of SOURCE_EXTS) {
|
|
1721
2059
|
const file = base + ext;
|
|
1722
|
-
if (
|
|
2060
|
+
if (fs4.existsSync(file)) return file;
|
|
1723
2061
|
}
|
|
1724
2062
|
for (const indexExt of INDEX_EXTS) {
|
|
1725
2063
|
const file = base + indexExt;
|
|
1726
|
-
if (
|
|
2064
|
+
if (fs4.existsSync(file)) return file;
|
|
1727
2065
|
}
|
|
1728
2066
|
return null;
|
|
1729
2067
|
}
|
|
1730
2068
|
function createAliasPlugin(config, options) {
|
|
1731
|
-
const
|
|
1732
|
-
const appDirAbs = options?.rootDir ? toRealPath(
|
|
2069
|
+
const SPEC_RE2 = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
2070
|
+
const appDirAbs = options?.rootDir ? toRealPath(path5.resolve(options.rootDir, APP_DIR)) : null;
|
|
1733
2071
|
return {
|
|
1734
2072
|
name: "faapi-alias",
|
|
1735
2073
|
setup(build) {
|
|
1736
2074
|
build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
|
|
1737
2075
|
let source;
|
|
1738
2076
|
try {
|
|
1739
|
-
source =
|
|
2077
|
+
source = fs4.readFileSync(args.path, "utf8");
|
|
1740
2078
|
} catch {
|
|
1741
2079
|
return void 0;
|
|
1742
2080
|
}
|
|
1743
2081
|
const importer = args.path;
|
|
1744
2082
|
const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
|
|
1745
2083
|
let modified = false;
|
|
1746
|
-
const newSource = source.replace(
|
|
2084
|
+
const newSource = source.replace(SPEC_RE2, (full, prefix, quote, specifier) => {
|
|
1747
2085
|
if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
|
|
1748
2086
|
return full;
|
|
1749
2087
|
}
|
|
@@ -1769,7 +2107,7 @@ function createAliasPlugin(config, options) {
|
|
|
1769
2107
|
for (const candidate of candidates) {
|
|
1770
2108
|
for (const ext of SOURCE_EXTS) {
|
|
1771
2109
|
const file = candidate + ext;
|
|
1772
|
-
if (
|
|
2110
|
+
if (fs4.existsSync(file)) {
|
|
1773
2111
|
modified = true;
|
|
1774
2112
|
if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
|
|
1775
2113
|
return `${prefix}${quote}${toStrippedProdImportPath(
|
|
@@ -1782,7 +2120,7 @@ function createAliasPlugin(config, options) {
|
|
|
1782
2120
|
}
|
|
1783
2121
|
for (const indexExt of INDEX_EXTS) {
|
|
1784
2122
|
const file = candidate + indexExt;
|
|
1785
|
-
if (
|
|
2123
|
+
if (fs4.existsSync(file)) {
|
|
1786
2124
|
modified = true;
|
|
1787
2125
|
if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
|
|
1788
2126
|
return `${prefix}${quote}${toStrippedProdImportPath(
|
|
@@ -1807,11 +2145,10 @@ function buildAliasPlugins(rootDir) {
|
|
|
1807
2145
|
return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
|
|
1808
2146
|
}
|
|
1809
2147
|
|
|
1810
|
-
// src/cli/
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const
|
|
1814
|
-
const entryPoints = files ?? await fg([`${APP_DIR2}/**/*.ts`], {
|
|
2148
|
+
// src/cli/compileSourceFiles.ts
|
|
2149
|
+
async function compileSourceFiles(options) {
|
|
2150
|
+
const { rootDir, dist, files, logLevel = "silent", production, atomicWrite } = options;
|
|
2151
|
+
const entryPoints = files ?? await fg([`${APP_DIR}/**/*.ts`], {
|
|
1815
2152
|
cwd: rootDir,
|
|
1816
2153
|
onlyFiles: true,
|
|
1817
2154
|
absolute: true,
|
|
@@ -1820,11 +2157,11 @@ async function compileDevRoutes(options) {
|
|
|
1820
2157
|
if (entryPoints.length === 0) {
|
|
1821
2158
|
return { compiledFiles: [] };
|
|
1822
2159
|
}
|
|
1823
|
-
const absDist =
|
|
1824
|
-
await
|
|
2160
|
+
const absDist = path6.resolve(rootDir, dist);
|
|
2161
|
+
await fs5.promises.mkdir(absDist, { recursive: true });
|
|
1825
2162
|
const plugins = buildAliasPlugins(rootDir);
|
|
1826
2163
|
const esbuild = await import("esbuild");
|
|
1827
|
-
const outbase =
|
|
2164
|
+
const outbase = path6.resolve(rootDir, APP_DIR);
|
|
1828
2165
|
const result = await esbuild.build({
|
|
1829
2166
|
entryPoints,
|
|
1830
2167
|
outdir: absDist,
|
|
@@ -1835,29 +2172,106 @@ async function compileDevRoutes(options) {
|
|
|
1835
2172
|
sourcemap: true,
|
|
1836
2173
|
packages: "external",
|
|
1837
2174
|
plugins,
|
|
2175
|
+
// build 语义:编译期 NODE_ENV 替换 + 死分支删除(见 AGENTS.md §5.3)
|
|
2176
|
+
...production ? { define: { "process.env.NODE_ENV": '"production"' }, minifySyntax: true } : {},
|
|
1838
2177
|
logLevel,
|
|
1839
|
-
|
|
2178
|
+
// dev 语义:esbuild 返回内存内容,由下方原子写落盘
|
|
2179
|
+
...atomicWrite ? { write: false } : {}
|
|
1840
2180
|
});
|
|
1841
|
-
if (result.outputFiles) {
|
|
2181
|
+
if (atomicWrite && result.outputFiles) {
|
|
1842
2182
|
await Promise.all(
|
|
1843
2183
|
result.outputFiles.map(async (file) => {
|
|
1844
|
-
await
|
|
2184
|
+
await fs5.promises.mkdir(path6.dirname(file.path), { recursive: true });
|
|
1845
2185
|
const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1846
|
-
await
|
|
1847
|
-
await
|
|
2186
|
+
await fs5.promises.writeFile(tmp, file.contents);
|
|
2187
|
+
await fs5.promises.rename(tmp, file.path);
|
|
1848
2188
|
})
|
|
1849
2189
|
);
|
|
1850
2190
|
}
|
|
1851
2191
|
return { compiledFiles: entryPoints };
|
|
1852
2192
|
}
|
|
1853
2193
|
|
|
2194
|
+
// src/cli/compileDevRoutes.ts
|
|
2195
|
+
async function compileDevRoutes(options) {
|
|
2196
|
+
return compileSourceFiles({ ...options, atomicWrite: true });
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1854
2199
|
// src/cli/compileOnDemand.ts
|
|
1855
2200
|
init_generateSchemaFiles();
|
|
1856
2201
|
init_generateSchemaFiles();
|
|
2202
|
+
|
|
2203
|
+
// src/cli/collectImports.ts
|
|
2204
|
+
import path9 from "path";
|
|
2205
|
+
import fs7 from "fs";
|
|
2206
|
+
var SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
2207
|
+
function extractImportSpecifiers(source) {
|
|
2208
|
+
const specifiers = [];
|
|
2209
|
+
let match;
|
|
2210
|
+
SPEC_RE.lastIndex = 0;
|
|
2211
|
+
while ((match = SPEC_RE.exec(source)) !== null) {
|
|
2212
|
+
specifiers.push(match[3]);
|
|
2213
|
+
}
|
|
2214
|
+
return specifiers;
|
|
2215
|
+
}
|
|
2216
|
+
function resolveSpecifierFromDir(dir, specifier) {
|
|
2217
|
+
return resolveRelativeSpecifier(path9.join(dir, "__faapi_probe__.ts"), specifier);
|
|
2218
|
+
}
|
|
2219
|
+
async function collectRelativeImports(entryFiles, rootDir) {
|
|
2220
|
+
const appDirAbs = toRealPath(path9.resolve(rootDir, "src"));
|
|
2221
|
+
const tsconfig = readTsconfig(rootDir);
|
|
2222
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2223
|
+
const insideFiles = /* @__PURE__ */ new Set();
|
|
2224
|
+
const outsideFiles = /* @__PURE__ */ new Set();
|
|
2225
|
+
async function collect(filePath) {
|
|
2226
|
+
if (visited.has(filePath)) return;
|
|
2227
|
+
visited.add(filePath);
|
|
2228
|
+
let source;
|
|
2229
|
+
try {
|
|
2230
|
+
source = await fs7.promises.readFile(filePath, "utf8");
|
|
2231
|
+
} catch {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
for (const specifier of extractImportSpecifiers(source)) {
|
|
2235
|
+
if (/\.(js|mjs|cjs)$/.test(specifier)) continue;
|
|
2236
|
+
let resolved;
|
|
2237
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
2238
|
+
resolved = resolveRelativeSpecifier(filePath, specifier);
|
|
2239
|
+
} else if (tsconfig) {
|
|
2240
|
+
resolved = null;
|
|
2241
|
+
for (const candidate of resolveAlias(specifier, tsconfig)) {
|
|
2242
|
+
const probed = resolveSpecifierFromDir(rootDir, candidate);
|
|
2243
|
+
if (probed) {
|
|
2244
|
+
resolved = probed;
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
} else {
|
|
2249
|
+
resolved = null;
|
|
2250
|
+
}
|
|
2251
|
+
if (!resolved) continue;
|
|
2252
|
+
if (!isInsideDir(toRealPath(resolved), toRealPath(path9.resolve(rootDir)))) continue;
|
|
2253
|
+
if (isInsideDir(toRealPath(resolved), appDirAbs)) {
|
|
2254
|
+
insideFiles.add(resolved);
|
|
2255
|
+
} else {
|
|
2256
|
+
outsideFiles.add(resolved);
|
|
2257
|
+
}
|
|
2258
|
+
await collect(resolved);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
for (const entry of entryFiles) {
|
|
2262
|
+
await collect(entry);
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
insideFiles: Array.from(insideFiles),
|
|
2266
|
+
outsideFiles: Array.from(outsideFiles)
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// src/cli/compileOnDemand.ts
|
|
1857
2271
|
function isProductFresh(sourceAbsPath, productAbsPath) {
|
|
1858
2272
|
try {
|
|
1859
|
-
const srcStat =
|
|
1860
|
-
const prodStat =
|
|
2273
|
+
const srcStat = fs8.statSync(sourceAbsPath);
|
|
2274
|
+
const prodStat = fs8.statSync(productAbsPath);
|
|
1861
2275
|
return prodStat.mtimeMs >= srcStat.mtimeMs;
|
|
1862
2276
|
} catch {
|
|
1863
2277
|
return false;
|
|
@@ -1877,18 +2291,19 @@ var state = createDevOnDemandState();
|
|
|
1877
2291
|
function clearCompiledFiles() {
|
|
1878
2292
|
state.compiledFiles.clear();
|
|
1879
2293
|
state.inFlightCompilations.clear();
|
|
2294
|
+
sourcePathCache.clear();
|
|
1880
2295
|
}
|
|
1881
2296
|
async function ensureCompiled(sourceAbsPath, rootDir, dist) {
|
|
1882
|
-
const
|
|
1883
|
-
if (
|
|
1884
|
-
await
|
|
2297
|
+
const inFlight2 = state.inFlightCompilations.get(sourceAbsPath);
|
|
2298
|
+
if (inFlight2) {
|
|
2299
|
+
await inFlight2.catch(() => {
|
|
1885
2300
|
});
|
|
1886
2301
|
return false;
|
|
1887
2302
|
}
|
|
1888
2303
|
if (state.compiledFiles.has(sourceAbsPath)) {
|
|
1889
2304
|
return false;
|
|
1890
2305
|
}
|
|
1891
|
-
if (!
|
|
2306
|
+
if (!fs8.existsSync(sourceAbsPath)) {
|
|
1892
2307
|
return false;
|
|
1893
2308
|
}
|
|
1894
2309
|
const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
|
|
@@ -1897,12 +2312,23 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
|
|
|
1897
2312
|
return false;
|
|
1898
2313
|
}
|
|
1899
2314
|
const compilePromise = (async () => {
|
|
2315
|
+
const { insideFiles } = await collectRelativeImports([sourceAbsPath], rootDir);
|
|
2316
|
+
const files = [sourceAbsPath];
|
|
2317
|
+
for (const dep of insideFiles) {
|
|
2318
|
+
if (state.compiledFiles.has(dep)) continue;
|
|
2319
|
+
const depProduct = prodSourcePathToProductPath(dep, rootDir, dist);
|
|
2320
|
+
if (depProduct && isProductFresh(dep, depProduct)) continue;
|
|
2321
|
+
files.push(dep);
|
|
2322
|
+
}
|
|
1900
2323
|
await compileDevRoutes({
|
|
1901
2324
|
rootDir,
|
|
1902
2325
|
dist,
|
|
1903
|
-
files
|
|
2326
|
+
files,
|
|
1904
2327
|
logLevel: "silent"
|
|
1905
2328
|
});
|
|
2329
|
+
for (const file of files) {
|
|
2330
|
+
state.compiledFiles.add(file);
|
|
2331
|
+
}
|
|
1906
2332
|
state.compiledFiles.add(sourceAbsPath);
|
|
1907
2333
|
})();
|
|
1908
2334
|
state.inFlightCompilations.set(sourceAbsPath, compilePromise);
|
|
@@ -1913,30 +2339,43 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
|
|
|
1913
2339
|
state.inFlightCompilations.delete(sourceAbsPath);
|
|
1914
2340
|
}
|
|
1915
2341
|
}
|
|
2342
|
+
async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
|
|
2343
|
+
if (!isDevOnDemandEnabled() || middlewarePaths.length === 0) return;
|
|
2344
|
+
const dist = getDevDist();
|
|
2345
|
+
if (!dist) return;
|
|
2346
|
+
for (const mwPath of middlewarePaths) {
|
|
2347
|
+
const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
|
|
2348
|
+
try {
|
|
2349
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
1916
2355
|
function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
1917
|
-
const rel =
|
|
2356
|
+
const rel = path10.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
1918
2357
|
if (!rel.startsWith("src/")) return null;
|
|
1919
2358
|
const relWithoutSrc = rel.slice(4);
|
|
1920
2359
|
const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
|
|
1921
|
-
return
|
|
2360
|
+
return path10.resolve(rootDir, dist, jsRel);
|
|
1922
2361
|
}
|
|
1923
2362
|
function clearGeneratedSchemas() {
|
|
1924
2363
|
state.generatedSchemas.clear();
|
|
1925
2364
|
state.inFlightSchemaGenerations.clear();
|
|
1926
2365
|
}
|
|
1927
2366
|
async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
|
|
1928
|
-
const
|
|
1929
|
-
if (
|
|
1930
|
-
await
|
|
2367
|
+
const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
|
|
2368
|
+
if (inFlight2) {
|
|
2369
|
+
await inFlight2.catch(() => {
|
|
1931
2370
|
});
|
|
1932
2371
|
return false;
|
|
1933
2372
|
}
|
|
1934
2373
|
if (state.generatedSchemas.has(schemaPath)) {
|
|
1935
2374
|
return false;
|
|
1936
2375
|
}
|
|
1937
|
-
const prodAbsPath =
|
|
2376
|
+
const prodAbsPath = path10.resolve(rootDir, routeFilePath);
|
|
1938
2377
|
const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
|
|
1939
|
-
if (!
|
|
2378
|
+
if (!fs8.existsSync(sourceAbsPath)) {
|
|
1940
2379
|
return false;
|
|
1941
2380
|
}
|
|
1942
2381
|
if (isProductFresh(sourceAbsPath, schemaPath)) {
|
|
@@ -1947,7 +2386,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
|
|
|
1947
2386
|
if (fileRoutes.length === 0) {
|
|
1948
2387
|
return false;
|
|
1949
2388
|
}
|
|
1950
|
-
const sourceRelPath =
|
|
2389
|
+
const sourceRelPath = path10.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
|
|
1951
2390
|
const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
|
|
1952
2391
|
const generatePromise = (async () => {
|
|
1953
2392
|
await generateSchemaFiles(sourceRoutes, rootDir, dist);
|
|
@@ -1968,22 +2407,31 @@ async function deleteSchemaFiles(routes, rootDir, dist) {
|
|
|
1968
2407
|
if (deleted.has(schemaPath)) continue;
|
|
1969
2408
|
deleted.add(schemaPath);
|
|
1970
2409
|
try {
|
|
1971
|
-
await
|
|
2410
|
+
await fs8.promises.unlink(schemaPath);
|
|
1972
2411
|
} catch {
|
|
1973
2412
|
}
|
|
1974
2413
|
}
|
|
1975
2414
|
}
|
|
2415
|
+
var sourcePathCache = /* @__PURE__ */ new Map();
|
|
1976
2416
|
function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
|
|
1977
|
-
const
|
|
2417
|
+
const cached = sourcePathCache.get(prodAbsPath);
|
|
2418
|
+
if (cached) return cached;
|
|
2419
|
+
const rel = path10.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
|
|
1978
2420
|
let relWithoutDist = rel;
|
|
1979
2421
|
if (relWithoutDist.startsWith(`${dist}/`)) {
|
|
1980
2422
|
relWithoutDist = relWithoutDist.slice(dist.length + 1);
|
|
1981
2423
|
}
|
|
1982
2424
|
const srcRel = `src/${relWithoutDist}`;
|
|
1983
2425
|
const tsRel = srcRel.replace(/\.js$/, ".ts");
|
|
1984
|
-
const tsAbs =
|
|
1985
|
-
|
|
1986
|
-
|
|
2426
|
+
const tsAbs = path10.resolve(rootDir, tsRel);
|
|
2427
|
+
let result;
|
|
2428
|
+
if (fs8.existsSync(tsAbs)) {
|
|
2429
|
+
result = tsAbs;
|
|
2430
|
+
} else {
|
|
2431
|
+
result = path10.resolve(rootDir, srcRel);
|
|
2432
|
+
}
|
|
2433
|
+
sourcePathCache.set(prodAbsPath, result);
|
|
2434
|
+
return result;
|
|
1987
2435
|
}
|
|
1988
2436
|
function isDevOnDemandEnabled() {
|
|
1989
2437
|
return state.enabled;
|
|
@@ -1998,7 +2446,7 @@ async function loadAgentModule(filePath, hasRun, rootDir) {
|
|
|
1998
2446
|
const dist = getDevDist();
|
|
1999
2447
|
if (dist) {
|
|
2000
2448
|
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
2001
|
-
if (sourcePath &&
|
|
2449
|
+
if (sourcePath && fs9.existsSync(sourcePath)) {
|
|
2002
2450
|
try {
|
|
2003
2451
|
await ensureCompiled(sourcePath, rootDir, dist);
|
|
2004
2452
|
} catch (compileErr) {
|
|
@@ -2031,13 +2479,13 @@ async function loadAgentModule(filePath, hasRun, rootDir) {
|
|
|
2031
2479
|
}
|
|
2032
2480
|
|
|
2033
2481
|
// src/loader/loadToolModule.ts
|
|
2034
|
-
import
|
|
2482
|
+
import fs10 from "fs";
|
|
2035
2483
|
async function loadToolModule(filePath, functionName, rootDir) {
|
|
2036
2484
|
if (isDevOnDemandEnabled() && rootDir) {
|
|
2037
2485
|
const dist = getDevDist();
|
|
2038
2486
|
if (dist) {
|
|
2039
2487
|
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
2040
|
-
if (sourcePath &&
|
|
2488
|
+
if (sourcePath && fs10.existsSync(sourcePath)) {
|
|
2041
2489
|
try {
|
|
2042
2490
|
await ensureCompiled(sourcePath, rootDir, dist);
|
|
2043
2491
|
} catch (compileErr) {
|
|
@@ -2069,12 +2517,46 @@ async function loadToolModule(filePath, functionName, rootDir) {
|
|
|
2069
2517
|
import { existsSync as existsSync2 } from "fs";
|
|
2070
2518
|
|
|
2071
2519
|
// src/cli/generateToolArtifacts.ts
|
|
2072
|
-
import
|
|
2073
|
-
import fs9 from "fs/promises";
|
|
2520
|
+
import path11 from "path";
|
|
2074
2521
|
import { existsSync } from "fs";
|
|
2075
2522
|
|
|
2076
2523
|
// src/ast/extractToolMetadata.ts
|
|
2524
|
+
import ts8 from "typescript";
|
|
2525
|
+
|
|
2526
|
+
// src/ast/jsDocMetadata.ts
|
|
2077
2527
|
import ts7 from "typescript";
|
|
2528
|
+
function hasExportModifier(node) {
|
|
2529
|
+
if (!ts7.canHaveModifiers(node)) return false;
|
|
2530
|
+
const modifiers = ts7.getModifiers(node);
|
|
2531
|
+
return !!modifiers?.some((m) => m.kind === ts7.SyntaxKind.ExportKeyword);
|
|
2532
|
+
}
|
|
2533
|
+
function getJSDocFromNode(node) {
|
|
2534
|
+
const apiDocs = ts7.getJSDocCommentsAndTags(node).filter((entry) => ts7.isJSDoc(entry));
|
|
2535
|
+
if (apiDocs.length > 0) return apiDocs[0];
|
|
2536
|
+
const directDocs = node.jsDoc;
|
|
2537
|
+
if (directDocs && directDocs.length > 0) return directDocs[0];
|
|
2538
|
+
return void 0;
|
|
2539
|
+
}
|
|
2540
|
+
function extractDescription(jsDoc) {
|
|
2541
|
+
if (!jsDoc) return void 0;
|
|
2542
|
+
if (typeof jsDoc.comment !== "string") return void 0;
|
|
2543
|
+
const trimmed = jsDoc.comment.trim();
|
|
2544
|
+
return trimmed || void 0;
|
|
2545
|
+
}
|
|
2546
|
+
function extractJSDocTagValue(jsDoc, tagName) {
|
|
2547
|
+
if (!jsDoc || !jsDoc.tags) return void 0;
|
|
2548
|
+
for (const tag of jsDoc.tags) {
|
|
2549
|
+
if (tag.tagName.text !== tagName) continue;
|
|
2550
|
+
if (typeof tag.comment !== "string") return void 0;
|
|
2551
|
+
const text = tag.comment.trim();
|
|
2552
|
+
if (!text) return void 0;
|
|
2553
|
+
const cleaned = text.replace(/^\{|\}$/g, "").trim();
|
|
2554
|
+
return cleaned || void 0;
|
|
2555
|
+
}
|
|
2556
|
+
return void 0;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/ast/extractToolMetadata.ts
|
|
2078
2560
|
function extractToolMetadata(program, filePath, functionName, pathMeta) {
|
|
2079
2561
|
const sourceFile = program.getSourceFile(filePath);
|
|
2080
2562
|
if (!sourceFile) return null;
|
|
@@ -2083,7 +2565,7 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
|
|
|
2083
2565
|
const { fn, jsDocOwner } = found;
|
|
2084
2566
|
const jsDoc = getJSDocFromNode(jsDocOwner);
|
|
2085
2567
|
const description = extractDescription(jsDoc);
|
|
2086
|
-
const toolNameOverride =
|
|
2568
|
+
const toolNameOverride = extractJSDocTagValue(jsDoc, "tool");
|
|
2087
2569
|
const inputTypeName = getFirstParamTypeName(fn, sourceFile);
|
|
2088
2570
|
return {
|
|
2089
2571
|
name: toolNameOverride ?? pathMeta.name,
|
|
@@ -2095,18 +2577,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
|
|
|
2095
2577
|
}
|
|
2096
2578
|
function findExportedFunction(sourceFile, functionName) {
|
|
2097
2579
|
let result = null;
|
|
2098
|
-
|
|
2580
|
+
ts8.forEachChild(sourceFile, (node) => {
|
|
2099
2581
|
if (result) return;
|
|
2100
|
-
if (
|
|
2582
|
+
if (ts8.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
|
|
2101
2583
|
result = { fn: node, jsDocOwner: node };
|
|
2102
2584
|
return;
|
|
2103
2585
|
}
|
|
2104
|
-
if (
|
|
2586
|
+
if (ts8.isVariableStatement(node) && hasExportModifier(node)) {
|
|
2105
2587
|
for (const decl of node.declarationList.declarations) {
|
|
2106
2588
|
if (result) break;
|
|
2107
|
-
const nameText =
|
|
2589
|
+
const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
|
|
2108
2590
|
if (nameText !== functionName || !decl.initializer) continue;
|
|
2109
|
-
if (
|
|
2591
|
+
if (ts8.isArrowFunction(decl.initializer) || ts8.isFunctionExpression(decl.initializer)) {
|
|
2110
2592
|
result = { fn: decl.initializer, jsDocOwner: node };
|
|
2111
2593
|
}
|
|
2112
2594
|
}
|
|
@@ -2114,78 +2596,23 @@ function findExportedFunction(sourceFile, functionName) {
|
|
|
2114
2596
|
});
|
|
2115
2597
|
return result;
|
|
2116
2598
|
}
|
|
2117
|
-
function hasExportModifier(node) {
|
|
2118
|
-
if (!ts7.canHaveModifiers(node)) return false;
|
|
2119
|
-
const modifiers = ts7.getModifiers(node);
|
|
2120
|
-
return !!modifiers?.some((m) => m.kind === ts7.SyntaxKind.ExportKeyword);
|
|
2121
|
-
}
|
|
2122
|
-
function getJSDocFromNode(node) {
|
|
2123
|
-
const apiDocs = ts7.getJSDocCommentsAndTags(node).filter((entry) => ts7.isJSDoc(entry));
|
|
2124
|
-
if (apiDocs.length > 0) return apiDocs[0];
|
|
2125
|
-
const directDocs = node.jsDoc;
|
|
2126
|
-
if (directDocs && directDocs.length > 0) return directDocs[0];
|
|
2127
|
-
return void 0;
|
|
2128
|
-
}
|
|
2129
|
-
function extractDescription(jsDoc) {
|
|
2130
|
-
if (!jsDoc) return void 0;
|
|
2131
|
-
if (typeof jsDoc.comment !== "string") return void 0;
|
|
2132
|
-
const trimmed = jsDoc.comment.trim();
|
|
2133
|
-
return trimmed || void 0;
|
|
2134
|
-
}
|
|
2135
|
-
function extractToolTagValue(jsDoc) {
|
|
2136
|
-
if (!jsDoc || !jsDoc.tags) return void 0;
|
|
2137
|
-
for (const tag of jsDoc.tags) {
|
|
2138
|
-
if (tag.tagName.text !== "tool") continue;
|
|
2139
|
-
if (typeof tag.comment !== "string") return void 0;
|
|
2140
|
-
const text = tag.comment.trim();
|
|
2141
|
-
if (!text) return void 0;
|
|
2142
|
-
const cleaned = text.replace(/^\{|\}$/g, "").trim();
|
|
2143
|
-
return cleaned || void 0;
|
|
2144
|
-
}
|
|
2145
|
-
return void 0;
|
|
2146
|
-
}
|
|
2147
2599
|
function getFirstParamTypeName(fn, sourceFile) {
|
|
2148
2600
|
const firstParam = fn.parameters[0];
|
|
2149
2601
|
if (!firstParam) return void 0;
|
|
2150
2602
|
if (!firstParam.type) return void 0;
|
|
2151
|
-
if (!
|
|
2603
|
+
if (!ts8.isTypeReferenceNode(firstParam.type)) return void 0;
|
|
2152
2604
|
return firstParam.type.typeName.getText(sourceFile);
|
|
2153
2605
|
}
|
|
2154
2606
|
|
|
2155
2607
|
// src/cli/generateToolArtifacts.ts
|
|
2156
2608
|
init_createProgram();
|
|
2609
|
+
init_atomicWrite();
|
|
2610
|
+
init_generateSchemaFiles();
|
|
2157
2611
|
init_extractHandlerTypes();
|
|
2158
2612
|
init_generateZodSchema();
|
|
2159
2613
|
init_generateSchemaFiles();
|
|
2614
|
+
init_generateSchemaFiles();
|
|
2160
2615
|
var TOOLS_FILE = "faapi-tools.js";
|
|
2161
|
-
function getToolSchemaOutputPath(sourceFile, dist, rootDir) {
|
|
2162
|
-
let rel = sourceFile.replace(/\\/g, "/");
|
|
2163
|
-
if (rel.startsWith("src/")) {
|
|
2164
|
-
rel = rel.slice(4);
|
|
2165
|
-
}
|
|
2166
|
-
const idx = rel.lastIndexOf("/");
|
|
2167
|
-
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2168
|
-
return path8.resolve(rootDir, dist, relDir, "zod.js");
|
|
2169
|
-
}
|
|
2170
|
-
function getRuntimeToolSchemaPath(filePath, dist, rootDir) {
|
|
2171
|
-
let rel = filePath.replace(/\\/g, "/");
|
|
2172
|
-
if (rel.startsWith("src/")) {
|
|
2173
|
-
rel = rel.slice(4);
|
|
2174
|
-
} else if (rel.startsWith(`${dist}/`)) {
|
|
2175
|
-
rel = rel.slice(dist.length + 1);
|
|
2176
|
-
}
|
|
2177
|
-
const idx = rel.lastIndexOf("/");
|
|
2178
|
-
const relDir = idx >= 0 ? rel.slice(0, idx) : "";
|
|
2179
|
-
return path8.resolve(rootDir, dist, relDir, "zod.js");
|
|
2180
|
-
}
|
|
2181
|
-
function toProdFilePath(filePath, dist) {
|
|
2182
|
-
let rel = filePath.replace(/\\/g, "/");
|
|
2183
|
-
if (rel.startsWith("src/")) {
|
|
2184
|
-
rel = rel.slice(4);
|
|
2185
|
-
}
|
|
2186
|
-
const jsPath = rel.replace(/\.ts$/, ".js");
|
|
2187
|
-
return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
|
|
2188
|
-
}
|
|
2189
2616
|
function serializeTools(tools, dist = "dist") {
|
|
2190
2617
|
return tools.map((t) => ({
|
|
2191
2618
|
name: t.name,
|
|
@@ -2196,12 +2623,10 @@ function serializeTools(tools, dist = "dist") {
|
|
|
2196
2623
|
}));
|
|
2197
2624
|
}
|
|
2198
2625
|
async function writeToolsModule(manifest, outputPath) {
|
|
2199
|
-
const dir = path8.dirname(outputPath);
|
|
2200
|
-
await fs9.mkdir(dir, { recursive: true });
|
|
2201
2626
|
const content = `// \u81EA\u52A8\u751F\u6210,\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91(faapi build/dev \u4EA7\u7269)
|
|
2202
2627
|
export const tools = ${JSON.stringify(manifest, null, 2)};
|
|
2203
2628
|
`;
|
|
2204
|
-
await
|
|
2629
|
+
await atomicWriteFile(outputPath, content);
|
|
2205
2630
|
}
|
|
2206
2631
|
function hydrateTools(manifest) {
|
|
2207
2632
|
return manifest.map((t) => ({
|
|
@@ -2216,7 +2641,7 @@ function collectToolSchemaSources(tools, rootDir) {
|
|
|
2216
2641
|
const toolsByFile = /* @__PURE__ */ new Map();
|
|
2217
2642
|
for (const tool of tools) {
|
|
2218
2643
|
if (!tool.inputTypeName) continue;
|
|
2219
|
-
const absPath =
|
|
2644
|
+
const absPath = path11.resolve(rootDir, tool.filePath);
|
|
2220
2645
|
let list = toolsByFile.get(absPath);
|
|
2221
2646
|
if (!list) {
|
|
2222
2647
|
list = [];
|
|
@@ -2224,15 +2649,14 @@ function collectToolSchemaSources(tools, rootDir) {
|
|
|
2224
2649
|
}
|
|
2225
2650
|
list.push(tool);
|
|
2226
2651
|
}
|
|
2227
|
-
const
|
|
2652
|
+
const programByFile = createPrograms([...toolsByFile.keys()]);
|
|
2653
|
+
const resolversByFile = /* @__PURE__ */ new Map();
|
|
2228
2654
|
for (const filePath of toolsByFile.keys()) {
|
|
2229
|
-
|
|
2230
|
-
const allTypes = extractAllTypes(program, filePath);
|
|
2231
|
-
allTypesByFile.set(filePath, allTypes);
|
|
2655
|
+
resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
|
|
2232
2656
|
}
|
|
2233
2657
|
const sources = [];
|
|
2234
2658
|
for (const [filePath, fileTools] of toolsByFile) {
|
|
2235
|
-
const program =
|
|
2659
|
+
const program = programByFile.get(filePath);
|
|
2236
2660
|
for (const tool of fileTools) {
|
|
2237
2661
|
const inputTypeName = tool.inputTypeName;
|
|
2238
2662
|
const typeInfo = extractTypeInfo(program, filePath, inputTypeName);
|
|
@@ -2245,10 +2669,9 @@ function collectToolSchemaSources(tools, rootDir) {
|
|
|
2245
2669
|
});
|
|
2246
2670
|
}
|
|
2247
2671
|
}
|
|
2248
|
-
return { sources,
|
|
2672
|
+
return { sources, resolversByFile };
|
|
2249
2673
|
}
|
|
2250
|
-
function generateToolSchemaFileSource(sources,
|
|
2251
|
-
const resolveType = (name) => allTypes.get(name)?.runtimeType;
|
|
2674
|
+
function generateToolSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
2252
2675
|
const lines = ["import { z } from 'zod';"];
|
|
2253
2676
|
const schemaBlocks = [];
|
|
2254
2677
|
for (const source of sources) {
|
|
@@ -2278,16 +2701,16 @@ function generateToolSchemaFileSource(sources, allTypes, helpersImportPath) {
|
|
|
2278
2701
|
}
|
|
2279
2702
|
async function maybeGenerateHelpers(allSourceCode, distDir) {
|
|
2280
2703
|
if (!usesCoerceHelpers(allSourceCode)) return;
|
|
2281
|
-
const helpersPath =
|
|
2704
|
+
const helpersPath = path11.resolve(distDir, HELPERS_FILENAME);
|
|
2282
2705
|
if (existsSync(helpersPath)) return;
|
|
2283
|
-
await
|
|
2284
|
-
await fs9.writeFile(helpersPath, generateHelpersFileSource(), "utf-8");
|
|
2706
|
+
await atomicWriteFile(helpersPath, generateHelpersFileSource());
|
|
2285
2707
|
}
|
|
2286
2708
|
async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
2287
2709
|
const metadata = [];
|
|
2710
|
+
const programByFile = createPrograms(tools.map((m) => path11.resolve(rootDir, m.filePath)));
|
|
2288
2711
|
for (const manifest of tools) {
|
|
2289
|
-
const absPath =
|
|
2290
|
-
const program =
|
|
2712
|
+
const absPath = path11.resolve(rootDir, manifest.filePath);
|
|
2713
|
+
const program = programByFile.get(absPath);
|
|
2291
2714
|
const result = extractToolMetadata(program, absPath, manifest.functionName, {
|
|
2292
2715
|
name: manifest.name,
|
|
2293
2716
|
filePath: manifest.filePath
|
|
@@ -2297,7 +2720,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
|
2297
2720
|
}
|
|
2298
2721
|
}
|
|
2299
2722
|
const serialized = serializeTools(metadata, dist);
|
|
2300
|
-
const toolsPath =
|
|
2723
|
+
const toolsPath = path11.resolve(rootDir, dist, TOOLS_FILE);
|
|
2301
2724
|
await writeToolsModule(serialized, toolsPath);
|
|
2302
2725
|
if (options?.skipSchema) {
|
|
2303
2726
|
return metadata;
|
|
@@ -2305,7 +2728,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
|
2305
2728
|
if (metadata.length === 0) {
|
|
2306
2729
|
return metadata;
|
|
2307
2730
|
}
|
|
2308
|
-
const { sources,
|
|
2731
|
+
const { sources, resolversByFile } = collectToolSchemaSources(metadata, rootDir);
|
|
2309
2732
|
if (sources.length === 0) {
|
|
2310
2733
|
return metadata;
|
|
2311
2734
|
}
|
|
@@ -2320,9 +2743,9 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
|
2320
2743
|
}
|
|
2321
2744
|
const fileEntries = [];
|
|
2322
2745
|
for (const [filePath, fileSources] of sourcesByFile) {
|
|
2323
|
-
const relFile =
|
|
2324
|
-
const outputPath =
|
|
2325
|
-
const
|
|
2746
|
+
const relFile = path11.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
2747
|
+
const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
|
|
2748
|
+
const resolver = resolversByFile.get(filePath);
|
|
2326
2749
|
let relForDir = relFile;
|
|
2327
2750
|
if (relForDir.startsWith("src/")) {
|
|
2328
2751
|
relForDir = relForDir.slice(4);
|
|
@@ -2330,11 +2753,15 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
|
2330
2753
|
const dirIdx = relForDir.lastIndexOf("/");
|
|
2331
2754
|
const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
|
|
2332
2755
|
const helpersImportPath = getHelpersImportPath(zodRelDir);
|
|
2333
|
-
const source = generateToolSchemaFileSource(
|
|
2756
|
+
const source = generateToolSchemaFileSource(
|
|
2757
|
+
fileSources,
|
|
2758
|
+
(name) => resolver?.resolve(name)?.runtimeType,
|
|
2759
|
+
helpersImportPath
|
|
2760
|
+
);
|
|
2334
2761
|
fileEntries.push({ outputPath, source });
|
|
2335
2762
|
}
|
|
2336
2763
|
const allSourceCode = fileEntries.map((e) => e.source).join("\n");
|
|
2337
|
-
const distDir =
|
|
2764
|
+
const distDir = path11.resolve(rootDir, dist);
|
|
2338
2765
|
await maybeGenerateHelpers(allSourceCode, distDir);
|
|
2339
2766
|
await Promise.all(
|
|
2340
2767
|
fileEntries.map(({ outputPath, source }) => writeToolSchemaFile(outputPath, source))
|
|
@@ -2342,8 +2769,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
|
|
|
2342
2769
|
return metadata;
|
|
2343
2770
|
}
|
|
2344
2771
|
async function writeToolSchemaFile(outputPath, source) {
|
|
2345
|
-
await
|
|
2346
|
-
await fs9.writeFile(outputPath, source, "utf-8");
|
|
2772
|
+
await atomicWriteFile(outputPath, source);
|
|
2347
2773
|
}
|
|
2348
2774
|
|
|
2349
2775
|
// src/loader/loadToolSchema.ts
|
|
@@ -2353,11 +2779,14 @@ function getDist() {
|
|
|
2353
2779
|
}
|
|
2354
2780
|
return process.env.FAAPI_DIST ?? "dist";
|
|
2355
2781
|
}
|
|
2782
|
+
function getToolSchemaPath(tool, rootDir) {
|
|
2783
|
+
const dist = getDist();
|
|
2784
|
+
return getRuntimeSchemaPath(tool.filePath, dist, rootDir ?? process.cwd());
|
|
2785
|
+
}
|
|
2356
2786
|
async function loadToolSchema(tool, rootDir) {
|
|
2357
2787
|
if (!tool.inputTypeName) return void 0;
|
|
2358
2788
|
const schemaName = `${tool.inputTypeName}Schema`;
|
|
2359
|
-
const
|
|
2360
|
-
const zodPath = getRuntimeToolSchemaPath(tool.filePath, dist, rootDir ?? process.cwd());
|
|
2789
|
+
const zodPath = getToolSchemaPath(tool, rootDir);
|
|
2361
2790
|
if (!existsSync2(zodPath)) return void 0;
|
|
2362
2791
|
try {
|
|
2363
2792
|
const mod = await importWithCacheBust(zodPath, isDevOnDemandEnabled());
|
|
@@ -2370,16 +2799,14 @@ async function loadToolSchema(tool, rootDir) {
|
|
|
2370
2799
|
}
|
|
2371
2800
|
|
|
2372
2801
|
// src/injection/agentHandle.ts
|
|
2373
|
-
var currentFactory = null;
|
|
2374
2802
|
function registerAgentHandleFactory(factory) {
|
|
2375
|
-
|
|
2803
|
+
defaultRegistries.agentHandle.register(factory);
|
|
2376
2804
|
}
|
|
2377
2805
|
function getAgentHandle(ctx) {
|
|
2378
|
-
|
|
2379
|
-
return currentFactory(ctx);
|
|
2806
|
+
return defaultRegistries.agentHandle.get(ctx);
|
|
2380
2807
|
}
|
|
2381
2808
|
function clearAgentHandleFactory() {
|
|
2382
|
-
|
|
2809
|
+
defaultRegistries.agentHandle.clear();
|
|
2383
2810
|
}
|
|
2384
2811
|
|
|
2385
2812
|
// src/middleware/cors.ts
|
|
@@ -2407,11 +2834,6 @@ function cors(options = {}) {
|
|
|
2407
2834
|
} else if (Array.isArray(origin)) {
|
|
2408
2835
|
allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
|
|
2409
2836
|
}
|
|
2410
|
-
if (!allowOrigin) {
|
|
2411
|
-
await next();
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
2415
2837
|
if (origin === true || Array.isArray(origin)) {
|
|
2416
2838
|
const existingVary = ctx.headers.get("vary");
|
|
2417
2839
|
if (existingVary) {
|
|
@@ -2422,6 +2844,11 @@ function cors(options = {}) {
|
|
|
2422
2844
|
ctx.setHeader("Vary", "Origin");
|
|
2423
2845
|
}
|
|
2424
2846
|
}
|
|
2847
|
+
if (!allowOrigin) {
|
|
2848
|
+
await next();
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
2425
2852
|
ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
2426
2853
|
if (allowedHeaders) {
|
|
2427
2854
|
ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
@@ -2543,16 +2970,16 @@ function helmet(options = {}) {
|
|
|
2543
2970
|
}
|
|
2544
2971
|
|
|
2545
2972
|
// src/config/loadConfig.ts
|
|
2546
|
-
import
|
|
2547
|
-
import
|
|
2973
|
+
import path12 from "path";
|
|
2974
|
+
import fs11 from "fs";
|
|
2548
2975
|
var CONFIG_PRODUCT_FILE = "faapi-config.js";
|
|
2549
2976
|
async function loadConfig(rootDir, dist) {
|
|
2550
|
-
const configProductPath =
|
|
2551
|
-
if (
|
|
2977
|
+
const configProductPath = path12.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
|
|
2978
|
+
if (fs11.existsSync(configProductPath)) {
|
|
2552
2979
|
const module = await importWithCacheBust(configProductPath);
|
|
2553
2980
|
return module.default ?? {};
|
|
2554
2981
|
}
|
|
2555
|
-
const hasSourceConfig =
|
|
2982
|
+
const hasSourceConfig = fs11.existsSync(path12.join(rootDir, "faapi.config.ts")) || fs11.existsSync(path12.join(rootDir, "faapi.config.js"));
|
|
2556
2983
|
if (hasSourceConfig) {
|
|
2557
2984
|
throw new Error(
|
|
2558
2985
|
`[faapi] ${dist}/${CONFIG_PRODUCT_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
@@ -2562,8 +2989,8 @@ async function loadConfig(rootDir, dist) {
|
|
|
2562
2989
|
}
|
|
2563
2990
|
|
|
2564
2991
|
// src/cli/loadEnv.ts
|
|
2565
|
-
import
|
|
2566
|
-
import
|
|
2992
|
+
import fs12 from "fs";
|
|
2993
|
+
import path13 from "path";
|
|
2567
2994
|
function resolveEnv() {
|
|
2568
2995
|
return process.env.NODE_ENV || "development";
|
|
2569
2996
|
}
|
|
@@ -2578,7 +3005,8 @@ function parseEnvFile(content, fileVars) {
|
|
|
2578
3005
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2579
3006
|
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(trimmed);
|
|
2580
3007
|
if (!match) continue;
|
|
2581
|
-
const
|
|
3008
|
+
const key = match[1];
|
|
3009
|
+
const rawValue = match[2];
|
|
2582
3010
|
const value = parseValue(rawValue, { ...fileVars, ...result });
|
|
2583
3011
|
result[key] = value;
|
|
2584
3012
|
}
|
|
@@ -2629,9 +3057,9 @@ function loadEnv(rootDir) {
|
|
|
2629
3057
|
const files = getEnvFiles(env);
|
|
2630
3058
|
const merged = {};
|
|
2631
3059
|
for (const file of files) {
|
|
2632
|
-
const filePath =
|
|
2633
|
-
if (!
|
|
2634
|
-
const content =
|
|
3060
|
+
const filePath = path13.join(rootDir, file);
|
|
3061
|
+
if (!fs12.existsSync(filePath)) continue;
|
|
3062
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
2635
3063
|
const parsed = parseEnvFile(content, merged);
|
|
2636
3064
|
Object.assign(merged, parsed);
|
|
2637
3065
|
}
|
|
@@ -2668,14 +3096,14 @@ var ValidationError = class extends FaapiError {
|
|
|
2668
3096
|
issues;
|
|
2669
3097
|
};
|
|
2670
3098
|
var RouteNotFoundError = class extends FaapiError {
|
|
2671
|
-
constructor(
|
|
2672
|
-
super("ROUTE_NOT_FOUND", `Route not found: ${
|
|
3099
|
+
constructor(path23) {
|
|
3100
|
+
super("ROUTE_NOT_FOUND", `Route not found: ${path23}`, 404);
|
|
2673
3101
|
this.name = "RouteNotFoundError";
|
|
2674
3102
|
}
|
|
2675
3103
|
};
|
|
2676
3104
|
var MethodNotAllowedError = class extends FaapiError {
|
|
2677
|
-
constructor(method,
|
|
2678
|
-
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${
|
|
3105
|
+
constructor(method, path23, allowedMethods) {
|
|
3106
|
+
super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path23}`, 405);
|
|
2679
3107
|
this.allowedMethods = allowedMethods;
|
|
2680
3108
|
this.name = "MethodNotAllowedError";
|
|
2681
3109
|
}
|
|
@@ -2701,8 +3129,8 @@ var PayloadTooLargeError = class extends FaapiError {
|
|
|
2701
3129
|
};
|
|
2702
3130
|
|
|
2703
3131
|
// src/cli/createAppCore.ts
|
|
2704
|
-
import
|
|
2705
|
-
import
|
|
3132
|
+
import fs15 from "fs";
|
|
3133
|
+
import path19 from "path";
|
|
2706
3134
|
import { PassThrough, Readable as Readable3 } from "stream";
|
|
2707
3135
|
|
|
2708
3136
|
// src/router/sortRoutes.ts
|
|
@@ -2755,45 +3183,119 @@ import {
|
|
|
2755
3183
|
import { createSecureServer as createHttp2SecureServer } from "http2";
|
|
2756
3184
|
import { readFileSync } from "fs";
|
|
2757
3185
|
import { Readable as Readable2 } from "stream";
|
|
2758
|
-
import
|
|
3186
|
+
import path15 from "path";
|
|
2759
3187
|
|
|
2760
3188
|
// src/router/matchRoute.ts
|
|
2761
|
-
|
|
3189
|
+
var httpIndexCache = /* @__PURE__ */ new WeakMap();
|
|
3190
|
+
var wsIndexCache = /* @__PURE__ */ new WeakMap();
|
|
3191
|
+
function getHttpIndex(routes) {
|
|
3192
|
+
let index = httpIndexCache.get(routes);
|
|
3193
|
+
if (index) return index;
|
|
3194
|
+
index = { static: /* @__PURE__ */ new Map(), methodsByStaticPath: /* @__PURE__ */ new Map(), dynamics: [] };
|
|
3195
|
+
for (const route of routes) {
|
|
3196
|
+
if (route.isDynamic) {
|
|
3197
|
+
index.dynamics.push({
|
|
3198
|
+
route,
|
|
3199
|
+
segments: route.urlPath.split("/").filter(Boolean)
|
|
3200
|
+
});
|
|
3201
|
+
} else {
|
|
3202
|
+
index.static.set(`${route.method}|${route.urlPath}`, route);
|
|
3203
|
+
let methods = index.methodsByStaticPath.get(route.urlPath);
|
|
3204
|
+
if (!methods) {
|
|
3205
|
+
methods = /* @__PURE__ */ new Set();
|
|
3206
|
+
index.methodsByStaticPath.set(route.urlPath, methods);
|
|
3207
|
+
}
|
|
3208
|
+
methods.add(route.method);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
httpIndexCache.set(routes, index);
|
|
3212
|
+
return index;
|
|
3213
|
+
}
|
|
3214
|
+
function getWsIndex(routes) {
|
|
3215
|
+
let index = wsIndexCache.get(routes);
|
|
3216
|
+
if (index) return index;
|
|
3217
|
+
index = { static: /* @__PURE__ */ new Map(), dynamics: [] };
|
|
2762
3218
|
for (const route of routes) {
|
|
3219
|
+
if (route.isDynamic) {
|
|
3220
|
+
index.dynamics.push(route);
|
|
3221
|
+
} else {
|
|
3222
|
+
index.static.set(route.urlPath, route);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
wsIndexCache.set(routes, index);
|
|
3226
|
+
return index;
|
|
3227
|
+
}
|
|
3228
|
+
function matchRoute(routes, method, path23) {
|
|
3229
|
+
const index = getHttpIndex(routes);
|
|
3230
|
+
const upper = method.toUpperCase();
|
|
3231
|
+
const hit = matchByMethod(index, upper, path23);
|
|
3232
|
+
if (hit) return hit;
|
|
3233
|
+
if (upper === "HEAD") {
|
|
3234
|
+
return matchByMethod(index, "GET", path23);
|
|
3235
|
+
}
|
|
3236
|
+
return null;
|
|
3237
|
+
}
|
|
3238
|
+
function matchByMethod(index, method, path23) {
|
|
3239
|
+
const staticHit = index.static.get(`${method}|${path23}`);
|
|
3240
|
+
if (staticHit) {
|
|
3241
|
+
return { route: staticHit, params: {} };
|
|
3242
|
+
}
|
|
3243
|
+
for (const entry of index.dynamics) {
|
|
3244
|
+
const route = entry.route;
|
|
2763
3245
|
if (route.method !== method) {
|
|
2764
3246
|
continue;
|
|
2765
3247
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
}
|
|
2770
|
-
continue;
|
|
3248
|
+
const params = matchSegments(entry.segments, path23, route.paramNames, route.isCatchAll);
|
|
3249
|
+
if (params !== null) {
|
|
3250
|
+
return { route, params };
|
|
2771
3251
|
}
|
|
2772
|
-
|
|
3252
|
+
}
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
function matchWsRoute(wsRoutes, path23) {
|
|
3256
|
+
const index = getWsIndex(wsRoutes);
|
|
3257
|
+
const staticHit = index.static.get(path23);
|
|
3258
|
+
if (staticHit) {
|
|
3259
|
+
return { route: staticHit, params: {} };
|
|
3260
|
+
}
|
|
3261
|
+
for (const route of index.dynamics) {
|
|
3262
|
+
const params = matchDynamicPath(route.urlPath, path23, route.paramNames, route.isCatchAll);
|
|
2773
3263
|
if (params !== null) {
|
|
2774
3264
|
return { route, params };
|
|
2775
3265
|
}
|
|
2776
3266
|
}
|
|
2777
3267
|
return null;
|
|
2778
3268
|
}
|
|
2779
|
-
function
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
}
|
|
2787
|
-
|
|
3269
|
+
function findAllowedMethods(routes, path23) {
|
|
3270
|
+
const index = getHttpIndex(routes);
|
|
3271
|
+
const methods = /* @__PURE__ */ new Set();
|
|
3272
|
+
const staticMethods = index.methodsByStaticPath.get(path23);
|
|
3273
|
+
if (staticMethods) {
|
|
3274
|
+
for (const method of staticMethods) {
|
|
3275
|
+
methods.add(method);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
for (const entry of index.dynamics) {
|
|
3279
|
+
const params = matchSegments(
|
|
3280
|
+
entry.segments,
|
|
3281
|
+
path23,
|
|
3282
|
+
entry.route.paramNames,
|
|
3283
|
+
entry.route.isCatchAll
|
|
3284
|
+
);
|
|
2788
3285
|
if (params !== null) {
|
|
2789
|
-
|
|
3286
|
+
methods.add(entry.route.method);
|
|
2790
3287
|
}
|
|
2791
3288
|
}
|
|
2792
|
-
|
|
3289
|
+
if (methods.has("GET")) {
|
|
3290
|
+
methods.add("HEAD");
|
|
3291
|
+
}
|
|
3292
|
+
return Array.from(methods);
|
|
2793
3293
|
}
|
|
2794
|
-
function matchDynamicPath(pattern,
|
|
2795
|
-
|
|
2796
|
-
|
|
3294
|
+
function matchDynamicPath(pattern, path23, paramNames, isCatchAll) {
|
|
3295
|
+
return matchSegments(pattern.split("/").filter(Boolean), path23, paramNames, isCatchAll);
|
|
3296
|
+
}
|
|
3297
|
+
function matchSegments(patternSegments, path23, paramNames, isCatchAll) {
|
|
3298
|
+
const pathSegments = path23.split("/").filter(Boolean);
|
|
2797
3299
|
if (isCatchAll) {
|
|
2798
3300
|
const nonCatchAllCount = patternSegments.length - 1;
|
|
2799
3301
|
if (pathSegments.length <= nonCatchAllCount) {
|
|
@@ -2838,9 +3340,6 @@ function matchDynamicPath(pattern, path19, paramNames, isCatchAll) {
|
|
|
2838
3340
|
return params;
|
|
2839
3341
|
}
|
|
2840
3342
|
|
|
2841
|
-
// src/loader/loadRouteModule.ts
|
|
2842
|
-
import fs12 from "fs";
|
|
2843
|
-
|
|
2844
3343
|
// src/loader/validateRouteModule.ts
|
|
2845
3344
|
function validateRouteModule(value, method, filePath) {
|
|
2846
3345
|
if (typeof value !== "function") {
|
|
@@ -2856,7 +3355,7 @@ async function loadRouteModule(filePath, method, rootDir) {
|
|
|
2856
3355
|
const dist = getDevDist();
|
|
2857
3356
|
if (dist) {
|
|
2858
3357
|
const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
|
|
2859
|
-
if (sourcePath
|
|
3358
|
+
if (sourcePath) {
|
|
2860
3359
|
try {
|
|
2861
3360
|
await ensureCompiled(sourcePath, rootDir, dist);
|
|
2862
3361
|
} catch (compileErr) {
|
|
@@ -3035,10 +3534,14 @@ function formatErrorResponse(error, config) {
|
|
|
3035
3534
|
code: error.code,
|
|
3036
3535
|
message: error.message
|
|
3037
3536
|
});
|
|
3038
|
-
const bodyObj = typeof body2 === "object" && body2 !== null ? body2 : { error: body2 };
|
|
3039
|
-
const
|
|
3040
|
-
if (
|
|
3041
|
-
|
|
3537
|
+
const bodyObj = typeof body2 === "object" && body2 !== null ? { ...body2 } : { error: body2 };
|
|
3538
|
+
const existingError = bodyObj.error;
|
|
3539
|
+
if (existingError && typeof existingError === "object") {
|
|
3540
|
+
bodyObj.error = { ...existingError, issues: error.issues };
|
|
3541
|
+
} else if (typeof body2 === "object" && body2 !== null) {
|
|
3542
|
+
bodyObj.issues = error.issues;
|
|
3543
|
+
} else {
|
|
3544
|
+
bodyObj.error = { issues: error.issues };
|
|
3042
3545
|
}
|
|
3043
3546
|
return jsonOk(bodyObj, error.statusCode);
|
|
3044
3547
|
}
|
|
@@ -3095,8 +3598,10 @@ function formatSetCookie(name, value, options) {
|
|
|
3095
3598
|
if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
3096
3599
|
return cookie;
|
|
3097
3600
|
}
|
|
3098
|
-
function createContext(request, params, config = {}, ip = "") {
|
|
3099
|
-
|
|
3601
|
+
function createContext(request, params, config = {}, ip = "", registries) {
|
|
3602
|
+
return createContextFromUrl(request, new URL(request.url), params, config, ip, registries);
|
|
3603
|
+
}
|
|
3604
|
+
function createContextFromUrl(request, url, params, config = {}, ip = "", registries) {
|
|
3100
3605
|
const meta = { headers: {}, setCookies: [] };
|
|
3101
3606
|
const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
|
|
3102
3607
|
const cookiesObj = {};
|
|
@@ -3193,6 +3698,9 @@ function createContext(request, params, config = {}, ip = "") {
|
|
|
3193
3698
|
return formatFailResponse(options, config);
|
|
3194
3699
|
}
|
|
3195
3700
|
};
|
|
3701
|
+
if (registries) {
|
|
3702
|
+
ctx.registries = registries;
|
|
3703
|
+
}
|
|
3196
3704
|
const extend = config?.extendContext;
|
|
3197
3705
|
if (typeof extend === "function") {
|
|
3198
3706
|
extend(ctx);
|
|
@@ -3204,7 +3712,14 @@ function createContext(request, params, config = {}, ip = "") {
|
|
|
3204
3712
|
function queryToObject(params) {
|
|
3205
3713
|
const result = {};
|
|
3206
3714
|
for (const [key, value] of params) {
|
|
3207
|
-
result[key]
|
|
3715
|
+
const existing = result[key];
|
|
3716
|
+
if (existing === void 0) {
|
|
3717
|
+
result[key] = value;
|
|
3718
|
+
} else if (Array.isArray(existing)) {
|
|
3719
|
+
existing.push(value);
|
|
3720
|
+
} else {
|
|
3721
|
+
result[key] = [existing, value];
|
|
3722
|
+
}
|
|
3208
3723
|
}
|
|
3209
3724
|
return result;
|
|
3210
3725
|
}
|
|
@@ -3251,7 +3766,7 @@ async function parseMultipart(request) {
|
|
|
3251
3766
|
|
|
3252
3767
|
// src/runtime/resolveInput.ts
|
|
3253
3768
|
init_inputType();
|
|
3254
|
-
async function
|
|
3769
|
+
async function resolveInputFromUrl(method, request, url) {
|
|
3255
3770
|
const inputType = getInputTypeForMethod(method);
|
|
3256
3771
|
if (inputType === "body") {
|
|
3257
3772
|
const contentType = request.headers.get("content-type") ?? "";
|
|
@@ -3260,7 +3775,7 @@ async function resolveInput(method, request) {
|
|
|
3260
3775
|
}
|
|
3261
3776
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
3262
3777
|
const text2 = await request.text();
|
|
3263
|
-
if (text2
|
|
3778
|
+
if (isBlankText(text2)) return null;
|
|
3264
3779
|
const params = new URLSearchParams(text2);
|
|
3265
3780
|
const obj = {};
|
|
3266
3781
|
for (const [key, value] of params) {
|
|
@@ -3269,7 +3784,7 @@ async function resolveInput(method, request) {
|
|
|
3269
3784
|
return obj;
|
|
3270
3785
|
}
|
|
3271
3786
|
const text = await request.text();
|
|
3272
|
-
if (text
|
|
3787
|
+
if (isBlankText(text)) {
|
|
3273
3788
|
return null;
|
|
3274
3789
|
}
|
|
3275
3790
|
const result = parseJsonBody(text);
|
|
@@ -3286,9 +3801,28 @@ async function resolveInput(method, request) {
|
|
|
3286
3801
|
}
|
|
3287
3802
|
return result.data;
|
|
3288
3803
|
}
|
|
3289
|
-
const url = new URL(request.url);
|
|
3290
3804
|
return queryToObject(url.searchParams);
|
|
3291
3805
|
}
|
|
3806
|
+
function isBlankText(text) {
|
|
3807
|
+
return text.length === 0 || !/\S/.test(text);
|
|
3808
|
+
}
|
|
3809
|
+
async function resolveBodyForQueryMethod(request) {
|
|
3810
|
+
const text = await request.text();
|
|
3811
|
+
if (isBlankText(text)) return void 0;
|
|
3812
|
+
const result = parseJsonBody(text);
|
|
3813
|
+
if (!result.success) {
|
|
3814
|
+
throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
|
|
3815
|
+
{
|
|
3816
|
+
path: "body",
|
|
3817
|
+
code: "INVALID_FORMAT",
|
|
3818
|
+
expected: "JSON",
|
|
3819
|
+
received: "text",
|
|
3820
|
+
message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
|
|
3821
|
+
}
|
|
3822
|
+
]);
|
|
3823
|
+
}
|
|
3824
|
+
return result.data;
|
|
3825
|
+
}
|
|
3292
3826
|
|
|
3293
3827
|
// src/utils/isPlainObject.ts
|
|
3294
3828
|
function isPlainObject(value) {
|
|
@@ -3302,6 +3836,25 @@ function isPlainObject(value) {
|
|
|
3302
3836
|
return proto === null || proto === Object.prototype;
|
|
3303
3837
|
}
|
|
3304
3838
|
|
|
3839
|
+
// src/response/pendingMeta.ts
|
|
3840
|
+
var pending = /* @__PURE__ */ new WeakMap();
|
|
3841
|
+
function deferMetaHeaders(response, headers) {
|
|
3842
|
+
const existing = pending.get(response);
|
|
3843
|
+
if (existing) {
|
|
3844
|
+
Object.assign(existing, headers);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
pending.set(response, { ...headers });
|
|
3848
|
+
}
|
|
3849
|
+
function consumePendingMetaHeaders(response) {
|
|
3850
|
+
const headers = pending.get(response);
|
|
3851
|
+
if (headers) pending.delete(response);
|
|
3852
|
+
return headers;
|
|
3853
|
+
}
|
|
3854
|
+
function isHeadersOnlyMeta(meta) {
|
|
3855
|
+
return meta.status === void 0 && meta.setCookies.length === 0;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3305
3858
|
// src/response/toResponse.ts
|
|
3306
3859
|
async function toResponse(value, meta) {
|
|
3307
3860
|
if (value instanceof Promise) {
|
|
@@ -3318,6 +3871,10 @@ async function toResponse(value, meta) {
|
|
|
3318
3871
|
};
|
|
3319
3872
|
if (value instanceof Response) {
|
|
3320
3873
|
if (meta && (meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0)) {
|
|
3874
|
+
if (isHeadersOnlyMeta(meta)) {
|
|
3875
|
+
deferMetaHeaders(value, meta.headers);
|
|
3876
|
+
return value;
|
|
3877
|
+
}
|
|
3321
3878
|
const headers2 = new Headers(value.headers);
|
|
3322
3879
|
applyMeta(headers2);
|
|
3323
3880
|
return new Response(value.body, {
|
|
@@ -3426,11 +3983,12 @@ function getBuiltinInjectionValue(type, ctx, body) {
|
|
|
3426
3983
|
}
|
|
3427
3984
|
return {};
|
|
3428
3985
|
// Phase 2.3:注入所有已注册 agent 元数据列表
|
|
3986
|
+
// 方案 A:优先读 app 实例注册表,无实例(编程式直调 ctx)回退默认全局实例
|
|
3429
3987
|
case "agents":
|
|
3430
|
-
return listAgents();
|
|
3988
|
+
return ctx.registries ? ctx.registries.agent.listAgents() : listAgents();
|
|
3431
3989
|
// Phase 3.5:调 @faapi/agent 插件注册的工厂获取 AgentHandle
|
|
3432
3990
|
case "agent":
|
|
3433
|
-
return getAgentHandle(ctx);
|
|
3991
|
+
return ctx.registries ? ctx.registries.agentHandle.get(ctx) : getAgentHandle(ctx);
|
|
3434
3992
|
default:
|
|
3435
3993
|
return void 0;
|
|
3436
3994
|
}
|
|
@@ -3461,6 +4019,10 @@ function wrapResult(result, ctx) {
|
|
|
3461
4019
|
function mergeMeta(response, meta) {
|
|
3462
4020
|
const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
|
|
3463
4021
|
if (!hasMeta) return response;
|
|
4022
|
+
if (isHeadersOnlyMeta(meta)) {
|
|
4023
|
+
deferMetaHeaders(response, meta.headers);
|
|
4024
|
+
return response;
|
|
4025
|
+
}
|
|
3464
4026
|
const headers = new Headers(response.headers);
|
|
3465
4027
|
for (const [key, value] of Object.entries(meta.headers)) {
|
|
3466
4028
|
headers.set(key, value);
|
|
@@ -3553,11 +4115,24 @@ async function sendNodeResponse(response, res) {
|
|
|
3553
4115
|
res.setHeader(key, value);
|
|
3554
4116
|
}
|
|
3555
4117
|
}
|
|
4118
|
+
const deferred = consumePendingMetaHeaders(response);
|
|
4119
|
+
if (deferred) {
|
|
4120
|
+
for (const [key, value] of Object.entries(deferred)) {
|
|
4121
|
+
res.setHeader(key, value);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
3556
4124
|
if (response.body) {
|
|
3557
4125
|
const nodeStream = Readable.fromWeb(response.body);
|
|
3558
4126
|
await new Promise((resolve, reject) => {
|
|
3559
4127
|
nodeStream.on("error", reject);
|
|
3560
|
-
|
|
4128
|
+
const abort = () => {
|
|
4129
|
+
nodeStream.destroy();
|
|
4130
|
+
resolve();
|
|
4131
|
+
};
|
|
4132
|
+
res.on("error", abort);
|
|
4133
|
+
res.on("close", () => {
|
|
4134
|
+
if (!res.writableEnded) abort();
|
|
4135
|
+
});
|
|
3561
4136
|
res.on("finish", resolve);
|
|
3562
4137
|
nodeStream.pipe(res);
|
|
3563
4138
|
});
|
|
@@ -3592,28 +4167,25 @@ async function validateInput(schemaPath, method, inputType, input) {
|
|
|
3592
4167
|
}
|
|
3593
4168
|
const schema = mod[schemaKey];
|
|
3594
4169
|
if (schema === void 0 || schema === null) {
|
|
3595
|
-
|
|
3596
|
-
return { valid: true, issues: [], data };
|
|
4170
|
+
return { valid: true, issues: [], data: input };
|
|
3597
4171
|
}
|
|
3598
4172
|
if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
|
|
3599
4173
|
throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
|
|
3600
4174
|
}
|
|
3601
|
-
const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
3602
4175
|
const zodSchema = schema;
|
|
3603
|
-
const result = zodSchema.safeParse(
|
|
4176
|
+
const result = zodSchema.safeParse(input);
|
|
3604
4177
|
if (result.success) {
|
|
3605
|
-
|
|
3606
|
-
return { valid: true, issues: [], data };
|
|
4178
|
+
return { valid: true, issues: [], data: result.data };
|
|
3607
4179
|
}
|
|
3608
4180
|
const issues = mapZodIssues(result.error);
|
|
3609
|
-
return { valid: false, issues, data:
|
|
4181
|
+
return { valid: false, issues, data: input };
|
|
3610
4182
|
}
|
|
3611
4183
|
function mapZodIssues(error) {
|
|
3612
4184
|
return error.issues.map((issue) => {
|
|
3613
|
-
const code = mapZodCode(issue
|
|
3614
|
-
const
|
|
4185
|
+
const code = mapZodCode(issue);
|
|
4186
|
+
const path23 = issue.path.map(String).join(".") || "";
|
|
3615
4187
|
return {
|
|
3616
|
-
path:
|
|
4188
|
+
path: path23,
|
|
3617
4189
|
code,
|
|
3618
4190
|
expected: issue.expected ?? mapExpectedFromMessage(issue.message),
|
|
3619
4191
|
received: issue.received ?? mapReceivedFromMessage(issue.message),
|
|
@@ -3621,27 +4193,29 @@ function mapZodIssues(error) {
|
|
|
3621
4193
|
};
|
|
3622
4194
|
});
|
|
3623
4195
|
}
|
|
3624
|
-
function mapZodCode(
|
|
3625
|
-
switch (
|
|
4196
|
+
function mapZodCode(issue) {
|
|
4197
|
+
switch (issue.code) {
|
|
3626
4198
|
case "invalid_type":
|
|
4199
|
+
if (issue.received === "undefined" || /received undefined/i.test(issue.message)) {
|
|
4200
|
+
return "MISSING_FIELD";
|
|
4201
|
+
}
|
|
4202
|
+
return "TYPE_MISMATCH";
|
|
3627
4203
|
case "invalid_union":
|
|
3628
4204
|
case "invalid_union_discriminator":
|
|
3629
4205
|
return "TYPE_MISMATCH";
|
|
3630
4206
|
case "unrecognized_keys":
|
|
3631
4207
|
return "INVALID_FORMAT";
|
|
3632
4208
|
case "invalid_value":
|
|
3633
|
-
case "
|
|
4209
|
+
case "invalid_format":
|
|
4210
|
+
case "invalid_key":
|
|
4211
|
+
case "invalid_element":
|
|
3634
4212
|
case "too_small":
|
|
3635
4213
|
case "too_big":
|
|
3636
4214
|
case "invalid_intersection_types":
|
|
3637
4215
|
case "not_multiple_of":
|
|
3638
|
-
return "INVALID_VALUE";
|
|
3639
4216
|
case "custom":
|
|
3640
4217
|
return "INVALID_VALUE";
|
|
3641
4218
|
default:
|
|
3642
|
-
if (message.includes("Required") || message.includes("required")) {
|
|
3643
|
-
return "MISSING_FIELD";
|
|
3644
|
-
}
|
|
3645
4219
|
return "INVALID_VALUE";
|
|
3646
4220
|
}
|
|
3647
4221
|
}
|
|
@@ -3658,11 +4232,13 @@ function mapReceivedFromMessage(message) {
|
|
|
3658
4232
|
init_inputType();
|
|
3659
4233
|
|
|
3660
4234
|
// src/utils/getClientIp.ts
|
|
3661
|
-
function getClientIp(req) {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
4235
|
+
function getClientIp(req, trustedProxy = false) {
|
|
4236
|
+
if (trustedProxy) {
|
|
4237
|
+
const xff = req.headers["x-forwarded-for"];
|
|
4238
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
4239
|
+
const first = xff.split(",")[0]?.trim();
|
|
4240
|
+
if (first) return first;
|
|
4241
|
+
}
|
|
3666
4242
|
}
|
|
3667
4243
|
const remote = req.socket?.remoteAddress;
|
|
3668
4244
|
if (remote) {
|
|
@@ -3674,10 +4250,176 @@ function getClientIp(req) {
|
|
|
3674
4250
|
return "";
|
|
3675
4251
|
}
|
|
3676
4252
|
|
|
4253
|
+
// src/middleware/compression.ts
|
|
4254
|
+
import zlib from "zlib";
|
|
4255
|
+
import { promisify } from "util";
|
|
4256
|
+
var gzipAsync = promisify(zlib.gzip);
|
|
4257
|
+
var deflateAsync = promisify(zlib.deflate);
|
|
4258
|
+
var brotliAsync = promisify(zlib.brotliCompress);
|
|
4259
|
+
var DEFAULT_THRESHOLD = 1024;
|
|
4260
|
+
var COMPRESSIBLE_CHECKS = [
|
|
4261
|
+
(ct) => ct === "application/json",
|
|
4262
|
+
(ct) => ct === "application/javascript",
|
|
4263
|
+
(ct) => ct === "image/svg+xml",
|
|
4264
|
+
(ct) => ct.startsWith("text/") && ct !== "text/event-stream"
|
|
4265
|
+
];
|
|
4266
|
+
function isCompressible(contentType) {
|
|
4267
|
+
const ct = contentType.split(";")[0].trim().toLowerCase();
|
|
4268
|
+
if (ct === "") return false;
|
|
4269
|
+
return COMPRESSIBLE_CHECKS.some((check) => check(ct));
|
|
4270
|
+
}
|
|
4271
|
+
function parseAcceptEncoding(header) {
|
|
4272
|
+
return header.split(",").map((part) => part.trim()).filter((part) => part !== "").map((part) => {
|
|
4273
|
+
const [encoding, ...params] = part.split(";");
|
|
4274
|
+
let quality = 1;
|
|
4275
|
+
for (const param of params) {
|
|
4276
|
+
const trimmed = param.trim();
|
|
4277
|
+
if (trimmed.startsWith("q=")) {
|
|
4278
|
+
const q = Number(trimmed.slice(2));
|
|
4279
|
+
if (Number.isFinite(q)) quality = q;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
return { encoding: (encoding ?? "").trim().toLowerCase(), quality };
|
|
4283
|
+
}).filter((e) => e.encoding !== "" && e.quality > 0);
|
|
4284
|
+
}
|
|
4285
|
+
function selectEncoding(accepted) {
|
|
4286
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4287
|
+
for (const { encoding, quality } of accepted) {
|
|
4288
|
+
byName.set(encoding, Math.max(byName.get(encoding) ?? 0, quality));
|
|
4289
|
+
}
|
|
4290
|
+
for (const [encoding, quality] of byName) {
|
|
4291
|
+
if (quality === 0) byName.delete(encoding);
|
|
4292
|
+
}
|
|
4293
|
+
for (const candidate of ["br", "gzip", "deflate"]) {
|
|
4294
|
+
if ((byName.get(candidate) ?? 0) > 0) return candidate;
|
|
4295
|
+
}
|
|
4296
|
+
if ((byName.get("*") ?? 0) > 0) return "gzip";
|
|
4297
|
+
return null;
|
|
4298
|
+
}
|
|
4299
|
+
async function compressBody(encoding, body) {
|
|
4300
|
+
const buf = Buffer.from(body, "utf-8");
|
|
4301
|
+
switch (encoding) {
|
|
4302
|
+
case "br":
|
|
4303
|
+
return brotliAsync(buf);
|
|
4304
|
+
case "gzip":
|
|
4305
|
+
return gzipAsync(buf);
|
|
4306
|
+
case "deflate":
|
|
4307
|
+
return deflateAsync(buf);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function mergeVary(meta, value) {
|
|
4311
|
+
const existing = meta.headers["Vary"] ?? meta.headers["vary"];
|
|
4312
|
+
if (!existing) {
|
|
4313
|
+
meta.headers["Vary"] = value;
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
if (!existing.toLowerCase().includes(value.toLowerCase())) {
|
|
4317
|
+
const key = meta.headers["Vary"] !== void 0 ? "Vary" : "vary";
|
|
4318
|
+
meta.headers[key] = `${existing}, ${value}`;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
function compression(options = {}) {
|
|
4322
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
4323
|
+
return async (ctx, next) => {
|
|
4324
|
+
const meta = ctx.meta;
|
|
4325
|
+
const response = await next();
|
|
4326
|
+
if (!response) return response;
|
|
4327
|
+
mergeVary(meta, "Accept-Encoding");
|
|
4328
|
+
const acceptEncoding = ctx.request.headers.get("accept-encoding") ?? "";
|
|
4329
|
+
const encoding = selectEncoding(parseAcceptEncoding(acceptEncoding));
|
|
4330
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
4331
|
+
if (!encoding || !isCompressible(contentType) || response.headers.has("content-encoding") || (response.headers.get("cache-control") ?? "").includes("no-transform") || response.status === 204 || response.status === 304 || response.body === null) {
|
|
4332
|
+
return response;
|
|
4333
|
+
}
|
|
4334
|
+
const deferredHeaders = consumePendingMetaHeaders(response);
|
|
4335
|
+
const bodyText = await response.text();
|
|
4336
|
+
if (bodyText.length < threshold) {
|
|
4337
|
+
const headers2 = new Headers(response.headers);
|
|
4338
|
+
for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
|
|
4339
|
+
headers2.set(key, value);
|
|
4340
|
+
}
|
|
4341
|
+
return new Response(bodyText, {
|
|
4342
|
+
status: response.status,
|
|
4343
|
+
statusText: response.statusText,
|
|
4344
|
+
headers: headers2
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
const compressed = await compressBody(encoding, bodyText);
|
|
4348
|
+
const headers = new Headers(response.headers);
|
|
4349
|
+
for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
|
|
4350
|
+
headers.set(key, value);
|
|
4351
|
+
}
|
|
4352
|
+
headers.set("Content-Encoding", encoding);
|
|
4353
|
+
headers.delete("Content-Length");
|
|
4354
|
+
return new Response(compressed, {
|
|
4355
|
+
status: response.status,
|
|
4356
|
+
statusText: response.statusText,
|
|
4357
|
+
headers
|
|
4358
|
+
});
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
// src/middleware/etag.ts
|
|
4363
|
+
import { createHash } from "crypto";
|
|
4364
|
+
var DEFAULTS2 = { weak: true };
|
|
4365
|
+
function computeEtag(body, weak) {
|
|
4366
|
+
const hash = createHash("sha1").update(body).digest("base64");
|
|
4367
|
+
return weak ? `W/"${hash}"` : `"${hash}"`;
|
|
4368
|
+
}
|
|
4369
|
+
function ifNoneMatchMatches(ifNoneMatch, etag2, weak) {
|
|
4370
|
+
const normalize = (tag) => tag.trim().replace(/^W\//i, "");
|
|
4371
|
+
if (weak) {
|
|
4372
|
+
const target = normalize(etag2);
|
|
4373
|
+
return ifNoneMatch.split(",").some((tag) => {
|
|
4374
|
+
const trimmed = tag.trim();
|
|
4375
|
+
return trimmed === "*" || normalize(trimmed) === target;
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
return ifNoneMatch.split(",").some((tag) => tag.trim() === etag2 || tag.trim() === "*");
|
|
4379
|
+
}
|
|
4380
|
+
function etag(options = {}) {
|
|
4381
|
+
const opts = { ...DEFAULTS2, ...options };
|
|
4382
|
+
return async (ctx, next) => {
|
|
4383
|
+
const meta = ctx.meta;
|
|
4384
|
+
const response = await next();
|
|
4385
|
+
if (!response) return response;
|
|
4386
|
+
const method = ctx.request.method.toUpperCase();
|
|
4387
|
+
if (method !== "GET" && method !== "HEAD") return response;
|
|
4388
|
+
if (response.status < 200 || response.status >= 300) return response;
|
|
4389
|
+
if (meta.headers["etag"] !== void 0 || response.headers.has("etag")) {
|
|
4390
|
+
return response;
|
|
4391
|
+
}
|
|
4392
|
+
if (response.body === null || (response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
4393
|
+
return response;
|
|
4394
|
+
}
|
|
4395
|
+
const bodyText = await response.text();
|
|
4396
|
+
const etagValue = computeEtag(bodyText, opts.weak);
|
|
4397
|
+
const ifNoneMatch = ctx.request.headers.get("if-none-match");
|
|
4398
|
+
if (ifNoneMatch && ifNoneMatchMatches(ifNoneMatch, etagValue, opts.weak)) {
|
|
4399
|
+
return new Response(null, {
|
|
4400
|
+
status: 304,
|
|
4401
|
+
statusText: response.statusText,
|
|
4402
|
+
headers: { ETag: etagValue }
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
meta.headers["etag"] = etagValue;
|
|
4406
|
+
const headers = new Headers(response.headers);
|
|
4407
|
+
const deferredHeaders = consumePendingMetaHeaders(response);
|
|
4408
|
+
for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
|
|
4409
|
+
headers.set(key, value);
|
|
4410
|
+
}
|
|
4411
|
+
return new Response(bodyText, {
|
|
4412
|
+
status: response.status,
|
|
4413
|
+
statusText: response.statusText,
|
|
4414
|
+
headers
|
|
4415
|
+
});
|
|
4416
|
+
};
|
|
4417
|
+
}
|
|
4418
|
+
|
|
3677
4419
|
// src/server/handleWsUpgrade.ts
|
|
3678
4420
|
import fs13 from "fs";
|
|
3679
4421
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3680
|
-
import
|
|
4422
|
+
import path14 from "path";
|
|
3681
4423
|
|
|
3682
4424
|
// src/server/serverUtils.ts
|
|
3683
4425
|
function nodeHttpToWebHeaders(req) {
|
|
@@ -3708,8 +4450,10 @@ function buildErrorResponse(err, config) {
|
|
|
3708
4450
|
|
|
3709
4451
|
// src/middleware/loadMiddlewares.ts
|
|
3710
4452
|
var middlewareCache = /* @__PURE__ */ new Map();
|
|
4453
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
3711
4454
|
function invalidateMiddlewareCache() {
|
|
3712
4455
|
middlewareCache.clear();
|
|
4456
|
+
inFlight.clear();
|
|
3713
4457
|
}
|
|
3714
4458
|
function getCachedMiddlewares(absPath) {
|
|
3715
4459
|
return middlewareCache.get(absPath);
|
|
@@ -3762,8 +4506,15 @@ async function loadMergedMiddlewares(middlewarePaths) {
|
|
|
3762
4506
|
for (const absMwPath of middlewarePaths) {
|
|
3763
4507
|
let bundle = getCachedMiddlewares(absMwPath);
|
|
3764
4508
|
if (bundle === void 0) {
|
|
3765
|
-
|
|
3766
|
-
|
|
4509
|
+
let loading = inFlight.get(absMwPath);
|
|
4510
|
+
if (!loading) {
|
|
4511
|
+
loading = loadMiddlewaresFile(absMwPath).then((result) => {
|
|
4512
|
+
setCachedMiddlewares(absMwPath, result);
|
|
4513
|
+
return result;
|
|
4514
|
+
});
|
|
4515
|
+
inFlight.set(absMwPath, loading);
|
|
4516
|
+
}
|
|
4517
|
+
bundle = await loading;
|
|
3767
4518
|
}
|
|
3768
4519
|
mergedMiddlewares.push(...bundle.middlewares);
|
|
3769
4520
|
for (const [name, injector] of Object.entries(bundle.injectors)) {
|
|
@@ -3826,9 +4577,9 @@ function bindEvents(rawSocket, handlers) {
|
|
|
3826
4577
|
}
|
|
3827
4578
|
}
|
|
3828
4579
|
if (handlers.onMessage) {
|
|
3829
|
-
rawSocket.on("message", (data) => {
|
|
3830
|
-
const buf =
|
|
3831
|
-
handlers.onMessage(ws, buf.toString("utf8"));
|
|
4580
|
+
rawSocket.on("message", (data, isBinary) => {
|
|
4581
|
+
const buf = toWsBuffer(data);
|
|
4582
|
+
handlers.onMessage(ws, isBinary ? buf : buf.toString("utf8"));
|
|
3832
4583
|
});
|
|
3833
4584
|
}
|
|
3834
4585
|
if (handlers.onClose) {
|
|
@@ -3842,12 +4593,22 @@ function bindEvents(rawSocket, handlers) {
|
|
|
3842
4593
|
});
|
|
3843
4594
|
}
|
|
3844
4595
|
}
|
|
4596
|
+
function toWsBuffer(data) {
|
|
4597
|
+
if (Buffer.isBuffer(data)) return data;
|
|
4598
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
4599
|
+
return Buffer.from(data);
|
|
4600
|
+
}
|
|
3845
4601
|
async function sendResponseToSocket(socket, response) {
|
|
3846
4602
|
const body = await response.text().catch(() => "");
|
|
3847
4603
|
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
|
|
3848
4604
|
`;
|
|
3849
4605
|
const headerLines = [];
|
|
3850
4606
|
let hasContentLength = false;
|
|
4607
|
+
const deferredHeaders = consumePendingMetaHeaders(response);
|
|
4608
|
+
for (const [key, value] of Object.entries(deferredHeaders ?? {})) {
|
|
4609
|
+
if (key.toLowerCase() === "content-length") hasContentLength = true;
|
|
4610
|
+
headerLines.push(`${key}: ${value}`);
|
|
4611
|
+
}
|
|
3851
4612
|
for (const [key, value] of response.headers) {
|
|
3852
4613
|
if (key.toLowerCase() === "content-length") {
|
|
3853
4614
|
hasContentLength = true;
|
|
@@ -3861,9 +4622,33 @@ async function sendResponseToSocket(socket, response) {
|
|
|
3861
4622
|
socket.destroy();
|
|
3862
4623
|
}
|
|
3863
4624
|
function attachWebSocket(options) {
|
|
3864
|
-
const {
|
|
4625
|
+
const {
|
|
4626
|
+
server,
|
|
4627
|
+
routesRef,
|
|
4628
|
+
rootDir,
|
|
4629
|
+
config,
|
|
4630
|
+
globalMiddlewares,
|
|
4631
|
+
trustedProxy = false,
|
|
4632
|
+
registries
|
|
4633
|
+
} = options;
|
|
3865
4634
|
const wss = new WebSocketServer({ noServer: true });
|
|
3866
4635
|
server.on("upgrade", async (req, socket, head) => {
|
|
4636
|
+
try {
|
|
4637
|
+
await handleUpgradeRequest(req, socket, head);
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
console.error("[faapi] WS upgrade \u5904\u7406\u5931\u8D25:", err);
|
|
4640
|
+
if (!socket.destroyed) {
|
|
4641
|
+
try {
|
|
4642
|
+
if (!socket.writableEnded) {
|
|
4643
|
+
socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
|
|
4644
|
+
}
|
|
4645
|
+
} catch {
|
|
4646
|
+
}
|
|
4647
|
+
socket.destroy();
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
});
|
|
4651
|
+
async function handleUpgradeRequest(req, socket, head) {
|
|
3867
4652
|
const currentWsRoutes = routesRef.wsCurrent;
|
|
3868
4653
|
const pathname = getPathname(req);
|
|
3869
4654
|
const match = matchWsRoute(currentWsRoutes, pathname);
|
|
@@ -3877,13 +4662,13 @@ function attachWebSocket(options) {
|
|
|
3877
4662
|
const host = req.headers.host ?? "localhost";
|
|
3878
4663
|
const url = `http://${host}${req.url ?? "/"}`;
|
|
3879
4664
|
const request = new Request(url, { method: "GET", headers });
|
|
3880
|
-
const ctx = createContext(request, params, config, getClientIp(req));
|
|
4665
|
+
const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
|
|
3881
4666
|
const meta = ctx.meta;
|
|
3882
4667
|
let upgraded = false;
|
|
3883
4668
|
const finalHandler = async () => {
|
|
3884
4669
|
let handlers;
|
|
3885
4670
|
try {
|
|
3886
|
-
const absoluteFilePath =
|
|
4671
|
+
const absoluteFilePath = path14.resolve(rootDir, route.filePath);
|
|
3887
4672
|
handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
|
|
3888
4673
|
} catch (err) {
|
|
3889
4674
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -3907,6 +4692,7 @@ function attachWebSocket(options) {
|
|
|
3907
4692
|
let response;
|
|
3908
4693
|
try {
|
|
3909
4694
|
if (route.middlewares === void 0 && route.middlewarePaths) {
|
|
4695
|
+
await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
|
|
3910
4696
|
const bundle = await loadMergedMiddlewares(route.middlewarePaths);
|
|
3911
4697
|
if (bundle) {
|
|
3912
4698
|
route.middlewares = bundle.middlewares;
|
|
@@ -3932,7 +4718,7 @@ function attachWebSocket(options) {
|
|
|
3932
4718
|
return;
|
|
3933
4719
|
}
|
|
3934
4720
|
await sendResponseToSocket(socket, mergeMeta(response, meta));
|
|
3935
|
-
}
|
|
4721
|
+
}
|
|
3936
4722
|
return wss;
|
|
3937
4723
|
}
|
|
3938
4724
|
|
|
@@ -3947,16 +4733,26 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
|
|
|
3947
4733
|
const headers = nodeHttpToWebHeaders(req);
|
|
3948
4734
|
const method = req.method ?? "GET";
|
|
3949
4735
|
if (method === "GET" || method === "HEAD") {
|
|
3950
|
-
return new Request(url.toString(), { method, headers });
|
|
4736
|
+
return { request: new Request(url.toString(), { method, headers }), url };
|
|
4737
|
+
}
|
|
4738
|
+
const contentLength = req.headers["content-length"];
|
|
4739
|
+
if (contentLength !== void 0) {
|
|
4740
|
+
const declared = Number(Array.isArray(contentLength) ? contentLength[0] : contentLength);
|
|
4741
|
+
if (Number.isFinite(declared) && declared > bodyLimit) {
|
|
4742
|
+
throw new PayloadTooLargeError(bodyLimit);
|
|
4743
|
+
}
|
|
3951
4744
|
}
|
|
3952
4745
|
const stream = Readable2.toWeb(req);
|
|
3953
4746
|
const limitedStream = limitStreamSize(stream, bodyLimit);
|
|
3954
|
-
return
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4747
|
+
return {
|
|
4748
|
+
request: new Request(url.toString(), {
|
|
4749
|
+
method,
|
|
4750
|
+
headers,
|
|
4751
|
+
body: limitedStream,
|
|
4752
|
+
duplex: "half"
|
|
4753
|
+
}),
|
|
4754
|
+
url
|
|
4755
|
+
};
|
|
3960
4756
|
}
|
|
3961
4757
|
function limitStreamSize(stream, maxSize) {
|
|
3962
4758
|
let totalSize = 0;
|
|
@@ -4008,22 +4804,6 @@ function limitStreamSize(stream, maxSize) {
|
|
|
4008
4804
|
}
|
|
4009
4805
|
});
|
|
4010
4806
|
}
|
|
4011
|
-
function findAllowedMethods(routes, path19) {
|
|
4012
|
-
const methods = /* @__PURE__ */ new Set();
|
|
4013
|
-
for (const route of routes) {
|
|
4014
|
-
if (route.urlPath === path19) {
|
|
4015
|
-
methods.add(route.method);
|
|
4016
|
-
continue;
|
|
4017
|
-
}
|
|
4018
|
-
if (route.isDynamic) {
|
|
4019
|
-
const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
|
|
4020
|
-
if (params !== null) {
|
|
4021
|
-
methods.add(route.method);
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
return Array.from(methods);
|
|
4026
|
-
}
|
|
4027
4807
|
function createServer(options) {
|
|
4028
4808
|
const {
|
|
4029
4809
|
routes,
|
|
@@ -4036,12 +4816,20 @@ function createServer(options) {
|
|
|
4036
4816
|
middlewares: globalMiddlewares,
|
|
4037
4817
|
injectors: globalInjectors,
|
|
4038
4818
|
helmet: helmetOption,
|
|
4819
|
+
compression: compressionOption,
|
|
4820
|
+
etag: etagOption,
|
|
4821
|
+
registries,
|
|
4039
4822
|
logger: loggerOption,
|
|
4040
4823
|
bodyLimit = DEFAULT_BODY_LIMIT,
|
|
4041
|
-
http2: http2Option
|
|
4824
|
+
http2: http2Option,
|
|
4825
|
+
trustedProxy = false
|
|
4042
4826
|
} = options;
|
|
4043
4827
|
const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
|
|
4044
4828
|
const configMiddlewares = [];
|
|
4829
|
+
if (compressionOption) {
|
|
4830
|
+
const compOpts = typeof compressionOption === "object" ? compressionOption : {};
|
|
4831
|
+
configMiddlewares.push(compression(compOpts));
|
|
4832
|
+
}
|
|
4045
4833
|
const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
|
|
4046
4834
|
if (corsMiddleware) configMiddlewares.push(corsMiddleware);
|
|
4047
4835
|
if (helmetOption) {
|
|
@@ -4050,6 +4838,14 @@ function createServer(options) {
|
|
|
4050
4838
|
}
|
|
4051
4839
|
const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
|
|
4052
4840
|
if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
|
|
4841
|
+
if (etagOption) {
|
|
4842
|
+
const etagOpts = typeof etagOption === "object" ? etagOption : {};
|
|
4843
|
+
configMiddlewares.push(etag(etagOpts));
|
|
4844
|
+
}
|
|
4845
|
+
const outerMiddlewares = [...configMiddlewares];
|
|
4846
|
+
if (globalMiddlewares && globalMiddlewares.length > 0) {
|
|
4847
|
+
outerMiddlewares.push(...globalMiddlewares);
|
|
4848
|
+
}
|
|
4053
4849
|
const server = (() => {
|
|
4054
4850
|
if (http2Option) {
|
|
4055
4851
|
const h2Opts = typeof http2Option === "object" ? http2Option : {};
|
|
@@ -4069,29 +4865,43 @@ function createServer(options) {
|
|
|
4069
4865
|
dist,
|
|
4070
4866
|
req,
|
|
4071
4867
|
res,
|
|
4072
|
-
|
|
4868
|
+
outerMiddlewares,
|
|
4073
4869
|
onError,
|
|
4074
4870
|
config,
|
|
4075
|
-
globalMiddlewares,
|
|
4076
4871
|
globalInjectors,
|
|
4077
|
-
bodyLimit
|
|
4872
|
+
bodyLimit,
|
|
4873
|
+
trustedProxy,
|
|
4874
|
+
registries
|
|
4078
4875
|
).catch(() => {
|
|
4079
4876
|
res.statusCode = 500;
|
|
4080
4877
|
res.end();
|
|
4081
4878
|
});
|
|
4082
4879
|
});
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4880
|
+
attachWebSocket({
|
|
4881
|
+
server,
|
|
4882
|
+
routesRef,
|
|
4883
|
+
rootDir,
|
|
4884
|
+
config,
|
|
4885
|
+
globalMiddlewares,
|
|
4886
|
+
trustedProxy,
|
|
4887
|
+
registries
|
|
4888
|
+
});
|
|
4086
4889
|
return { server, routesRef };
|
|
4087
4890
|
}
|
|
4088
|
-
function prepareRequest(req, config, bodyLimit) {
|
|
4089
|
-
const request = toWebRequest(req, bodyLimit);
|
|
4891
|
+
function prepareRequest(req, config, bodyLimit, trustedProxy, registries) {
|
|
4892
|
+
const { request, url } = toWebRequest(req, bodyLimit);
|
|
4090
4893
|
const method = request.method.toUpperCase();
|
|
4091
|
-
const urlPath =
|
|
4092
|
-
const ctx =
|
|
4894
|
+
const urlPath = url.pathname;
|
|
4895
|
+
const ctx = createContextFromUrl(
|
|
4896
|
+
request,
|
|
4897
|
+
url,
|
|
4898
|
+
{},
|
|
4899
|
+
config,
|
|
4900
|
+
getClientIp(req, trustedProxy),
|
|
4901
|
+
registries
|
|
4902
|
+
);
|
|
4093
4903
|
const meta = ctx.meta;
|
|
4094
|
-
return { request, ctx, meta, method, urlPath };
|
|
4904
|
+
return { request, url, ctx, meta, method, urlPath };
|
|
4095
4905
|
}
|
|
4096
4906
|
function resolveRouteOrThrow(routes, method, urlPath) {
|
|
4097
4907
|
const match = matchRoute(routes, method, urlPath);
|
|
@@ -4102,17 +4912,28 @@ function resolveRouteOrThrow(routes, method, urlPath) {
|
|
|
4102
4912
|
}
|
|
4103
4913
|
throw new RouteNotFoundError(urlPath);
|
|
4104
4914
|
}
|
|
4915
|
+
var routePathCache = /* @__PURE__ */ new WeakMap();
|
|
4916
|
+
function getRoutePaths(route, rootDir, dist) {
|
|
4917
|
+
let cached = routePathCache.get(route);
|
|
4918
|
+
if (!cached) {
|
|
4919
|
+
cached = {
|
|
4920
|
+
absFilePath: path15.resolve(rootDir, route.filePath),
|
|
4921
|
+
schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
|
|
4922
|
+
};
|
|
4923
|
+
routePathCache.set(route, cached);
|
|
4924
|
+
}
|
|
4925
|
+
return cached;
|
|
4926
|
+
}
|
|
4105
4927
|
function createRoutePipeline(opts) {
|
|
4106
|
-
const { routes, method, urlPath, ctx, request, rootDir, dist, globalInjectors } = opts;
|
|
4928
|
+
const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
|
|
4107
4929
|
return async () => {
|
|
4108
4930
|
const match = resolveRouteOrThrow(routes, method, urlPath);
|
|
4109
4931
|
ctx.params = match.params;
|
|
4110
4932
|
const { route } = match;
|
|
4111
|
-
const
|
|
4112
|
-
const routeModule = await loadRouteModule(
|
|
4113
|
-
const input = await
|
|
4933
|
+
const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
|
|
4934
|
+
const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
|
|
4935
|
+
const input = await resolveInputFromUrl(route.method, request, url);
|
|
4114
4936
|
const inputType = getInputTypeForMethod(route.method);
|
|
4115
|
-
const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
|
|
4116
4937
|
if (isDevOnDemandEnabled()) {
|
|
4117
4938
|
const devDist = getDevDist();
|
|
4118
4939
|
if (devDist) {
|
|
@@ -4123,8 +4944,9 @@ function createRoutePipeline(opts) {
|
|
|
4123
4944
|
if (!result.valid) {
|
|
4124
4945
|
throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
|
|
4125
4946
|
}
|
|
4126
|
-
const body = hasBody(route.method) ? result.data : void 0;
|
|
4947
|
+
const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
|
|
4127
4948
|
if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
|
|
4949
|
+
await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
|
|
4128
4950
|
const bundle = await loadMergedMiddlewares(route.middlewarePaths);
|
|
4129
4951
|
if (bundle) {
|
|
4130
4952
|
route.middlewares = bundle.middlewares;
|
|
@@ -4142,36 +4964,37 @@ async function sendSuccessResponse(response, res) {
|
|
|
4142
4964
|
await sendNodeResponse(response, res);
|
|
4143
4965
|
}
|
|
4144
4966
|
async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
4145
|
-
await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx
|
|
4146
|
-
if (onError) {
|
|
4967
|
+
await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx?.config), meta), res);
|
|
4968
|
+
if (onError && ctx) {
|
|
4147
4969
|
try {
|
|
4148
4970
|
await onError(err, ctx);
|
|
4149
4971
|
} catch {
|
|
4150
4972
|
}
|
|
4151
4973
|
}
|
|
4152
4974
|
}
|
|
4153
|
-
async function handleRequest(routes, rootDir, dist, req, res,
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
routes,
|
|
4157
|
-
method,
|
|
4158
|
-
urlPath,
|
|
4159
|
-
ctx,
|
|
4160
|
-
request,
|
|
4161
|
-
rootDir,
|
|
4162
|
-
dist,
|
|
4163
|
-
globalMiddlewares,
|
|
4164
|
-
globalInjectors
|
|
4165
|
-
});
|
|
4166
|
-
const outerMiddlewares = [];
|
|
4167
|
-
if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
|
|
4168
|
-
if (globalMiddlewares && globalMiddlewares.length > 0) {
|
|
4169
|
-
outerMiddlewares.push(...globalMiddlewares);
|
|
4170
|
-
}
|
|
4975
|
+
async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
|
|
4976
|
+
let meta = { headers: {}, setCookies: [] };
|
|
4977
|
+
let ctx;
|
|
4171
4978
|
try {
|
|
4979
|
+
const prepared = prepareRequest(req, config, bodyLimit, trustedProxy, registries);
|
|
4980
|
+
ctx = prepared.ctx;
|
|
4981
|
+
meta = prepared.meta;
|
|
4982
|
+
const { request, url, method, urlPath } = prepared;
|
|
4983
|
+
const routePipeline = createRoutePipeline({
|
|
4984
|
+
routes,
|
|
4985
|
+
method,
|
|
4986
|
+
urlPath,
|
|
4987
|
+
url,
|
|
4988
|
+
ctx,
|
|
4989
|
+
request,
|
|
4990
|
+
rootDir,
|
|
4991
|
+
dist,
|
|
4992
|
+
globalInjectors
|
|
4993
|
+
});
|
|
4172
4994
|
const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
|
|
4173
4995
|
await sendSuccessResponse(response, res);
|
|
4174
4996
|
} catch (err) {
|
|
4997
|
+
if (res.destroyed || res.writableEnded) return;
|
|
4175
4998
|
await sendErrorResponse(err, meta, res, onError, ctx);
|
|
4176
4999
|
}
|
|
4177
5000
|
}
|
|
@@ -4206,7 +5029,7 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
|
|
|
4206
5029
|
|
|
4207
5030
|
// src/cli/generateRoutes.ts
|
|
4208
5031
|
import fs14 from "fs";
|
|
4209
|
-
import
|
|
5032
|
+
import path16 from "path";
|
|
4210
5033
|
async function hydrateRoutes(manifest) {
|
|
4211
5034
|
const hydrateRoute = (serialized) => ({
|
|
4212
5035
|
method: serialized.method,
|
|
@@ -4231,11 +5054,10 @@ async function hydrateRoutes(manifest) {
|
|
|
4231
5054
|
}
|
|
4232
5055
|
|
|
4233
5056
|
// src/cli/generateAgentArtifacts.ts
|
|
4234
|
-
import
|
|
4235
|
-
import fs15 from "fs/promises";
|
|
5057
|
+
import path17 from "path";
|
|
4236
5058
|
|
|
4237
5059
|
// src/ast/extractAgentMetadata.ts
|
|
4238
|
-
import
|
|
5060
|
+
import ts9 from "typescript";
|
|
4239
5061
|
function extractAgentMetadata(program, filePath, pathMeta) {
|
|
4240
5062
|
const sourceFile = program.getSourceFile(filePath);
|
|
4241
5063
|
if (!sourceFile) return null;
|
|
@@ -4251,9 +5073,9 @@ function extractAgentMetadata(program, filePath, pathMeta) {
|
|
|
4251
5073
|
jsDocOwner = runNode;
|
|
4252
5074
|
}
|
|
4253
5075
|
}
|
|
4254
|
-
const jsDoc = jsDocOwner ?
|
|
4255
|
-
const description =
|
|
4256
|
-
const agentNameOverride =
|
|
5076
|
+
const jsDoc = jsDocOwner ? getJSDocFromNode(jsDocOwner) : void 0;
|
|
5077
|
+
const description = extractDescription(jsDoc);
|
|
5078
|
+
const agentNameOverride = extractJSDocTagValue(jsDoc, "agent");
|
|
4257
5079
|
let systemPrompt;
|
|
4258
5080
|
let tools;
|
|
4259
5081
|
let agents;
|
|
@@ -4281,22 +5103,22 @@ function extractAgentMetadata(program, filePath, pathMeta) {
|
|
|
4281
5103
|
}
|
|
4282
5104
|
function findConfigExport(sourceFile) {
|
|
4283
5105
|
let result = null;
|
|
4284
|
-
|
|
5106
|
+
ts9.forEachChild(sourceFile, (node) => {
|
|
4285
5107
|
if (result) return;
|
|
4286
|
-
if (
|
|
5108
|
+
if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
|
|
4287
5109
|
for (const decl of node.declarationList.declarations) {
|
|
4288
5110
|
if (result) break;
|
|
4289
|
-
const nameText =
|
|
5111
|
+
const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
|
|
4290
5112
|
if (nameText !== "config" || !decl.initializer) continue;
|
|
4291
|
-
if (
|
|
5113
|
+
if (ts9.isObjectLiteralExpression(decl.initializer)) {
|
|
4292
5114
|
result = { jsDocOwner: node, objectLiteral: decl.initializer };
|
|
4293
|
-
} else if (
|
|
5115
|
+
} else if (ts9.isArrowFunction(decl.initializer)) {
|
|
4294
5116
|
const returnObj = getReturnObjectLiteral(decl.initializer);
|
|
4295
5117
|
result = { jsDocOwner: node, objectLiteral: returnObj };
|
|
4296
5118
|
}
|
|
4297
5119
|
}
|
|
4298
5120
|
}
|
|
4299
|
-
if (
|
|
5121
|
+
if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
|
|
4300
5122
|
const returnObj = getReturnObjectLiteral(node);
|
|
4301
5123
|
result = { jsDocOwner: node, objectLiteral: returnObj };
|
|
4302
5124
|
}
|
|
@@ -4305,18 +5127,18 @@ function findConfigExport(sourceFile) {
|
|
|
4305
5127
|
}
|
|
4306
5128
|
function findRunExport(sourceFile) {
|
|
4307
5129
|
let result = null;
|
|
4308
|
-
|
|
5130
|
+
ts9.forEachChild(sourceFile, (node) => {
|
|
4309
5131
|
if (result) return;
|
|
4310
|
-
if (
|
|
5132
|
+
if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "run") {
|
|
4311
5133
|
result = node;
|
|
4312
5134
|
return;
|
|
4313
5135
|
}
|
|
4314
|
-
if (
|
|
5136
|
+
if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
|
|
4315
5137
|
for (const decl of node.declarationList.declarations) {
|
|
4316
5138
|
if (result) break;
|
|
4317
|
-
const nameText =
|
|
5139
|
+
const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
|
|
4318
5140
|
if (nameText !== "run" || !decl.initializer) continue;
|
|
4319
|
-
if (
|
|
5141
|
+
if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
|
|
4320
5142
|
result = node;
|
|
4321
5143
|
}
|
|
4322
5144
|
}
|
|
@@ -4327,52 +5149,22 @@ function findRunExport(sourceFile) {
|
|
|
4327
5149
|
function getReturnObjectLiteral(fn) {
|
|
4328
5150
|
const body = fn.body;
|
|
4329
5151
|
if (!body) return null;
|
|
4330
|
-
if (
|
|
5152
|
+
if (ts9.isObjectLiteralExpression(body)) {
|
|
4331
5153
|
return body;
|
|
4332
5154
|
}
|
|
4333
|
-
if (
|
|
5155
|
+
if (ts9.isBlock(body)) {
|
|
4334
5156
|
for (const stmt of body.statements) {
|
|
4335
|
-
if (
|
|
5157
|
+
if (ts9.isReturnStatement(stmt) && stmt.expression && ts9.isObjectLiteralExpression(stmt.expression)) {
|
|
4336
5158
|
return stmt.expression;
|
|
4337
5159
|
}
|
|
4338
5160
|
}
|
|
4339
5161
|
}
|
|
4340
5162
|
return null;
|
|
4341
5163
|
}
|
|
4342
|
-
function hasExportModifier2(node) {
|
|
4343
|
-
if (!ts8.canHaveModifiers(node)) return false;
|
|
4344
|
-
const modifiers = ts8.getModifiers(node);
|
|
4345
|
-
return !!modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword);
|
|
4346
|
-
}
|
|
4347
|
-
function getJSDocFromNode2(node) {
|
|
4348
|
-
const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
|
|
4349
|
-
if (apiDocs.length > 0) return apiDocs[0];
|
|
4350
|
-
const directDocs = node.jsDoc;
|
|
4351
|
-
if (directDocs && directDocs.length > 0) return directDocs[0];
|
|
4352
|
-
return void 0;
|
|
4353
|
-
}
|
|
4354
|
-
function extractDescription2(jsDoc) {
|
|
4355
|
-
if (!jsDoc) return void 0;
|
|
4356
|
-
if (typeof jsDoc.comment !== "string") return void 0;
|
|
4357
|
-
const trimmed = jsDoc.comment.trim();
|
|
4358
|
-
return trimmed || void 0;
|
|
4359
|
-
}
|
|
4360
|
-
function extractAgentTagValue(jsDoc) {
|
|
4361
|
-
if (!jsDoc || !jsDoc.tags) return void 0;
|
|
4362
|
-
for (const tag of jsDoc.tags) {
|
|
4363
|
-
if (tag.tagName.text !== "agent") continue;
|
|
4364
|
-
if (typeof tag.comment !== "string") return void 0;
|
|
4365
|
-
const text = tag.comment.trim();
|
|
4366
|
-
if (!text) return void 0;
|
|
4367
|
-
const cleaned = text.replace(/^\{|\}$/g, "").trim();
|
|
4368
|
-
return cleaned || void 0;
|
|
4369
|
-
}
|
|
4370
|
-
return void 0;
|
|
4371
|
-
}
|
|
4372
5164
|
function extractConfigFields(objLit) {
|
|
4373
5165
|
const result = {};
|
|
4374
5166
|
for (const prop of objLit.properties) {
|
|
4375
|
-
if (!
|
|
5167
|
+
if (!ts9.isPropertyAssignment(prop)) continue;
|
|
4376
5168
|
const propName = getPropertyName(prop.name);
|
|
4377
5169
|
if (!propName) continue;
|
|
4378
5170
|
switch (propName) {
|
|
@@ -4396,26 +5188,26 @@ function extractConfigFields(objLit) {
|
|
|
4396
5188
|
return result;
|
|
4397
5189
|
}
|
|
4398
5190
|
function getPropertyName(name) {
|
|
4399
|
-
if (
|
|
4400
|
-
if (
|
|
5191
|
+
if (ts9.isIdentifier(name)) return name.text;
|
|
5192
|
+
if (ts9.isStringLiteral(name)) return name.text;
|
|
4401
5193
|
return null;
|
|
4402
5194
|
}
|
|
4403
5195
|
function extractStringValue(expr) {
|
|
4404
|
-
if (
|
|
5196
|
+
if (ts9.isStringLiteral(expr)) return expr.text;
|
|
4405
5197
|
return void 0;
|
|
4406
5198
|
}
|
|
4407
5199
|
function extractNumberValue(expr) {
|
|
4408
|
-
if (
|
|
5200
|
+
if (ts9.isNumericLiteral(expr)) {
|
|
4409
5201
|
const num = Number(expr.text);
|
|
4410
5202
|
return Number.isNaN(num) ? void 0 : num;
|
|
4411
5203
|
}
|
|
4412
5204
|
return void 0;
|
|
4413
5205
|
}
|
|
4414
5206
|
function extractStringArrayValue(expr) {
|
|
4415
|
-
if (!
|
|
5207
|
+
if (!ts9.isArrayLiteralExpression(expr)) return void 0;
|
|
4416
5208
|
const values = [];
|
|
4417
5209
|
for (const element of expr.elements) {
|
|
4418
|
-
if (!
|
|
5210
|
+
if (!ts9.isStringLiteral(element)) return void 0;
|
|
4419
5211
|
values.push(element.text);
|
|
4420
5212
|
}
|
|
4421
5213
|
return values;
|
|
@@ -4423,15 +5215,8 @@ function extractStringArrayValue(expr) {
|
|
|
4423
5215
|
|
|
4424
5216
|
// src/cli/generateAgentArtifacts.ts
|
|
4425
5217
|
init_createProgram();
|
|
5218
|
+
init_atomicWrite();
|
|
4426
5219
|
var AGENTS_FILE = "faapi-agents.js";
|
|
4427
|
-
function toProdFilePath2(filePath, dist) {
|
|
4428
|
-
let rel = filePath.replace(/\\/g, "/");
|
|
4429
|
-
if (rel.startsWith("src/")) {
|
|
4430
|
-
rel = rel.slice(4);
|
|
4431
|
-
}
|
|
4432
|
-
const jsPath = rel.replace(/\.ts$/, ".js");
|
|
4433
|
-
return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
|
|
4434
|
-
}
|
|
4435
5220
|
function serializeAgents(agents, dist = "dist") {
|
|
4436
5221
|
return agents.map((a) => ({
|
|
4437
5222
|
name: a.name,
|
|
@@ -4442,16 +5227,14 @@ function serializeAgents(agents, dist = "dist") {
|
|
|
4442
5227
|
agents: a.agents,
|
|
4443
5228
|
model: a.model,
|
|
4444
5229
|
maxTurns: a.maxTurns,
|
|
4445
|
-
filePath:
|
|
5230
|
+
filePath: toProdFilePath(a.filePath, dist)
|
|
4446
5231
|
}));
|
|
4447
5232
|
}
|
|
4448
5233
|
async function writeAgentsModule(manifest, outputPath) {
|
|
4449
|
-
const dir = path14.dirname(outputPath);
|
|
4450
|
-
await fs15.mkdir(dir, { recursive: true });
|
|
4451
5234
|
const content = `// \u81EA\u52A8\u751F\u6210,\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91(faapi build/dev \u4EA7\u7269)
|
|
4452
5235
|
export const agents = ${JSON.stringify(manifest, null, 2)};
|
|
4453
5236
|
`;
|
|
4454
|
-
await
|
|
5237
|
+
await atomicWriteFile(outputPath, content);
|
|
4455
5238
|
}
|
|
4456
5239
|
function hydrateAgents(manifest) {
|
|
4457
5240
|
return manifest.map((a) => ({
|
|
@@ -4468,9 +5251,10 @@ function hydrateAgents(manifest) {
|
|
|
4468
5251
|
}
|
|
4469
5252
|
async function generateAgentArtifacts(agents, rootDir, dist) {
|
|
4470
5253
|
const metadata = [];
|
|
5254
|
+
const programByFile = createPrograms(agents.map((m) => path17.resolve(rootDir, m.filePath)));
|
|
4471
5255
|
for (const manifest of agents) {
|
|
4472
|
-
const absPath =
|
|
4473
|
-
const program =
|
|
5256
|
+
const absPath = path17.resolve(rootDir, manifest.filePath);
|
|
5257
|
+
const program = programByFile.get(absPath);
|
|
4474
5258
|
const result = extractAgentMetadata(program, absPath, {
|
|
4475
5259
|
name: manifest.name,
|
|
4476
5260
|
filePath: manifest.filePath,
|
|
@@ -4481,17 +5265,20 @@ async function generateAgentArtifacts(agents, rootDir, dist) {
|
|
|
4481
5265
|
}
|
|
4482
5266
|
}
|
|
4483
5267
|
const serialized = serializeAgents(metadata, dist);
|
|
4484
|
-
const agentsPath =
|
|
5268
|
+
const agentsPath = path17.resolve(rootDir, dist, AGENTS_FILE);
|
|
4485
5269
|
await writeAgentsModule(serialized, agentsPath);
|
|
4486
5270
|
return metadata;
|
|
4487
5271
|
}
|
|
4488
5272
|
|
|
4489
5273
|
// src/cli/loadPlugins.ts
|
|
4490
|
-
|
|
5274
|
+
import path18 from "path";
|
|
5275
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5276
|
+
async function loadPlugins(declarations, ctx, rootDir) {
|
|
4491
5277
|
const handlerWrappers = [];
|
|
4492
5278
|
const upgradeWrappers = [];
|
|
5279
|
+
const failures = [];
|
|
4493
5280
|
if (!declarations || declarations.length === 0) {
|
|
4494
|
-
return { handlerWrappers, upgradeWrappers };
|
|
5281
|
+
return { handlerWrappers, upgradeWrappers, failures };
|
|
4495
5282
|
}
|
|
4496
5283
|
const fullCtx = {
|
|
4497
5284
|
...ctx,
|
|
@@ -4504,7 +5291,18 @@ async function loadPlugins(declarations, ctx) {
|
|
|
4504
5291
|
};
|
|
4505
5292
|
const loaded = /* @__PURE__ */ new Set();
|
|
4506
5293
|
for (const decl of declarations) {
|
|
4507
|
-
|
|
5294
|
+
let specifier;
|
|
5295
|
+
let options;
|
|
5296
|
+
let enable;
|
|
5297
|
+
try {
|
|
5298
|
+
({ specifier, options, enable } = resolveDeclaration(decl));
|
|
5299
|
+
} catch (err) {
|
|
5300
|
+
failures.push({
|
|
5301
|
+
specifier: JSON.stringify(decl),
|
|
5302
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5303
|
+
});
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
4508
5306
|
if (enable === false) continue;
|
|
4509
5307
|
if (loaded.has(specifier)) {
|
|
4510
5308
|
console.warn(`! Plugin already loaded: ${specifier}, skipping`);
|
|
@@ -4512,21 +5310,38 @@ async function loadPlugins(declarations, ctx) {
|
|
|
4512
5310
|
}
|
|
4513
5311
|
loaded.add(specifier);
|
|
4514
5312
|
try {
|
|
4515
|
-
const mod = await import(specifier);
|
|
5313
|
+
const mod = await import(resolveSpecifier(specifier, rootDir));
|
|
4516
5314
|
const plugin = mod.default ?? mod;
|
|
4517
5315
|
if (typeof plugin.setup !== "function") {
|
|
4518
|
-
|
|
5316
|
+
failures.push({ specifier, reason: "plugin has no setup function" });
|
|
4519
5317
|
continue;
|
|
4520
5318
|
}
|
|
4521
5319
|
await plugin.setup({ ...fullCtx, options });
|
|
4522
5320
|
console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
|
|
4523
5321
|
} catch (err) {
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
5322
|
+
failures.push({
|
|
5323
|
+
specifier,
|
|
5324
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5325
|
+
});
|
|
4527
5326
|
}
|
|
4528
5327
|
}
|
|
4529
|
-
|
|
5328
|
+
if (failures.length > 0) {
|
|
5329
|
+
console.error(
|
|
5330
|
+
`[faapi] ${failures.length} plugin(s) failed to load:
|
|
5331
|
+
` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
return { handlerWrappers, upgradeWrappers, failures };
|
|
5335
|
+
}
|
|
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;
|
|
5340
|
+
}
|
|
5341
|
+
if (path18.isAbsolute(specifier)) {
|
|
5342
|
+
return pathToFileURL2(specifier).href;
|
|
5343
|
+
}
|
|
5344
|
+
return specifier;
|
|
4530
5345
|
}
|
|
4531
5346
|
function resolveDeclaration(decl) {
|
|
4532
5347
|
if (typeof decl === "string") {
|
|
@@ -4551,25 +5366,24 @@ var DEFAULT_PORT = 3e3;
|
|
|
4551
5366
|
var ROUTES_FILE = "faapi-routes.js";
|
|
4552
5367
|
var TOOLS_FILE2 = "faapi-tools.js";
|
|
4553
5368
|
var AGENTS_FILE2 = "faapi-agents.js";
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
if (!fs16.existsSync(toolsPath)) {
|
|
5369
|
+
async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries) {
|
|
5370
|
+
const toolsPath = path19.resolve(rootDir, dist, TOOLS_FILE2);
|
|
5371
|
+
if (!fs15.existsSync(toolsPath)) {
|
|
4558
5372
|
return [];
|
|
4559
5373
|
}
|
|
4560
5374
|
const serialized = await importWithCacheBust(toolsPath);
|
|
4561
5375
|
const hydrated = hydrateTools(serialized.tools ?? []);
|
|
4562
|
-
|
|
5376
|
+
registries.tool.hydrate(hydrated);
|
|
4563
5377
|
return hydrated;
|
|
4564
5378
|
}
|
|
4565
|
-
async function loadAndHydrateAgents(rootDir, dist) {
|
|
4566
|
-
const agentsPath =
|
|
4567
|
-
if (!
|
|
5379
|
+
async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistries) {
|
|
5380
|
+
const agentsPath = path19.resolve(rootDir, dist, AGENTS_FILE2);
|
|
5381
|
+
if (!fs15.existsSync(agentsPath)) {
|
|
4568
5382
|
return [];
|
|
4569
5383
|
}
|
|
4570
5384
|
const serialized = await importWithCacheBust(agentsPath);
|
|
4571
5385
|
const hydrated = hydrateAgents(serialized.agents ?? []);
|
|
4572
|
-
|
|
5386
|
+
registries.agent.hydrate(hydrated);
|
|
4573
5387
|
return hydrated;
|
|
4574
5388
|
}
|
|
4575
5389
|
var APP_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("faapi.app.instance");
|
|
@@ -4583,6 +5397,23 @@ function setCurrentApp(app) {
|
|
|
4583
5397
|
globalThis[APP_INSTANCE_KEY] = app;
|
|
4584
5398
|
}
|
|
4585
5399
|
}
|
|
5400
|
+
var SHUTDOWN_INSTALLED_KEY = /* @__PURE__ */ Symbol.for("faapi.defaultShutdownInstalled");
|
|
5401
|
+
function registerDefaultShutdownHandlers() {
|
|
5402
|
+
const g = globalThis;
|
|
5403
|
+
if (g[SHUTDOWN_INSTALLED_KEY]) return;
|
|
5404
|
+
g[SHUTDOWN_INSTALLED_KEY] = true;
|
|
5405
|
+
const shutdown = (signal) => {
|
|
5406
|
+
console.log(`
|
|
5407
|
+
- Received ${signal}, shutting down...`);
|
|
5408
|
+
void (async () => {
|
|
5409
|
+
const app = getCurrentApp();
|
|
5410
|
+
if (app) await app.close();
|
|
5411
|
+
process.exit(0);
|
|
5412
|
+
})();
|
|
5413
|
+
};
|
|
5414
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5415
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5416
|
+
}
|
|
4586
5417
|
function getApp() {
|
|
4587
5418
|
const app = getCurrentApp();
|
|
4588
5419
|
if (!app) {
|
|
@@ -4600,9 +5431,12 @@ var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
4600
5431
|
"extendContext",
|
|
4601
5432
|
"plugins",
|
|
4602
5433
|
"helmet",
|
|
5434
|
+
"compression",
|
|
5435
|
+
"etag",
|
|
4603
5436
|
"bodyLimit",
|
|
4604
5437
|
"logger",
|
|
4605
5438
|
"http2",
|
|
5439
|
+
"trustedProxy",
|
|
4606
5440
|
"response"
|
|
4607
5441
|
]);
|
|
4608
5442
|
function isFaapiConfigKey(key) {
|
|
@@ -4611,8 +5445,8 @@ function isFaapiConfigKey(key) {
|
|
|
4611
5445
|
async function createAppBase(options) {
|
|
4612
5446
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
4613
5447
|
const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
|
|
4614
|
-
const routesPath =
|
|
4615
|
-
if (!
|
|
5448
|
+
const routesPath = path19.resolve(rootDir, dist, ROUTES_FILE);
|
|
5449
|
+
if (!fs15.existsSync(routesPath)) {
|
|
4616
5450
|
throw new Error(
|
|
4617
5451
|
`[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
|
|
4618
5452
|
);
|
|
@@ -4631,8 +5465,9 @@ async function createAppBase(options) {
|
|
|
4631
5465
|
}
|
|
4632
5466
|
}
|
|
4633
5467
|
}
|
|
4634
|
-
const
|
|
4635
|
-
const
|
|
5468
|
+
const registries = createAppRegistries();
|
|
5469
|
+
const tools = await loadAndHydrateTools(rootDir, dist, registries);
|
|
5470
|
+
const agents = await loadAndHydrateAgents(rootDir, dist, registries);
|
|
4636
5471
|
const pluginConfig = config ? Object.fromEntries(Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))) : {};
|
|
4637
5472
|
const { server, routesRef } = createServer({
|
|
4638
5473
|
routes: sorted,
|
|
@@ -4645,29 +5480,52 @@ async function createAppBase(options) {
|
|
|
4645
5480
|
middlewares: config?.middlewares,
|
|
4646
5481
|
injectors: config?.injectors,
|
|
4647
5482
|
helmet: config?.helmet,
|
|
5483
|
+
compression: config?.compression,
|
|
5484
|
+
etag: config?.etag,
|
|
4648
5485
|
logger: config?.logger,
|
|
4649
5486
|
bodyLimit: config?.bodyLimit,
|
|
4650
|
-
http2: config?.http2
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
rootDir,
|
|
4654
|
-
routes: sorted,
|
|
4655
|
-
getRoutes: () => sorted,
|
|
4656
|
-
server,
|
|
4657
|
-
config: pluginConfig
|
|
5487
|
+
http2: config?.http2,
|
|
5488
|
+
trustedProxy: config?.trustedProxy,
|
|
5489
|
+
registries
|
|
4658
5490
|
});
|
|
5491
|
+
const { handlerWrappers, upgradeWrappers } = await loadPlugins(
|
|
5492
|
+
config?.plugins,
|
|
5493
|
+
{
|
|
5494
|
+
rootDir,
|
|
5495
|
+
registries,
|
|
5496
|
+
routes: sorted,
|
|
5497
|
+
getRoutes: () => sorted,
|
|
5498
|
+
server,
|
|
5499
|
+
config: pluginConfig
|
|
5500
|
+
},
|
|
5501
|
+
rootDir
|
|
5502
|
+
);
|
|
4659
5503
|
applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
|
|
4660
5504
|
let closed = false;
|
|
4661
5505
|
const app = {
|
|
4662
5506
|
server: null,
|
|
5507
|
+
registries,
|
|
4663
5508
|
routes: sorted,
|
|
4664
5509
|
wsRoutes,
|
|
4665
5510
|
rootDir,
|
|
4666
5511
|
async listen(listenPort) {
|
|
4667
5512
|
const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
|
|
4668
5513
|
const actualPort = listenPort ?? options?.port ?? envPort ?? DEFAULT_PORT;
|
|
4669
|
-
return new Promise((resolve) => {
|
|
5514
|
+
return new Promise((resolve, reject) => {
|
|
5515
|
+
const onListenError = (err) => {
|
|
5516
|
+
if (err.code === "EADDRINUSE") {
|
|
5517
|
+
reject(
|
|
5518
|
+
new Error(
|
|
5519
|
+
`Port ${actualPort} is already in use. Is another faapi instance running? Change the port via the PORT env var.`
|
|
5520
|
+
)
|
|
5521
|
+
);
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
5524
|
+
reject(err);
|
|
5525
|
+
};
|
|
5526
|
+
server.once("error", onListenError);
|
|
4670
5527
|
server.listen(actualPort, async () => {
|
|
5528
|
+
server.off("error", onListenError);
|
|
4671
5529
|
const address = server.address();
|
|
4672
5530
|
const p = typeof address === "object" && address !== null ? address.port : actualPort;
|
|
4673
5531
|
console.log("faapi server started");
|
|
@@ -4695,18 +5553,9 @@ async function createAppBase(options) {
|
|
|
4695
5553
|
console.log(` ${agent.name} [${exports}] ${agent.filePath}`);
|
|
4696
5554
|
}
|
|
4697
5555
|
}
|
|
4698
|
-
|
|
4699
|
-
const graceful = async (signal) => {
|
|
4700
|
-
console.log(`
|
|
4701
|
-
- Received ${signal}, shutting down...`);
|
|
4702
|
-
await app.close();
|
|
4703
|
-
process.exit(0);
|
|
4704
|
-
};
|
|
4705
|
-
process.on("SIGTERM", () => void graceful("SIGTERM"));
|
|
4706
|
-
process.on("SIGINT", () => void graceful("SIGINT"));
|
|
4707
|
-
}
|
|
5556
|
+
registerDefaultShutdownHandlers();
|
|
4708
5557
|
if (config?.lifecycle?.onReady) {
|
|
4709
|
-
await config.lifecycle.onReady({ rootDir, routes: sorted, server });
|
|
5558
|
+
await config.lifecycle.onReady({ rootDir, routes: sorted, server, registries });
|
|
4710
5559
|
console.log("- onReady hook executed");
|
|
4711
5560
|
}
|
|
4712
5561
|
app.server = server;
|
|
@@ -4783,39 +5632,50 @@ async function createAppBase(options) {
|
|
|
4783
5632
|
if (closed) return;
|
|
4784
5633
|
closed = true;
|
|
4785
5634
|
const s = server;
|
|
4786
|
-
|
|
4787
|
-
s.closeIdleConnections();
|
|
4788
|
-
}
|
|
4789
|
-
if (typeof s.closeAllConnections === "function") {
|
|
4790
|
-
s.closeAllConnections();
|
|
4791
|
-
}
|
|
5635
|
+
s.closeIdleConnections?.();
|
|
4792
5636
|
if (config?.lifecycle?.onClose) {
|
|
4793
|
-
await config.lifecycle.onClose({ rootDir, routes: sorted, server });
|
|
5637
|
+
await config.lifecycle.onClose({ rootDir, routes: sorted, server, registries });
|
|
4794
5638
|
}
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5639
|
+
registries.tool.clear();
|
|
5640
|
+
registries.agent.clear();
|
|
5641
|
+
registries.skill.clear();
|
|
5642
|
+
registries.agentHandle.clear();
|
|
4799
5643
|
if (!server.listening) {
|
|
4800
5644
|
app.server = null;
|
|
4801
5645
|
if (getCurrentApp() === app) setCurrentApp(null);
|
|
4802
5646
|
return;
|
|
4803
5647
|
}
|
|
4804
|
-
|
|
5648
|
+
const drained = new Promise((resolve) => {
|
|
4805
5649
|
server.close((err) => {
|
|
4806
5650
|
if (err) console.error("Error closing server:", err);
|
|
4807
|
-
app.server = null;
|
|
4808
|
-
if (getCurrentApp() === app) setCurrentApp(null);
|
|
4809
5651
|
resolve();
|
|
4810
5652
|
});
|
|
4811
5653
|
});
|
|
5654
|
+
const drainTimeoutMs = Number(process.env.FAAPI_SHUTDOWN_TIMEOUT_MS ?? 1e4);
|
|
5655
|
+
if (typeof s.closeAllConnections === "function" && Number.isFinite(drainTimeoutMs) && drainTimeoutMs >= 0) {
|
|
5656
|
+
const forceClose = new Promise((resolve) => {
|
|
5657
|
+
const timer = setTimeout(() => {
|
|
5658
|
+
s.closeAllConnections?.();
|
|
5659
|
+
resolve();
|
|
5660
|
+
}, drainTimeoutMs);
|
|
5661
|
+
timer.unref?.();
|
|
5662
|
+
});
|
|
5663
|
+
await Promise.race([drained, forceClose]);
|
|
5664
|
+
const tail = new Promise((resolve) => setTimeout(resolve, 250).unref?.());
|
|
5665
|
+
await Promise.race([drained, tail]);
|
|
5666
|
+
} else {
|
|
5667
|
+
await drained;
|
|
5668
|
+
}
|
|
5669
|
+
app.server = null;
|
|
5670
|
+
if (getCurrentApp() === app) setCurrentApp(null);
|
|
4812
5671
|
}
|
|
4813
5672
|
};
|
|
4814
5673
|
setCurrentApp(app);
|
|
4815
5674
|
const ctx = {
|
|
4816
5675
|
rootDir,
|
|
5676
|
+
registries,
|
|
4817
5677
|
dist,
|
|
4818
|
-
patterns:
|
|
5678
|
+
patterns: ROUTE_PATTERNS,
|
|
4819
5679
|
server,
|
|
4820
5680
|
routesRef,
|
|
4821
5681
|
config,
|
|
@@ -4833,17 +5693,17 @@ async function createAppBase(options) {
|
|
|
4833
5693
|
|
|
4834
5694
|
// src/router/scanRoutes.ts
|
|
4835
5695
|
import fg2 from "fast-glob";
|
|
4836
|
-
import
|
|
4837
|
-
import
|
|
5696
|
+
import path20 from "path";
|
|
5697
|
+
import fs16 from "fs";
|
|
4838
5698
|
|
|
4839
5699
|
// src/router/constants.ts
|
|
4840
5700
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
4841
5701
|
var HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
4842
5702
|
|
|
4843
5703
|
// src/utils/normalizePath.ts
|
|
4844
|
-
function normalizePath(
|
|
4845
|
-
if (!
|
|
4846
|
-
let result =
|
|
5704
|
+
function normalizePath(path23) {
|
|
5705
|
+
if (!path23) return "";
|
|
5706
|
+
let result = path23.replace(/\\/g, "/");
|
|
4847
5707
|
result = result.replace(/\/+/g, "/");
|
|
4848
5708
|
result = result.replace(/\/+$/, "");
|
|
4849
5709
|
if (result && !result.startsWith("/")) {
|
|
@@ -4904,48 +5764,40 @@ function extractExportsFromSource(source) {
|
|
|
4904
5764
|
return names;
|
|
4905
5765
|
}
|
|
4906
5766
|
function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
|
|
4907
|
-
const routeDir =
|
|
4908
|
-
const resolvedRoot =
|
|
5767
|
+
const routeDir = path20.dirname(routeFilePath);
|
|
5768
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
4909
5769
|
const paths = [];
|
|
4910
|
-
let currentDir =
|
|
5770
|
+
let currentDir = path20.resolve(rootDir, routeDir);
|
|
4911
5771
|
while (true) {
|
|
4912
5772
|
if (dist) {
|
|
4913
|
-
const mwTsPath =
|
|
4914
|
-
const mwJsPath =
|
|
4915
|
-
const absTsPath =
|
|
4916
|
-
const absJsPath =
|
|
4917
|
-
const absMwPath =
|
|
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;
|
|
4918
5778
|
if (absMwPath) {
|
|
4919
|
-
const relMwPath =
|
|
4920
|
-
const prodAbsPath =
|
|
5779
|
+
const relMwPath = path20.relative(rootDir, absMwPath);
|
|
5780
|
+
const prodAbsPath = path20.resolve(rootDir, toProdFilePath(relMwPath, dist));
|
|
4921
5781
|
paths.push(prodAbsPath);
|
|
4922
5782
|
}
|
|
4923
5783
|
} else {
|
|
4924
5784
|
for (const ext of [".ts", ".js"]) {
|
|
4925
|
-
const mwPath =
|
|
4926
|
-
const absMwPath =
|
|
4927
|
-
if (
|
|
5785
|
+
const mwPath = path20.join(currentDir, `middlewares${ext}`);
|
|
5786
|
+
const absMwPath = path20.resolve(rootDir, mwPath);
|
|
5787
|
+
if (fs16.existsSync(absMwPath)) {
|
|
4928
5788
|
paths.push(absMwPath);
|
|
4929
5789
|
break;
|
|
4930
5790
|
}
|
|
4931
5791
|
}
|
|
4932
5792
|
}
|
|
4933
5793
|
if (currentDir === resolvedRoot) break;
|
|
4934
|
-
const parentDir =
|
|
5794
|
+
const parentDir = path20.dirname(currentDir);
|
|
4935
5795
|
if (parentDir === currentDir) break;
|
|
4936
5796
|
currentDir = parentDir;
|
|
4937
5797
|
}
|
|
4938
5798
|
paths.reverse();
|
|
4939
5799
|
return paths;
|
|
4940
5800
|
}
|
|
4941
|
-
function toProdFilePath3(filePath, dist) {
|
|
4942
|
-
let rel = filePath.replace(/\\/g, "/");
|
|
4943
|
-
if (rel.startsWith("src/")) {
|
|
4944
|
-
rel = rel.slice(4);
|
|
4945
|
-
}
|
|
4946
|
-
const jsPath = rel.replace(/\.ts$/, ".js");
|
|
4947
|
-
return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
|
|
4948
|
-
}
|
|
4949
5801
|
async function scanRoutes(rootDir, patterns, dist) {
|
|
4950
5802
|
const files = await fg2(patterns, {
|
|
4951
5803
|
cwd: rootDir,
|
|
@@ -4958,7 +5810,7 @@ async function scanRoutes(rootDir, patterns, dist) {
|
|
|
4958
5810
|
const normalizedFile = file.replace(/\\/g, "/");
|
|
4959
5811
|
const fileName = normalizedFile.split("/").pop();
|
|
4960
5812
|
if (fileName === "handler.ts" || fileName === "handler.js") {
|
|
4961
|
-
const absPath =
|
|
5813
|
+
const absPath = path20.resolve(rootDir, normalizedFile);
|
|
4962
5814
|
const urlPath = filePathToUrlPath(normalizedFile);
|
|
4963
5815
|
const paramNames = extractParamNames(urlPath);
|
|
4964
5816
|
const isDynamic = paramNames.length > 0;
|
|
@@ -4971,7 +5823,7 @@ async function scanRoutes(rootDir, patterns, dist) {
|
|
|
4971
5823
|
const mwPaths = collectMiddlewarePaths(normalizedFile, rootDir);
|
|
4972
5824
|
middlewareBundle = await loadMergedMiddlewares(mwPaths);
|
|
4973
5825
|
}
|
|
4974
|
-
const source = await
|
|
5826
|
+
const source = await fs16.promises.readFile(absPath, "utf8").catch(() => "");
|
|
4975
5827
|
const exportNames = extractExportsFromSource(source);
|
|
4976
5828
|
const methods = HTTP_METHODS.filter((m) => exportNames.has(m));
|
|
4977
5829
|
for (const method of methods) {
|
|
@@ -5007,8 +5859,8 @@ async function scanRoutes(rootDir, patterns, dist) {
|
|
|
5007
5859
|
|
|
5008
5860
|
// src/tools/scanTools.ts
|
|
5009
5861
|
import fg3 from "fast-glob";
|
|
5010
|
-
import
|
|
5011
|
-
import
|
|
5862
|
+
import path21 from "path";
|
|
5863
|
+
import fs17 from "fs";
|
|
5012
5864
|
var TOOL_PATTERNS = ["src/tools/**/*.ts"];
|
|
5013
5865
|
var TOOL_EXPORT_RE = new RegExp(
|
|
5014
5866
|
String.raw`export\s+(?:async\s+)?(?:function\s+|const\s+)([A-Za-z_$][\w$]*)\s*(?:\(|=)`,
|
|
@@ -5058,8 +5910,8 @@ async function scanTools(rootDir, patterns) {
|
|
|
5058
5910
|
if (fileName !== "handler.ts" && fileName !== "handler.js") {
|
|
5059
5911
|
continue;
|
|
5060
5912
|
}
|
|
5061
|
-
const absPath =
|
|
5062
|
-
const source = await
|
|
5913
|
+
const absPath = path21.resolve(rootDir, normalizedFile);
|
|
5914
|
+
const source = await fs17.promises.readFile(absPath, "utf8").catch(() => "");
|
|
5063
5915
|
const exportNames = extractToolExportsFromSource(source);
|
|
5064
5916
|
const namespace = filePathToToolNamespace(normalizedFile);
|
|
5065
5917
|
for (const fnName of exportNames) {
|
|
@@ -5083,8 +5935,8 @@ async function scanTools(rootDir, patterns) {
|
|
|
5083
5935
|
|
|
5084
5936
|
// src/agents/scanAgents.ts
|
|
5085
5937
|
import fg4 from "fast-glob";
|
|
5086
|
-
import
|
|
5087
|
-
import
|
|
5938
|
+
import path22 from "path";
|
|
5939
|
+
import fs18 from "fs";
|
|
5088
5940
|
var DEFAULT_AGENT_PATTERNS = ["src/agents/*/handler.ts"];
|
|
5089
5941
|
var RUN_EXPORT_RE = /export\s+(?:async\s+)?(?:function\s+|const\s+)run\b/;
|
|
5090
5942
|
function extractAgentNameFromPath(filePath) {
|
|
@@ -5116,8 +5968,8 @@ async function scanAgents(rootDir, patterns) {
|
|
|
5116
5968
|
if (fileName !== "handler.ts" && fileName !== "handler.js") {
|
|
5117
5969
|
continue;
|
|
5118
5970
|
}
|
|
5119
|
-
const absPath =
|
|
5120
|
-
const source = await
|
|
5971
|
+
const absPath = path22.resolve(rootDir, normalizedFile);
|
|
5972
|
+
const source = await fs18.promises.readFile(absPath, "utf8").catch(() => "");
|
|
5121
5973
|
const { hasRun } = detectAgentExports(source);
|
|
5122
5974
|
const name = extractAgentNameFromPath(normalizedFile);
|
|
5123
5975
|
const prevFile = seen.get(name);
|
|
@@ -5152,6 +6004,14 @@ async function createDevApp(options) {
|
|
|
5152
6004
|
if (isDevOnDemandEnabled()) {
|
|
5153
6005
|
await deleteSchemaFiles(sorted, ctx.rootDir, ctx.dist);
|
|
5154
6006
|
clearGeneratedSchemas();
|
|
6007
|
+
void (async () => {
|
|
6008
|
+
try {
|
|
6009
|
+
const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
|
|
6010
|
+
await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
|
|
6011
|
+
} catch (err) {
|
|
6012
|
+
console.error("[faapi] Background schema regeneration failed:", err);
|
|
6013
|
+
}
|
|
6014
|
+
})();
|
|
5155
6015
|
} else {
|
|
5156
6016
|
const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
|
|
5157
6017
|
await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
|
|
@@ -5165,14 +6025,14 @@ async function createDevApp(options) {
|
|
|
5165
6025
|
await generateToolArtifacts(tools, ctx.rootDir, ctx.dist, {
|
|
5166
6026
|
skipSchema: isDevOnDemandEnabled()
|
|
5167
6027
|
});
|
|
5168
|
-
await loadAndHydrateTools(ctx.rootDir, ctx.dist);
|
|
6028
|
+
await loadAndHydrateTools(ctx.rootDir, ctx.dist, ctx.registries);
|
|
5169
6029
|
};
|
|
5170
6030
|
devApp.reloadAgents = async () => {
|
|
5171
6031
|
setLoadTimestamp(Date.now());
|
|
5172
6032
|
invalidateProgramCache();
|
|
5173
6033
|
const agents = await scanAgents(ctx.rootDir, DEFAULT_AGENT_PATTERNS);
|
|
5174
6034
|
await generateAgentArtifacts(agents, ctx.rootDir, ctx.dist);
|
|
5175
|
-
await loadAndHydrateAgents(ctx.rootDir, ctx.dist);
|
|
6035
|
+
await loadAndHydrateAgents(ctx.rootDir, ctx.dist, ctx.registries);
|
|
5176
6036
|
};
|
|
5177
6037
|
return devApp;
|
|
5178
6038
|
}
|
|
@@ -5194,9 +6054,11 @@ export {
|
|
|
5194
6054
|
collectRouteSchemaSources,
|
|
5195
6055
|
cors,
|
|
5196
6056
|
createProdApp as createApp,
|
|
6057
|
+
createAppRegistries,
|
|
5197
6058
|
createDevApp,
|
|
5198
6059
|
createProdApp,
|
|
5199
6060
|
createProgram,
|
|
6061
|
+
createPrograms,
|
|
5200
6062
|
extractTypeInfo,
|
|
5201
6063
|
getAgent,
|
|
5202
6064
|
getAgentEntry,
|
|
@@ -5204,6 +6066,7 @@ export {
|
|
|
5204
6066
|
getInputTypeForMethod,
|
|
5205
6067
|
getSkill,
|
|
5206
6068
|
getTool,
|
|
6069
|
+
getToolSchemaPath,
|
|
5207
6070
|
helmet,
|
|
5208
6071
|
hydrateSkillRegistry,
|
|
5209
6072
|
invalidateProgramCache,
|