@faapi/faapi 3.3.0 → 4.1.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
@@ -90,9 +90,10 @@ function createPrograms(filePaths) {
90
90
  }
91
91
  }
92
92
  for (const { tsconfigPath, files } of groups.values()) {
93
- const cacheKey = `shared::${tsconfigPath}::${[...files].sort().join("|")}`;
93
+ const cacheKey = `shared::${tsconfigPath}`;
94
94
  let program = programCache.get(cacheKey);
95
- if (!program) {
95
+ const coversAll = program !== void 0 && files.every((f) => program.getSourceFile(f) !== void 0);
96
+ if (!program || !coversAll) {
96
97
  program = buildProgram(files, tsconfigPath);
97
98
  programCache.set(cacheKey, program);
98
99
  }
@@ -147,7 +148,7 @@ import ts2 from "typescript";
147
148
  function setProgramContext(program) {
148
149
  currentProgram = program;
149
150
  }
150
- function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
151
+ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
151
152
  const kind = typeNode.kind;
152
153
  switch (kind) {
153
154
  case ts2.SyntaxKind.StringKeyword:
@@ -203,13 +204,13 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
203
204
  if (ts2.isArrayTypeNode(typeNode)) {
204
205
  return {
205
206
  kind: "array",
206
- element: resolveTypeNode(typeNode.elementType, checker, visited)
207
+ element: resolveTypeNode(typeNode.elementType, checker, visited, bindings)
207
208
  };
208
209
  }
209
210
  if (ts2.isTupleTypeNode(typeNode)) {
210
211
  const elements = typeNode.elements.map((e) => {
211
212
  if (ts2.isRestTypeNode(e)) {
212
- const inner = resolveTypeNode(e.type, checker, visited);
213
+ const inner = resolveTypeNode(e.type, checker, visited, bindings);
213
214
  if (inner.kind === "array") {
214
215
  return { type: inner.element, optional: false, rest: true };
215
216
  }
@@ -217,20 +218,20 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
217
218
  }
218
219
  if (ts2.isNamedTupleMember(e)) {
219
220
  return {
220
- type: resolveTypeNode(e.type, checker, visited),
221
+ type: resolveTypeNode(e.type, checker, visited, bindings),
221
222
  optional: !!e.questionToken,
222
223
  rest: false
223
224
  };
224
225
  }
225
226
  if (ts2.isOptionalTypeNode(e)) {
226
227
  return {
227
- type: resolveTypeNode(e.type, checker, visited),
228
+ type: resolveTypeNode(e.type, checker, visited, bindings),
228
229
  optional: true,
229
230
  rest: false
230
231
  };
231
232
  }
232
233
  return {
233
- type: resolveTypeNode(e, checker, visited),
234
+ type: resolveTypeNode(e, checker, visited, bindings),
234
235
  optional: false,
235
236
  rest: false
236
237
  };
@@ -238,40 +239,80 @@ function resolveTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set())
238
239
  return { kind: "tuple", elements };
239
240
  }
240
241
  if (ts2.isUnionTypeNode(typeNode)) {
241
- const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited));
242
+ const members = typeNode.types.map((t) => resolveTypeNode(t, checker, visited, bindings));
242
243
  return { kind: "union", members };
243
244
  }
244
245
  if (ts2.isIntersectionTypeNode(typeNode)) {
245
- const properties = [];
246
+ const propMap = /* @__PURE__ */ new Map();
246
247
  for (const t of typeNode.types) {
247
- const resolved = resolveTypeNode(t, checker, visited);
248
- if (resolved.kind === "object") {
249
- 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
+ }
250
271
  }
251
272
  }
252
- return { kind: "object", properties };
273
+ return { kind: "object", properties: [...propMap.values()] };
253
274
  }
254
275
  if (ts2.isTypeLiteralNode(typeNode)) {
255
- return resolveTypeLiteral(typeNode, checker, visited);
276
+ return resolveTypeLiteral(typeNode, checker, visited, bindings);
256
277
  }
257
278
  if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.KeyOfKeyword) {
258
279
  return resolveKeyOf(typeNode, checker);
259
280
  }
260
281
  if (ts2.isTypeOperatorNode(typeNode) && typeNode.operator === ts2.SyntaxKind.ReadonlyKeyword) {
261
- return resolveTypeNode(typeNode.type, checker, visited);
282
+ return resolveTypeNode(typeNode.type, checker, visited, bindings);
262
283
  }
263
284
  if (ts2.isTypeReferenceNode(typeNode)) {
264
- 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
+ );
265
298
  }
266
- 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");
267
300
  }
268
- function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
301
+ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
269
302
  const properties = [];
303
+ let catchall;
270
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
+ }
271
312
  if (ts2.isPropertySignature(member) && member.name) {
272
313
  const name = member.name.getText();
273
314
  const optional = !!member.questionToken;
274
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
315
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
275
316
  const constraints = extractConstraintsFromJsDoc(member, name);
276
317
  validateConstraints(constraints, type, name);
277
318
  properties.push(
@@ -279,12 +320,10 @@ function resolveTypeLiteral(typeNode, checker, visited = /* @__PURE__ */ new Set
279
320
  );
280
321
  }
281
322
  if (ts2.isIndexSignatureDeclaration(member)) {
282
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
283
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
284
- return { kind: "record", key: keyType, value: valueType };
323
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
285
324
  }
286
325
  }
287
- return { kind: "object", properties };
326
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
288
327
  }
289
328
  function extractLiteralKeys(type) {
290
329
  if (type.kind === "literal" && typeof type.value === "string") {
@@ -353,36 +392,48 @@ function resolveKeyOf(typeNode, checker) {
353
392
  }
354
393
  throw new SchemaExtractionError(typeNode.getText(), "keyof T \u7684\u7ED3\u679C\u65E0\u6CD5\u89E3\u6790\u4E3A\u5B57\u9762\u91CF\u8054\u5408");
355
394
  }
356
- function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
395
+ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
357
396
  const typeName = typeNode.typeName.getText();
397
+ const bound = bindings.get(typeName);
398
+ if (bound) {
399
+ return bound;
400
+ }
358
401
  if (typeName === "Date") {
359
402
  return { kind: "date" };
360
403
  }
361
404
  if ((typeName === "Array" || typeName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
405
+ const [arg] = typeNode.typeArguments;
362
406
  return {
363
407
  kind: "array",
364
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
408
+ element: resolveTypeNode(arg, checker, visited, bindings)
365
409
  };
366
410
  }
367
411
  if (typeName === "Record" && typeNode.typeArguments?.length === 2) {
412
+ const [keyArg, valueArg] = typeNode.typeArguments;
368
413
  return {
369
414
  kind: "record",
370
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
371
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
415
+ key: resolveTypeNode(keyArg, checker, visited, bindings),
416
+ value: resolveTypeNode(valueArg, checker, visited, bindings)
372
417
  };
373
418
  }
374
419
  if ((typeName === "Partial" || typeName === "Required" || typeName === "Readonly") && typeNode.typeArguments?.length === 1) {
375
- const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
420
+ const inner = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
376
421
  if (inner.kind === "object" && typeName === "Partial") {
377
422
  return {
378
423
  kind: "object",
379
424
  properties: inner.properties.map((p) => ({ ...p, optional: true }))
380
425
  };
381
426
  }
427
+ if (inner.kind === "object" && typeName === "Required") {
428
+ return {
429
+ kind: "object",
430
+ properties: inner.properties.map((p) => ({ ...p, optional: false }))
431
+ };
432
+ }
382
433
  return inner;
383
434
  }
384
435
  if ((typeName === "Pick" || typeName === "Omit") && typeNode.typeArguments?.length === 2) {
385
- const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited);
436
+ const innerType = resolveTypeNode(typeNode.typeArguments[0], checker, visited, bindings);
386
437
  if (innerType.kind !== "object") {
387
438
  throw new SchemaExtractionError(
388
439
  typeNode.getText(),
@@ -390,7 +441,7 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
390
441
  );
391
442
  }
392
443
  const keyTypeNode = typeNode.typeArguments[1];
393
- let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited));
444
+ let keys = extractLiteralKeys(resolveTypeNode(keyTypeNode, checker, visited, bindings));
394
445
  if (keys === null) {
395
446
  keys = extractKeysFromChecker(keyTypeNode, checker);
396
447
  }
@@ -408,10 +459,11 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
408
459
  "Map \u5FC5\u987B\u5E26 2 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Map<K, V>\uFF0C\u88F8 Map \u4E0D\u652F\u6301"
409
460
  );
410
461
  }
462
+ const [mapKey, mapValue] = typeNode.typeArguments;
411
463
  return {
412
464
  kind: "map",
413
- key: resolveTypeNode(typeNode.typeArguments[0], checker, visited),
414
- value: resolveTypeNode(typeNode.typeArguments[1], checker, visited)
465
+ key: resolveTypeNode(mapKey, checker, visited, bindings),
466
+ value: resolveTypeNode(mapValue, checker, visited, bindings)
415
467
  };
416
468
  }
417
469
  if (typeName === "Set") {
@@ -421,9 +473,10 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
421
473
  "Set \u5FC5\u987B\u5E26 1 \u4E2A\u7C7B\u578B\u53C2\u6570\uFF0C\u5982 Set<T>\uFF0C\u88F8 Set \u4E0D\u652F\u6301"
422
474
  );
423
475
  }
476
+ const [setArg] = typeNode.typeArguments;
424
477
  return {
425
478
  kind: "set",
426
- element: resolveTypeNode(typeNode.typeArguments[0], checker, visited)
479
+ element: resolveTypeNode(setArg, checker, visited, bindings)
427
480
  };
428
481
  }
429
482
  if (typeName === "WeakMap" || typeName === "WeakSet") {
@@ -446,21 +499,42 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
446
499
  }
447
500
  visited.add(typeName);
448
501
  if (checker) {
449
- 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;
450
503
  if (symbol) {
451
504
  const declaration = symbol.declarations?.[0];
452
505
  if (declaration) {
453
506
  if (ts2.isInterfaceDeclaration(declaration)) {
454
- return resolveInterfaceDeclaration(declaration, checker, visited);
507
+ return resolveInterfaceDeclaration(
508
+ declaration,
509
+ checker,
510
+ visited,
511
+ bindings,
512
+ typeNode.typeArguments
513
+ );
455
514
  }
456
515
  if (ts2.isTypeAliasDeclaration(declaration)) {
457
- 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);
458
525
  }
459
526
  if (ts2.isEnumDeclaration(declaration)) {
460
527
  return resolveEnumDeclaration(declaration);
461
528
  }
462
529
  if (ts2.isImportSpecifier(declaration) || ts2.isImportClause(declaration)) {
463
- 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
+ );
464
538
  if (resolved) return resolved;
465
539
  }
466
540
  }
@@ -468,17 +542,45 @@ function resolveTypeReference(typeNode, checker, visited = /* @__PURE__ */ new S
468
542
  }
469
543
  throw new SchemaExtractionError(typeNode.getText(), `\u65E0\u6CD5\u89E3\u6790\u7684\u5F15\u7528\u7C7B\u578B "${typeName}"`);
470
544
  }
471
- 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) {
472
566
  const typeName = typeNode.typeName.getText();
473
567
  try {
474
568
  const aliased = checker.getAliasedSymbol(symbol);
475
569
  if (aliased && aliased.declarations && aliased.declarations.length > 0) {
476
570
  const decl = aliased.declarations[0];
477
571
  if (ts2.isInterfaceDeclaration(decl)) {
478
- return resolveInterfaceDeclaration(decl, checker, visited);
572
+ return resolveInterfaceDeclaration(decl, checker, visited, bindings, typeArguments);
479
573
  }
480
574
  if (ts2.isTypeAliasDeclaration(decl)) {
481
- 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);
482
584
  }
483
585
  if (ts2.isEnumDeclaration(decl)) {
484
586
  return resolveEnumDeclaration(decl);
@@ -496,10 +598,18 @@ function resolveImportAlias(typeNode, symbol, checker, visited) {
496
598
  const found = findTopLevelDecl(sourceFile, typeName);
497
599
  if (found) {
498
600
  if (found.kind === "interface") {
499
- return resolveInterfaceDeclaration(found.node, checker, visited);
601
+ return resolveInterfaceDeclaration(found.node, checker, visited, bindings, typeArguments);
500
602
  }
501
603
  if (found.kind === "typeAlias") {
502
- 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);
503
613
  }
504
614
  if (found.kind === "enum") {
505
615
  return resolveEnumDeclaration(found.node);
@@ -546,13 +656,22 @@ function resolveEnumDeclaration(node) {
546
656
  }
547
657
  return { kind: "union", members };
548
658
  }
549
- function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set()) {
659
+ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ new Set(), outerBindings = /* @__PURE__ */ new Map(), typeArguments) {
550
660
  const properties = [];
551
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
+ );
552
671
  for (const heritageClause of node.heritageClauses ?? []) {
553
672
  if (heritageClause.token === ts2.SyntaxKind.ExtendsKeyword) {
554
673
  for (const expr of heritageClause.types) {
555
- const parentType = resolveTypeNode(expr, checker, visited);
674
+ const parentType = resolveTypeNode(expr, checker, visited, bindings);
556
675
  if (parentType.kind === "object") {
557
676
  for (const prop of parentType.properties) {
558
677
  propMap.set(prop.name, prop);
@@ -562,10 +681,17 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
562
681
  }
563
682
  }
564
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
+ }
565
691
  if (ts2.isPropertySignature(member) && member.name) {
566
692
  const name = member.name.getText();
567
693
  const optional = !!member.questionToken;
568
- const type = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
694
+ const type = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
569
695
  const constraints = extractConstraintsFromJsDoc(member, name);
570
696
  validateConstraints(constraints, type, name);
571
697
  propMap.set(
@@ -574,15 +700,13 @@ function resolveInterfaceDeclaration(node, checker, visited = /* @__PURE__ */ ne
574
700
  );
575
701
  }
576
702
  if (ts2.isIndexSignatureDeclaration(member)) {
577
- const keyType = member.parameters[0]?.type ? resolveTypeNode(member.parameters[0].type, checker, visited) : { kind: "any" };
578
- const valueType = member.type ? resolveTypeNode(member.type, checker, visited) : { kind: "any" };
579
- return { kind: "record", key: keyType, value: valueType };
703
+ catchall = member.type ? resolveTypeNode(member.type, checker, visited, bindings) : { kind: "any" };
580
704
  }
581
705
  }
582
706
  for (const prop of propMap.values()) {
583
707
  properties.push(prop);
584
708
  }
585
- return { kind: "object", properties };
709
+ return catchall !== void 0 ? { kind: "object", properties, catchall } : { kind: "object", properties };
586
710
  }
587
711
  function extractConstraintsFromJsDoc(node, fieldName) {
588
712
  const jsDocs = ts2.getJSDocCommentsAndTags(node).filter((entry) => ts2.isJSDoc(entry));
@@ -705,15 +829,38 @@ var init_resolveTypeNode = __esm({
705
829
  "src/ast/resolveTypeNode.ts"() {
706
830
  "use strict";
707
831
  currentProgram = null;
708
- SchemaExtractionError = class extends Error {
709
- constructor(typeText, reason, options) {
710
- 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
+ );
711
838
  this.typeText = typeText;
712
839
  this.reason = reason;
840
+ this.location = location;
713
841
  this.name = "SchemaExtractionError";
714
842
  }
715
843
  typeText;
716
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
+ }
717
864
  };
718
865
  NUMBER_CONSTRAINT_KINDS = /* @__PURE__ */ new Set([
719
866
  "max",
@@ -780,55 +927,47 @@ function extractTypeInfo(program, filePath, typeName) {
780
927
  return;
781
928
  }
782
929
  });
783
- return result;
784
- } finally {
785
- setProgramContext(null);
786
- }
787
- }
788
- function extractAllTypes(program, filePath) {
789
- const sourceFile = program.getSourceFile(filePath);
790
- if (!sourceFile) return /* @__PURE__ */ new Map();
791
- const checker = program.getTypeChecker();
792
- setProgramContext(program);
793
- try {
794
- const result = /* @__PURE__ */ new Map();
795
- ts3.forEachChild(sourceFile, (node) => {
796
- if (ts3.isInterfaceDeclaration(node)) {
797
- const visited = /* @__PURE__ */ new Set();
798
- visited.add(node.name.text);
799
- const runtimeType = withFileContext(
800
- filePath,
801
- node.name.text,
802
- () => resolveInterfaceDeclaration(node, checker, visited)
803
- );
804
- result.set(node.name.text, {
805
- name: node.name.text,
806
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
807
- runtimeType
808
- });
809
- return;
810
- }
811
- if (ts3.isTypeAliasDeclaration(node)) {
812
- const visited = /* @__PURE__ */ new Set();
813
- visited.add(node.name.text);
814
- const runtimeType = withFileContext(
815
- filePath,
816
- node.name.text,
817
- () => resolveTypeNode(node.type, checker, visited)
818
- );
819
- result.set(node.name.text, {
820
- name: node.name.text,
821
- properties: runtimeType.kind === "object" ? runtimeType.properties : [],
822
- runtimeType
823
- });
824
- 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;
825
935
  }
826
- });
827
- 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;
828
956
  } finally {
829
957
  setProgramContext(null);
830
958
  }
831
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
+ }
832
971
  function withFileContext(filePath, typeName, fn) {
833
972
  try {
834
973
  return fn();
@@ -838,7 +977,8 @@ function withFileContext(filePath, typeName, fn) {
838
977
  const enriched = new SchemaExtractionError(
839
978
  err.typeText,
840
979
  `${err.reason}\uFF08\u6587\u4EF6: ${fileName}, \u7C7B\u578B: ${typeName}\uFF09`,
841
- { cause: err }
980
+ { cause: err },
981
+ err.location
842
982
  );
843
983
  throw enriched;
844
984
  }
@@ -883,9 +1023,13 @@ var init_schemaName = __esm({
883
1023
  // src/injection/resolveInjection.ts
884
1024
  import ts4 from "typescript";
885
1025
  function resolveInjection(fn) {
1026
+ const cached = injectionCache.get(fn);
1027
+ if (cached) {
1028
+ return cached;
1029
+ }
886
1030
  const fnStr = fn.toString();
887
1031
  const params = extractParamsWithAst(fnStr);
888
- return params.map((param) => {
1032
+ const items = params.map((param) => {
889
1033
  const type = PARAM_TYPE_MAP[param.name] || "unknown";
890
1034
  return {
891
1035
  name: param.name,
@@ -894,6 +1038,8 @@ function resolveInjection(fn) {
894
1038
  // 运行时类型已擦除
895
1039
  };
896
1040
  });
1041
+ injectionCache.set(fn, items);
1042
+ return items;
897
1043
  }
898
1044
  function extractParamsWithAst(fnStr) {
899
1045
  const sourceFile = ts4.createSourceFile(
@@ -950,7 +1096,7 @@ function extractParamName(param, names) {
950
1096
  return;
951
1097
  }
952
1098
  }
953
- var PARAM_TYPE_MAP;
1099
+ var PARAM_TYPE_MAP, injectionCache;
954
1100
  var init_resolveInjection = __esm({
955
1101
  "src/injection/resolveInjection.ts"() {
956
1102
  "use strict";
@@ -973,13 +1119,13 @@ var init_resolveInjection = __esm({
973
1119
  agents: "agents"
974
1120
  // Phase 2.3
975
1121
  };
1122
+ injectionCache = /* @__PURE__ */ new WeakMap();
976
1123
  }
977
1124
  });
978
1125
 
979
1126
  // src/injection/analyzeInjection.ts
980
1127
  import ts5 from "typescript";
981
- function analyzeInjection(code, functionName) {
982
- const sourceFile = ts5.createSourceFile("temp.ts", code, ts5.ScriptTarget.Latest, true);
1128
+ function analyzeInjectionInSourceFile(sourceFile, functionName) {
983
1129
  const params = [];
984
1130
  ts5.forEachChild(sourceFile, (node) => {
985
1131
  if (ts5.isFunctionDeclaration(node) && node.name?.text === functionName) {
@@ -1041,28 +1187,23 @@ function collectRouteSchemaSources(routes, rootDir) {
1041
1187
  entry.methods.add(route.method);
1042
1188
  }
1043
1189
  const programByFile = createPrograms([...methodsByFile.keys()]);
1044
- const allTypesByFile = /* @__PURE__ */ new Map();
1045
- const mergedAllTypes = /* @__PURE__ */ new Map();
1190
+ const resolversByFile = /* @__PURE__ */ new Map();
1046
1191
  for (const filePath of methodsByFile.keys()) {
1047
- const program = programByFile.get(filePath);
1048
- const allTypes = extractAllTypes(program, filePath);
1049
- allTypesByFile.set(filePath, allTypes);
1050
- for (const [name, info] of allTypes) {
1051
- mergedAllTypes.set(name, info);
1052
- }
1192
+ resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
1053
1193
  }
1054
1194
  const sources = [];
1055
1195
  for (const [filePath, entry] of methodsByFile) {
1056
1196
  const program = programByFile.get(filePath);
1057
1197
  const sourceFile = program.getSourceFile(filePath);
1058
- const code = sourceFile?.text ?? "";
1198
+ if (!sourceFile) continue;
1199
+ const resolver = resolversByFile.get(filePath);
1059
1200
  for (const method of entry.methods) {
1060
1201
  const inputType = getInputTypeForMethod(method);
1061
1202
  const schemaName = getSchemaName(method, inputType);
1062
- const meta = analyzeInjection(code, method);
1203
+ const meta = analyzeInjectionInSourceFile(sourceFile, method);
1063
1204
  const param = meta.params.find((p) => p.type === inputType) ?? (inputType === "body" ? meta.params.find((p) => p.type === "form") : void 0);
1064
1205
  const isForm = param?.type === "form";
1065
- const typeInfo = param?.typeName ? extractTypeInfo(program, filePath, param.typeName) : null;
1206
+ const typeInfo = param?.typeName ? resolver.resolve(param.typeName) : null;
1066
1207
  sources.push({
1067
1208
  urlPath: entry.urlPath,
1068
1209
  filePath,
@@ -1072,7 +1213,7 @@ function collectRouteSchemaSources(routes, rootDir) {
1072
1213
  });
1073
1214
  }
1074
1215
  }
1075
- return { sources, allTypesByFile, mergedAllTypes };
1216
+ return { sources, resolversByFile };
1076
1217
  }
1077
1218
  var init_collectRouteSchemaSources = __esm({
1078
1219
  "src/cli/collectRouteSchemaSources.ts"() {
@@ -1085,6 +1226,21 @@ var init_collectRouteSchemaSources = __esm({
1085
1226
  }
1086
1227
  });
1087
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
+
1088
1244
  // src/ast/generateZodSchema.ts
1089
1245
  function collectNamedTypes(type, ctx) {
1090
1246
  switch (type.kind) {
@@ -1135,8 +1291,12 @@ function collectNamedTypes(type, ctx) {
1135
1291
  if (resolved) {
1136
1292
  ctx.namedTypes.set(type.name, resolved);
1137
1293
  collectNamedTypes(resolved, ctx);
1294
+ return;
1138
1295
  }
1139
- 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
+ );
1140
1300
  }
1141
1301
  }
1142
1302
  }
@@ -1146,6 +1306,12 @@ function runtimeTypeToZodExpression(type, ctx, constraints) {
1146
1306
  if (ctx.coerce && (type.kind === "number" || type.kind === "boolean")) {
1147
1307
  return wrapCoercePreprocess(type.kind, withConstraints);
1148
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
+ }
1149
1315
  return withConstraints;
1150
1316
  }
1151
1317
  function applyConstraints(baseExpr, constraints, typeKind) {
@@ -1210,7 +1376,7 @@ function baseExpression(type, ctx) {
1210
1376
  case "tuple":
1211
1377
  return generateTupleExpression(type.elements, ctx);
1212
1378
  case "object":
1213
- return generateObjectExpression(type.properties, ctx);
1379
+ return generateObjectExpression(type, ctx);
1214
1380
  case "union":
1215
1381
  return generateUnionExpression(type.members, ctx);
1216
1382
  case "date":
@@ -1262,7 +1428,10 @@ function generateTupleExpression(elements, ctx) {
1262
1428
  }
1263
1429
  }
1264
1430
  if (restExpression) {
1265
- 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})`;
1266
1435
  }
1267
1436
  const hasOptional = fixedOptional.some((o) => o);
1268
1437
  if (!hasOptional) {
@@ -1283,13 +1452,14 @@ function generateTupleExpression(elements, ctx) {
1283
1452
  }
1284
1453
  return `z.union([${variants.join(", ")}])`;
1285
1454
  }
1286
- function generateObjectExpression(properties, ctx) {
1287
- const fields = properties.map((prop) => {
1455
+ function generateObjectExpression(type, ctx) {
1456
+ const fields = type.properties.map((prop) => {
1288
1457
  const expr = runtimeTypeToZodExpression(prop.type, ctx, prop.constraints);
1289
1458
  const finalExpr = prop.optional ? `${expr}.optional()` : expr;
1290
1459
  return `${JSON.stringify(prop.name)}: ${finalExpr}`;
1291
1460
  });
1292
- 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;
1293
1463
  }
1294
1464
  function generateUnionExpression(members, ctx) {
1295
1465
  const hasNull = members.some((m) => m.kind === "null");
@@ -1335,6 +1505,23 @@ function containsRef(type, visited) {
1335
1505
  }
1336
1506
  }
1337
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) {
1338
1525
  const ctx = new CodeGenContext(resolveType);
1339
1526
  const name = exportName ?? typeInfo.name;
1340
1527
  ctx.entryTypeName = typeInfo.name;
@@ -1342,26 +1529,20 @@ function generateZodSchemaSource(typeInfo, resolveType, exportName, coerce = fal
1342
1529
  ctx.coerce = coerce;
1343
1530
  collectNamedTypes(typeInfo.runtimeType, ctx);
1344
1531
  ctx.namedTypes.delete(typeInfo.name);
1345
- const lines = [];
1346
- lines.push("import { z } from 'zod';");
1347
- lines.push("");
1348
- for (const [n, type] of ctx.namedTypes) {
1349
- lines.push(generateNamedTypeDeclaration(n, type, ctx));
1350
- }
1351
- 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
+ }));
1352
1536
  const entryExpr = runtimeTypeToZodExpression(typeInfo.runtimeType, ctx);
1353
1537
  const hasSelfRef = containsRef(typeInfo.runtimeType, /* @__PURE__ */ new Set([typeInfo.name]));
1354
- if (hasSelfRef) {
1355
- lines.push(`export const ${name}Schema = z.lazy(() => ${entryExpr});`);
1356
- } else {
1357
- lines.push(`export const ${name}Schema = ${entryExpr};`);
1358
- }
1359
- return lines.join("\n");
1538
+ const entryDeclaration = hasSelfRef ? `export const ${name}Schema = z.lazy(() => ${entryExpr});` : `export const ${name}Schema = ${entryExpr};`;
1539
+ return { namedTypeDeclarations, entryDeclaration };
1360
1540
  }
1361
1541
  var CodeGenContext, COERCE_NUMBER_HELPER, COERCE_BOOLEAN_HELPER, COERCE_MAP_HELPER, COERCE_SET_HELPER, HELPERS_FILENAME;
1362
1542
  var init_generateZodSchema = __esm({
1363
1543
  "src/ast/generateZodSchema.ts"() {
1364
1544
  "use strict";
1545
+ init_resolveTypeNode();
1365
1546
  CodeGenContext = class {
1366
1547
  /** 命名类型集合:name → RuntimeType */
1367
1548
  namedTypes = /* @__PURE__ */ new Map();
@@ -1383,7 +1564,7 @@ var init_generateZodSchema = __esm({
1383
1564
  }
1384
1565
  };
1385
1566
  COERCE_NUMBER_HELPER = 'export const coerceNumber = (v) => typeof v === "string" && v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v;';
1386
- 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};';
1387
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);';
1388
1569
  COERCE_SET_HELPER = "export const coerceSet = (v) => v instanceof Set ? v : (Array.isArray(v) ? new Set(v) : v);";
1389
1570
  HELPERS_FILENAME = "faapi-helpers.js";
@@ -1399,8 +1580,7 @@ __export(generateSchemaFiles_exports, {
1399
1580
  getRuntimeSchemaPath: () => getRuntimeSchemaPath,
1400
1581
  getSchemaOutputPath: () => getSchemaOutputPath
1401
1582
  });
1402
- import path6 from "path";
1403
- import fs5 from "fs/promises";
1583
+ import path8 from "path";
1404
1584
  function getSchemaOutputPath(sourceFile, dist, rootDir) {
1405
1585
  let rel = sourceFile.replace(/\\/g, "/");
1406
1586
  if (rel.startsWith("src/")) {
@@ -1408,7 +1588,7 @@ function getSchemaOutputPath(sourceFile, dist, rootDir) {
1408
1588
  }
1409
1589
  const idx = rel.lastIndexOf("/");
1410
1590
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1411
- return path6.resolve(rootDir, dist, relDir, "zod.js");
1591
+ return path8.resolve(rootDir, dist, relDir, "zod.js");
1412
1592
  }
1413
1593
  function getRuntimeSchemaPath(filePath, dist, rootDir) {
1414
1594
  let rel = filePath.replace(/\\/g, "/");
@@ -1419,16 +1599,17 @@ function getRuntimeSchemaPath(filePath, dist, rootDir) {
1419
1599
  }
1420
1600
  const idx = rel.lastIndexOf("/");
1421
1601
  const relDir = idx >= 0 ? rel.slice(0, idx) : "";
1422
- return path6.resolve(rootDir, dist, relDir, "zod.js");
1602
+ return path8.resolve(rootDir, dist, relDir, "zod.js");
1423
1603
  }
1424
1604
  function getHelpersImportPath(relDir) {
1425
1605
  if (!relDir) return `./${HELPERS_FILENAME}`;
1426
1606
  const depth = relDir.split("/").filter(Boolean).length;
1427
1607
  return `${"../".repeat(depth)}${HELPERS_FILENAME}`;
1428
1608
  }
1429
- function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1430
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
1609
+ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
1431
1610
  const lines = ["import { z } from 'zod';"];
1611
+ const namedTypeDeclarations = [];
1612
+ const seenNamedTypes = /* @__PURE__ */ new Set();
1432
1613
  const schemaBlocks = [];
1433
1614
  for (const source of sources) {
1434
1615
  const { schemaName, typeInfo } = source;
@@ -1436,16 +1617,24 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1436
1617
  continue;
1437
1618
  }
1438
1619
  const coerce = source.coerce ?? /(?:Query|Params)$/.test(schemaName);
1439
- const block = [`// ${schemaName}`];
1440
- const schemaCode = generateZodSchemaSource(typeInfo, resolveType, schemaName, coerce).replace(
1441
- /^import \{ z \} from 'zod';\s*\n\s*\n/,
1442
- ""
1620
+ const { namedTypeDeclarations: decls, entryDeclaration } = generateZodSchemaSourceParts(
1621
+ typeInfo,
1622
+ resolveType,
1623
+ schemaName,
1624
+ coerce
1443
1625
  );
1444
- block.push(schemaCode);
1445
- block.push("");
1446
- 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"));
1447
1632
  }
1448
- 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");
1449
1638
  if (helpersImportPath && usesCoerceHelpers(allSchemaCode)) {
1450
1639
  lines.push(
1451
1640
  `import { coerceNumber, coerceBoolean, coerceMap, coerceSet } from '${helpersImportPath}';`
@@ -1457,7 +1646,7 @@ function generateSchemaFileSource(sources, allTypes, helpersImportPath) {
1457
1646
  }
1458
1647
  async function generateSchemaFiles(routes, rootDir, dist) {
1459
1648
  if (routes.length === 0) return;
1460
- const { sources, allTypesByFile } = collectRouteSchemaSources(routes, rootDir);
1649
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
1461
1650
  const sourcesByFile = /* @__PURE__ */ new Map();
1462
1651
  for (const source of sources) {
1463
1652
  let list = sourcesByFile.get(source.filePath);
@@ -1469,9 +1658,8 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1469
1658
  }
1470
1659
  const fileEntries = [];
1471
1660
  for (const [filePath, fileSources] of sourcesByFile) {
1472
- const relFile = path6.relative(rootDir, filePath).replace(/\\/g, "/");
1661
+ const relFile = path8.relative(rootDir, filePath).replace(/\\/g, "/");
1473
1662
  const outputPath = getSchemaOutputPath(relFile, dist, rootDir);
1474
- const allTypes = allTypesByFile.get(filePath) ?? /* @__PURE__ */ new Map();
1475
1663
  let relForDir = relFile;
1476
1664
  if (relForDir.startsWith("src/")) {
1477
1665
  relForDir = relForDir.slice(4);
@@ -1479,12 +1667,17 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1479
1667
  const dirIdx = relForDir.lastIndexOf("/");
1480
1668
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
1481
1669
  const helpersImportPath = getHelpersImportPath(zodRelDir);
1482
- 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
+ );
1483
1676
  fileEntries.push({ outputPath, source });
1484
1677
  }
1485
1678
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
1486
1679
  if (usesCoerceHelpers(allSourceCode)) {
1487
- const helpersPath = path6.resolve(rootDir, dist, HELPERS_FILENAME);
1680
+ const helpersPath = path8.resolve(rootDir, dist, HELPERS_FILENAME);
1488
1681
  await writeSchemaFile(helpersPath, generateHelpersFileSource());
1489
1682
  }
1490
1683
  await Promise.all(
@@ -1492,12 +1685,12 @@ async function generateSchemaFiles(routes, rootDir, dist) {
1492
1685
  );
1493
1686
  }
1494
1687
  async function writeSchemaFile(outputPath, source) {
1495
- await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
1496
- await fs5.writeFile(outputPath, source, "utf-8");
1688
+ await atomicWriteFile(outputPath, source);
1497
1689
  }
1498
1690
  var init_generateSchemaFiles = __esm({
1499
1691
  "src/cli/generateSchemaFiles.ts"() {
1500
1692
  "use strict";
1693
+ init_atomicWrite();
1501
1694
  init_collectRouteSchemaSources();
1502
1695
  init_generateZodSchema();
1503
1696
  }
@@ -1510,95 +1703,178 @@ init_resolveTypeNode();
1510
1703
  init_inputType();
1511
1704
  init_collectRouteSchemaSources();
1512
1705
 
1513
- // src/injection/toolRegistry.ts
1514
- var registry = /* @__PURE__ */ new Map();
1515
- function hydrateToolRegistry(tools) {
1516
- const next = /* @__PURE__ */ new Map();
1517
- for (const tool of tools) {
1518
- next.set(tool.name, tool);
1519
- }
1520
- 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
+ };
1521
1727
  }
1522
- function clearToolRegistry() {
1523
- 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
+ };
1524
1785
  }
1525
- function getTool(name) {
1526
- 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
+ };
1527
1812
  }
1528
-
1529
- // src/injection/agentRegistry.ts
1530
- var registry2 = /* @__PURE__ */ new Map();
1531
- function hydrateAgentRegistry(agents) {
1532
- const next = /* @__PURE__ */ new Map();
1533
- for (const agent of agents) {
1534
- next.set(agent.name, agent);
1535
- }
1536
- 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
+ };
1537
1827
  }
1538
- function clearAgentRegistry() {
1539
- 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 };
1540
1834
  }
1835
+ var defaultRegistries = createAppRegistries();
1836
+
1837
+ // src/injection/agentRegistry.ts
1541
1838
  function getAgent(name) {
1542
- return registry2.get(name);
1839
+ return defaultRegistries.agent.getAgent(name);
1543
1840
  }
1544
1841
  function getAgentEntry(name) {
1545
- return registry2.get(name);
1842
+ return defaultRegistries.agent.getAgentEntry(name);
1546
1843
  }
1547
1844
  function listAgents() {
1548
- const merged = /* @__PURE__ */ new Map();
1549
- for (const agent of registry2.values()) merged.set(agent.name, agent);
1550
- return Array.from(merged.values());
1845
+ return defaultRegistries.agent.listAgents();
1551
1846
  }
1552
1847
  function resolveAgentTools(name) {
1553
- const agent = getAgent(name);
1554
- if (!agent) return [];
1555
- const result = /* @__PURE__ */ new Map();
1556
- if (agent.tools) {
1557
- for (const toolName of agent.tools) {
1558
- const tool = getTool(toolName);
1559
- if (tool) result.set(tool.name, tool);
1560
- }
1561
- }
1562
- return Array.from(result.values());
1848
+ return defaultRegistries.agent.resolveAgentTools(name);
1563
1849
  }
1564
1850
  function resolveSubAgents(name) {
1565
- const agent = getAgent(name);
1566
- if (!agent || !agent.agents) return [];
1567
- const result = [];
1568
- for (const agentName of agent.agents) {
1569
- const subAgent = getAgent(agentName);
1570
- if (subAgent) result.push(subAgent);
1571
- }
1572
- 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);
1573
1857
  }
1574
1858
 
1575
1859
  // src/injection/skillRegistry.ts
1576
- var registry3 = /* @__PURE__ */ new Map();
1577
1860
  function hydrateSkillRegistry(skills) {
1578
- const next = /* @__PURE__ */ new Map();
1579
- for (const skill of skills) {
1580
- next.set(skill.name, skill);
1581
- }
1582
- registry3 = next;
1583
- }
1584
- function clearSkillRegistry() {
1585
- registry3 = /* @__PURE__ */ new Map();
1861
+ defaultRegistries.skill.hydrate(skills);
1586
1862
  }
1587
1863
  function upsertSkill(core) {
1588
- registry3.set(core.name, core);
1864
+ defaultRegistries.skill.upsert(core);
1589
1865
  }
1590
1866
  function removeSkill(name) {
1591
- registry3.delete(name);
1867
+ defaultRegistries.skill.remove(name);
1592
1868
  }
1593
1869
  function getSkill(name) {
1594
- return registry3.get(name);
1870
+ return defaultRegistries.skill.get(name);
1595
1871
  }
1596
1872
  function listSkills() {
1597
- return Array.from(registry3.values());
1873
+ return defaultRegistries.skill.list();
1598
1874
  }
1599
1875
 
1600
1876
  // src/loader/loadAgentModule.ts
1601
- import fs7 from "fs";
1877
+ import fs9 from "fs";
1602
1878
 
1603
1879
  // src/loader/resolveExports.ts
1604
1880
  function resolveExport(module, exportName) {
@@ -1618,8 +1894,8 @@ function resolveExport(module, exportName) {
1618
1894
  // src/utils/importWithCacheBust.ts
1619
1895
  import { pathToFileURL } from "url";
1620
1896
  var loadTs;
1621
- function setLoadTimestamp(ts9) {
1622
- loadTs = ts9;
1897
+ function setLoadTimestamp(ts10) {
1898
+ loadTs = ts10;
1623
1899
  }
1624
1900
  function getVitestImportActual() {
1625
1901
  const vi = globalThis.vi;
@@ -1644,17 +1920,17 @@ async function importWithCacheBust(filePath, bustViteCache = false) {
1644
1920
  }
1645
1921
 
1646
1922
  // src/cli/compileOnDemand.ts
1647
- import path7 from "path";
1648
- import fs6 from "fs";
1923
+ import path10 from "path";
1924
+ import fs8 from "fs";
1649
1925
 
1650
- // src/cli/compileDevRoutes.ts
1651
- import path5 from "path";
1652
- import fs4 from "fs";
1926
+ // src/cli/compileSourceFiles.ts
1927
+ import path6 from "path";
1928
+ import fs5 from "fs";
1653
1929
  import fg from "fast-glob";
1654
1930
 
1655
1931
  // src/cli/aliasPlugin.ts
1656
- import path4 from "path";
1657
- import fs3 from "fs";
1932
+ import path5 from "path";
1933
+ import fs4 from "fs";
1658
1934
 
1659
1935
  // src/utils/resolveAlias.ts
1660
1936
  function resolveAlias(specifier, config) {
@@ -1679,57 +1955,84 @@ function resolveAlias(specifier, config) {
1679
1955
  return candidates;
1680
1956
  }
1681
1957
 
1682
- // src/utils/readTsconfig.ts
1683
- import ts6 from "typescript";
1958
+ // src/utils/prodPaths.ts
1684
1959
  import path3 from "path";
1685
1960
  import fs2 from "fs";
1686
- function readTsconfig(rootDir) {
1687
- const tsconfigPath = path3.resolve(rootDir, "tsconfig.json");
1688
- if (!fs2.existsSync(tsconfigPath)) return null;
1689
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
1690
- if (configFile.error || !configFile.config) return null;
1691
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
1692
- const baseUrl = parsed.options.baseUrl ?? rootDir;
1693
- const rawPaths = parsed.options.paths;
1694
- if (!rawPaths) return null;
1695
- const paths = {};
1696
- for (const [pattern, targets] of Object.entries(rawPaths)) {
1697
- 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);
1698
1967
  }
1699
- return { baseUrl, paths };
1968
+ const jsPath = rel.replace(/\.ts$/, ".js");
1969
+ return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
1700
1970
  }
1701
-
1702
- // src/cli/aliasPlugin.ts
1703
1971
  function toProdExtension(filePath) {
1704
1972
  if (filePath.endsWith(".ts")) return filePath.slice(0, -3) + ".js";
1705
1973
  if (filePath.endsWith(".tsx")) return filePath.slice(0, -4) + ".js";
1706
1974
  if (filePath.endsWith(".jsx")) return filePath.slice(0, -4) + ".js";
1707
1975
  return filePath;
1708
1976
  }
1709
- function toProdImportPath(sourceFile, importer) {
1710
- const importerDir = path4.dirname(importer);
1711
- let rel = path4.relative(importerDir, sourceFile);
1712
- rel = rel.split(path4.sep).join("/");
1713
- if (!rel.startsWith(".")) rel = "./" + rel;
1714
- return toProdExtension(rel);
1715
- }
1716
1977
  function toRealPath(p) {
1717
1978
  try {
1718
- return fs3.realpathSync(p);
1979
+ return fs2.realpathSync(p);
1719
1980
  } catch {
1720
1981
  return p;
1721
1982
  }
1722
1983
  }
1723
1984
  function isInsideDir(filePath, dir) {
1724
- const rel = path4.relative(dir, filePath);
1725
- 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);
1726
2030
  }
1727
- var APP_DIR = "src";
1728
2031
  function toStrippedProdImportPath(sourceFile, rootDir) {
1729
- const appDirAbs = toRealPath(path4.resolve(rootDir, APP_DIR));
2032
+ const appDirAbs = toRealPath(path5.resolve(rootDir, APP_DIR));
1730
2033
  const sourceReal = toRealPath(sourceFile);
1731
- let rel = path4.relative(appDirAbs, sourceReal);
1732
- rel = rel.split(path4.sep).join("/");
2034
+ let rel = path5.relative(appDirAbs, sourceReal);
2035
+ rel = rel.split(path5.sep).join("/");
1733
2036
  if (!rel.startsWith(".")) rel = "./" + rel;
1734
2037
  return toProdExtension(rel);
1735
2038
  }
@@ -1744,41 +2047,41 @@ var INDEX_EXTS = [
1744
2047
  "/index.cjs"
1745
2048
  ];
1746
2049
  function resolveRelativeSpecifier(importer, specifier) {
1747
- const importerDir = path4.dirname(importer);
1748
- const base = path4.resolve(importerDir, specifier);
2050
+ const importerDir = path5.dirname(importer);
2051
+ const base = path5.resolve(importerDir, specifier);
1749
2052
  if (PROD_EXTS.some((ext) => specifier.endsWith(ext))) {
1750
- return fs3.existsSync(base) ? base : null;
2053
+ return fs4.existsSync(base) ? base : null;
1751
2054
  }
1752
2055
  if (/\.(ts|tsx|jsx)$/.test(specifier)) {
1753
- return fs3.existsSync(base) ? base : null;
2056
+ return fs4.existsSync(base) ? base : null;
1754
2057
  }
1755
2058
  for (const ext of SOURCE_EXTS) {
1756
2059
  const file = base + ext;
1757
- if (fs3.existsSync(file)) return file;
2060
+ if (fs4.existsSync(file)) return file;
1758
2061
  }
1759
2062
  for (const indexExt of INDEX_EXTS) {
1760
2063
  const file = base + indexExt;
1761
- if (fs3.existsSync(file)) return file;
2064
+ if (fs4.existsSync(file)) return file;
1762
2065
  }
1763
2066
  return null;
1764
2067
  }
1765
2068
  function createAliasPlugin(config, options) {
1766
- const SPEC_RE = /(\bfrom\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
1767
- 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;
1768
2071
  return {
1769
2072
  name: "faapi-alias",
1770
2073
  setup(build) {
1771
2074
  build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs|cjs)$/ }, (args) => {
1772
2075
  let source;
1773
2076
  try {
1774
- source = fs3.readFileSync(args.path, "utf8");
2077
+ source = fs4.readFileSync(args.path, "utf8");
1775
2078
  } catch {
1776
2079
  return void 0;
1777
2080
  }
1778
2081
  const importer = args.path;
1779
2082
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
1780
2083
  let modified = false;
1781
- const newSource = source.replace(SPEC_RE, (full, prefix, quote, specifier) => {
2084
+ const newSource = source.replace(SPEC_RE2, (full, prefix, quote, specifier) => {
1782
2085
  if (specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:")) {
1783
2086
  return full;
1784
2087
  }
@@ -1804,7 +2107,7 @@ function createAliasPlugin(config, options) {
1804
2107
  for (const candidate of candidates) {
1805
2108
  for (const ext of SOURCE_EXTS) {
1806
2109
  const file = candidate + ext;
1807
- if (fs3.existsSync(file)) {
2110
+ if (fs4.existsSync(file)) {
1808
2111
  modified = true;
1809
2112
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
1810
2113
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -1817,7 +2120,7 @@ function createAliasPlugin(config, options) {
1817
2120
  }
1818
2121
  for (const indexExt of INDEX_EXTS) {
1819
2122
  const file = candidate + indexExt;
1820
- if (fs3.existsSync(file)) {
2123
+ if (fs4.existsSync(file)) {
1821
2124
  modified = true;
1822
2125
  if (appDirAbs && importerOutsideAppDir && isInsideDir(file, appDirAbs)) {
1823
2126
  return `${prefix}${quote}${toStrippedProdImportPath(
@@ -1842,11 +2145,10 @@ function buildAliasPlugins(rootDir) {
1842
2145
  return [createAliasPlugin(tsconfig ?? { baseUrl: ".", paths: {} }, { rootDir })];
1843
2146
  }
1844
2147
 
1845
- // src/cli/compileDevRoutes.ts
1846
- var APP_DIR2 = "src";
1847
- async function compileDevRoutes(options) {
1848
- const { rootDir, dist, files, logLevel = "silent" } = options;
1849
- 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`], {
1850
2152
  cwd: rootDir,
1851
2153
  onlyFiles: true,
1852
2154
  absolute: true,
@@ -1855,11 +2157,11 @@ async function compileDevRoutes(options) {
1855
2157
  if (entryPoints.length === 0) {
1856
2158
  return { compiledFiles: [] };
1857
2159
  }
1858
- const absDist = path5.resolve(rootDir, dist);
1859
- await fs4.promises.mkdir(absDist, { recursive: true });
2160
+ const absDist = path6.resolve(rootDir, dist);
2161
+ await fs5.promises.mkdir(absDist, { recursive: true });
1860
2162
  const plugins = buildAliasPlugins(rootDir);
1861
2163
  const esbuild = await import("esbuild");
1862
- const outbase = path5.resolve(rootDir, APP_DIR2);
2164
+ const outbase = path6.resolve(rootDir, APP_DIR);
1863
2165
  const result = await esbuild.build({
1864
2166
  entryPoints,
1865
2167
  outdir: absDist,
@@ -1870,29 +2172,106 @@ async function compileDevRoutes(options) {
1870
2172
  sourcemap: true,
1871
2173
  packages: "external",
1872
2174
  plugins,
2175
+ // build 语义:编译期 NODE_ENV 替换 + 死分支删除(见 AGENTS.md §5.3)
2176
+ ...production ? { define: { "process.env.NODE_ENV": '"production"' }, minifySyntax: true } : {},
1873
2177
  logLevel,
1874
- write: false
2178
+ // dev 语义:esbuild 返回内存内容,由下方原子写落盘
2179
+ ...atomicWrite ? { write: false } : {}
1875
2180
  });
1876
- if (result.outputFiles) {
2181
+ if (atomicWrite && result.outputFiles) {
1877
2182
  await Promise.all(
1878
2183
  result.outputFiles.map(async (file) => {
1879
- await fs4.promises.mkdir(path5.dirname(file.path), { recursive: true });
2184
+ await fs5.promises.mkdir(path6.dirname(file.path), { recursive: true });
1880
2185
  const tmp = `${file.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1881
- await fs4.promises.writeFile(tmp, file.contents);
1882
- await fs4.promises.rename(tmp, file.path);
2186
+ await fs5.promises.writeFile(tmp, file.contents);
2187
+ await fs5.promises.rename(tmp, file.path);
1883
2188
  })
1884
2189
  );
1885
2190
  }
1886
2191
  return { compiledFiles: entryPoints };
1887
2192
  }
1888
2193
 
2194
+ // src/cli/compileDevRoutes.ts
2195
+ async function compileDevRoutes(options) {
2196
+ return compileSourceFiles({ ...options, atomicWrite: true });
2197
+ }
2198
+
1889
2199
  // src/cli/compileOnDemand.ts
1890
2200
  init_generateSchemaFiles();
1891
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
1892
2271
  function isProductFresh(sourceAbsPath, productAbsPath) {
1893
2272
  try {
1894
- const srcStat = fs6.statSync(sourceAbsPath);
1895
- const prodStat = fs6.statSync(productAbsPath);
2273
+ const srcStat = fs8.statSync(sourceAbsPath);
2274
+ const prodStat = fs8.statSync(productAbsPath);
1896
2275
  return prodStat.mtimeMs >= srcStat.mtimeMs;
1897
2276
  } catch {
1898
2277
  return false;
@@ -1915,16 +2294,16 @@ function clearCompiledFiles() {
1915
2294
  sourcePathCache.clear();
1916
2295
  }
1917
2296
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
1918
- const inFlight = state.inFlightCompilations.get(sourceAbsPath);
1919
- if (inFlight) {
1920
- await inFlight.catch(() => {
2297
+ const inFlight2 = state.inFlightCompilations.get(sourceAbsPath);
2298
+ if (inFlight2) {
2299
+ await inFlight2.catch(() => {
1921
2300
  });
1922
2301
  return false;
1923
2302
  }
1924
2303
  if (state.compiledFiles.has(sourceAbsPath)) {
1925
2304
  return false;
1926
2305
  }
1927
- if (!fs6.existsSync(sourceAbsPath)) {
2306
+ if (!fs8.existsSync(sourceAbsPath)) {
1928
2307
  return false;
1929
2308
  }
1930
2309
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
@@ -1933,12 +2312,23 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
1933
2312
  return false;
1934
2313
  }
1935
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
+ }
1936
2323
  await compileDevRoutes({
1937
2324
  rootDir,
1938
2325
  dist,
1939
- files: [sourceAbsPath],
2326
+ files,
1940
2327
  logLevel: "silent"
1941
2328
  });
2329
+ for (const file of files) {
2330
+ state.compiledFiles.add(file);
2331
+ }
1942
2332
  state.compiledFiles.add(sourceAbsPath);
1943
2333
  })();
1944
2334
  state.inFlightCompilations.set(sourceAbsPath, compilePromise);
@@ -1949,30 +2339,43 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
1949
2339
  state.inFlightCompilations.delete(sourceAbsPath);
1950
2340
  }
1951
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
+ }
1952
2355
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
1953
- const rel = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2356
+ const rel = path10.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
1954
2357
  if (!rel.startsWith("src/")) return null;
1955
2358
  const relWithoutSrc = rel.slice(4);
1956
2359
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
1957
- return path7.resolve(rootDir, dist, jsRel);
2360
+ return path10.resolve(rootDir, dist, jsRel);
1958
2361
  }
1959
2362
  function clearGeneratedSchemas() {
1960
2363
  state.generatedSchemas.clear();
1961
2364
  state.inFlightSchemaGenerations.clear();
1962
2365
  }
1963
2366
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
1964
- const inFlight = state.inFlightSchemaGenerations.get(schemaPath);
1965
- if (inFlight) {
1966
- await inFlight.catch(() => {
2367
+ const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
2368
+ if (inFlight2) {
2369
+ await inFlight2.catch(() => {
1967
2370
  });
1968
2371
  return false;
1969
2372
  }
1970
2373
  if (state.generatedSchemas.has(schemaPath)) {
1971
2374
  return false;
1972
2375
  }
1973
- const prodAbsPath = path7.resolve(rootDir, routeFilePath);
2376
+ const prodAbsPath = path10.resolve(rootDir, routeFilePath);
1974
2377
  const sourceAbsPath = prodPathToSourcePath(prodAbsPath, rootDir, dist);
1975
- if (!fs6.existsSync(sourceAbsPath)) {
2378
+ if (!fs8.existsSync(sourceAbsPath)) {
1976
2379
  return false;
1977
2380
  }
1978
2381
  if (isProductFresh(sourceAbsPath, schemaPath)) {
@@ -1983,7 +2386,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
1983
2386
  if (fileRoutes.length === 0) {
1984
2387
  return false;
1985
2388
  }
1986
- const sourceRelPath = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2389
+ const sourceRelPath = path10.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
1987
2390
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
1988
2391
  const generatePromise = (async () => {
1989
2392
  await generateSchemaFiles(sourceRoutes, rootDir, dist);
@@ -2004,7 +2407,7 @@ async function deleteSchemaFiles(routes, rootDir, dist) {
2004
2407
  if (deleted.has(schemaPath)) continue;
2005
2408
  deleted.add(schemaPath);
2006
2409
  try {
2007
- await fs6.promises.unlink(schemaPath);
2410
+ await fs8.promises.unlink(schemaPath);
2008
2411
  } catch {
2009
2412
  }
2010
2413
  }
@@ -2013,19 +2416,19 @@ var sourcePathCache = /* @__PURE__ */ new Map();
2013
2416
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2014
2417
  const cached = sourcePathCache.get(prodAbsPath);
2015
2418
  if (cached) return cached;
2016
- const rel = path7.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2419
+ const rel = path10.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
2017
2420
  let relWithoutDist = rel;
2018
2421
  if (relWithoutDist.startsWith(`${dist}/`)) {
2019
2422
  relWithoutDist = relWithoutDist.slice(dist.length + 1);
2020
2423
  }
2021
2424
  const srcRel = `src/${relWithoutDist}`;
2022
2425
  const tsRel = srcRel.replace(/\.js$/, ".ts");
2023
- const tsAbs = path7.resolve(rootDir, tsRel);
2426
+ const tsAbs = path10.resolve(rootDir, tsRel);
2024
2427
  let result;
2025
- if (fs6.existsSync(tsAbs)) {
2428
+ if (fs8.existsSync(tsAbs)) {
2026
2429
  result = tsAbs;
2027
2430
  } else {
2028
- result = path7.resolve(rootDir, srcRel);
2431
+ result = path10.resolve(rootDir, srcRel);
2029
2432
  }
2030
2433
  sourcePathCache.set(prodAbsPath, result);
2031
2434
  return result;
@@ -2043,7 +2446,7 @@ async function loadAgentModule(filePath, hasRun, rootDir) {
2043
2446
  const dist = getDevDist();
2044
2447
  if (dist) {
2045
2448
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2046
- if (sourcePath && fs7.existsSync(sourcePath)) {
2449
+ if (sourcePath && fs9.existsSync(sourcePath)) {
2047
2450
  try {
2048
2451
  await ensureCompiled(sourcePath, rootDir, dist);
2049
2452
  } catch (compileErr) {
@@ -2076,13 +2479,13 @@ async function loadAgentModule(filePath, hasRun, rootDir) {
2076
2479
  }
2077
2480
 
2078
2481
  // src/loader/loadToolModule.ts
2079
- import fs8 from "fs";
2482
+ import fs10 from "fs";
2080
2483
  async function loadToolModule(filePath, functionName, rootDir) {
2081
2484
  if (isDevOnDemandEnabled() && rootDir) {
2082
2485
  const dist = getDevDist();
2083
2486
  if (dist) {
2084
2487
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
2085
- if (sourcePath && fs8.existsSync(sourcePath)) {
2488
+ if (sourcePath && fs10.existsSync(sourcePath)) {
2086
2489
  try {
2087
2490
  await ensureCompiled(sourcePath, rootDir, dist);
2088
2491
  } catch (compileErr) {
@@ -2114,12 +2517,46 @@ async function loadToolModule(filePath, functionName, rootDir) {
2114
2517
  import { existsSync as existsSync2 } from "fs";
2115
2518
 
2116
2519
  // src/cli/generateToolArtifacts.ts
2117
- import path8 from "path";
2118
- import fs9 from "fs/promises";
2520
+ import path11 from "path";
2119
2521
  import { existsSync } from "fs";
2120
2522
 
2121
2523
  // src/ast/extractToolMetadata.ts
2524
+ import ts8 from "typescript";
2525
+
2526
+ // src/ast/jsDocMetadata.ts
2122
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
2123
2560
  function extractToolMetadata(program, filePath, functionName, pathMeta) {
2124
2561
  const sourceFile = program.getSourceFile(filePath);
2125
2562
  if (!sourceFile) return null;
@@ -2128,7 +2565,7 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
2128
2565
  const { fn, jsDocOwner } = found;
2129
2566
  const jsDoc = getJSDocFromNode(jsDocOwner);
2130
2567
  const description = extractDescription(jsDoc);
2131
- const toolNameOverride = extractToolTagValue(jsDoc);
2568
+ const toolNameOverride = extractJSDocTagValue(jsDoc, "tool");
2132
2569
  const inputTypeName = getFirstParamTypeName(fn, sourceFile);
2133
2570
  return {
2134
2571
  name: toolNameOverride ?? pathMeta.name,
@@ -2140,18 +2577,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
2140
2577
  }
2141
2578
  function findExportedFunction(sourceFile, functionName) {
2142
2579
  let result = null;
2143
- ts7.forEachChild(sourceFile, (node) => {
2580
+ ts8.forEachChild(sourceFile, (node) => {
2144
2581
  if (result) return;
2145
- if (ts7.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2582
+ if (ts8.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2146
2583
  result = { fn: node, jsDocOwner: node };
2147
2584
  return;
2148
2585
  }
2149
- if (ts7.isVariableStatement(node) && hasExportModifier(node)) {
2586
+ if (ts8.isVariableStatement(node) && hasExportModifier(node)) {
2150
2587
  for (const decl of node.declarationList.declarations) {
2151
2588
  if (result) break;
2152
- 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);
2153
2590
  if (nameText !== functionName || !decl.initializer) continue;
2154
- if (ts7.isArrowFunction(decl.initializer) || ts7.isFunctionExpression(decl.initializer)) {
2591
+ if (ts8.isArrowFunction(decl.initializer) || ts8.isFunctionExpression(decl.initializer)) {
2155
2592
  result = { fn: decl.initializer, jsDocOwner: node };
2156
2593
  }
2157
2594
  }
@@ -2159,78 +2596,23 @@ function findExportedFunction(sourceFile, functionName) {
2159
2596
  });
2160
2597
  return result;
2161
2598
  }
2162
- function hasExportModifier(node) {
2163
- if (!ts7.canHaveModifiers(node)) return false;
2164
- const modifiers = ts7.getModifiers(node);
2165
- return !!modifiers?.some((m) => m.kind === ts7.SyntaxKind.ExportKeyword);
2166
- }
2167
- function getJSDocFromNode(node) {
2168
- const apiDocs = ts7.getJSDocCommentsAndTags(node).filter((entry) => ts7.isJSDoc(entry));
2169
- if (apiDocs.length > 0) return apiDocs[0];
2170
- const directDocs = node.jsDoc;
2171
- if (directDocs && directDocs.length > 0) return directDocs[0];
2172
- return void 0;
2173
- }
2174
- function extractDescription(jsDoc) {
2175
- if (!jsDoc) return void 0;
2176
- if (typeof jsDoc.comment !== "string") return void 0;
2177
- const trimmed = jsDoc.comment.trim();
2178
- return trimmed || void 0;
2179
- }
2180
- function extractToolTagValue(jsDoc) {
2181
- if (!jsDoc || !jsDoc.tags) return void 0;
2182
- for (const tag of jsDoc.tags) {
2183
- if (tag.tagName.text !== "tool") continue;
2184
- if (typeof tag.comment !== "string") return void 0;
2185
- const text = tag.comment.trim();
2186
- if (!text) return void 0;
2187
- const cleaned = text.replace(/^\{|\}$/g, "").trim();
2188
- return cleaned || void 0;
2189
- }
2190
- return void 0;
2191
- }
2192
2599
  function getFirstParamTypeName(fn, sourceFile) {
2193
2600
  const firstParam = fn.parameters[0];
2194
2601
  if (!firstParam) return void 0;
2195
2602
  if (!firstParam.type) return void 0;
2196
- if (!ts7.isTypeReferenceNode(firstParam.type)) return void 0;
2603
+ if (!ts8.isTypeReferenceNode(firstParam.type)) return void 0;
2197
2604
  return firstParam.type.typeName.getText(sourceFile);
2198
2605
  }
2199
-
2200
- // src/cli/generateToolArtifacts.ts
2201
- init_createProgram();
2202
- init_extractHandlerTypes();
2203
- init_generateZodSchema();
2204
- init_generateSchemaFiles();
2205
- var TOOLS_FILE = "faapi-tools.js";
2206
- function getToolSchemaOutputPath(sourceFile, dist, rootDir) {
2207
- let rel = sourceFile.replace(/\\/g, "/");
2208
- if (rel.startsWith("src/")) {
2209
- rel = rel.slice(4);
2210
- }
2211
- const idx = rel.lastIndexOf("/");
2212
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2213
- return path8.resolve(rootDir, dist, relDir, "zod.js");
2214
- }
2215
- function getRuntimeToolSchemaPath(filePath, dist, rootDir) {
2216
- let rel = filePath.replace(/\\/g, "/");
2217
- if (rel.startsWith("src/")) {
2218
- rel = rel.slice(4);
2219
- } else if (rel.startsWith(`${dist}/`)) {
2220
- rel = rel.slice(dist.length + 1);
2221
- }
2222
- const idx = rel.lastIndexOf("/");
2223
- const relDir = idx >= 0 ? rel.slice(0, idx) : "";
2224
- return path8.resolve(rootDir, dist, relDir, "zod.js");
2225
- }
2226
- function toProdFilePath(filePath, dist) {
2227
- let rel = filePath.replace(/\\/g, "/");
2228
- if (rel.startsWith("src/")) {
2229
- rel = rel.slice(4);
2230
- }
2231
- const jsPath = rel.replace(/\.ts$/, ".js");
2232
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
2233
- }
2606
+
2607
+ // src/cli/generateToolArtifacts.ts
2608
+ init_createProgram();
2609
+ init_atomicWrite();
2610
+ init_generateSchemaFiles();
2611
+ init_extractHandlerTypes();
2612
+ init_generateZodSchema();
2613
+ init_generateSchemaFiles();
2614
+ init_generateSchemaFiles();
2615
+ var TOOLS_FILE = "faapi-tools.js";
2234
2616
  function serializeTools(tools, dist = "dist") {
2235
2617
  return tools.map((t) => ({
2236
2618
  name: t.name,
@@ -2241,12 +2623,10 @@ function serializeTools(tools, dist = "dist") {
2241
2623
  }));
2242
2624
  }
2243
2625
  async function writeToolsModule(manifest, outputPath) {
2244
- const dir = path8.dirname(outputPath);
2245
- await fs9.mkdir(dir, { recursive: true });
2246
2626
  const content = `// \u81EA\u52A8\u751F\u6210,\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91(faapi build/dev \u4EA7\u7269)
2247
2627
  export const tools = ${JSON.stringify(manifest, null, 2)};
2248
2628
  `;
2249
- await fs9.writeFile(outputPath, content, "utf-8");
2629
+ await atomicWriteFile(outputPath, content);
2250
2630
  }
2251
2631
  function hydrateTools(manifest) {
2252
2632
  return manifest.map((t) => ({
@@ -2261,7 +2641,7 @@ function collectToolSchemaSources(tools, rootDir) {
2261
2641
  const toolsByFile = /* @__PURE__ */ new Map();
2262
2642
  for (const tool of tools) {
2263
2643
  if (!tool.inputTypeName) continue;
2264
- const absPath = path8.resolve(rootDir, tool.filePath);
2644
+ const absPath = path11.resolve(rootDir, tool.filePath);
2265
2645
  let list = toolsByFile.get(absPath);
2266
2646
  if (!list) {
2267
2647
  list = [];
@@ -2270,11 +2650,9 @@ function collectToolSchemaSources(tools, rootDir) {
2270
2650
  list.push(tool);
2271
2651
  }
2272
2652
  const programByFile = createPrograms([...toolsByFile.keys()]);
2273
- const allTypesByFile = /* @__PURE__ */ new Map();
2653
+ const resolversByFile = /* @__PURE__ */ new Map();
2274
2654
  for (const filePath of toolsByFile.keys()) {
2275
- const program = programByFile.get(filePath);
2276
- const allTypes = extractAllTypes(program, filePath);
2277
- allTypesByFile.set(filePath, allTypes);
2655
+ resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
2278
2656
  }
2279
2657
  const sources = [];
2280
2658
  for (const [filePath, fileTools] of toolsByFile) {
@@ -2291,10 +2669,9 @@ function collectToolSchemaSources(tools, rootDir) {
2291
2669
  });
2292
2670
  }
2293
2671
  }
2294
- return { sources, allTypesByFile };
2672
+ return { sources, resolversByFile };
2295
2673
  }
2296
- function generateToolSchemaFileSource(sources, allTypes, helpersImportPath) {
2297
- const resolveType = (name) => allTypes.get(name)?.runtimeType;
2674
+ function generateToolSchemaFileSource(sources, resolveType, helpersImportPath) {
2298
2675
  const lines = ["import { z } from 'zod';"];
2299
2676
  const schemaBlocks = [];
2300
2677
  for (const source of sources) {
@@ -2324,16 +2701,15 @@ function generateToolSchemaFileSource(sources, allTypes, helpersImportPath) {
2324
2701
  }
2325
2702
  async function maybeGenerateHelpers(allSourceCode, distDir) {
2326
2703
  if (!usesCoerceHelpers(allSourceCode)) return;
2327
- const helpersPath = path8.resolve(distDir, HELPERS_FILENAME);
2704
+ const helpersPath = path11.resolve(distDir, HELPERS_FILENAME);
2328
2705
  if (existsSync(helpersPath)) return;
2329
- await fs9.mkdir(path8.dirname(helpersPath), { recursive: true });
2330
- await fs9.writeFile(helpersPath, generateHelpersFileSource(), "utf-8");
2706
+ await atomicWriteFile(helpersPath, generateHelpersFileSource());
2331
2707
  }
2332
2708
  async function generateToolArtifacts(tools, rootDir, dist, options) {
2333
2709
  const metadata = [];
2334
- const programByFile = createPrograms(tools.map((m) => path8.resolve(rootDir, m.filePath)));
2710
+ const programByFile = createPrograms(tools.map((m) => path11.resolve(rootDir, m.filePath)));
2335
2711
  for (const manifest of tools) {
2336
- const absPath = path8.resolve(rootDir, manifest.filePath);
2712
+ const absPath = path11.resolve(rootDir, manifest.filePath);
2337
2713
  const program = programByFile.get(absPath);
2338
2714
  const result = extractToolMetadata(program, absPath, manifest.functionName, {
2339
2715
  name: manifest.name,
@@ -2344,7 +2720,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
2344
2720
  }
2345
2721
  }
2346
2722
  const serialized = serializeTools(metadata, dist);
2347
- const toolsPath = path8.resolve(rootDir, dist, TOOLS_FILE);
2723
+ const toolsPath = path11.resolve(rootDir, dist, TOOLS_FILE);
2348
2724
  await writeToolsModule(serialized, toolsPath);
2349
2725
  if (options?.skipSchema) {
2350
2726
  return metadata;
@@ -2352,7 +2728,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
2352
2728
  if (metadata.length === 0) {
2353
2729
  return metadata;
2354
2730
  }
2355
- const { sources, allTypesByFile } = collectToolSchemaSources(metadata, rootDir);
2731
+ const { sources, resolversByFile } = collectToolSchemaSources(metadata, rootDir);
2356
2732
  if (sources.length === 0) {
2357
2733
  return metadata;
2358
2734
  }
@@ -2367,9 +2743,9 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
2367
2743
  }
2368
2744
  const fileEntries = [];
2369
2745
  for (const [filePath, fileSources] of sourcesByFile) {
2370
- const relFile = path8.relative(rootDir, filePath).replace(/\\/g, "/");
2371
- const outputPath = getToolSchemaOutputPath(relFile, dist, rootDir);
2372
- 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);
2373
2749
  let relForDir = relFile;
2374
2750
  if (relForDir.startsWith("src/")) {
2375
2751
  relForDir = relForDir.slice(4);
@@ -2377,11 +2753,15 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
2377
2753
  const dirIdx = relForDir.lastIndexOf("/");
2378
2754
  const zodRelDir = dirIdx >= 0 ? relForDir.slice(0, dirIdx) : "";
2379
2755
  const helpersImportPath = getHelpersImportPath(zodRelDir);
2380
- const source = generateToolSchemaFileSource(fileSources, allTypes, helpersImportPath);
2756
+ const source = generateToolSchemaFileSource(
2757
+ fileSources,
2758
+ (name) => resolver?.resolve(name)?.runtimeType,
2759
+ helpersImportPath
2760
+ );
2381
2761
  fileEntries.push({ outputPath, source });
2382
2762
  }
2383
2763
  const allSourceCode = fileEntries.map((e) => e.source).join("\n");
2384
- const distDir = path8.resolve(rootDir, dist);
2764
+ const distDir = path11.resolve(rootDir, dist);
2385
2765
  await maybeGenerateHelpers(allSourceCode, distDir);
2386
2766
  await Promise.all(
2387
2767
  fileEntries.map(({ outputPath, source }) => writeToolSchemaFile(outputPath, source))
@@ -2389,8 +2769,7 @@ async function generateToolArtifacts(tools, rootDir, dist, options) {
2389
2769
  return metadata;
2390
2770
  }
2391
2771
  async function writeToolSchemaFile(outputPath, source) {
2392
- await fs9.mkdir(path8.dirname(outputPath), { recursive: true });
2393
- await fs9.writeFile(outputPath, source, "utf-8");
2772
+ await atomicWriteFile(outputPath, source);
2394
2773
  }
2395
2774
 
2396
2775
  // src/loader/loadToolSchema.ts
@@ -2402,7 +2781,7 @@ function getDist() {
2402
2781
  }
2403
2782
  function getToolSchemaPath(tool, rootDir) {
2404
2783
  const dist = getDist();
2405
- return getRuntimeToolSchemaPath(tool.filePath, dist, rootDir ?? process.cwd());
2784
+ return getRuntimeSchemaPath(tool.filePath, dist, rootDir ?? process.cwd());
2406
2785
  }
2407
2786
  async function loadToolSchema(tool, rootDir) {
2408
2787
  if (!tool.inputTypeName) return void 0;
@@ -2420,16 +2799,14 @@ async function loadToolSchema(tool, rootDir) {
2420
2799
  }
2421
2800
 
2422
2801
  // src/injection/agentHandle.ts
2423
- var currentFactory = null;
2424
2802
  function registerAgentHandleFactory(factory) {
2425
- currentFactory = factory;
2803
+ defaultRegistries.agentHandle.register(factory);
2426
2804
  }
2427
2805
  function getAgentHandle(ctx) {
2428
- if (currentFactory === null) return void 0;
2429
- return currentFactory(ctx);
2806
+ return defaultRegistries.agentHandle.get(ctx);
2430
2807
  }
2431
2808
  function clearAgentHandleFactory() {
2432
- currentFactory = null;
2809
+ defaultRegistries.agentHandle.clear();
2433
2810
  }
2434
2811
 
2435
2812
  // src/middleware/cors.ts
@@ -2457,11 +2834,6 @@ function cors(options = {}) {
2457
2834
  } else if (Array.isArray(origin)) {
2458
2835
  allowOrigin = origin.includes(reqOrigin) ? reqOrigin : null;
2459
2836
  }
2460
- if (!allowOrigin) {
2461
- await next();
2462
- return;
2463
- }
2464
- ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
2465
2837
  if (origin === true || Array.isArray(origin)) {
2466
2838
  const existingVary = ctx.headers.get("vary");
2467
2839
  if (existingVary) {
@@ -2472,6 +2844,11 @@ function cors(options = {}) {
2472
2844
  ctx.setHeader("Vary", "Origin");
2473
2845
  }
2474
2846
  }
2847
+ if (!allowOrigin) {
2848
+ await next();
2849
+ return;
2850
+ }
2851
+ ctx.setHeader("Access-Control-Allow-Origin", allowOrigin);
2475
2852
  ctx.setHeader("Access-Control-Allow-Methods", methods.join(", "));
2476
2853
  if (allowedHeaders) {
2477
2854
  ctx.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
@@ -2593,16 +2970,16 @@ function helmet(options = {}) {
2593
2970
  }
2594
2971
 
2595
2972
  // src/config/loadConfig.ts
2596
- import path9 from "path";
2597
- import fs10 from "fs";
2973
+ import path12 from "path";
2974
+ import fs11 from "fs";
2598
2975
  var CONFIG_PRODUCT_FILE = "faapi-config.js";
2599
2976
  async function loadConfig(rootDir, dist) {
2600
- const configProductPath = path9.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
2601
- if (fs10.existsSync(configProductPath)) {
2977
+ const configProductPath = path12.resolve(rootDir, dist, CONFIG_PRODUCT_FILE);
2978
+ if (fs11.existsSync(configProductPath)) {
2602
2979
  const module = await importWithCacheBust(configProductPath);
2603
2980
  return module.default ?? {};
2604
2981
  }
2605
- 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"));
2606
2983
  if (hasSourceConfig) {
2607
2984
  throw new Error(
2608
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`
@@ -2612,8 +2989,8 @@ async function loadConfig(rootDir, dist) {
2612
2989
  }
2613
2990
 
2614
2991
  // src/cli/loadEnv.ts
2615
- import fs11 from "fs";
2616
- import path10 from "path";
2992
+ import fs12 from "fs";
2993
+ import path13 from "path";
2617
2994
  function resolveEnv() {
2618
2995
  return process.env.NODE_ENV || "development";
2619
2996
  }
@@ -2628,7 +3005,8 @@ function parseEnvFile(content, fileVars) {
2628
3005
  if (!trimmed || trimmed.startsWith("#")) continue;
2629
3006
  const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(trimmed);
2630
3007
  if (!match) continue;
2631
- const [, key, rawValue] = match;
3008
+ const key = match[1];
3009
+ const rawValue = match[2];
2632
3010
  const value = parseValue(rawValue, { ...fileVars, ...result });
2633
3011
  result[key] = value;
2634
3012
  }
@@ -2679,9 +3057,9 @@ function loadEnv(rootDir) {
2679
3057
  const files = getEnvFiles(env);
2680
3058
  const merged = {};
2681
3059
  for (const file of files) {
2682
- const filePath = path10.join(rootDir, file);
2683
- if (!fs11.existsSync(filePath)) continue;
2684
- 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");
2685
3063
  const parsed = parseEnvFile(content, merged);
2686
3064
  Object.assign(merged, parsed);
2687
3065
  }
@@ -2718,14 +3096,14 @@ var ValidationError = class extends FaapiError {
2718
3096
  issues;
2719
3097
  };
2720
3098
  var RouteNotFoundError = class extends FaapiError {
2721
- constructor(path19) {
2722
- super("ROUTE_NOT_FOUND", `Route not found: ${path19}`, 404);
3099
+ constructor(path23) {
3100
+ super("ROUTE_NOT_FOUND", `Route not found: ${path23}`, 404);
2723
3101
  this.name = "RouteNotFoundError";
2724
3102
  }
2725
3103
  };
2726
3104
  var MethodNotAllowedError = class extends FaapiError {
2727
- constructor(method, path19, allowedMethods) {
2728
- 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);
2729
3107
  this.allowedMethods = allowedMethods;
2730
3108
  this.name = "MethodNotAllowedError";
2731
3109
  }
@@ -2752,7 +3130,7 @@ var PayloadTooLargeError = class extends FaapiError {
2752
3130
 
2753
3131
  // src/cli/createAppCore.ts
2754
3132
  import fs15 from "fs";
2755
- import path15 from "path";
3133
+ import path19 from "path";
2756
3134
  import { PassThrough, Readable as Readable3 } from "stream";
2757
3135
 
2758
3136
  // src/router/sortRoutes.ts
@@ -2805,7 +3183,7 @@ import {
2805
3183
  import { createSecureServer as createHttp2SecureServer } from "http2";
2806
3184
  import { readFileSync } from "fs";
2807
3185
  import { Readable as Readable2 } from "stream";
2808
- import path12 from "path";
3186
+ import path15 from "path";
2809
3187
 
2810
3188
  // src/router/matchRoute.ts
2811
3189
  var httpIndexCache = /* @__PURE__ */ new WeakMap();
@@ -2816,7 +3194,10 @@ function getHttpIndex(routes) {
2816
3194
  index = { static: /* @__PURE__ */ new Map(), methodsByStaticPath: /* @__PURE__ */ new Map(), dynamics: [] };
2817
3195
  for (const route of routes) {
2818
3196
  if (route.isDynamic) {
2819
- index.dynamics.push(route);
3197
+ index.dynamics.push({
3198
+ route,
3199
+ segments: route.urlPath.split("/").filter(Boolean)
3200
+ });
2820
3201
  } else {
2821
3202
  index.static.set(`${route.method}|${route.urlPath}`, route);
2822
3203
  let methods = index.methodsByStaticPath.get(route.urlPath);
@@ -2844,57 +3225,77 @@ function getWsIndex(routes) {
2844
3225
  wsIndexCache.set(routes, index);
2845
3226
  return index;
2846
3227
  }
2847
- function matchRoute(routes, method, path19) {
3228
+ function matchRoute(routes, method, path23) {
2848
3229
  const index = getHttpIndex(routes);
2849
- const staticHit = index.static.get(`${method}|${path19}`);
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}`);
2850
3240
  if (staticHit) {
2851
3241
  return { route: staticHit, params: {} };
2852
3242
  }
2853
- for (const route of index.dynamics) {
3243
+ for (const entry of index.dynamics) {
3244
+ const route = entry.route;
2854
3245
  if (route.method !== method) {
2855
3246
  continue;
2856
3247
  }
2857
- const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
3248
+ const params = matchSegments(entry.segments, path23, route.paramNames, route.isCatchAll);
2858
3249
  if (params !== null) {
2859
3250
  return { route, params };
2860
3251
  }
2861
3252
  }
2862
3253
  return null;
2863
3254
  }
2864
- function matchWsRoute(wsRoutes, path19) {
3255
+ function matchWsRoute(wsRoutes, path23) {
2865
3256
  const index = getWsIndex(wsRoutes);
2866
- const staticHit = index.static.get(path19);
3257
+ const staticHit = index.static.get(path23);
2867
3258
  if (staticHit) {
2868
3259
  return { route: staticHit, params: {} };
2869
3260
  }
2870
3261
  for (const route of index.dynamics) {
2871
- const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
3262
+ const params = matchDynamicPath(route.urlPath, path23, route.paramNames, route.isCatchAll);
2872
3263
  if (params !== null) {
2873
3264
  return { route, params };
2874
3265
  }
2875
3266
  }
2876
3267
  return null;
2877
3268
  }
2878
- function findAllowedMethods(routes, path19) {
3269
+ function findAllowedMethods(routes, path23) {
2879
3270
  const index = getHttpIndex(routes);
2880
3271
  const methods = /* @__PURE__ */ new Set();
2881
- const staticMethods = index.methodsByStaticPath.get(path19);
3272
+ const staticMethods = index.methodsByStaticPath.get(path23);
2882
3273
  if (staticMethods) {
2883
3274
  for (const method of staticMethods) {
2884
3275
  methods.add(method);
2885
3276
  }
2886
3277
  }
2887
- for (const route of index.dynamics) {
2888
- const params = matchDynamicPath(route.urlPath, path19, route.paramNames, route.isCatchAll);
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
+ );
2889
3285
  if (params !== null) {
2890
- methods.add(route.method);
3286
+ methods.add(entry.route.method);
2891
3287
  }
2892
3288
  }
3289
+ if (methods.has("GET")) {
3290
+ methods.add("HEAD");
3291
+ }
2893
3292
  return Array.from(methods);
2894
3293
  }
2895
- function matchDynamicPath(pattern, path19, paramNames, isCatchAll) {
2896
- const patternSegments = pattern.split("/").filter(Boolean);
2897
- 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);
2898
3299
  if (isCatchAll) {
2899
3300
  const nonCatchAllCount = patternSegments.length - 1;
2900
3301
  if (pathSegments.length <= nonCatchAllCount) {
@@ -3133,10 +3534,14 @@ function formatErrorResponse(error, config) {
3133
3534
  code: error.code,
3134
3535
  message: error.message
3135
3536
  });
3136
- const bodyObj = typeof body2 === "object" && body2 !== null ? body2 : { error: body2 };
3137
- const errorObj = bodyObj.error ?? bodyObj;
3138
- if (errorObj) {
3139
- 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 };
3140
3545
  }
3141
3546
  return jsonOk(bodyObj, error.statusCode);
3142
3547
  }
@@ -3193,10 +3598,10 @@ function formatSetCookie(name, value, options) {
3193
3598
  if (options?.sameSite) cookie += `; SameSite=${options.sameSite}`;
3194
3599
  return cookie;
3195
3600
  }
3196
- function createContext(request, params, config = {}, ip = "") {
3197
- return createContextFromUrl(request, new URL(request.url), params, config, ip);
3601
+ function createContext(request, params, config = {}, ip = "", registries) {
3602
+ return createContextFromUrl(request, new URL(request.url), params, config, ip, registries);
3198
3603
  }
3199
- function createContextFromUrl(request, url, params, config = {}, ip = "") {
3604
+ function createContextFromUrl(request, url, params, config = {}, ip = "", registries) {
3200
3605
  const meta = { headers: {}, setCookies: [] };
3201
3606
  const parsedCookies = parseCookies(request.headers.get("cookie") ?? "");
3202
3607
  const cookiesObj = {};
@@ -3293,6 +3698,9 @@ function createContextFromUrl(request, url, params, config = {}, ip = "") {
3293
3698
  return formatFailResponse(options, config);
3294
3699
  }
3295
3700
  };
3701
+ if (registries) {
3702
+ ctx.registries = registries;
3703
+ }
3296
3704
  const extend = config?.extendContext;
3297
3705
  if (typeof extend === "function") {
3298
3706
  extend(ctx);
@@ -3304,7 +3712,14 @@ function createContextFromUrl(request, url, params, config = {}, ip = "") {
3304
3712
  function queryToObject(params) {
3305
3713
  const result = {};
3306
3714
  for (const [key, value] of params) {
3307
- 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
+ }
3308
3723
  }
3309
3724
  return result;
3310
3725
  }
@@ -3360,7 +3775,7 @@ async function resolveInputFromUrl(method, request, url) {
3360
3775
  }
3361
3776
  if (contentType.includes("application/x-www-form-urlencoded")) {
3362
3777
  const text2 = await request.text();
3363
- if (text2.trim() === "") return null;
3778
+ if (isBlankText(text2)) return null;
3364
3779
  const params = new URLSearchParams(text2);
3365
3780
  const obj = {};
3366
3781
  for (const [key, value] of params) {
@@ -3369,7 +3784,7 @@ async function resolveInputFromUrl(method, request, url) {
3369
3784
  return obj;
3370
3785
  }
3371
3786
  const text = await request.text();
3372
- if (text.trim() === "") {
3787
+ if (isBlankText(text)) {
3373
3788
  return null;
3374
3789
  }
3375
3790
  const result = parseJsonBody(text);
@@ -3388,6 +3803,26 @@ async function resolveInputFromUrl(method, request, url) {
3388
3803
  }
3389
3804
  return queryToObject(url.searchParams);
3390
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
+ }
3391
3826
 
3392
3827
  // src/utils/isPlainObject.ts
3393
3828
  function isPlainObject(value) {
@@ -3401,6 +3836,25 @@ function isPlainObject(value) {
3401
3836
  return proto === null || proto === Object.prototype;
3402
3837
  }
3403
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
+
3404
3858
  // src/response/toResponse.ts
3405
3859
  async function toResponse(value, meta) {
3406
3860
  if (value instanceof Promise) {
@@ -3417,6 +3871,10 @@ async function toResponse(value, meta) {
3417
3871
  };
3418
3872
  if (value instanceof Response) {
3419
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
+ }
3420
3878
  const headers2 = new Headers(value.headers);
3421
3879
  applyMeta(headers2);
3422
3880
  return new Response(value.body, {
@@ -3525,11 +3983,12 @@ function getBuiltinInjectionValue(type, ctx, body) {
3525
3983
  }
3526
3984
  return {};
3527
3985
  // Phase 2.3:注入所有已注册 agent 元数据列表
3986
+ // 方案 A:优先读 app 实例注册表,无实例(编程式直调 ctx)回退默认全局实例
3528
3987
  case "agents":
3529
- return listAgents();
3988
+ return ctx.registries ? ctx.registries.agent.listAgents() : listAgents();
3530
3989
  // Phase 3.5:调 @faapi/agent 插件注册的工厂获取 AgentHandle
3531
3990
  case "agent":
3532
- return getAgentHandle(ctx);
3991
+ return ctx.registries ? ctx.registries.agentHandle.get(ctx) : getAgentHandle(ctx);
3533
3992
  default:
3534
3993
  return void 0;
3535
3994
  }
@@ -3560,6 +4019,10 @@ function wrapResult(result, ctx) {
3560
4019
  function mergeMeta(response, meta) {
3561
4020
  const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
3562
4021
  if (!hasMeta) return response;
4022
+ if (isHeadersOnlyMeta(meta)) {
4023
+ deferMetaHeaders(response, meta.headers);
4024
+ return response;
4025
+ }
3563
4026
  const headers = new Headers(response.headers);
3564
4027
  for (const [key, value] of Object.entries(meta.headers)) {
3565
4028
  headers.set(key, value);
@@ -3652,11 +4115,24 @@ async function sendNodeResponse(response, res) {
3652
4115
  res.setHeader(key, value);
3653
4116
  }
3654
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
+ }
3655
4124
  if (response.body) {
3656
4125
  const nodeStream = Readable.fromWeb(response.body);
3657
4126
  await new Promise((resolve, reject) => {
3658
4127
  nodeStream.on("error", reject);
3659
- 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
+ });
3660
4136
  res.on("finish", resolve);
3661
4137
  nodeStream.pipe(res);
3662
4138
  });
@@ -3691,28 +4167,25 @@ async function validateInput(schemaPath, method, inputType, input) {
3691
4167
  }
3692
4168
  const schema = mod[schemaKey];
3693
4169
  if (schema === void 0 || schema === null) {
3694
- const data = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
3695
- return { valid: true, issues: [], data };
4170
+ return { valid: true, issues: [], data: input };
3696
4171
  }
3697
4172
  if (typeof schema !== "object" || typeof schema.safeParse !== "function") {
3698
4173
  throw new InternalError(`Schema \u4E0D\u662F\u6709\u6548\u7684 zod schema: ${schemaPath}#${schemaName}`);
3699
4174
  }
3700
- const inputObj = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
3701
4175
  const zodSchema = schema;
3702
- const result = zodSchema.safeParse(inputObj);
4176
+ const result = zodSchema.safeParse(input);
3703
4177
  if (result.success) {
3704
- const data = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : {};
3705
- return { valid: true, issues: [], data };
4178
+ return { valid: true, issues: [], data: result.data };
3706
4179
  }
3707
4180
  const issues = mapZodIssues(result.error);
3708
- return { valid: false, issues, data: inputObj };
4181
+ return { valid: false, issues, data: input };
3709
4182
  }
3710
4183
  function mapZodIssues(error) {
3711
4184
  return error.issues.map((issue) => {
3712
- const code = mapZodCode(issue.code, issue.message);
3713
- const path19 = issue.path.map(String).join(".") || "";
4185
+ const code = mapZodCode(issue);
4186
+ const path23 = issue.path.map(String).join(".") || "";
3714
4187
  return {
3715
- path: path19,
4188
+ path: path23,
3716
4189
  code,
3717
4190
  expected: issue.expected ?? mapExpectedFromMessage(issue.message),
3718
4191
  received: issue.received ?? mapReceivedFromMessage(issue.message),
@@ -3720,27 +4193,29 @@ function mapZodIssues(error) {
3720
4193
  };
3721
4194
  });
3722
4195
  }
3723
- function mapZodCode(zodCode, message) {
3724
- switch (zodCode) {
4196
+ function mapZodCode(issue) {
4197
+ switch (issue.code) {
3725
4198
  case "invalid_type":
4199
+ if (issue.received === "undefined" || /received undefined/i.test(issue.message)) {
4200
+ return "MISSING_FIELD";
4201
+ }
4202
+ return "TYPE_MISMATCH";
3726
4203
  case "invalid_union":
3727
4204
  case "invalid_union_discriminator":
3728
4205
  return "TYPE_MISMATCH";
3729
4206
  case "unrecognized_keys":
3730
4207
  return "INVALID_FORMAT";
3731
4208
  case "invalid_value":
3732
- case "invalid_string":
4209
+ case "invalid_format":
4210
+ case "invalid_key":
4211
+ case "invalid_element":
3733
4212
  case "too_small":
3734
4213
  case "too_big":
3735
4214
  case "invalid_intersection_types":
3736
4215
  case "not_multiple_of":
3737
- return "INVALID_VALUE";
3738
4216
  case "custom":
3739
4217
  return "INVALID_VALUE";
3740
4218
  default:
3741
- if (message.includes("Required") || message.includes("required")) {
3742
- return "MISSING_FIELD";
3743
- }
3744
4219
  return "INVALID_VALUE";
3745
4220
  }
3746
4221
  }
@@ -3775,10 +4250,176 @@ function getClientIp(req, trustedProxy = false) {
3775
4250
  return "";
3776
4251
  }
3777
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
+
3778
4419
  // src/server/handleWsUpgrade.ts
3779
- import fs12 from "fs";
4420
+ import fs13 from "fs";
3780
4421
  import { WebSocketServer, WebSocket } from "ws";
3781
- import path11 from "path";
4422
+ import path14 from "path";
3782
4423
 
3783
4424
  // src/server/serverUtils.ts
3784
4425
  function nodeHttpToWebHeaders(req) {
@@ -3809,8 +4450,10 @@ function buildErrorResponse(err, config) {
3809
4450
 
3810
4451
  // src/middleware/loadMiddlewares.ts
3811
4452
  var middlewareCache = /* @__PURE__ */ new Map();
4453
+ var inFlight = /* @__PURE__ */ new Map();
3812
4454
  function invalidateMiddlewareCache() {
3813
4455
  middlewareCache.clear();
4456
+ inFlight.clear();
3814
4457
  }
3815
4458
  function getCachedMiddlewares(absPath) {
3816
4459
  return middlewareCache.get(absPath);
@@ -3863,8 +4506,15 @@ async function loadMergedMiddlewares(middlewarePaths) {
3863
4506
  for (const absMwPath of middlewarePaths) {
3864
4507
  let bundle = getCachedMiddlewares(absMwPath);
3865
4508
  if (bundle === void 0) {
3866
- bundle = await loadMiddlewaresFile(absMwPath);
3867
- 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;
3868
4518
  }
3869
4519
  mergedMiddlewares.push(...bundle.middlewares);
3870
4520
  for (const [name, injector] of Object.entries(bundle.injectors)) {
@@ -3904,7 +4554,7 @@ async function loadWsHandler(filePath, ctx, rootDir) {
3904
4554
  const dist = getDevDist();
3905
4555
  if (dist) {
3906
4556
  const sourcePath = prodPathToSourcePath(filePath, rootDir, dist);
3907
- if (sourcePath && fs12.existsSync(sourcePath)) {
4557
+ if (sourcePath && fs13.existsSync(sourcePath)) {
3908
4558
  await ensureCompiled(sourcePath, rootDir, dist);
3909
4559
  }
3910
4560
  }
@@ -3927,9 +4577,9 @@ function bindEvents(rawSocket, handlers) {
3927
4577
  }
3928
4578
  }
3929
4579
  if (handlers.onMessage) {
3930
- rawSocket.on("message", (data) => {
3931
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
3932
- 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"));
3933
4583
  });
3934
4584
  }
3935
4585
  if (handlers.onClose) {
@@ -3943,12 +4593,22 @@ function bindEvents(rawSocket, handlers) {
3943
4593
  });
3944
4594
  }
3945
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
+ }
3946
4601
  async function sendResponseToSocket(socket, response) {
3947
4602
  const body = await response.text().catch(() => "");
3948
4603
  const statusLine = `HTTP/1.1 ${response.status} ${response.statusText || ""}\r
3949
4604
  `;
3950
4605
  const headerLines = [];
3951
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
+ }
3952
4612
  for (const [key, value] of response.headers) {
3953
4613
  if (key.toLowerCase() === "content-length") {
3954
4614
  hasContentLength = true;
@@ -3962,9 +4622,33 @@ async function sendResponseToSocket(socket, response) {
3962
4622
  socket.destroy();
3963
4623
  }
3964
4624
  function attachWebSocket(options) {
3965
- const { server, routesRef, rootDir, config, globalMiddlewares, trustedProxy = false } = options;
4625
+ const {
4626
+ server,
4627
+ routesRef,
4628
+ rootDir,
4629
+ config,
4630
+ globalMiddlewares,
4631
+ trustedProxy = false,
4632
+ registries
4633
+ } = options;
3966
4634
  const wss = new WebSocketServer({ noServer: true });
3967
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) {
3968
4652
  const currentWsRoutes = routesRef.wsCurrent;
3969
4653
  const pathname = getPathname(req);
3970
4654
  const match = matchWsRoute(currentWsRoutes, pathname);
@@ -3978,13 +4662,13 @@ function attachWebSocket(options) {
3978
4662
  const host = req.headers.host ?? "localhost";
3979
4663
  const url = `http://${host}${req.url ?? "/"}`;
3980
4664
  const request = new Request(url, { method: "GET", headers });
3981
- const ctx = createContext(request, params, config, getClientIp(req, trustedProxy));
4665
+ const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
3982
4666
  const meta = ctx.meta;
3983
4667
  let upgraded = false;
3984
4668
  const finalHandler = async () => {
3985
4669
  let handlers;
3986
4670
  try {
3987
- const absoluteFilePath = path11.resolve(rootDir, route.filePath);
4671
+ const absoluteFilePath = path14.resolve(rootDir, route.filePath);
3988
4672
  handlers = await loadWsHandler(absoluteFilePath, ctx, rootDir);
3989
4673
  } catch (err) {
3990
4674
  const reason = err instanceof Error ? err.message : String(err);
@@ -4008,6 +4692,7 @@ function attachWebSocket(options) {
4008
4692
  let response;
4009
4693
  try {
4010
4694
  if (route.middlewares === void 0 && route.middlewarePaths) {
4695
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
4011
4696
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
4012
4697
  if (bundle) {
4013
4698
  route.middlewares = bundle.middlewares;
@@ -4033,14 +4718,14 @@ function attachWebSocket(options) {
4033
4718
  return;
4034
4719
  }
4035
4720
  await sendResponseToSocket(socket, mergeMeta(response, meta));
4036
- });
4721
+ }
4037
4722
  return wss;
4038
4723
  }
4039
4724
 
4040
4725
  // src/server/createServer.ts
4041
4726
  init_generateSchemaFiles();
4042
4727
  var DEFAULT_BODY_LIMIT = 10 * 1024 * 1024;
4043
- function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4728
+ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT, requestSignal) {
4044
4729
  const forwardedProto = req.headers["x-forwarded-proto"];
4045
4730
  const protocol = Array.isArray(forwardedProto) ? forwardedProto[0]?.split(",")[0]?.trim() ?? "http" : forwardedProto?.split(",")[0]?.trim() ?? "http";
4046
4731
  const host = req.headers.host ?? "localhost";
@@ -4048,7 +4733,10 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4048
4733
  const headers = nodeHttpToWebHeaders(req);
4049
4734
  const method = req.method ?? "GET";
4050
4735
  if (method === "GET" || method === "HEAD") {
4051
- return { request: new Request(url.toString(), { method, headers }), url };
4736
+ return {
4737
+ request: new Request(url.toString(), { method, headers, signal: requestSignal }),
4738
+ url
4739
+ };
4052
4740
  }
4053
4741
  const contentLength = req.headers["content-length"];
4054
4742
  if (contentLength !== void 0) {
@@ -4064,7 +4752,8 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
4064
4752
  method,
4065
4753
  headers,
4066
4754
  body: limitedStream,
4067
- duplex: "half"
4755
+ duplex: "half",
4756
+ signal: requestSignal
4068
4757
  }),
4069
4758
  url
4070
4759
  };
@@ -4131,6 +4820,9 @@ function createServer(options) {
4131
4820
  middlewares: globalMiddlewares,
4132
4821
  injectors: globalInjectors,
4133
4822
  helmet: helmetOption,
4823
+ compression: compressionOption,
4824
+ etag: etagOption,
4825
+ registries,
4134
4826
  logger: loggerOption,
4135
4827
  bodyLimit = DEFAULT_BODY_LIMIT,
4136
4828
  http2: http2Option,
@@ -4138,6 +4830,10 @@ function createServer(options) {
4138
4830
  } = options;
4139
4831
  const routesRef = { current: routes, wsCurrent: wsRoutes ?? [] };
4140
4832
  const configMiddlewares = [];
4833
+ if (compressionOption) {
4834
+ const compOpts = typeof compressionOption === "object" ? compressionOption : {};
4835
+ configMiddlewares.push(compression(compOpts));
4836
+ }
4141
4837
  const corsMiddleware = corsOption === false ? null : corsOption === true || corsOption === void 0 ? cors() : cors(corsOption);
4142
4838
  if (corsMiddleware) configMiddlewares.push(corsMiddleware);
4143
4839
  if (helmetOption) {
@@ -4146,6 +4842,10 @@ function createServer(options) {
4146
4842
  }
4147
4843
  const loggerMiddlewareInst = loggerOption === false ? null : loggerOption === true || loggerOption === void 0 ? logger() : logger(loggerOption);
4148
4844
  if (loggerMiddlewareInst) configMiddlewares.push(loggerMiddlewareInst);
4845
+ if (etagOption) {
4846
+ const etagOpts = typeof etagOption === "object" ? etagOption : {};
4847
+ configMiddlewares.push(etag(etagOpts));
4848
+ }
4149
4849
  const outerMiddlewares = [...configMiddlewares];
4150
4850
  if (globalMiddlewares && globalMiddlewares.length > 0) {
4151
4851
  outerMiddlewares.push(...globalMiddlewares);
@@ -4174,22 +4874,36 @@ function createServer(options) {
4174
4874
  config,
4175
4875
  globalInjectors,
4176
4876
  bodyLimit,
4177
- trustedProxy
4877
+ trustedProxy,
4878
+ registries
4178
4879
  ).catch(() => {
4179
4880
  res.statusCode = 500;
4180
4881
  res.end();
4181
4882
  });
4182
4883
  });
4183
- if (routesRef.wsCurrent.length > 0) {
4184
- attachWebSocket({ server, routesRef, rootDir, config, globalMiddlewares, trustedProxy });
4185
- }
4884
+ attachWebSocket({
4885
+ server,
4886
+ routesRef,
4887
+ rootDir,
4888
+ config,
4889
+ globalMiddlewares,
4890
+ trustedProxy,
4891
+ registries
4892
+ });
4186
4893
  return { server, routesRef };
4187
4894
  }
4188
- function prepareRequest(req, config, bodyLimit, trustedProxy) {
4189
- const { request, url } = toWebRequest(req, bodyLimit);
4895
+ function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
4896
+ const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
4190
4897
  const method = request.method.toUpperCase();
4191
4898
  const urlPath = url.pathname;
4192
- const ctx = createContextFromUrl(request, url, {}, config, getClientIp(req, trustedProxy));
4899
+ const ctx = createContextFromUrl(
4900
+ request,
4901
+ url,
4902
+ {},
4903
+ config,
4904
+ getClientIp(req, trustedProxy),
4905
+ registries
4906
+ );
4193
4907
  const meta = ctx.meta;
4194
4908
  return { request, url, ctx, meta, method, urlPath };
4195
4909
  }
@@ -4202,17 +4916,28 @@ function resolveRouteOrThrow(routes, method, urlPath) {
4202
4916
  }
4203
4917
  throw new RouteNotFoundError(urlPath);
4204
4918
  }
4919
+ var routePathCache = /* @__PURE__ */ new WeakMap();
4920
+ function getRoutePaths(route, rootDir, dist) {
4921
+ let cached = routePathCache.get(route);
4922
+ if (!cached) {
4923
+ cached = {
4924
+ absFilePath: path15.resolve(rootDir, route.filePath),
4925
+ schemaPath: getRuntimeSchemaPath(route.filePath, dist, rootDir)
4926
+ };
4927
+ routePathCache.set(route, cached);
4928
+ }
4929
+ return cached;
4930
+ }
4205
4931
  function createRoutePipeline(opts) {
4206
4932
  const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
4207
4933
  return async () => {
4208
4934
  const match = resolveRouteOrThrow(routes, method, urlPath);
4209
4935
  ctx.params = match.params;
4210
4936
  const { route } = match;
4211
- const absoluteFilePath = path12.resolve(rootDir, route.filePath);
4212
- const routeModule = await loadRouteModule(absoluteFilePath, route.method, rootDir);
4937
+ const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
4938
+ const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
4213
4939
  const input = await resolveInputFromUrl(route.method, request, url);
4214
4940
  const inputType = getInputTypeForMethod(route.method);
4215
- const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
4216
4941
  if (isDevOnDemandEnabled()) {
4217
4942
  const devDist = getDevDist();
4218
4943
  if (devDist) {
@@ -4223,8 +4948,9 @@ function createRoutePipeline(opts) {
4223
4948
  if (!result.valid) {
4224
4949
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
4225
4950
  }
4226
- const body = hasBody(route.method) ? result.data : void 0;
4951
+ const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
4227
4952
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
4953
+ await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
4228
4954
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
4229
4955
  if (bundle) {
4230
4956
  route.middlewares = bundle.middlewares;
@@ -4250,11 +4976,22 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
4250
4976
  }
4251
4977
  }
4252
4978
  }
4253
- async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy) {
4979
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
4254
4980
  let meta = { headers: {}, setCookies: [] };
4255
4981
  let ctx;
4982
+ const abortController = new AbortController();
4983
+ res.on("close", () => {
4984
+ if (!res.writableEnded) abortController.abort();
4985
+ });
4256
4986
  try {
4257
- const prepared = prepareRequest(req, config, bodyLimit, trustedProxy);
4987
+ const prepared = prepareRequest(
4988
+ req,
4989
+ config,
4990
+ bodyLimit,
4991
+ trustedProxy,
4992
+ registries,
4993
+ abortController.signal
4994
+ );
4258
4995
  ctx = prepared.ctx;
4259
4996
  meta = prepared.meta;
4260
4997
  const { request, url, method, urlPath } = prepared;
@@ -4272,6 +5009,7 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
4272
5009
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
4273
5010
  await sendSuccessResponse(response, res);
4274
5011
  } catch (err) {
5012
+ if (res.destroyed || res.writableEnded) return;
4275
5013
  await sendErrorResponse(err, meta, res, onError, ctx);
4276
5014
  }
4277
5015
  }
@@ -4305,8 +5043,8 @@ function applyPluginWrappers(server, handlerWrappers, upgradeWrappers) {
4305
5043
  }
4306
5044
 
4307
5045
  // src/cli/generateRoutes.ts
4308
- import fs13 from "fs";
4309
- import path13 from "path";
5046
+ import fs14 from "fs";
5047
+ import path16 from "path";
4310
5048
  async function hydrateRoutes(manifest) {
4311
5049
  const hydrateRoute = (serialized) => ({
4312
5050
  method: serialized.method,
@@ -4331,11 +5069,10 @@ async function hydrateRoutes(manifest) {
4331
5069
  }
4332
5070
 
4333
5071
  // src/cli/generateAgentArtifacts.ts
4334
- import path14 from "path";
4335
- import fs14 from "fs/promises";
5072
+ import path17 from "path";
4336
5073
 
4337
5074
  // src/ast/extractAgentMetadata.ts
4338
- import ts8 from "typescript";
5075
+ import ts9 from "typescript";
4339
5076
  function extractAgentMetadata(program, filePath, pathMeta) {
4340
5077
  const sourceFile = program.getSourceFile(filePath);
4341
5078
  if (!sourceFile) return null;
@@ -4351,9 +5088,9 @@ function extractAgentMetadata(program, filePath, pathMeta) {
4351
5088
  jsDocOwner = runNode;
4352
5089
  }
4353
5090
  }
4354
- const jsDoc = jsDocOwner ? getJSDocFromNode2(jsDocOwner) : void 0;
4355
- const description = extractDescription2(jsDoc);
4356
- const agentNameOverride = extractAgentTagValue(jsDoc);
5091
+ const jsDoc = jsDocOwner ? getJSDocFromNode(jsDocOwner) : void 0;
5092
+ const description = extractDescription(jsDoc);
5093
+ const agentNameOverride = extractJSDocTagValue(jsDoc, "agent");
4357
5094
  let systemPrompt;
4358
5095
  let tools;
4359
5096
  let agents;
@@ -4381,22 +5118,22 @@ function extractAgentMetadata(program, filePath, pathMeta) {
4381
5118
  }
4382
5119
  function findConfigExport(sourceFile) {
4383
5120
  let result = null;
4384
- ts8.forEachChild(sourceFile, (node) => {
5121
+ ts9.forEachChild(sourceFile, (node) => {
4385
5122
  if (result) return;
4386
- if (ts8.isVariableStatement(node) && hasExportModifier2(node)) {
5123
+ if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
4387
5124
  for (const decl of node.declarationList.declarations) {
4388
5125
  if (result) break;
4389
- const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : "";
5126
+ const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
4390
5127
  if (nameText !== "config" || !decl.initializer) continue;
4391
- if (ts8.isObjectLiteralExpression(decl.initializer)) {
5128
+ if (ts9.isObjectLiteralExpression(decl.initializer)) {
4392
5129
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
4393
- } else if (ts8.isArrowFunction(decl.initializer)) {
5130
+ } else if (ts9.isArrowFunction(decl.initializer)) {
4394
5131
  const returnObj = getReturnObjectLiteral(decl.initializer);
4395
5132
  result = { jsDocOwner: node, objectLiteral: returnObj };
4396
5133
  }
4397
5134
  }
4398
5135
  }
4399
- if (ts8.isFunctionDeclaration(node) && hasExportModifier2(node) && node.name?.text === "config") {
5136
+ if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
4400
5137
  const returnObj = getReturnObjectLiteral(node);
4401
5138
  result = { jsDocOwner: node, objectLiteral: returnObj };
4402
5139
  }
@@ -4405,18 +5142,18 @@ function findConfigExport(sourceFile) {
4405
5142
  }
4406
5143
  function findRunExport(sourceFile) {
4407
5144
  let result = null;
4408
- ts8.forEachChild(sourceFile, (node) => {
5145
+ ts9.forEachChild(sourceFile, (node) => {
4409
5146
  if (result) return;
4410
- if (ts8.isFunctionDeclaration(node) && hasExportModifier2(node) && node.name?.text === "run") {
5147
+ if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "run") {
4411
5148
  result = node;
4412
5149
  return;
4413
5150
  }
4414
- if (ts8.isVariableStatement(node) && hasExportModifier2(node)) {
5151
+ if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
4415
5152
  for (const decl of node.declarationList.declarations) {
4416
5153
  if (result) break;
4417
- const nameText = ts8.isIdentifier(decl.name) ? decl.name.text : "";
5154
+ const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : "";
4418
5155
  if (nameText !== "run" || !decl.initializer) continue;
4419
- if (ts8.isArrowFunction(decl.initializer) || ts8.isFunctionExpression(decl.initializer)) {
5156
+ if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
4420
5157
  result = node;
4421
5158
  }
4422
5159
  }
@@ -4427,52 +5164,22 @@ function findRunExport(sourceFile) {
4427
5164
  function getReturnObjectLiteral(fn) {
4428
5165
  const body = fn.body;
4429
5166
  if (!body) return null;
4430
- if (ts8.isObjectLiteralExpression(body)) {
5167
+ if (ts9.isObjectLiteralExpression(body)) {
4431
5168
  return body;
4432
5169
  }
4433
- if (ts8.isBlock(body)) {
5170
+ if (ts9.isBlock(body)) {
4434
5171
  for (const stmt of body.statements) {
4435
- if (ts8.isReturnStatement(stmt) && stmt.expression && ts8.isObjectLiteralExpression(stmt.expression)) {
5172
+ if (ts9.isReturnStatement(stmt) && stmt.expression && ts9.isObjectLiteralExpression(stmt.expression)) {
4436
5173
  return stmt.expression;
4437
5174
  }
4438
5175
  }
4439
5176
  }
4440
5177
  return null;
4441
5178
  }
4442
- function hasExportModifier2(node) {
4443
- if (!ts8.canHaveModifiers(node)) return false;
4444
- const modifiers = ts8.getModifiers(node);
4445
- return !!modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword);
4446
- }
4447
- function getJSDocFromNode2(node) {
4448
- const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
4449
- if (apiDocs.length > 0) return apiDocs[0];
4450
- const directDocs = node.jsDoc;
4451
- if (directDocs && directDocs.length > 0) return directDocs[0];
4452
- return void 0;
4453
- }
4454
- function extractDescription2(jsDoc) {
4455
- if (!jsDoc) return void 0;
4456
- if (typeof jsDoc.comment !== "string") return void 0;
4457
- const trimmed = jsDoc.comment.trim();
4458
- return trimmed || void 0;
4459
- }
4460
- function extractAgentTagValue(jsDoc) {
4461
- if (!jsDoc || !jsDoc.tags) return void 0;
4462
- for (const tag of jsDoc.tags) {
4463
- if (tag.tagName.text !== "agent") continue;
4464
- if (typeof tag.comment !== "string") return void 0;
4465
- const text = tag.comment.trim();
4466
- if (!text) return void 0;
4467
- const cleaned = text.replace(/^\{|\}$/g, "").trim();
4468
- return cleaned || void 0;
4469
- }
4470
- return void 0;
4471
- }
4472
5179
  function extractConfigFields(objLit) {
4473
5180
  const result = {};
4474
5181
  for (const prop of objLit.properties) {
4475
- if (!ts8.isPropertyAssignment(prop)) continue;
5182
+ if (!ts9.isPropertyAssignment(prop)) continue;
4476
5183
  const propName = getPropertyName(prop.name);
4477
5184
  if (!propName) continue;
4478
5185
  switch (propName) {
@@ -4496,26 +5203,26 @@ function extractConfigFields(objLit) {
4496
5203
  return result;
4497
5204
  }
4498
5205
  function getPropertyName(name) {
4499
- if (ts8.isIdentifier(name)) return name.text;
4500
- if (ts8.isStringLiteral(name)) return name.text;
5206
+ if (ts9.isIdentifier(name)) return name.text;
5207
+ if (ts9.isStringLiteral(name)) return name.text;
4501
5208
  return null;
4502
5209
  }
4503
5210
  function extractStringValue(expr) {
4504
- if (ts8.isStringLiteral(expr)) return expr.text;
5211
+ if (ts9.isStringLiteral(expr)) return expr.text;
4505
5212
  return void 0;
4506
5213
  }
4507
5214
  function extractNumberValue(expr) {
4508
- if (ts8.isNumericLiteral(expr)) {
5215
+ if (ts9.isNumericLiteral(expr)) {
4509
5216
  const num = Number(expr.text);
4510
5217
  return Number.isNaN(num) ? void 0 : num;
4511
5218
  }
4512
5219
  return void 0;
4513
5220
  }
4514
5221
  function extractStringArrayValue(expr) {
4515
- if (!ts8.isArrayLiteralExpression(expr)) return void 0;
5222
+ if (!ts9.isArrayLiteralExpression(expr)) return void 0;
4516
5223
  const values = [];
4517
5224
  for (const element of expr.elements) {
4518
- if (!ts8.isStringLiteral(element)) return void 0;
5225
+ if (!ts9.isStringLiteral(element)) return void 0;
4519
5226
  values.push(element.text);
4520
5227
  }
4521
5228
  return values;
@@ -4523,15 +5230,8 @@ function extractStringArrayValue(expr) {
4523
5230
 
4524
5231
  // src/cli/generateAgentArtifacts.ts
4525
5232
  init_createProgram();
5233
+ init_atomicWrite();
4526
5234
  var AGENTS_FILE = "faapi-agents.js";
4527
- function toProdFilePath2(filePath, dist) {
4528
- let rel = filePath.replace(/\\/g, "/");
4529
- if (rel.startsWith("src/")) {
4530
- rel = rel.slice(4);
4531
- }
4532
- const jsPath = rel.replace(/\.ts$/, ".js");
4533
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
4534
- }
4535
5235
  function serializeAgents(agents, dist = "dist") {
4536
5236
  return agents.map((a) => ({
4537
5237
  name: a.name,
@@ -4542,16 +5242,14 @@ function serializeAgents(agents, dist = "dist") {
4542
5242
  agents: a.agents,
4543
5243
  model: a.model,
4544
5244
  maxTurns: a.maxTurns,
4545
- filePath: toProdFilePath2(a.filePath, dist)
5245
+ filePath: toProdFilePath(a.filePath, dist)
4546
5246
  }));
4547
5247
  }
4548
5248
  async function writeAgentsModule(manifest, outputPath) {
4549
- const dir = path14.dirname(outputPath);
4550
- await fs14.mkdir(dir, { recursive: true });
4551
5249
  const content = `// \u81EA\u52A8\u751F\u6210,\u8BF7\u52FF\u624B\u52A8\u7F16\u8F91(faapi build/dev \u4EA7\u7269)
4552
5250
  export const agents = ${JSON.stringify(manifest, null, 2)};
4553
5251
  `;
4554
- await fs14.writeFile(outputPath, content, "utf-8");
5252
+ await atomicWriteFile(outputPath, content);
4555
5253
  }
4556
5254
  function hydrateAgents(manifest) {
4557
5255
  return manifest.map((a) => ({
@@ -4568,9 +5266,9 @@ function hydrateAgents(manifest) {
4568
5266
  }
4569
5267
  async function generateAgentArtifacts(agents, rootDir, dist) {
4570
5268
  const metadata = [];
4571
- const programByFile = createPrograms(agents.map((m) => path14.resolve(rootDir, m.filePath)));
5269
+ const programByFile = createPrograms(agents.map((m) => path17.resolve(rootDir, m.filePath)));
4572
5270
  for (const manifest of agents) {
4573
- const absPath = path14.resolve(rootDir, manifest.filePath);
5271
+ const absPath = path17.resolve(rootDir, manifest.filePath);
4574
5272
  const program = programByFile.get(absPath);
4575
5273
  const result = extractAgentMetadata(program, absPath, {
4576
5274
  name: manifest.name,
@@ -4582,17 +5280,20 @@ async function generateAgentArtifacts(agents, rootDir, dist) {
4582
5280
  }
4583
5281
  }
4584
5282
  const serialized = serializeAgents(metadata, dist);
4585
- const agentsPath = path14.resolve(rootDir, dist, AGENTS_FILE);
5283
+ const agentsPath = path17.resolve(rootDir, dist, AGENTS_FILE);
4586
5284
  await writeAgentsModule(serialized, agentsPath);
4587
5285
  return metadata;
4588
5286
  }
4589
5287
 
4590
5288
  // src/cli/loadPlugins.ts
4591
- async function loadPlugins(declarations, ctx) {
5289
+ import path18 from "path";
5290
+ import { pathToFileURL as pathToFileURL2 } from "url";
5291
+ async function loadPlugins(declarations, ctx, rootDir) {
4592
5292
  const handlerWrappers = [];
4593
5293
  const upgradeWrappers = [];
5294
+ const failures = [];
4594
5295
  if (!declarations || declarations.length === 0) {
4595
- return { handlerWrappers, upgradeWrappers };
5296
+ return { handlerWrappers, upgradeWrappers, failures };
4596
5297
  }
4597
5298
  const fullCtx = {
4598
5299
  ...ctx,
@@ -4605,7 +5306,18 @@ async function loadPlugins(declarations, ctx) {
4605
5306
  };
4606
5307
  const loaded = /* @__PURE__ */ new Set();
4607
5308
  for (const decl of declarations) {
4608
- const { specifier, options, enable } = resolveDeclaration(decl);
5309
+ let specifier;
5310
+ let options;
5311
+ let enable;
5312
+ try {
5313
+ ({ specifier, options, enable } = resolveDeclaration(decl));
5314
+ } catch (err) {
5315
+ failures.push({
5316
+ specifier: JSON.stringify(decl),
5317
+ reason: err instanceof Error ? err.message : String(err)
5318
+ });
5319
+ continue;
5320
+ }
4609
5321
  if (enable === false) continue;
4610
5322
  if (loaded.has(specifier)) {
4611
5323
  console.warn(`! Plugin already loaded: ${specifier}, skipping`);
@@ -4613,21 +5325,38 @@ async function loadPlugins(declarations, ctx) {
4613
5325
  }
4614
5326
  loaded.add(specifier);
4615
5327
  try {
4616
- const mod = await import(specifier);
5328
+ const mod = await import(resolveSpecifier(specifier, rootDir));
4617
5329
  const plugin = mod.default ?? mod;
4618
5330
  if (typeof plugin.setup !== "function") {
4619
- console.warn(`! Plugin ${specifier} has no setup function, skipping`);
5331
+ failures.push({ specifier, reason: "plugin has no setup function" });
4620
5332
  continue;
4621
5333
  }
4622
5334
  await plugin.setup({ ...fullCtx, options });
4623
5335
  console.log(`- Plugin loaded: ${plugin.name ?? specifier}`);
4624
5336
  } catch (err) {
4625
- console.warn(
4626
- `! Failed to load plugin ${specifier}: ${err instanceof Error ? err.message : String(err)}`
4627
- );
5337
+ failures.push({
5338
+ specifier,
5339
+ reason: err instanceof Error ? err.message : String(err)
5340
+ });
4628
5341
  }
4629
5342
  }
4630
- return { handlerWrappers, upgradeWrappers };
5343
+ if (failures.length > 0) {
5344
+ console.error(
5345
+ `[faapi] ${failures.length} plugin(s) failed to load:
5346
+ ` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
5347
+ );
5348
+ }
5349
+ return { handlerWrappers, upgradeWrappers, failures };
5350
+ }
5351
+ function resolveSpecifier(specifier, rootDir) {
5352
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
5353
+ const base = rootDir ?? process.cwd();
5354
+ return pathToFileURL2(path18.resolve(base, specifier)).href;
5355
+ }
5356
+ if (path18.isAbsolute(specifier)) {
5357
+ return pathToFileURL2(specifier).href;
5358
+ }
5359
+ return specifier;
4631
5360
  }
4632
5361
  function resolveDeclaration(decl) {
4633
5362
  if (typeof decl === "string") {
@@ -4652,25 +5381,24 @@ var DEFAULT_PORT = 3e3;
4652
5381
  var ROUTES_FILE = "faapi-routes.js";
4653
5382
  var TOOLS_FILE2 = "faapi-tools.js";
4654
5383
  var AGENTS_FILE2 = "faapi-agents.js";
4655
- var PATTERNS = ["src/api/**/*.ts"];
4656
- async function loadAndHydrateTools(rootDir, dist) {
4657
- const toolsPath = path15.resolve(rootDir, dist, TOOLS_FILE2);
5384
+ async function loadAndHydrateTools(rootDir, dist, registries = defaultRegistries) {
5385
+ const toolsPath = path19.resolve(rootDir, dist, TOOLS_FILE2);
4658
5386
  if (!fs15.existsSync(toolsPath)) {
4659
5387
  return [];
4660
5388
  }
4661
5389
  const serialized = await importWithCacheBust(toolsPath);
4662
5390
  const hydrated = hydrateTools(serialized.tools ?? []);
4663
- hydrateToolRegistry(hydrated);
5391
+ registries.tool.hydrate(hydrated);
4664
5392
  return hydrated;
4665
5393
  }
4666
- async function loadAndHydrateAgents(rootDir, dist) {
4667
- const agentsPath = path15.resolve(rootDir, dist, AGENTS_FILE2);
5394
+ async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistries) {
5395
+ const agentsPath = path19.resolve(rootDir, dist, AGENTS_FILE2);
4668
5396
  if (!fs15.existsSync(agentsPath)) {
4669
5397
  return [];
4670
5398
  }
4671
5399
  const serialized = await importWithCacheBust(agentsPath);
4672
5400
  const hydrated = hydrateAgents(serialized.agents ?? []);
4673
- hydrateAgentRegistry(hydrated);
5401
+ registries.agent.hydrate(hydrated);
4674
5402
  return hydrated;
4675
5403
  }
4676
5404
  var APP_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("faapi.app.instance");
@@ -4684,6 +5412,23 @@ function setCurrentApp(app) {
4684
5412
  globalThis[APP_INSTANCE_KEY] = app;
4685
5413
  }
4686
5414
  }
5415
+ var SHUTDOWN_INSTALLED_KEY = /* @__PURE__ */ Symbol.for("faapi.defaultShutdownInstalled");
5416
+ function registerDefaultShutdownHandlers() {
5417
+ const g = globalThis;
5418
+ if (g[SHUTDOWN_INSTALLED_KEY]) return;
5419
+ g[SHUTDOWN_INSTALLED_KEY] = true;
5420
+ const shutdown = (signal) => {
5421
+ console.log(`
5422
+ - Received ${signal}, shutting down...`);
5423
+ void (async () => {
5424
+ const app = getCurrentApp();
5425
+ if (app) await app.close();
5426
+ process.exit(0);
5427
+ })();
5428
+ };
5429
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
5430
+ process.on("SIGINT", () => shutdown("SIGINT"));
5431
+ }
4687
5432
  function getApp() {
4688
5433
  const app = getCurrentApp();
4689
5434
  if (!app) {
@@ -4701,6 +5446,8 @@ var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
4701
5446
  "extendContext",
4702
5447
  "plugins",
4703
5448
  "helmet",
5449
+ "compression",
5450
+ "etag",
4704
5451
  "bodyLimit",
4705
5452
  "logger",
4706
5453
  "http2",
@@ -4713,7 +5460,7 @@ function isFaapiConfigKey(key) {
4713
5460
  async function createAppBase(options) {
4714
5461
  const rootDir = options?.rootDir ?? process.cwd();
4715
5462
  const dist = options?.dist ?? process.env.FAAPI_DIST ?? DEFAULT_DIST;
4716
- const routesPath = path15.resolve(rootDir, dist, ROUTES_FILE);
5463
+ const routesPath = path19.resolve(rootDir, dist, ROUTES_FILE);
4717
5464
  if (!fs15.existsSync(routesPath)) {
4718
5465
  throw new Error(
4719
5466
  `[faapi] ${dist}/${ROUTES_FILE} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C \`faapi build\`\uFF08\u6216 \`faapi dev\`\uFF09\u751F\u6210\u4EA7\u7269\u3002`
@@ -4733,8 +5480,9 @@ async function createAppBase(options) {
4733
5480
  }
4734
5481
  }
4735
5482
  }
4736
- const tools = await loadAndHydrateTools(rootDir, dist);
4737
- const agents = await loadAndHydrateAgents(rootDir, dist);
5483
+ const registries = createAppRegistries();
5484
+ const tools = await loadAndHydrateTools(rootDir, dist, registries);
5485
+ const agents = await loadAndHydrateAgents(rootDir, dist, registries);
4738
5486
  const pluginConfig = config ? Object.fromEntries(Object.entries(config).filter(([k]) => !isFaapiConfigKey(k))) : {};
4739
5487
  const { server, routesRef } = createServer({
4740
5488
  routes: sorted,
@@ -4747,30 +5495,52 @@ async function createAppBase(options) {
4747
5495
  middlewares: config?.middlewares,
4748
5496
  injectors: config?.injectors,
4749
5497
  helmet: config?.helmet,
5498
+ compression: config?.compression,
5499
+ etag: config?.etag,
4750
5500
  logger: config?.logger,
4751
5501
  bodyLimit: config?.bodyLimit,
4752
5502
  http2: config?.http2,
4753
- trustedProxy: config?.trustedProxy
4754
- });
4755
- const { handlerWrappers, upgradeWrappers } = await loadPlugins(config?.plugins, {
4756
- rootDir,
4757
- routes: sorted,
4758
- getRoutes: () => sorted,
4759
- server,
4760
- config: pluginConfig
5503
+ trustedProxy: config?.trustedProxy,
5504
+ registries
4761
5505
  });
5506
+ const { handlerWrappers, upgradeWrappers } = await loadPlugins(
5507
+ config?.plugins,
5508
+ {
5509
+ rootDir,
5510
+ registries,
5511
+ routes: sorted,
5512
+ getRoutes: () => sorted,
5513
+ server,
5514
+ config: pluginConfig
5515
+ },
5516
+ rootDir
5517
+ );
4762
5518
  applyPluginWrappers(server, handlerWrappers, upgradeWrappers);
4763
5519
  let closed = false;
4764
5520
  const app = {
4765
5521
  server: null,
5522
+ registries,
4766
5523
  routes: sorted,
4767
5524
  wsRoutes,
4768
5525
  rootDir,
4769
5526
  async listen(listenPort) {
4770
5527
  const envPort = process.env.PORT ? Number(process.env.PORT) : void 0;
4771
5528
  const actualPort = listenPort ?? options?.port ?? envPort ?? DEFAULT_PORT;
4772
- return new Promise((resolve) => {
5529
+ return new Promise((resolve, reject) => {
5530
+ const onListenError = (err) => {
5531
+ if (err.code === "EADDRINUSE") {
5532
+ reject(
5533
+ new Error(
5534
+ `Port ${actualPort} is already in use. Is another faapi instance running? Change the port via the PORT env var.`
5535
+ )
5536
+ );
5537
+ return;
5538
+ }
5539
+ reject(err);
5540
+ };
5541
+ server.once("error", onListenError);
4773
5542
  server.listen(actualPort, async () => {
5543
+ server.off("error", onListenError);
4774
5544
  const address = server.address();
4775
5545
  const p = typeof address === "object" && address !== null ? address.port : actualPort;
4776
5546
  console.log("faapi server started");
@@ -4798,18 +5568,9 @@ async function createAppBase(options) {
4798
5568
  console.log(` ${agent.name} [${exports}] ${agent.filePath}`);
4799
5569
  }
4800
5570
  }
4801
- if (config?.lifecycle?.onClose) {
4802
- const graceful = async (signal) => {
4803
- console.log(`
4804
- - Received ${signal}, shutting down...`);
4805
- await app.close();
4806
- process.exit(0);
4807
- };
4808
- process.on("SIGTERM", () => void graceful("SIGTERM"));
4809
- process.on("SIGINT", () => void graceful("SIGINT"));
4810
- }
5571
+ registerDefaultShutdownHandlers();
4811
5572
  if (config?.lifecycle?.onReady) {
4812
- await config.lifecycle.onReady({ rootDir, routes: sorted, server });
5573
+ await config.lifecycle.onReady({ rootDir, routes: sorted, server, registries });
4813
5574
  console.log("- onReady hook executed");
4814
5575
  }
4815
5576
  app.server = server;
@@ -4886,39 +5647,50 @@ async function createAppBase(options) {
4886
5647
  if (closed) return;
4887
5648
  closed = true;
4888
5649
  const s = server;
4889
- if (typeof s.closeIdleConnections === "function") {
4890
- s.closeIdleConnections();
4891
- }
4892
- if (typeof s.closeAllConnections === "function") {
4893
- s.closeAllConnections();
4894
- }
5650
+ s.closeIdleConnections?.();
4895
5651
  if (config?.lifecycle?.onClose) {
4896
- await config.lifecycle.onClose({ rootDir, routes: sorted, server });
5652
+ await config.lifecycle.onClose({ rootDir, routes: sorted, server, registries });
4897
5653
  }
4898
- clearToolRegistry();
4899
- clearAgentRegistry();
4900
- clearSkillRegistry();
4901
- clearAgentHandleFactory();
5654
+ registries.tool.clear();
5655
+ registries.agent.clear();
5656
+ registries.skill.clear();
5657
+ registries.agentHandle.clear();
4902
5658
  if (!server.listening) {
4903
5659
  app.server = null;
4904
5660
  if (getCurrentApp() === app) setCurrentApp(null);
4905
5661
  return;
4906
5662
  }
4907
- return new Promise((resolve) => {
5663
+ const drained = new Promise((resolve) => {
4908
5664
  server.close((err) => {
4909
5665
  if (err) console.error("Error closing server:", err);
4910
- app.server = null;
4911
- if (getCurrentApp() === app) setCurrentApp(null);
4912
5666
  resolve();
4913
5667
  });
4914
5668
  });
5669
+ const drainTimeoutMs = Number(process.env.FAAPI_SHUTDOWN_TIMEOUT_MS ?? 1e4);
5670
+ if (typeof s.closeAllConnections === "function" && Number.isFinite(drainTimeoutMs) && drainTimeoutMs >= 0) {
5671
+ const forceClose = new Promise((resolve) => {
5672
+ const timer = setTimeout(() => {
5673
+ s.closeAllConnections?.();
5674
+ resolve();
5675
+ }, drainTimeoutMs);
5676
+ timer.unref?.();
5677
+ });
5678
+ await Promise.race([drained, forceClose]);
5679
+ const tail = new Promise((resolve) => setTimeout(resolve, 250).unref?.());
5680
+ await Promise.race([drained, tail]);
5681
+ } else {
5682
+ await drained;
5683
+ }
5684
+ app.server = null;
5685
+ if (getCurrentApp() === app) setCurrentApp(null);
4915
5686
  }
4916
5687
  };
4917
5688
  setCurrentApp(app);
4918
5689
  const ctx = {
4919
5690
  rootDir,
5691
+ registries,
4920
5692
  dist,
4921
- patterns: PATTERNS,
5693
+ patterns: ROUTE_PATTERNS,
4922
5694
  server,
4923
5695
  routesRef,
4924
5696
  config,
@@ -4936,7 +5708,7 @@ async function createAppBase(options) {
4936
5708
 
4937
5709
  // src/router/scanRoutes.ts
4938
5710
  import fg2 from "fast-glob";
4939
- import path16 from "path";
5711
+ import path20 from "path";
4940
5712
  import fs16 from "fs";
4941
5713
 
4942
5714
  // src/router/constants.ts
@@ -4944,9 +5716,9 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
4944
5716
  var HTTP_METHOD_SET = new Set(HTTP_METHODS);
4945
5717
 
4946
5718
  // src/utils/normalizePath.ts
4947
- function normalizePath(path19) {
4948
- if (!path19) return "";
4949
- let result = path19.replace(/\\/g, "/");
5719
+ function normalizePath(path23) {
5720
+ if (!path23) return "";
5721
+ let result = path23.replace(/\\/g, "/");
4950
5722
  result = result.replace(/\/+/g, "/");
4951
5723
  result = result.replace(/\/+$/, "");
4952
5724
  if (result && !result.startsWith("/")) {
@@ -5007,26 +5779,26 @@ function extractExportsFromSource(source) {
5007
5779
  return names;
5008
5780
  }
5009
5781
  function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
5010
- const routeDir = path16.dirname(routeFilePath);
5011
- const resolvedRoot = path16.resolve(rootDir);
5782
+ const routeDir = path20.dirname(routeFilePath);
5783
+ const resolvedRoot = path20.resolve(rootDir);
5012
5784
  const paths = [];
5013
- let currentDir = path16.resolve(rootDir, routeDir);
5785
+ let currentDir = path20.resolve(rootDir, routeDir);
5014
5786
  while (true) {
5015
5787
  if (dist) {
5016
- const mwTsPath = path16.join(currentDir, "middlewares.ts");
5017
- const mwJsPath = path16.join(currentDir, "middlewares.js");
5018
- const absTsPath = path16.resolve(rootDir, mwTsPath);
5019
- const absJsPath = path16.resolve(rootDir, mwJsPath);
5788
+ const mwTsPath = path20.join(currentDir, "middlewares.ts");
5789
+ const mwJsPath = path20.join(currentDir, "middlewares.js");
5790
+ const absTsPath = path20.resolve(rootDir, mwTsPath);
5791
+ const absJsPath = path20.resolve(rootDir, mwJsPath);
5020
5792
  const absMwPath = fs16.existsSync(absTsPath) ? absTsPath : fs16.existsSync(absJsPath) ? absJsPath : null;
5021
5793
  if (absMwPath) {
5022
- const relMwPath = path16.relative(rootDir, absMwPath);
5023
- const prodAbsPath = path16.resolve(rootDir, toProdFilePath3(relMwPath, dist));
5794
+ const relMwPath = path20.relative(rootDir, absMwPath);
5795
+ const prodAbsPath = path20.resolve(rootDir, toProdFilePath(relMwPath, dist));
5024
5796
  paths.push(prodAbsPath);
5025
5797
  }
5026
5798
  } else {
5027
5799
  for (const ext of [".ts", ".js"]) {
5028
- const mwPath = path16.join(currentDir, `middlewares${ext}`);
5029
- const absMwPath = path16.resolve(rootDir, mwPath);
5800
+ const mwPath = path20.join(currentDir, `middlewares${ext}`);
5801
+ const absMwPath = path20.resolve(rootDir, mwPath);
5030
5802
  if (fs16.existsSync(absMwPath)) {
5031
5803
  paths.push(absMwPath);
5032
5804
  break;
@@ -5034,21 +5806,13 @@ function collectMiddlewarePaths(routeFilePath, rootDir, dist) {
5034
5806
  }
5035
5807
  }
5036
5808
  if (currentDir === resolvedRoot) break;
5037
- const parentDir = path16.dirname(currentDir);
5809
+ const parentDir = path20.dirname(currentDir);
5038
5810
  if (parentDir === currentDir) break;
5039
5811
  currentDir = parentDir;
5040
5812
  }
5041
5813
  paths.reverse();
5042
5814
  return paths;
5043
5815
  }
5044
- function toProdFilePath3(filePath, dist) {
5045
- let rel = filePath.replace(/\\/g, "/");
5046
- if (rel.startsWith("src/")) {
5047
- rel = rel.slice(4);
5048
- }
5049
- const jsPath = rel.replace(/\.ts$/, ".js");
5050
- return jsPath.startsWith(`${dist}/`) ? jsPath : `${dist}/${jsPath}`;
5051
- }
5052
5816
  async function scanRoutes(rootDir, patterns, dist) {
5053
5817
  const files = await fg2(patterns, {
5054
5818
  cwd: rootDir,
@@ -5061,7 +5825,7 @@ async function scanRoutes(rootDir, patterns, dist) {
5061
5825
  const normalizedFile = file.replace(/\\/g, "/");
5062
5826
  const fileName = normalizedFile.split("/").pop();
5063
5827
  if (fileName === "handler.ts" || fileName === "handler.js") {
5064
- const absPath = path16.resolve(rootDir, normalizedFile);
5828
+ const absPath = path20.resolve(rootDir, normalizedFile);
5065
5829
  const urlPath = filePathToUrlPath(normalizedFile);
5066
5830
  const paramNames = extractParamNames(urlPath);
5067
5831
  const isDynamic = paramNames.length > 0;
@@ -5110,7 +5874,7 @@ async function scanRoutes(rootDir, patterns, dist) {
5110
5874
 
5111
5875
  // src/tools/scanTools.ts
5112
5876
  import fg3 from "fast-glob";
5113
- import path17 from "path";
5877
+ import path21 from "path";
5114
5878
  import fs17 from "fs";
5115
5879
  var TOOL_PATTERNS = ["src/tools/**/*.ts"];
5116
5880
  var TOOL_EXPORT_RE = new RegExp(
@@ -5161,7 +5925,7 @@ async function scanTools(rootDir, patterns) {
5161
5925
  if (fileName !== "handler.ts" && fileName !== "handler.js") {
5162
5926
  continue;
5163
5927
  }
5164
- const absPath = path17.resolve(rootDir, normalizedFile);
5928
+ const absPath = path21.resolve(rootDir, normalizedFile);
5165
5929
  const source = await fs17.promises.readFile(absPath, "utf8").catch(() => "");
5166
5930
  const exportNames = extractToolExportsFromSource(source);
5167
5931
  const namespace = filePathToToolNamespace(normalizedFile);
@@ -5186,7 +5950,7 @@ async function scanTools(rootDir, patterns) {
5186
5950
 
5187
5951
  // src/agents/scanAgents.ts
5188
5952
  import fg4 from "fast-glob";
5189
- import path18 from "path";
5953
+ import path22 from "path";
5190
5954
  import fs18 from "fs";
5191
5955
  var DEFAULT_AGENT_PATTERNS = ["src/agents/*/handler.ts"];
5192
5956
  var RUN_EXPORT_RE = /export\s+(?:async\s+)?(?:function\s+|const\s+)run\b/;
@@ -5219,7 +5983,7 @@ async function scanAgents(rootDir, patterns) {
5219
5983
  if (fileName !== "handler.ts" && fileName !== "handler.js") {
5220
5984
  continue;
5221
5985
  }
5222
- const absPath = path18.resolve(rootDir, normalizedFile);
5986
+ const absPath = path22.resolve(rootDir, normalizedFile);
5223
5987
  const source = await fs18.promises.readFile(absPath, "utf8").catch(() => "");
5224
5988
  const { hasRun } = detectAgentExports(source);
5225
5989
  const name = extractAgentNameFromPath(normalizedFile);
@@ -5255,6 +6019,14 @@ async function createDevApp(options) {
5255
6019
  if (isDevOnDemandEnabled()) {
5256
6020
  await deleteSchemaFiles(sorted, ctx.rootDir, ctx.dist);
5257
6021
  clearGeneratedSchemas();
6022
+ void (async () => {
6023
+ try {
6024
+ const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
6025
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
6026
+ } catch (err) {
6027
+ console.error("[faapi] Background schema regeneration failed:", err);
6028
+ }
6029
+ })();
5258
6030
  } else {
5259
6031
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
5260
6032
  await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
@@ -5268,14 +6040,14 @@ async function createDevApp(options) {
5268
6040
  await generateToolArtifacts(tools, ctx.rootDir, ctx.dist, {
5269
6041
  skipSchema: isDevOnDemandEnabled()
5270
6042
  });
5271
- await loadAndHydrateTools(ctx.rootDir, ctx.dist);
6043
+ await loadAndHydrateTools(ctx.rootDir, ctx.dist, ctx.registries);
5272
6044
  };
5273
6045
  devApp.reloadAgents = async () => {
5274
6046
  setLoadTimestamp(Date.now());
5275
6047
  invalidateProgramCache();
5276
6048
  const agents = await scanAgents(ctx.rootDir, DEFAULT_AGENT_PATTERNS);
5277
6049
  await generateAgentArtifacts(agents, ctx.rootDir, ctx.dist);
5278
- await loadAndHydrateAgents(ctx.rootDir, ctx.dist);
6050
+ await loadAndHydrateAgents(ctx.rootDir, ctx.dist, ctx.registries);
5279
6051
  };
5280
6052
  return devApp;
5281
6053
  }
@@ -5297,6 +6069,7 @@ export {
5297
6069
  collectRouteSchemaSources,
5298
6070
  cors,
5299
6071
  createProdApp as createApp,
6072
+ createAppRegistries,
5300
6073
  createDevApp,
5301
6074
  createProdApp,
5302
6075
  createProgram,