@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/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
- let rootNames = [filePath];
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
- if (!tsOptions.fileNames.includes(filePath)) {
90
- rootNames = [filePath, ...tsOptions.fileNames];
91
- } else {
92
- rootNames = tsOptions.fileNames;
128
+ for (const fileName of tsOptions.fileNames) {
129
+ if (!rootNames.includes(fileName)) {
130
+ rootNames.push(fileName);
131
+ }
93
132
  }
94
133
  }
95
134
  }
96
- const program = ts.createProgram(rootNames, options);
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 properties = [];
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 === "object") {
213
- properties.push(...resolved.properties);
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 new SchemaExtractionError(typeNode.getText(), "\u4E0D\u652F\u6301\u7684\u7C7B\u578B\u8BED\u6CD5");
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
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
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(typeNode.typeArguments[0], checker, visited)
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(typeNode.typeArguments[0], checker, visited),
335
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
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(typeNode.typeArguments[0], checker, visited),
378
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
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(typeNode.typeArguments[0], checker, visited)
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.kind === ts2.SyntaxKind.Identifier ? checker.getSymbolAtLocation(typeNode.typeName) : void 0;
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(declaration, checker, visited);
507
+ return resolveInterfaceDeclaration(
508
+ declaration,
509
+ checker,
510
+ visited,
511
+ bindings,
512
+ typeNode.typeArguments
513
+ );
419
514
  }
420
515
  if (ts2.isTypeAliasDeclaration(declaration)) {
421
- return resolveTypeNode(declaration.type, checker, visited);
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(typeNode, symbol, checker, visited);
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 resolveImportAlias(typeNode, symbol, checker, visited) {
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
- return resolveTypeNode(decl.type, checker, visited);
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
- return resolveTypeNode(found.node.type, checker, visited);
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
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
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(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B "${typeText}": ${reason}`, options);
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
- } finally {
749
- setProgramContext(null);
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
- return result;
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
- return params.map((param) => {
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 analyzeInjection(code, functionName) {
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 = /* @__PURE__ */ new Map();
1008
- const allTypesByFile = /* @__PURE__ */ new Map();
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
- const program = createProgram(filePath);
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
- const code = sourceFile?.text ?? "";
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 = analyzeInjection(code, method);
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 ? extractTypeInfo(program, filePath, param.typeName) : null;
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, allTypesByFile, mergedAllTypes };
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
- return;
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.properties, ctx);
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
- return `z.tuple([${fixedExprs.join(", ")}]).rest(${restExpression})`;
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(properties, ctx) {
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
- return `z.object({ ${fields.join(", ")} })`;
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 lines = [];
1311
- lines.push("import { z } from 'zod';");
1312
- lines.push("");
1313
- for (const [n, type] of ctx.namedTypes) {
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
- if (hasSelfRef) {
1320
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
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" || v === "1" ? true : v === "false" || v === "0" ? false : v;';
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 path6 from "path";
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 path6.resolve(rootDir, dist, relDir, "zod.js");
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 path6.resolve(rootDir, dist, relDir, "zod.js");
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, allTypes, helpersImportPath) {
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 block = [`// ${schemaName}`];
1405
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1406
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
1407
- ""
1620
+ const { namedTypeDeclarations: decls, entryDeclaration } = generateZodSchemaSourceParts(
1621
+ typeInfo,
1622
+ resolveType,
1623
+ schemaName,
1624
+ coerce
1408
1625
  );
1409
- block.push(schemaCode);
1410
- block.push("");
1411
- schemaBlocks.push(block.join("\n"));
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
- const allSchemaCode = schemaBlocks.join("\n");
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, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
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 = path6.relative(rootDir, filePath).replace(/\\/g, "/");
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 source = generateSchemaFileSource(fileSources, allTypes, helpersImportPath);
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 = path6.resolve(rootDir, dist, HELPERS_FILENAME);
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 fs5.mkdir(path6.dirname(outputPath), { recursive: true });
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/toolRegistry.ts
1479
- var registry = /* @__PURE__ */ new Map();
1480
- function hydrateToolRegistry(tools) {
1481
- const next = /* @__PURE__ */ new Map();
1482
- for (const tool of tools) {
1483
- next.set(tool.name, tool);
1484
- }
1485
- registry = next;
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 clearToolRegistry() {
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 getTool(name) {
1491
- return registry.get(name);
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
- // src/injection/agentRegistry.ts
1495
- var registry2 = /* @__PURE__ */ new Map();
1496
- function hydrateAgentRegistry(agents) {
1497
- const next = /* @__PURE__ */ new Map();
1498
- for (const agent of agents) {
1499
- next.set(agent.name, agent);
1500
- }
1501
- registry2 = next;
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 clearAgentRegistry() {
1504
- registry2 = /* @__PURE__ */ new Map();
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 registry2.get(name);
1839
+ return defaultRegistries.agent.getAgent(name);
1508
1840
  }
1509
1841
  function getAgentEntry(name) {
1510
- return registry2.get(name);
1842
+ return defaultRegistries.agent.getAgentEntry(name);
1511
1843
  }
1512
1844
  function listAgents() {
1513
- const merged = /* @__PURE__ */ new Map();
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
- const agent = getAgent(name);
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
- const agent = getAgent(name);
1531
- if (!agent || !agent.agents) return [];
1532
- const result = [];
1533
- for (const agentName of agent.agents) {
1534
- const subAgent = getAgent(agentName);
1535
- if (subAgent) result.push(subAgent);
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
- const next = /* @__PURE__ */ new Map();
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
- registry3.set(core.name, core);
1864
+ defaultRegistries.skill.upsert(core);
1554
1865
  }
1555
1866
  function removeSkill(name) {
1556
- registry3.delete(name);
1867
+ defaultRegistries.skill.remove(name);
1557
1868
  }
1558
1869
  function getSkill(name) {
1559
- return registry3.get(name);
1870
+ return defaultRegistries.skill.get(name);
1560
1871
  }
1561
1872
  function listSkills() {
1562
- return Array.from(registry3.values());
1873
+ return defaultRegistries.skill.list();
1563
1874
  }
1564
1875
 
1565
1876
  // src/loader/loadAgentModule.ts
1566
- import fs7 from "fs";
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(ts9) {
1587
- loadTs = ts9;
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 path7 from "path";
1613
- import fs6 from "fs";
1923
+ import path10 from "path";
1924
+ import fs8 from "fs";
1614
1925
 
1615
- // src/cli/compileDevRoutes.ts
1616
- import path5 from "path";
1617
- import fs4 from "fs";
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 path4 from "path";
1622
- import fs3 from "fs";
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/readTsconfig.ts
1648
- import ts6 from "typescript";
1958
+ // src/utils/prodPaths.ts
1649
1959
  import path3 from "path";
1650
1960
  import fs2 from "fs";
1651
- function readTsconfig(rootDir) {
1652
- const tsconfigPath = path3.resolve(rootDir, "tsconfig.json");
1653
- if (!fs2.existsSync(tsconfigPath)) return null;
1654
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
1655
- if (configFile.error || !configFile.config) return null;
1656
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
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
- return { baseUrl, paths };
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 fs3.realpathSync(p);
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 = path4.relative(dir, filePath);
1690
- return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
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(path4.resolve(rootDir, APP_DIR));
2032
+ const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
1695
2033
  const sourceReal = toRealPath(sourceFile);
1696
- let rel = path4.relative(appDirAbs, sourceReal);
1697
- rel = rel.split(path4.sep).join("/");
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 = path4.dirname(importer);
1713
- const base = path4.resolve(importerDir, specifier);
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 fs3.existsSync(base) ? base : null;
2053
+ return fs4.existsSync(base) ? base : null;
1716
2054
  }
1717
2055
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
1718
- return fs3.existsSync(base) ? base : null;
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 (fs3.existsSync(file)) return file;
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 (fs3.existsSync(file)) return file;
2064
+ if (fs4.existsSync(file)) return file;
1727
2065
  }
1728
2066
  return null;
1729
2067
  }
1730
2068
  function createAliasPlugin(config, options) {
1731
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
1732
- const appDirAbs = options?.rootDir ? toRealPath(path4.resolve(options.rootDir, APP_DIR)) : null;
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 = fs3.readFileSync(args.path, "utf8");
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(SPEC_RE, (full, prefix, quote, specifier) => {
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 (fs3.existsSync(file)) {
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 (fs3.existsSync(file)) {
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/compileDevRoutes.ts
1811
- var APP_DIR2 = "src";
1812
- async function compileDevRoutes(options) {
1813
- const { rootDir, dist, files, logLevel = "silent" } = options;
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 = path5.resolve(rootDir, dist);
1824
- await fs4.promises.mkdir(absDist, { recursive: true });
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 = path5.resolve(rootDir, APP_DIR2);
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
- write: false
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 fs4.promises.mkdir(path5.dirname(file.path), { recursive: true });
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 fs4.promises.writeFile(tmp, file.contents);
1847
- await fs4.promises.rename(tmp, file.path);
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 = fs6.statSync(sourceAbsPath);
1860
- const prodStat = fs6.statSync(productAbsPath);
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 inFlight = state.inFlightCompilations.get(sourceAbsPath);
1883
- if (inFlight) {
1884
- await inFlight.catch(() => {
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 (!fs6.existsSync(sourceAbsPath)) {
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: [sourceAbsPath],
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 = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
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 path7.resolve(rootDir, dist, jsRel);
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 inFlight = state.inFlightSchemaGenerations.get(schemaPath);
1929
- if (inFlight) {
1930
- await inFlight.catch(() => {
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 = path7.resolve(rootDir, routeFilePath);
2376
+ const prodAbsPath = path10.resolve(rootDir, routeFilePath);
1938
2377
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
1939
- if (!fs6.existsSync(sourceAbsPath)) {
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 = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
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 fs6.promises.unlink(schemaPath);
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 rel = path7.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
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 = path7.resolve(rootDir, tsRel);
1985
- if (fs6.existsSync(tsAbs)) return tsAbs;
1986
- return path7.resolve(rootDir, srcRel);
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 && fs7.existsSync(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 fs8 from "fs";
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 && fs8.existsSync(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 path8 from "path";
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 = extractToolTagValue(jsDoc);
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
- ts7.forEachChild(sourceFile, (node) => {
2580
+ ts8.forEachChild(sourceFile, (node) => {
2099
2581
  if (result) return;
2100
- if (ts7.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
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 (ts7.isVariableStatement(node) && hasExportModifier(node)) {
2586
+ if (ts8.isVariableStatement(node) && hasExportModifier(node)) {
2105
2587
  for (const decl of node.declarationList.declarations) {
2106
2588
  if (result) break;
2107
- const nameText = ts7.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
2589
+ const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
2108
2590
  if (nameText !== functionName || !decl.initializer) continue;
2109
- if (ts7.isArrowFunction(decl.initializer) || ts7.isFunctionExpression(decl.initializer)) {
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 (!ts7.isTypeReferenceNode(firstParam.type)) return void 0;
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 fs9.writeFile(outputPath, content, "utf-8");
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 = path8.resolve(rootDir, tool.filePath);
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 allTypesByFile = /* @__PURE__ */ new Map();
2652
+ const programByFile = createPrograms([...toolsByFile.keys()]);
2653
+ const resolversByFile = /* @__PURE__ */ new Map();
2228
2654
  for (const filePath of toolsByFile.keys()) {
2229
- const program = createProgram(filePath);
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 = createProgram(filePath);
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, allTypesByFile };
2672
+ return { sources, resolversByFile };
2249
2673
  }
2250
- function generateToolSchemaFileSource(sources, allTypes, helpersImportPath) {
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 = path8.resolve(distDir, HELPERS_FILENAME);
2704
+ const helpersPath = path11.resolve(distDir, HELPERS_FILENAME);
2282
2705
  if (existsSync(helpersPath)) return;
2283
- await fs9.mkdir(path8.dirname(helpersPath), { recursive: true });
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 = path8.resolve(rootDir, manifest.filePath);
2290
- const program = createProgram(absPath);
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 = path8.resolve(rootDir, dist, TOOLS_FILE);
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, allTypesByFile } = collectToolSchemaSources(metadata, rootDir);
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 = path8.relative(rootDir, filePath).replace(/\\/g, "/");
2324
- const outputPath = getToolSchemaOutputPath(relFile, dist, rootDir);
2325
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
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(fileSources, allTypes, helpersImportPath);
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 = path8.resolve(rootDir, dist);
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 fs9.mkdir(path8.dirname(outputPath), { recursive: true });
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 dist = getDist();
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
- currentFactory = factory;
2803
+ defaultRegistries.agentHandle.register(factory);
2376
2804
  }
2377
2805
  function getAgentHandle(ctx) {
2378
- if (currentFactory === null) return void 0;
2379
- return currentFactory(ctx);
2806
+ return defaultRegistries.agentHandle.get(ctx);
2380
2807
  }
2381
2808
  function clearAgentHandleFactory() {
2382
- currentFactory = null;
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 path9 from "path";
2547
- import fs10 from "fs";
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 = path9.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
2551
- if (fs10.existsSync(configProductPath)) {
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 = fs10.existsSync(path9.join(rootDir, "faapi.config.ts")) || fs10.existsSync(path9.join(rootDir, "faapi.config.js"));
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 fs11 from "fs";
2566
- import path10 from "path";
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 [, key, rawValue] = match;
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 = path10.join(rootDir, file);
2633
- if (!fs11.existsSync(filePath)) continue;
2634
- const content = fs11.readFileSync(filePath, "utf-8");
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(path19) {
2672
- super("ROUTE_NOT_FOUND", `Route not found: ${path19}`, 404);
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, path19, allowedMethods) {
2678
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path19}`, 405);
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 fs16 from "fs";
2705
- import path15 from "path";
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 path12 from "path";
3186
+ import path15 from "path";
2759
3187
 
2760
3188
  // src/router/matchRoute.ts
2761
- function matchRoute(routes, method, path19) {
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
- if (!route.isDynamic) {
2767
- if (route.urlPath === path19) {
2768
- return { route, params: {} };
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
- const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
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 matchWsRoute(wsRoutes, path19) {
2780
- for (const route of wsRoutes) {
2781
- if (!route.isDynamic) {
2782
- if (route.urlPath === path19) {
2783
- return { route, params: {} };
2784
- }
2785
- continue;
2786
- }
2787
- const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
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
- return { route, params };
3286
+ methods.add(entry.route.method);
2790
3287
  }
2791
3288
  }
2792
- return null;
3289
+ if (methods.has("GET")) {
3290
+ methods.add("HEAD");
3291
+ }
3292
+ return Array.from(methods);
2793
3293
  }
2794
- function matchDynamicPath(pattern, path19, paramNames, isCatchAll) {
2795
- const patternSegments = pattern.split("/").filter(Boolean);
2796
- const pathSegments = path19.split("/").filter(Boolean);
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 && fs12.existsSync(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 errorObj = bodyObj.error ?? bodyObj;
3040
- if (errorObj) {
3041
- errorObj.issues = error.issues;
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
- const url = new URL(request.url);
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] = value;
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 resolveInput(method, request) {
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.trim() === "") return null;
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.trim() === "") {
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
- res.on("error", reject);
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
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
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(inputObj);
4176
+ const result = zodSchema.safeParse(input);
3604
4177
  if (result.success) {
3605
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
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: inputObj };
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.code, issue.message);
3614
- const path19 = issue.path.map(String).join(".") || "";
4185
+ const code = mapZodCode(issue);
4186
+ const path23 = issue.path.map(String).join(".") || "";
3615
4187
  return {
3616
- path: path19,
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(zodCode, message) {
3625
- switch (zodCode) {
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 "invalid_string":
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
- const xff = req.headers["x-forwarded-for"];
3663
- if (typeof xff === "string" && xff.length > 0) {
3664
- const first = xff.split(",")[0]?.trim();
3665
- if (first) return first;
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 path11 from "path";
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
- bundle = await loadMiddlewaresFile(absMwPath);
3766
- setCachedMiddlewares(absMwPath, bundle);
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 = Buffer.isBuffer(data) ? data : Buffer.from(data);
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 { server, routesRef, rootDir, config, globalMiddlewares } = options;
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 = path11.resolve(rootDir, route.filePath);
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 new Request(url.toString(), {
3955
- method,
3956
- headers,
3957
- body: limitedStream,
3958
- duplex: "half"
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
- configMiddlewares,
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
- if (routesRef.wsCurrent.length > 0) {
4084
- attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares });
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 = new URL(request.url).pathname;
4092
- const ctx = createContext(request, {}, config, getClientIp(req));
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 absoluteFilePath = path12.resolve(rootDir, route.filePath);
4112
- const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
4113
- const input = await resolveInput(route.method, request);
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.config), meta), res);
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, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
4154
- const { request, ctx, meta, method, urlPath } = prepareRequest(req, config, bodyLimit);
4155
- const routePipeline = createRoutePipeline({
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 path13 from "path";
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 path14 from "path";
4235
- import fs15 from "fs/promises";
5057
+ import path17 from "path";
4236
5058
 
4237
5059
  // src/ast/extractAgentMetadata.ts
4238
- import ts8 from "typescript";
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 ? getJSDocFromNode2(jsDocOwner) : void 0;
4255
- const description = extractDescription2(jsDoc);
4256
- const agentNameOverride = extractAgentTagValue(jsDoc);
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
- ts8.forEachChild(sourceFile, (node) => {
5106
+ ts9.forEachChild(sourceFile, (node) => {
4285
5107
  if (result) return;
4286
- if (ts8.isVariableStatement(node) && hasExportModifier2(node)) {
5108
+ if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
4287
5109
  for (const decl of node.declarationList.declarations) {
4288
5110
  if (result) break;
4289
- const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : "";
5111
+ const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
4290
5112
  if (nameText !== "config" || !decl.initializer) continue;
4291
- if (ts8.isObjectLiteralExpression(decl.initializer)) {
5113
+ if (ts9.isObjectLiteralExpression(decl.initializer)) {
4292
5114
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
4293
- } else if (ts8.isArrowFunction(decl.initializer)) {
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 (ts8.isFunctionDeclaration(node) && hasExportModifier2(node) && node.name?.text === "config") {
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
- ts8.forEachChild(sourceFile, (node) => {
5130
+ ts9.forEachChild(sourceFile, (node) => {
4309
5131
  if (result) return;
4310
- if (ts8.isFunctionDeclaration(node) && hasExportModifier2(node) && node.name?.text === "run") {
5132
+ if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "run") {
4311
5133
  result = node;
4312
5134
  return;
4313
5135
  }
4314
- if (ts8.isVariableStatement(node) && hasExportModifier2(node)) {
5136
+ if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
4315
5137
  for (const decl of node.declarationList.declarations) {
4316
5138
  if (result) break;
4317
- const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : "";
5139
+ const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
4318
5140
  if (nameText !== "run" || !decl.initializer) continue;
4319
- if (ts8.isArrowFunction(decl.initializer) || ts8.isFunctionExpression(decl.initializer)) {
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 (ts8.isObjectLiteralExpression(body)) {
5152
+ if (ts9.isObjectLiteralExpression(body)) {
4331
5153
  return body;
4332
5154
  }
4333
- if (ts8.isBlock(body)) {
5155
+ if (ts9.isBlock(body)) {
4334
5156
  for (const stmt of body.statements) {
4335
- if (ts8.isReturnStatement(stmt) && stmt.expression && ts8.isObjectLiteralExpression(stmt.expression)) {
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 (!ts8.isPropertyAssignment(prop)) continue;
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 (ts8.isIdentifier(name)) return name.text;
4400
- if (ts8.isStringLiteral(name)) return name.text;
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 (ts8.isStringLiteral(expr)) return expr.text;
5196
+ if (ts9.isStringLiteral(expr)) return expr.text;
4405
5197
  return void 0;
4406
5198
  }
4407
5199
  function extractNumberValue(expr) {
4408
- if (ts8.isNumericLiteral(expr)) {
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 (!ts8.isArrayLiteralExpression(expr)) return void 0;
5207
+ if (!ts9.isArrayLiteralExpression(expr)) return void 0;
4416
5208
  const values = [];
4417
5209
  for (const element of expr.elements) {
4418
- if (!ts8.isStringLiteral(element)) return void 0;
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: toProdFilePath2(a.filePath, dist)
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 fs15.writeFile(outputPath, content, "utf-8");
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 = path14.resolve(rootDir, manifest.filePath);
4473
- const program = createProgram(absPath);
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 = path14.resolve(rootDir, dist, AGENTS_FILE);
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
- async function loadPlugins(declarations, ctx) {
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
- const { specifier, options, enable } = resolveDeclaration(decl);
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
- console.warn(`! Plugin ${specifier} has no setup function, skipping`);
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
- console.warn(
4525
- `! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`
4526
- );
5322
+ failures.push({
5323
+ specifier,
5324
+ reason: err instanceof Error ? err.message : String(err)
5325
+ });
4527
5326
  }
4528
5327
  }
4529
- return { handlerWrappers, upgradeWrappers };
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
- var PATTERNS = ["src/api/**/*.ts"];
4555
- async function loadAndHydrateTools(rootDir, dist) {
4556
- const toolsPath = path15.resolve(rootDir, dist, TOOLS_FILE2);
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
- hydrateToolRegistry(hydrated);
5376
+ registries.tool.hydrate(hydrated);
4563
5377
  return hydrated;
4564
5378
  }
4565
- async function loadAndHydrateAgents(rootDir, dist) {
4566
- const agentsPath = path15.resolve(rootDir, dist, AGENTS_FILE2);
4567
- if (!fs16.existsSync(agentsPath)) {
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
- hydrateAgentRegistry(hydrated);
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 = path15.resolve(rootDir, dist, ROUTES_FILE);
4615
- if (!fs16.existsSync(routesPath)) {
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 tools = await loadAndHydrateTools(rootDir, dist);
4635
- const agents = await loadAndHydrateAgents(rootDir, dist);
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
- const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
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
- if (config?.lifecycle?.onClose) {
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
- if (typeof s.closeIdleConnections === "function") {
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
- clearToolRegistry();
4796
- clearAgentRegistry();
4797
- clearSkillRegistry();
4798
- clearAgentHandleFactory();
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
- return new Promise((resolve) => {
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: 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 path16 from "path";
4837
- import fs17 from "fs";
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(path19) {
4845
- if (!path19) return "";
4846
- let result = path19.replace(/\\/g, "/");
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 = path16.dirname(routeFilePath);
4908
- const resolvedRoot = path16.resolve(rootDir);
5767
+ const routeDir = path20.dirname(routeFilePath);
5768
+ const resolvedRoot = path20.resolve(rootDir);
4909
5769
  const paths = [];
4910
- let currentDir = path16.resolve(rootDir, routeDir);
5770
+ let currentDir = path20.resolve(rootDir, routeDir);
4911
5771
  while (true) {
4912
5772
  if (dist) {
4913
- const mwTsPath = path16.join(currentDir, "middlewares.ts");
4914
- const mwJsPath = path16.join(currentDir, "middlewares.js");
4915
- const absTsPath = path16.resolve(rootDir, mwTsPath);
4916
- const absJsPath = path16.resolve(rootDir, mwJsPath);
4917
- const absMwPath = fs17.existsSync(absTsPath) ? absTsPath : fs17.existsSync(absJsPath) ? absJsPath : null;
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 = path16.relative(rootDir, absMwPath);
4920
- const prodAbsPath = path16.resolve(rootDir, toProdFilePath3(relMwPath, dist));
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 = path16.join(currentDir, `middlewares${ext}`);
4926
- const absMwPath = path16.resolve(rootDir, mwPath);
4927
- if (fs17.existsSync(absMwPath)) {
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 = path16.dirname(currentDir);
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 = path16.resolve(rootDir, normalizedFile);
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 fs17.promises.readFile(absPath, "utf8").catch(() => "");
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 path17 from "path";
5011
- import fs18 from "fs";
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 = path17.resolve(rootDir, normalizedFile);
5062
- const source = await fs18.promises.readFile(absPath, "utf8").catch(() => "");
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 path18 from "path";
5087
- import fs19 from "fs";
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 = path18.resolve(rootDir, normalizedFile);
5120
- const source = await fs19.promises.readFile(absPath, "utf8").catch(() => "");
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,