@dudousxd/nestjs-codegen 0.28.0 → 0.29.1

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.cjs CHANGED
@@ -315,7 +315,7 @@ async function discoverPages(opts) {
315
315
  const rel = (0, import_node_path2.relative)(opts.cwd, file);
316
316
  const nameRel = (0, import_node_path2.relative)(pagesBase, file).split(import_node_path2.sep).join("/");
317
317
  const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
318
- const source = await (0, import_promises2.readFile)(file, "utf8");
318
+ const source = await (opts.read ? opts.read(file) : (0, import_promises2.readFile)(file, "utf8"));
319
319
  const propsSource = extractPropsSource(source, opts.propsExport);
320
320
  out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
321
321
  }
@@ -323,10 +323,14 @@ async function discoverPages(opts) {
323
323
  assertUniqueNames(out);
324
324
  return out;
325
325
  }
326
- async function discoverPageScopes(config) {
326
+ async function discoverPageScopes(config, read) {
327
327
  const pages = config.pages;
328
328
  if (!pages) return [];
329
- const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
329
+ const common = {
330
+ cwd: config.codegen.cwd,
331
+ propsExport: pages.propsExport,
332
+ ...read ? { read } : {}
333
+ };
330
334
  const out = await discoverPages({
331
335
  ...common,
332
336
  glob: pages.glob,
@@ -397,297 +401,60 @@ function extractPropsSource(source, exportName) {
397
401
  }
398
402
 
399
403
  // src/discovery/shared-props.ts
400
- var import_node_path3 = require("path");
401
- var import_ts_morph = require("ts-morph");
402
- function discoverSharedProps(project, moduleEntry) {
403
- try {
404
- let sourceFile = project.getSourceFile(moduleEntry);
405
- if (!sourceFile) {
404
+ var import_node_fs3 = require("fs");
405
+ var import_node_path5 = require("path");
406
+ var import_ts_morph13 = require("ts-morph");
407
+
408
+ // src/discovery/contracts-fast.ts
409
+ var import_node_fs2 = require("fs");
410
+ var import_node_path4 = require("path");
411
+ var import_fast_glob2 = __toESM(require("fast-glob"), 1);
412
+ var import_ts_morph12 = require("ts-morph");
413
+
414
+ // src/discovery/disk-fingerprints.ts
415
+ var import_node_fs = require("fs");
416
+ var DiskFingerprints = class {
417
+ stats = /* @__PURE__ */ new Map();
418
+ /** The paths whose disk content differs from `text`, or that no longer exist. */
419
+ changed(files) {
420
+ const changed = [];
421
+ for (const { path, text } of files) {
422
+ let key;
406
423
  try {
407
- sourceFile = project.addSourceFileAtPath(moduleEntry);
424
+ const stat2 = (0, import_node_fs.statSync)(path);
425
+ key = `${stat2.mtimeMs}:${stat2.size}`;
408
426
  } catch {
409
- return null;
410
- }
411
- }
412
- const forRootCall = findForRootCall(sourceFile);
413
- if (!forRootCall) return null;
414
- const initializer = findShareInitializer(forRootCall);
415
- if (!initializer) return null;
416
- return extractShareType(initializer, sourceFile, project);
417
- } catch {
418
- return null;
419
- }
420
- }
421
- function discoverSharedPropsFromConfig(config) {
422
- if (!config.app?.moduleEntry) return null;
423
- try {
424
- const tsconfigPath = config.app.tsconfig ?? (0, import_node_path3.join)(config.codegen.cwd, "tsconfig.json");
425
- let project;
426
- try {
427
- project = new import_ts_morph.Project({
428
- tsConfigFilePath: tsconfigPath,
429
- skipAddingFilesFromTsConfig: true,
430
- skipLoadingLibFiles: true,
431
- skipFileDependencyResolution: true
432
- });
433
- } catch {
434
- project = new import_ts_morph.Project({
435
- skipAddingFilesFromTsConfig: true,
436
- skipLoadingLibFiles: true,
437
- skipFileDependencyResolution: true,
438
- compilerOptions: { allowJs: true, strict: false }
439
- });
440
- }
441
- return discoverSharedProps(project, config.app.moduleEntry);
442
- } catch {
443
- return null;
444
- }
445
- }
446
- function findForRootCall(sourceFile) {
447
- const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
448
- for (const call of callExpressions) {
449
- const expr = call.getExpression();
450
- if (!import_ts_morph.Node.isPropertyAccessExpression(expr)) continue;
451
- const methodName = expr.getName();
452
- const objectExpr = expr.getExpression();
453
- if (methodName === "forRoot" && import_ts_morph.Node.isIdentifier(objectExpr)) {
454
- const name = objectExpr.getText();
455
- if (name === "InertiaModule") {
456
- return call;
457
- }
458
- }
459
- }
460
- return null;
461
- }
462
- function findShareInitializer(forRootCall) {
463
- if (!import_ts_morph.Node.isCallExpression(forRootCall)) return null;
464
- const args = forRootCall.getArguments();
465
- const firstArg2 = args[0];
466
- if (!firstArg2 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg2)) return null;
467
- for (const prop of firstArg2.getProperties()) {
468
- if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
469
- return prop.getInitializer() ?? null;
470
- }
471
- }
472
- return null;
473
- }
474
- function extractShareType(node, sourceFile, project) {
475
- if (import_ts_morph.Node.isIdentifier(node)) {
476
- const ref = resolveIdentifierToImportRef(node, sourceFile, project);
477
- if (ref) {
478
- return {
479
- typeString: `Awaited<ReturnType<typeof import('${ref.importPath}').${ref.exportName}>>`,
480
- properties: null,
481
- isImportRef: true
482
- };
483
- }
484
- }
485
- if (import_ts_morph.Node.isArrowFunction(node)) {
486
- const result = extractFromFunctionLike(node, sourceFile);
487
- return result ? { ...result, isImportRef: false } : null;
488
- }
489
- if (import_ts_morph.Node.isFunctionExpression(node)) {
490
- const result = extractFromFunctionLike(node, sourceFile);
491
- return result ? { ...result, isImportRef: false } : null;
492
- }
493
- if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
494
- const result = extractFromObjectLiteral(node);
495
- return result ? { ...result, isImportRef: false } : null;
496
- }
497
- return null;
498
- }
499
- function resolveIdentifierToImportRef(id, sourceFile, project) {
500
- if (!import_ts_morph.Node.isIdentifier(id)) return null;
501
- const name = id.getText();
502
- const localFunc = sourceFile.getFunction(name);
503
- if (localFunc?.isExported()) {
504
- const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
505
- return { importPath: filePath, exportName: name };
506
- }
507
- const localVar = sourceFile.getVariableDeclaration(name);
508
- if (localVar?.isExported()) {
509
- const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
510
- return { importPath: filePath, exportName: name };
511
- }
512
- for (const imp of sourceFile.getImportDeclarations()) {
513
- for (const named of imp.getNamedImports()) {
514
- const importedName = named.getAliasNode()?.getText() ?? named.getName();
515
- if (importedName !== name) continue;
516
- const resolvedSource = imp.getModuleSpecifierSourceFile();
517
- if (!resolvedSource) continue;
518
- const originalName = named.getName();
519
- const fn = resolvedSource.getFunction(originalName);
520
- if (fn?.isExported()) {
521
- const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
522
- return { importPath: filePath, exportName: originalName };
523
- }
524
- const v = resolvedSource.getVariableDeclaration(originalName);
525
- if (v?.isExported()) {
526
- const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
527
- return { importPath: filePath, exportName: originalName };
528
- }
529
- }
530
- }
531
- return null;
532
- }
533
- function extractFromFunctionLike(node, _sourceFile) {
534
- const returnTypeNode = import_ts_morph.Node.isArrowFunction(node) || import_ts_morph.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
535
- if (returnTypeNode) {
536
- return extractFromReturnTypeAnnotation(returnTypeNode);
537
- }
538
- if (import_ts_morph.Node.isArrowFunction(node)) {
539
- const body = node.getBody();
540
- if (import_ts_morph.Node.isParenthesizedExpression(body)) {
541
- const inner = body.getExpression();
542
- if (import_ts_morph.Node.isObjectLiteralExpression(inner)) {
543
- return extractFromObjectLiteral(inner);
544
- }
545
- }
546
- if (import_ts_morph.Node.isObjectLiteralExpression(body)) {
547
- return extractFromObjectLiteral(body);
548
- }
549
- if (import_ts_morph.Node.isBlock(body)) {
550
- return extractFromBlockReturn(body);
551
- }
552
- }
553
- if (import_ts_morph.Node.isFunctionExpression(node)) {
554
- const body = node.getBody();
555
- if (import_ts_morph.Node.isBlock(body)) {
556
- return extractFromBlockReturn(body);
557
- }
558
- }
559
- return null;
560
- }
561
- function extractFromReturnTypeAnnotation(typeNode) {
562
- if (import_ts_morph.Node.isTypeReference(typeNode)) {
563
- const typeName = typeNode.getTypeName();
564
- if (import_ts_morph.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
565
- const typeArgs = typeNode.getTypeArguments();
566
- const firstArg2 = typeArgs[0];
567
- if (firstArg2) {
568
- return extractFromReturnTypeAnnotation(firstArg2);
427
+ this.stats.delete(path);
428
+ changed.push(path);
429
+ continue;
569
430
  }
570
- return null;
571
- }
572
- }
573
- if (import_ts_morph.Node.isTypeLiteral(typeNode)) {
574
- const properties = [];
575
- for (const member of typeNode.getMembers()) {
576
- if (import_ts_morph.Node.isPropertySignature(member)) {
577
- const name = member.getName();
578
- const memberTypeNode = member.getTypeNode();
579
- const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
580
- properties.push({ name, type });
431
+ if (this.stats.get(path) === key) continue;
432
+ let disk;
433
+ try {
434
+ disk = (0, import_node_fs.readFileSync)(path, "utf-8");
435
+ } catch {
436
+ disk = void 0;
581
437
  }
582
- }
583
- if (properties.length === 0) return null;
584
- const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
585
- return { typeString, properties, isImportRef: false };
586
- }
587
- return null;
588
- }
589
- function extractFromBlockReturn(block) {
590
- if (!import_ts_morph.Node.isBlock(block)) return null;
591
- const statements = block.getStatements();
592
- for (let i = statements.length - 1; i >= 0; i--) {
593
- const stmt = statements[i];
594
- if (!import_ts_morph.Node.isReturnStatement(stmt)) continue;
595
- const expr = stmt.getExpression();
596
- if (!expr) continue;
597
- if (import_ts_morph.Node.isObjectLiteralExpression(expr)) {
598
- return extractFromObjectLiteral(expr);
599
- }
600
- if (import_ts_morph.Node.isParenthesizedExpression(expr)) {
601
- const inner = expr.getExpression();
602
- if (import_ts_morph.Node.isObjectLiteralExpression(inner)) {
603
- return extractFromObjectLiteral(inner);
438
+ if (disk === text) {
439
+ this.stats.set(path, key);
440
+ } else {
441
+ this.stats.delete(path);
442
+ changed.push(path);
604
443
  }
605
444
  }
606
- break;
607
- }
608
- return null;
609
- }
610
- function extractFromObjectLiteral(objLiteral) {
611
- if (!import_ts_morph.Node.isObjectLiteralExpression(objLiteral)) return null;
612
- const properties = [];
613
- for (const prop of objLiteral.getProperties()) {
614
- if (!import_ts_morph.Node.isPropertyAssignment(prop)) continue;
615
- const name = prop.getName();
616
- const initializer = prop.getInitializer();
617
- if (!initializer) continue;
618
- const type = inferExpressionType(initializer);
619
- properties.push({ name, type });
620
- }
621
- if (properties.length === 0) return null;
622
- const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
623
- return { typeString, properties, isImportRef: false };
624
- }
625
- function inferExpressionType(node) {
626
- if (import_ts_morph.Node.isStringLiteral(node)) return "string";
627
- if (import_ts_morph.Node.isTemplateExpression(node) || import_ts_morph.Node.isNoSubstitutionTemplateLiteral(node))
628
- return "string";
629
- if (import_ts_morph.Node.isNumericLiteral(node)) return "number";
630
- if (node.getKind() === import_ts_morph.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph.SyntaxKind.FalseKeyword) {
631
- return "boolean";
632
- }
633
- if (node.getKind() === import_ts_morph.SyntaxKind.NullKeyword) return "null";
634
- if (import_ts_morph.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
635
- if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
636
- const props = node.getProperties();
637
- if (props.length === 0) return "Record<string, unknown>";
638
- const entries = [];
639
- for (const prop of props) {
640
- if (!import_ts_morph.Node.isPropertyAssignment(prop)) continue;
641
- const key = prop.getName();
642
- const init = prop.getInitializer();
643
- if (!init) continue;
644
- entries.push(`${key}: ${inferExpressionType(init)}`);
645
- }
646
- if (entries.length === 0) return "Record<string, unknown>";
647
- return `{ ${entries.join("; ")} }`;
648
- }
649
- if (import_ts_morph.Node.isArrayLiteralExpression(node)) {
650
- const elements = node.getElements();
651
- if (elements.length === 0) return "Array<unknown>";
652
- const first = elements[0];
653
- if (first) return `Array<${inferExpressionType(first)}>`;
654
- return "Array<unknown>";
655
- }
656
- if (import_ts_morph.Node.isConditionalExpression(node)) {
657
- const whenTrue = inferExpressionType(node.getWhenTrue());
658
- const whenFalse = inferExpressionType(node.getWhenFalse());
659
- if (whenTrue === whenFalse) return whenTrue;
660
- return `${whenTrue} | ${whenFalse}`;
661
- }
662
- if (import_ts_morph.Node.isParenthesizedExpression(node)) {
663
- return inferExpressionType(node.getExpression());
445
+ return changed;
664
446
  }
665
- if (import_ts_morph.Node.isAsExpression(node)) {
666
- const typeNode = node.getTypeNode();
667
- if (typeNode) return typeNode.getText();
447
+ /** Forget `path`, so its next check compares content again. */
448
+ forget(path) {
449
+ this.stats.delete(path);
668
450
  }
669
- return "unknown";
670
- }
671
-
672
- // src/emit/emit-api.ts
673
- var import_promises3 = require("fs/promises");
674
- var import_node_path7 = require("path");
675
-
676
- // src/extension/registry.ts
677
- var import_node_path6 = require("path");
678
- var import_ts_morph13 = require("ts-morph");
679
-
680
- // src/discovery/contracts-fast.ts
681
- var import_node_fs = require("fs");
682
- var import_node_path5 = require("path");
683
- var import_fast_glob2 = __toESM(require("fast-glob"), 1);
684
- var import_ts_morph12 = require("ts-morph");
451
+ };
685
452
 
686
453
  // src/discovery/dto-type-resolver.ts
687
454
  var import_ts_morph11 = require("ts-morph");
688
455
 
689
456
  // src/discovery/dto-to-ir.ts
690
- var import_ts_morph3 = require("ts-morph");
457
+ var import_ts_morph2 = require("ts-morph");
691
458
 
692
459
  // src/util/debug-log.ts
693
460
  var debugEnabled = false;
@@ -699,8 +466,8 @@ function debugWarn(message) {
699
466
  }
700
467
 
701
468
  // src/discovery/type-ref-resolution.ts
702
- var import_node_path4 = require("path");
703
- var import_ts_morph2 = require("ts-morph");
469
+ var import_node_path3 = require("path");
470
+ var import_ts_morph = require("ts-morph");
704
471
  var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
705
472
  var _ctxByProject = /* @__PURE__ */ new WeakMap();
706
473
  function setDiscoveryContext(project, ctx) {
@@ -741,17 +508,17 @@ function findTypeInFile(name, file) {
741
508
  }
742
509
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
743
510
  if (moduleSpecifier.startsWith(".")) {
744
- const dir = (0, import_node_path4.dirname)(sourceFile.getFilePath());
511
+ const dir = (0, import_node_path3.dirname)(sourceFile.getFilePath());
745
512
  const ext = /\.(c|m)?(j|t)sx?$/.exec(moduleSpecifier)?.[0];
746
513
  const base = ext ? moduleSpecifier.slice(0, -ext.length) : moduleSpecifier;
747
514
  const flavour = ext?.startsWith(".c") ? "c" : ext?.startsWith(".m") ? "m" : "";
748
515
  const candidates = [
749
- (0, import_node_path4.resolve)(dir, `${base}.${flavour}ts`),
750
- ...flavour === "" ? [(0, import_node_path4.resolve)(dir, `${base}.tsx`)] : [],
751
- (0, import_node_path4.resolve)(dir, `${moduleSpecifier}.ts`),
752
- (0, import_node_path4.resolve)(dir, moduleSpecifier, "index.ts"),
753
- (0, import_node_path4.resolve)(dir, `${base}.d.${flavour}ts`),
754
- ...ext === void 0 ? [(0, import_node_path4.resolve)(dir, moduleSpecifier, "index.d.ts")] : []
516
+ (0, import_node_path3.resolve)(dir, `${base}.${flavour}ts`),
517
+ ...flavour === "" ? [(0, import_node_path3.resolve)(dir, `${base}.tsx`)] : [],
518
+ (0, import_node_path3.resolve)(dir, `${moduleSpecifier}.ts`),
519
+ (0, import_node_path3.resolve)(dir, moduleSpecifier, "index.ts"),
520
+ (0, import_node_path3.resolve)(dir, `${base}.d.${flavour}ts`),
521
+ ...ext === void 0 ? [(0, import_node_path3.resolve)(dir, moduleSpecifier, "index.d.ts")] : []
755
522
  ];
756
523
  return [...new Set(candidates)];
757
524
  }
@@ -773,8 +540,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
773
540
  const rest = moduleSpecifier.slice(prefix.length);
774
541
  const candidates = [];
775
542
  for (const mapping of mappings) {
776
- const resolved = (0, import_node_path4.resolve)(baseUrl, mapping.replace("*", rest));
777
- candidates.push(`${resolved}.ts`, (0, import_node_path4.resolve)(resolved, "index.ts"));
543
+ const resolved = (0, import_node_path3.resolve)(baseUrl, mapping.replace("*", rest));
544
+ candidates.push(`${resolved}.ts`, (0, import_node_path3.resolve)(resolved, "index.ts"));
778
545
  }
779
546
  dbg(" resolved candidates:", candidates);
780
547
  return candidates;
@@ -965,13 +732,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
965
732
  name = nodeOrName;
966
733
  } else {
967
734
  const typeNode = nodeOrName;
968
- if (opts.unwrapContainers && import_ts_morph2.Node.isArrayTypeNode(typeNode)) {
735
+ if (opts.unwrapContainers && import_ts_morph.Node.isArrayTypeNode(typeNode)) {
969
736
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
970
737
  return inner ? { ...inner, isArray: true } : null;
971
738
  }
972
- if (!import_ts_morph2.Node.isTypeReference(typeNode)) return null;
739
+ if (!import_ts_morph.Node.isTypeReference(typeNode)) return null;
973
740
  const typeName = typeNode.getTypeName();
974
- const refName = import_ts_morph2.Node.isIdentifier(typeName) ? typeName.getText() : null;
741
+ const refName = import_ts_morph.Node.isIdentifier(typeName) ? typeName.getText() : null;
975
742
  if (!refName) return null;
976
743
  if (opts.unwrapContainers && refName === "Promise") {
977
744
  const first = typeNode.getTypeArguments()[0];
@@ -1105,7 +872,7 @@ function buildProperty(prop, classFile, ctx) {
1105
872
  const dec = (n) => decorators.get(n);
1106
873
  const typeNode = prop.getTypeNode();
1107
874
  const typeText = typeNode?.getText() ?? "unknown";
1108
- const isArrayType = !!typeNode && import_ts_morph3.Node.isArrayTypeNode(typeNode);
875
+ const isArrayType = !!typeNode && import_ts_morph2.Node.isArrayTypeNode(typeNode);
1109
876
  const asField = (child) => applyPresence(
1110
877
  has("IsArray") || isArrayType ? { kind: "array", element: child } : child,
1111
878
  decorators
@@ -1319,22 +1086,22 @@ function firstArgText(decorator) {
1319
1086
  }
1320
1087
  function numericArg(decorator) {
1321
1088
  const arg = firstArg(decorator);
1322
- if (arg && import_ts_morph3.Node.isNumericLiteral(arg)) return arg.getText();
1089
+ if (arg && import_ts_morph2.Node.isNumericLiteral(arg)) return arg.getText();
1323
1090
  return null;
1324
1091
  }
1325
1092
  function numericArgs(decorator) {
1326
1093
  const args = decorator?.getArguments() ?? [];
1327
- const num = (n) => n && import_ts_morph3.Node.isNumericLiteral(n) ? n.getText() : null;
1094
+ const num = (n) => n && import_ts_morph2.Node.isNumericLiteral(n) ? n.getText() : null;
1328
1095
  return [num(args[0]), num(args[1])];
1329
1096
  }
1330
1097
  function messageRaw(decorator) {
1331
1098
  const args = decorator?.getArguments() ?? [];
1332
1099
  for (const arg of args) {
1333
- if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1100
+ if (import_ts_morph2.Node.isObjectLiteralExpression(arg)) {
1334
1101
  for (const prop of arg.getProperties()) {
1335
- if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
1102
+ if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
1336
1103
  const init = prop.getInitializer();
1337
- if (init && import_ts_morph3.Node.isStringLiteral(init)) return init.getText();
1104
+ if (init && import_ts_morph2.Node.isStringLiteral(init)) return init.getText();
1338
1105
  }
1339
1106
  }
1340
1107
  }
@@ -1343,30 +1110,30 @@ function messageRaw(decorator) {
1343
1110
  }
1344
1111
  function resolveDiscriminator(decorator) {
1345
1112
  const optsArg = decorator?.getArguments()[1];
1346
- if (!optsArg || !import_ts_morph3.Node.isObjectLiteralExpression(optsArg)) return null;
1113
+ if (!optsArg || !import_ts_morph2.Node.isObjectLiteralExpression(optsArg)) return null;
1347
1114
  let discProp;
1348
1115
  for (const prop of optsArg.getProperties()) {
1349
- if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
1116
+ if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
1350
1117
  discProp = prop.getInitializer();
1351
1118
  }
1352
1119
  }
1353
- if (!discProp || !import_ts_morph3.Node.isObjectLiteralExpression(discProp)) return null;
1120
+ if (!discProp || !import_ts_morph2.Node.isObjectLiteralExpression(discProp)) return null;
1354
1121
  let property = null;
1355
1122
  const subTypes = [];
1356
1123
  for (const prop of discProp.getProperties()) {
1357
- if (!import_ts_morph3.Node.isPropertyAssignment(prop)) continue;
1124
+ if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
1358
1125
  const name = prop.getName();
1359
1126
  const init = prop.getInitializer();
1360
1127
  if (!init) continue;
1361
- if (name === "property" && import_ts_morph3.Node.isStringLiteral(init)) {
1128
+ if (name === "property" && import_ts_morph2.Node.isStringLiteral(init)) {
1362
1129
  property = init.getLiteralValue();
1363
- } else if (name === "subTypes" && import_ts_morph3.Node.isArrayLiteralExpression(init)) {
1130
+ } else if (name === "subTypes" && import_ts_morph2.Node.isArrayLiteralExpression(init)) {
1364
1131
  for (const el of init.getElements()) {
1365
- if (!import_ts_morph3.Node.isObjectLiteralExpression(el)) continue;
1132
+ if (!import_ts_morph2.Node.isObjectLiteralExpression(el)) continue;
1366
1133
  for (const p of el.getProperties()) {
1367
- if (!import_ts_morph3.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
1134
+ if (!import_ts_morph2.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
1368
1135
  const nameInit = p.getInitializer();
1369
- if (nameInit && import_ts_morph3.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
1136
+ if (nameInit && import_ts_morph2.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
1370
1137
  }
1371
1138
  }
1372
1139
  }
@@ -1377,9 +1144,9 @@ function resolveDiscriminator(decorator) {
1377
1144
  function resolveTypeFactoryName(decorator) {
1378
1145
  const arg = firstArg(decorator);
1379
1146
  if (!arg) return null;
1380
- if (import_ts_morph3.Node.isArrowFunction(arg)) {
1147
+ if (import_ts_morph2.Node.isArrowFunction(arg)) {
1381
1148
  const body = arg.getBody();
1382
- if (import_ts_morph3.Node.isIdentifier(body)) return body.getText();
1149
+ if (import_ts_morph2.Node.isIdentifier(body)) return body.getText();
1383
1150
  }
1384
1151
  return null;
1385
1152
  }
@@ -1388,12 +1155,12 @@ function singularClassName(typeText) {
1388
1155
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
1389
1156
  }
1390
1157
  function genericTypeArgNames(typeNode) {
1391
- if (!typeNode || !import_ts_morph3.Node.isTypeReference(typeNode)) return [];
1158
+ if (!typeNode || !import_ts_morph2.Node.isTypeReference(typeNode)) return [];
1392
1159
  const names = [];
1393
1160
  for (const arg of typeNode.getTypeArguments()) {
1394
- if (!import_ts_morph3.Node.isTypeReference(arg)) return [];
1161
+ if (!import_ts_morph2.Node.isTypeReference(arg)) return [];
1395
1162
  const tn = arg.getTypeName();
1396
- if (!import_ts_morph3.Node.isIdentifier(tn)) return [];
1163
+ if (!import_ts_morph2.Node.isIdentifier(tn)) return [];
1397
1164
  names.push(tn.getText());
1398
1165
  }
1399
1166
  return names;
@@ -1401,7 +1168,7 @@ function genericTypeArgNames(typeNode) {
1401
1168
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
1402
1169
  const arg = firstArg(decorator);
1403
1170
  if (!arg) return null;
1404
- if (import_ts_morph3.Node.isIdentifier(arg)) {
1171
+ if (import_ts_morph2.Node.isIdentifier(arg)) {
1405
1172
  const name = arg.getText();
1406
1173
  const resolved = findType(name, classFile, ctx.project);
1407
1174
  if (resolved && resolved.kind === "enum") {
@@ -1415,12 +1182,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
1415
1182
  }
1416
1183
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
1417
1184
  }
1418
- if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1185
+ if (import_ts_morph2.Node.isObjectLiteralExpression(arg)) {
1419
1186
  const values = [];
1420
1187
  for (const p of arg.getProperties()) {
1421
- if (!import_ts_morph3.Node.isPropertyAssignment(p)) continue;
1188
+ if (!import_ts_morph2.Node.isPropertyAssignment(p)) continue;
1422
1189
  const init = p.getInitializer();
1423
- if (init && import_ts_morph3.Node.isStringLiteral(init)) values.push(init.getText());
1190
+ if (init && import_ts_morph2.Node.isStringLiteral(init)) values.push(init.getText());
1424
1191
  }
1425
1192
  if (values.length > 0) return { kind: "enum", literals: values };
1426
1193
  }
@@ -1428,9 +1195,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
1428
1195
  }
1429
1196
  function inSchemaFromDecorator(decorator) {
1430
1197
  const arg = firstArg(decorator);
1431
- if (arg && import_ts_morph3.Node.isArrayLiteralExpression(arg)) {
1198
+ if (arg && import_ts_morph2.Node.isArrayLiteralExpression(arg)) {
1432
1199
  const elements = arg.getElements();
1433
- const allStrings = elements.every((e) => import_ts_morph3.Node.isStringLiteral(e));
1200
+ const allStrings = elements.every((e) => import_ts_morph2.Node.isStringLiteral(e));
1434
1201
  if (allStrings && elements.length > 0) {
1435
1202
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
1436
1203
  }
@@ -1448,7 +1215,7 @@ function inSchemaFromDecorator(decorator) {
1448
1215
  var import_ts_morph6 = require("ts-morph");
1449
1216
 
1450
1217
  // src/discovery/filter-field-types.ts
1451
- var import_ts_morph4 = require("ts-morph");
1218
+ var import_ts_morph3 = require("ts-morph");
1452
1219
 
1453
1220
  // src/discovery/enum-resolution.ts
1454
1221
  var _enumCache = /* @__PURE__ */ new WeakMap();
@@ -1501,28 +1268,28 @@ function markNullable(r, nullable) {
1501
1268
  }
1502
1269
  var TRANSPARENT_TYPE_BRANDS = /* @__PURE__ */ new Set(["Opt", "Hidden"]);
1503
1270
  function classifyTypeNode(typeNode, sourceFile, project, opts) {
1504
- if (import_ts_morph4.Node.isUnionTypeNode(typeNode)) {
1271
+ if (import_ts_morph3.Node.isUnionTypeNode(typeNode)) {
1505
1272
  let nullable = false;
1506
1273
  const stringLits = [];
1507
1274
  const numberLits = [];
1508
1275
  const others = [];
1509
1276
  for (const member of typeNode.getTypeNodes()) {
1510
1277
  const kind = member.getKind();
1511
- if (kind === import_ts_morph4.SyntaxKind.NullKeyword || kind === import_ts_morph4.SyntaxKind.UndefinedKeyword) {
1278
+ if (kind === import_ts_morph3.SyntaxKind.NullKeyword || kind === import_ts_morph3.SyntaxKind.UndefinedKeyword) {
1512
1279
  nullable = true;
1513
1280
  continue;
1514
1281
  }
1515
- if (import_ts_morph4.Node.isLiteralTypeNode(member)) {
1282
+ if (import_ts_morph3.Node.isLiteralTypeNode(member)) {
1516
1283
  const lit = member.getLiteral();
1517
- if (import_ts_morph4.Node.isStringLiteral(lit)) {
1284
+ if (import_ts_morph3.Node.isStringLiteral(lit)) {
1518
1285
  stringLits.push(lit.getLiteralValue());
1519
1286
  continue;
1520
1287
  }
1521
- if (import_ts_morph4.Node.isNumericLiteral(lit)) {
1288
+ if (import_ts_morph3.Node.isNumericLiteral(lit)) {
1522
1289
  numberLits.push(lit.getText());
1523
1290
  continue;
1524
1291
  }
1525
- if (lit.getKind() === import_ts_morph4.SyntaxKind.NullKeyword) {
1292
+ if (lit.getKind() === import_ts_morph3.SyntaxKind.NullKeyword) {
1526
1293
  nullable = true;
1527
1294
  continue;
1528
1295
  }
@@ -1542,19 +1309,19 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1542
1309
  return markNullable({ kind: "unknown" }, nullable);
1543
1310
  }
1544
1311
  switch (typeNode.getKind()) {
1545
- case import_ts_morph4.SyntaxKind.StringKeyword:
1312
+ case import_ts_morph3.SyntaxKind.StringKeyword:
1546
1313
  return { kind: "string" };
1547
- case import_ts_morph4.SyntaxKind.NumberKeyword:
1314
+ case import_ts_morph3.SyntaxKind.NumberKeyword:
1548
1315
  return { kind: "number" };
1549
- case import_ts_morph4.SyntaxKind.BooleanKeyword:
1316
+ case import_ts_morph3.SyntaxKind.BooleanKeyword:
1550
1317
  return { kind: "boolean" };
1551
- case import_ts_morph4.SyntaxKind.AnyKeyword:
1552
- case import_ts_morph4.SyntaxKind.UnknownKeyword:
1318
+ case import_ts_morph3.SyntaxKind.AnyKeyword:
1319
+ case import_ts_morph3.SyntaxKind.UnknownKeyword:
1553
1320
  return { kind: "unknown" };
1554
1321
  default:
1555
1322
  break;
1556
1323
  }
1557
- if (import_ts_morph4.Node.isTypeReference(typeNode)) {
1324
+ if (import_ts_morph3.Node.isTypeReference(typeNode)) {
1558
1325
  const refName = typeNode.getTypeName().getText();
1559
1326
  if (refName === "Date") return { kind: "date" };
1560
1327
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -1572,25 +1339,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1572
1339
  if (typeRef) return { kind: "unknown", typeRef };
1573
1340
  return { kind: "unknown" };
1574
1341
  }
1575
- if (import_ts_morph4.Node.isTypeLiteral(typeNode)) return { kind: "json" };
1342
+ if (import_ts_morph3.Node.isTypeLiteral(typeNode)) return { kind: "json" };
1576
1343
  return { kind: "unknown" };
1577
1344
  }
1578
1345
  function enumFromDecoratorArgs(args, sourceFile, project) {
1579
1346
  for (const arg of args) {
1580
- if (import_ts_morph4.Node.isArrowFunction(arg)) {
1347
+ if (import_ts_morph3.Node.isArrowFunction(arg)) {
1581
1348
  const body = arg.getBody();
1582
- if (import_ts_morph4.Node.isIdentifier(body)) {
1349
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1583
1350
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1584
1351
  if (en) return en;
1585
1352
  }
1586
1353
  }
1587
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1354
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1588
1355
  const itemsProp = arg.getProperty("items");
1589
- if (itemsProp && import_ts_morph4.Node.isPropertyAssignment(itemsProp)) {
1356
+ if (itemsProp && import_ts_morph3.Node.isPropertyAssignment(itemsProp)) {
1590
1357
  const init = itemsProp.getInitializer();
1591
- if (init && import_ts_morph4.Node.isArrowFunction(init)) {
1358
+ if (init && import_ts_morph3.Node.isArrowFunction(init)) {
1592
1359
  const body = init.getBody();
1593
- if (import_ts_morph4.Node.isIdentifier(body)) {
1360
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1594
1361
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1595
1362
  if (en) return en;
1596
1363
  }
@@ -1613,7 +1380,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1613
1380
  return { kind: "string" };
1614
1381
  }
1615
1382
  for (const arg of args) {
1616
- if (import_ts_morph4.Node.isStringLiteral(arg)) {
1383
+ if (import_ts_morph3.Node.isStringLiteral(arg)) {
1617
1384
  const raw = arg.getLiteralValue();
1618
1385
  const kind = classifyTypeKeyword(raw);
1619
1386
  if (kind) {
@@ -1621,11 +1388,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1621
1388
  return { kind };
1622
1389
  }
1623
1390
  }
1624
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1391
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1625
1392
  const enumProp = arg.getProperty("enum");
1626
- if (enumProp && import_ts_morph4.Node.isPropertyAssignment(enumProp)) {
1393
+ if (enumProp && import_ts_morph3.Node.isPropertyAssignment(enumProp)) {
1627
1394
  const init = enumProp.getInitializer();
1628
- if (init && import_ts_morph4.Node.isIdentifier(init)) {
1395
+ if (init && import_ts_morph3.Node.isIdentifier(init)) {
1629
1396
  const en = resolveEnumValues(init.getText(), sourceFile, project);
1630
1397
  if (en) {
1631
1398
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -1635,15 +1402,15 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1635
1402
  }
1636
1403
  for (const key of ["columnType", "type"]) {
1637
1404
  const typeProp = arg.getProperty(key);
1638
- if (!typeProp || !import_ts_morph4.Node.isPropertyAssignment(typeProp)) continue;
1405
+ if (!typeProp || !import_ts_morph3.Node.isPropertyAssignment(typeProp)) continue;
1639
1406
  const init = typeProp.getInitializer();
1640
1407
  if (!init) continue;
1641
- if (import_ts_morph4.Node.isStringLiteral(init)) {
1408
+ if (import_ts_morph3.Node.isStringLiteral(init)) {
1642
1409
  const kind = classifyTypeKeyword(init.getLiteralValue());
1643
1410
  if (kind) return { kind };
1644
1411
  continue;
1645
1412
  }
1646
- if (import_ts_morph4.Node.isIdentifier(init)) {
1413
+ if (import_ts_morph3.Node.isIdentifier(init)) {
1647
1414
  const kind = classifyTypeKeyword(init.getText());
1648
1415
  if (kind) return { kind };
1649
1416
  }
@@ -1686,6 +1453,49 @@ function toFilterFieldType(name, r) {
1686
1453
 
1687
1454
  // src/discovery/heritage.ts
1688
1455
  var import_ts_morph5 = require("ts-morph");
1456
+
1457
+ // src/discovery/unresolved-imports.ts
1458
+ var import_ts_morph4 = require("ts-morph");
1459
+ var UnresolvedImports = class {
1460
+ read;
1461
+ /**
1462
+ * The files with an import that failed to resolve in `program` and now resolves
1463
+ * to a TypeScript file the program does not hold.
1464
+ */
1465
+ nowResolving(program) {
1466
+ if (this.read?.program !== program) {
1467
+ this.read = { program, imports: failedImports(program) };
1468
+ }
1469
+ const options = program.getCompilerOptions();
1470
+ const files = /* @__PURE__ */ new Set();
1471
+ for (const { specifier, from } of this.read.imports) {
1472
+ const file = import_ts_morph4.ts.resolveModuleName(specifier, from, options, import_ts_morph4.ts.sys).resolvedModule?.resolvedFileName;
1473
+ if (file !== void 0 && /\.(m|c)?tsx?$/.test(file) && !program.getSourceFile(file)) {
1474
+ files.add(from);
1475
+ }
1476
+ }
1477
+ return [...files];
1478
+ }
1479
+ };
1480
+ function failedImports(program) {
1481
+ const checker = program.getTypeChecker();
1482
+ const imports = [];
1483
+ for (const file of program.getSourceFiles()) {
1484
+ if (program.isSourceFileDefaultLibrary(file) || file.isDeclarationFile) continue;
1485
+ if (program.isSourceFileFromExternalLibrary(file)) continue;
1486
+ for (const statement of file.statements) {
1487
+ if (!import_ts_morph4.ts.isImportDeclaration(statement) && !import_ts_morph4.ts.isExportDeclaration(statement)) continue;
1488
+ const specifier = statement.moduleSpecifier;
1489
+ if (!specifier || !import_ts_morph4.ts.isStringLiteral(specifier)) continue;
1490
+ if (!checker.getSymbolAtLocation(specifier)) {
1491
+ imports.push({ specifier: specifier.text, from: file.fileName });
1492
+ }
1493
+ }
1494
+ }
1495
+ return imports;
1496
+ }
1497
+
1498
+ // src/discovery/heritage.ts
1689
1499
  function resolveHeritageCall(node) {
1690
1500
  if (!node) return void 0;
1691
1501
  const expr = unwrapExpression(node);
@@ -1885,15 +1695,42 @@ function resolvePropertyClassReference(prop) {
1885
1695
  return { key, ref };
1886
1696
  }
1887
1697
  var mixinTypeProject;
1698
+ var mixinFiles = new DiskFingerprints();
1699
+ var mixinStale = false;
1700
+ var mixinUnresolved = new UnresolvedImports();
1888
1701
  function getMixinTypeProject() {
1889
- mixinTypeProject ??= new import_ts_morph5.Project({
1890
- skipAddingFilesFromTsConfig: true,
1891
- compilerOptions: { strict: true }
1892
- });
1702
+ if (mixinTypeProject && mixinStale && !refreshMixinTypeProject(mixinTypeProject)) {
1703
+ mixinTypeProject = void 0;
1704
+ }
1705
+ mixinStale = false;
1706
+ if (!mixinTypeProject) {
1707
+ mixinTypeProject = new import_ts_morph5.Project({
1708
+ skipAddingFilesFromTsConfig: true,
1709
+ compilerOptions: { strict: true }
1710
+ });
1711
+ mixinFiles = new DiskFingerprints();
1712
+ mixinUnresolved = new UnresolvedImports();
1713
+ }
1893
1714
  return mixinTypeProject;
1894
1715
  }
1895
1716
  function clearMixinTypeProject() {
1896
1717
  mixinTypeProject = void 0;
1718
+ mixinStale = false;
1719
+ }
1720
+ function invalidateMixinTypeProject() {
1721
+ mixinStale = true;
1722
+ }
1723
+ function refreshMixinTypeProject(project) {
1724
+ const program = project.getProgram().compilerObject;
1725
+ const files = program.getSourceFiles().filter((file) => !program.isSourceFileDefaultLibrary(file)).map((file) => ({ path: file.fileName, text: file.text }));
1726
+ const changed = mixinFiles.changed(files);
1727
+ if (changed.length === 0) return mixinUnresolved.nowResolving(program).length === 0;
1728
+ for (const path of changed) {
1729
+ const sf = project.getSourceFile(path);
1730
+ if (sf?.refreshFromFileSystemSync() !== import_ts_morph5.FileSystemRefreshResult.Updated) return false;
1731
+ }
1732
+ project.resolveSourceFileDependencies();
1733
+ return true;
1897
1734
  }
1898
1735
  function resolveInstantiatedReturnType(cls, methodName) {
1899
1736
  const filePath = cls.getSourceFile().getFilePath();
@@ -2251,8 +2088,7 @@ var SchemaTypeProgram = class {
2251
2088
  skipAddingFilesFromTsConfig: true,
2252
2089
  compilerOptions: this.options
2253
2090
  });
2254
- for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
2255
- this.flush();
2091
+ this.requestDiscoveryFiles();
2256
2092
  }
2257
2093
  project;
2258
2094
  options;
@@ -2265,6 +2101,53 @@ var SchemaTypeProgram = class {
2265
2101
  needs = /* @__PURE__ */ new Map();
2266
2102
  /** Pruned text per file, as last written into {@link project}. */
2267
2103
  written = /* @__PURE__ */ new Map();
2104
+ /** What {@link project} read from disk unpruned (packages, declaration files). */
2105
+ externalFiles = new DiskFingerprints();
2106
+ scratchFiles = new DiskFingerprints();
2107
+ /** The last pruning of each file, with the source text and needs it was pruned for. */
2108
+ pruned = /* @__PURE__ */ new Map();
2109
+ /**
2110
+ * Bring the program up to date with discovery's files and the disk, keeping the
2111
+ * lib and package declarations it has already parsed and checked. Leaves it
2112
+ * holding exactly the files a new program built now would hold, with the same
2113
+ * text. Returns false when a file the program read unpruned has changed on disk
2114
+ * (a package was reinstalled, a `.d.ts` was edited): the caller builds a new
2115
+ * program instead.
2116
+ */
2117
+ refresh() {
2118
+ const program = this.project.getProgram().compilerObject;
2119
+ const external = program.getSourceFiles().filter(
2120
+ (file) => !program.isSourceFileDefaultLibrary(file) && !this.written.has(file.fileName)
2121
+ ).map((file) => ({ path: file.fileName, text: file.text }));
2122
+ if (this.externalFiles.changed(external).length > 0) return false;
2123
+ const scratch = this.scratch.getSourceFiles().map((sf) => ({ path: sf.getFilePath(), text: sf.getFullText() }));
2124
+ for (const path of this.scratchFiles.changed(scratch)) {
2125
+ const sf = this.scratch.getSourceFile(path);
2126
+ if (sf?.refreshFromFileSystemSync() === import_ts_morph7.FileSystemRefreshResult.Deleted) {
2127
+ this.scratchFiles.forget(path);
2128
+ }
2129
+ }
2130
+ this.needs.clear();
2131
+ this.requestDiscoveryFiles();
2132
+ for (const path of [...this.written.keys()]) {
2133
+ if (this.needs.has(path)) continue;
2134
+ const sf = this.project.getSourceFile(path);
2135
+ if (sf) this.project.removeSourceFile(sf);
2136
+ this.written.delete(path);
2137
+ }
2138
+ return true;
2139
+ }
2140
+ /**
2141
+ * Everything discovery holds now (the controllers) goes in before the first
2142
+ * query: a file added afterwards discards the program and its checker, and
2143
+ * rebuilding them once per controller is exactly the cost this avoids.
2144
+ */
2145
+ requestDiscoveryFiles() {
2146
+ for (const sf of this.discovery.getSourceFiles()) {
2147
+ this.request(sf.getFilePath(), { schema: true });
2148
+ }
2149
+ this.flush();
2150
+ }
2268
2151
  /**
2269
2152
  * The typed copy of a discovery file, with its schema positions kept. Adding one
2270
2153
  * that was not there yet (a contract file discovery reached lazily) rebuilds the
@@ -2305,12 +2188,21 @@ var SchemaTypeProgram = class {
2305
2188
  while (this.queue.length > 0) {
2306
2189
  const path = this.queue.pop();
2307
2190
  const original = this.original(path);
2308
- if (!original) continue;
2309
- const needs = this.needs.get(path);
2310
- const text = this.prune(original, needs);
2191
+ if (!original) {
2192
+ if (this.written.has(path)) changed.set(path, void 0);
2193
+ continue;
2194
+ }
2195
+ const { text, requests } = this.pruneCached(path, original, this.needs.get(path));
2196
+ for (const { path: target, want } of requests) this.request(target, want);
2311
2197
  if (this.written.get(path) !== text) changed.set(path, text);
2312
2198
  }
2313
2199
  for (const [path, text] of changed) {
2200
+ if (text === void 0) {
2201
+ const sf = this.project.getSourceFile(path);
2202
+ if (sf) this.project.removeSourceFile(sf);
2203
+ this.written.delete(path);
2204
+ continue;
2205
+ }
2314
2206
  this.project.createSourceFile(path, text, { overwrite: true });
2315
2207
  this.written.set(path, text);
2316
2208
  }
@@ -2319,11 +2211,29 @@ var SchemaTypeProgram = class {
2319
2211
  original(path) {
2320
2212
  return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
2321
2213
  }
2214
+ /** {@link prune}, reusing the last result while the file's text and needs are the same. */
2215
+ pruneCached(path, sf, needs) {
2216
+ const source = sf.getFullText();
2217
+ const key = `${needs.schema}:${needs.all}:${[...needs.names].sort().join(",")}`;
2218
+ const last = this.pruned.get(path);
2219
+ if (last && last.source === source && last.needs === key) return last;
2220
+ const result = this.prune(sf, needs);
2221
+ if (result.resolved) this.pruned.set(path, { ...result, source, needs: key });
2222
+ else this.pruned.delete(path);
2223
+ return result;
2224
+ }
2322
2225
  /**
2323
2226
  * `sf`'s text with every import/re-export nothing in `needs` references blanked,
2324
- * requesting from each kept one's target the names it brings in.
2227
+ * and the names each kept one brings in from its target.
2325
2228
  */
2326
2229
  prune(sf, needs) {
2230
+ const requests = [];
2231
+ let resolved = true;
2232
+ const resolveFrom = (specifier) => {
2233
+ const target = this.resolve(specifier, sf);
2234
+ if (target === null) resolved = false;
2235
+ return target ?? void 0;
2236
+ };
2327
2237
  const locals = topLevelDeclarations(sf);
2328
2238
  const roots = [];
2329
2239
  if (needs.all) roots.push(...sf.getStatements());
@@ -2376,7 +2286,7 @@ var SchemaTypeProgram = class {
2376
2286
  }
2377
2287
  };
2378
2288
  for (const decl of sf.getImportDeclarations()) {
2379
- const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2289
+ const target = resolveFrom(decl.getModuleSpecifierValue());
2380
2290
  const namespace = decl.getNamespaceImport()?.getText();
2381
2291
  const def = decl.getDefaultImport()?.getText();
2382
2292
  const named = decl.getNamedImports().map((n) => ({
@@ -2390,21 +2300,24 @@ var SchemaTypeProgram = class {
2390
2300
  continue;
2391
2301
  }
2392
2302
  if (target) {
2393
- this.request(target, {
2394
- all: wantsNamespace,
2395
- names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2303
+ requests.push({
2304
+ path: target,
2305
+ want: {
2306
+ all: wantsNamespace,
2307
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2308
+ }
2396
2309
  });
2397
2310
  }
2398
2311
  }
2399
2312
  for (const exp of sf.getExportDeclarations()) {
2400
2313
  const spec = exp.getModuleSpecifierValue();
2401
2314
  if (spec === void 0) continue;
2402
- const target = this.resolve(spec, sf);
2315
+ const target = resolveFrom(spec);
2403
2316
  if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
2404
2317
  if (!needs.all && unresolvedNames.length === 0) {
2405
2318
  blank(exp);
2406
2319
  } else if (target) {
2407
- this.request(target, { all: needs.all, names: unresolvedNames });
2320
+ requests.push({ path: target, want: { all: needs.all, names: unresolvedNames } });
2408
2321
  }
2409
2322
  continue;
2410
2323
  }
@@ -2412,13 +2325,14 @@ var SchemaTypeProgram = class {
2412
2325
  (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
2413
2326
  ).map((n) => n.getNameNode().getText());
2414
2327
  if (wanted.length === 0) blank(exp);
2415
- else if (target) this.request(target, { names: wanted });
2328
+ else if (target) requests.push({ path: target, want: { names: wanted } });
2416
2329
  }
2417
- return chars.join("");
2330
+ return { text: chars.join(""), requests, resolved };
2418
2331
  }
2419
2332
  /**
2420
- * The project source file a module specifier resolves to, or undefined for a
2421
- * package (read unpruned by the program itself) or an unresolvable one.
2333
+ * The project source file a module specifier resolves to; undefined for a
2334
+ * package or a declaration file (read unpruned by the program itself), null for
2335
+ * one that does not resolve.
2422
2336
  */
2423
2337
  resolve(specifier, from) {
2424
2338
  const resolved = import_ts_morph7.ts.resolveModuleName(
@@ -2427,7 +2341,8 @@ var SchemaTypeProgram = class {
2427
2341
  this.options,
2428
2342
  import_ts_morph7.ts.sys
2429
2343
  ).resolvedModule;
2430
- if (!resolved || resolved.isExternalLibraryImport) return void 0;
2344
+ if (!resolved) return null;
2345
+ if (resolved.isExternalLibraryImport) return void 0;
2431
2346
  const file = resolved.resolvedFileName;
2432
2347
  if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
2433
2348
  return file;
@@ -2482,8 +2397,13 @@ function defaultExport(sf) {
2482
2397
 
2483
2398
  // src/discovery/schema-type-inference.ts
2484
2399
  var programs = /* @__PURE__ */ new WeakMap();
2400
+ var stale = /* @__PURE__ */ new WeakSet();
2485
2401
  function programFor(discovery) {
2486
2402
  let program = programs.get(discovery);
2403
+ if (program && stale.has(discovery)) {
2404
+ stale.delete(discovery);
2405
+ if (!program.refresh()) program = void 0;
2406
+ }
2487
2407
  if (!program) {
2488
2408
  program = new SchemaTypeProgram(discovery);
2489
2409
  programs.set(discovery, program);
@@ -2492,6 +2412,10 @@ function programFor(discovery) {
2492
2412
  }
2493
2413
  function clearSchemaTypeProject(discovery) {
2494
2414
  programs.delete(discovery);
2415
+ stale.delete(discovery);
2416
+ }
2417
+ function invalidateSchemaTypeProject(discovery) {
2418
+ if (programs.has(discovery)) stale.add(discovery);
2495
2419
  }
2496
2420
  function counterpart(typedSf, node) {
2497
2421
  const start = node.getStart();
@@ -3439,7 +3363,7 @@ async function discoverContractsFast(opts) {
3439
3363
  return routes;
3440
3364
  }
3441
3365
  function resolveTsconfigPath(cwd, tsconfig) {
3442
- return tsconfig ? (0, import_node_path5.resolve)(tsconfig) : (0, import_node_path5.join)(cwd, "tsconfig.json");
3366
+ return tsconfig ? (0, import_node_path4.resolve)(tsconfig) : (0, import_node_path4.join)(cwd, "tsconfig.json");
3443
3367
  }
3444
3368
  var NO_TSCONFIG_OPTIONS = {
3445
3369
  allowJs: true,
@@ -3447,7 +3371,7 @@ var NO_TSCONFIG_OPTIONS = {
3447
3371
  strict: false
3448
3372
  };
3449
3373
  function loadDiscoveryTsconfig(tsconfigPath) {
3450
- if (!(0, import_node_fs.existsSync)(tsconfigPath)) return { options: null, files: [] };
3374
+ if (!(0, import_node_fs2.existsSync)(tsconfigPath)) return { options: null, files: [] };
3451
3375
  const read = import_ts_morph12.ts.readConfigFile(tsconfigPath, (path) => import_ts_morph12.ts.sys.readFile(path));
3452
3376
  if (read.error) {
3453
3377
  return {
@@ -3469,7 +3393,7 @@ function loadDiscoveryTsconfig(tsconfigPath) {
3469
3393
  const parsed = import_ts_morph12.ts.parseJsonSourceFileConfigFileContent(
3470
3394
  sourceFile,
3471
3395
  host,
3472
- (0, import_node_path5.dirname)(tsconfigPath),
3396
+ (0, import_node_path4.dirname)(tsconfigPath),
3473
3397
  void 0,
3474
3398
  tsconfigPath
3475
3399
  );
@@ -3531,6 +3455,9 @@ var PersistentDiscovery = class _PersistentDiscovery {
3531
3455
  glob;
3532
3456
  /** Absolute paths of the controllers currently loaded as extraction roots. */
3533
3457
  controllerPaths = /* @__PURE__ */ new Set();
3458
+ /** Every file in {@link project}, as last read from disk. */
3459
+ files = new DiskFingerprints();
3460
+ unresolved = new UnresolvedImports();
3534
3461
  constructor(project, cwd, glob) {
3535
3462
  this.project = project;
3536
3463
  this.cwd = cwd;
@@ -3556,6 +3483,8 @@ var PersistentDiscovery = class _PersistentDiscovery {
3556
3483
  }
3557
3484
  /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3558
3485
  discover() {
3486
+ clearMixinTypeProject();
3487
+ clearSchemaTypeProject(this.project);
3559
3488
  return this.runExtraction();
3560
3489
  }
3561
3490
  /**
@@ -3563,26 +3492,39 @@ var PersistentDiscovery = class _PersistentDiscovery {
3563
3492
  * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3564
3493
  * to pick up added/removed controllers, clears the per-Project caches, then
3565
3494
  * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3566
- * being exhaustive because re-globbing + refresh-on-presence covers the set.
3495
+ * being exhaustive because re-globbing + comparing every loaded file with the
3496
+ * disk covers the set.
3567
3497
  */
3568
- async rediscover(changedPaths) {
3569
- if (changedPaths) {
3570
- for (const p of changedPaths) {
3571
- const abs = (0, import_node_path5.resolve)(p);
3572
- const sf = this.project.getSourceFile(abs);
3573
- if (sf) {
3574
- await sf.refreshFromFileSystem();
3575
- }
3498
+ async rediscover(changedPaths, options = {}) {
3499
+ const hinted = new Set([...changedPaths ?? []].map((p) => (0, import_node_path4.resolve)(p)));
3500
+ const program = this.project.getProgram().compilerObject;
3501
+ const sources = program.getSourceFiles().filter(
3502
+ (file) => !program.isSourceFileDefaultLibrary(file) && !program.isSourceFileFromExternalLibrary(file)
3503
+ ).map((file) => ({ path: file.fileName, text: file.text }));
3504
+ for (const path of /* @__PURE__ */ new Set([...hinted, ...this.files.changed(sources)])) {
3505
+ const sf = this.project.getSourceFile(path);
3506
+ if (sf && await sf.refreshFromFileSystem() === import_ts_morph12.FileSystemRefreshResult.Deleted) {
3507
+ this.files.forget(path);
3576
3508
  }
3577
3509
  }
3510
+ const inProject = new Set(this.project.getSourceFiles().map((sf) => sf.getFilePath()));
3511
+ const definitions = this.project.getLanguageService().compilerObject.getProgram();
3512
+ for (const path of definitions ? this.unresolved.nowResolving(definitions) : []) {
3513
+ const sf = this.project.getSourceFile(path);
3514
+ if (!sf) continue;
3515
+ this.project.removeSourceFile(sf);
3516
+ if (inProject.has(path)) this.project.addSourceFileAtPathIfExists(path);
3517
+ }
3578
3518
  const globbed = new Set(
3579
3519
  await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3580
3520
  );
3521
+ let controllersChanged = false;
3581
3522
  for (const f of globbed) {
3582
3523
  if (!this.controllerPaths.has(f)) {
3583
3524
  try {
3584
3525
  this.project.addSourceFileAtPath(f);
3585
3526
  this.controllerPaths.add(f);
3527
+ controllersChanged = true;
3586
3528
  } catch {
3587
3529
  }
3588
3530
  }
@@ -3592,16 +3534,22 @@ var PersistentDiscovery = class _PersistentDiscovery {
3592
3534
  const sf = this.project.getSourceFile(f);
3593
3535
  if (sf) this.project.removeSourceFile(sf);
3594
3536
  this.controllerPaths.delete(f);
3537
+ controllersChanged = true;
3595
3538
  }
3596
3539
  }
3540
+ if (controllersChanged || options.filesAddedOrRemoved) {
3541
+ clearMixinTypeProject();
3542
+ clearSchemaTypeProject(this.project);
3543
+ } else {
3544
+ invalidateMixinTypeProject();
3545
+ invalidateSchemaTypeProject(this.project);
3546
+ }
3597
3547
  return this.runExtraction();
3598
3548
  }
3599
3549
  /** Clear stale per-Project caches, then extract over the controller set. */
3600
3550
  runExtraction() {
3601
3551
  clearTypeResolutionCaches(this.project);
3602
3552
  clearEnumCache(this.project);
3603
- clearMixinTypeProject();
3604
- clearSchemaTypeProject(this.project);
3605
3553
  return extractRoutesFrom(this.project, this.controllerPaths);
3606
3554
  }
3607
3555
  };
@@ -3751,144 +3699,437 @@ function extractContractRoute(args) {
3751
3699
  if (contractDef.body !== null) {
3752
3700
  bodyZodRef = { name: `${identName}.body`, filePath };
3753
3701
  }
3754
- if (contractDef.query !== null) {
3755
- queryZodRef = { name: `${identName}.query`, filePath };
3702
+ if (contractDef.query !== null) {
3703
+ queryZodRef = { name: `${identName}.query`, filePath };
3704
+ }
3705
+ }
3706
+ } else {
3707
+ console.warn(
3708
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3709
+ );
3710
+ return null;
3711
+ }
3712
+ if (!contractDef) return null;
3713
+ if (!verb) return null;
3714
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3715
+ const methodName = method.getName();
3716
+ const classAs = readAsDecorator(cls, `class ${className}`);
3717
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3718
+ return buildRoute({
3719
+ className,
3720
+ methodName,
3721
+ resolvedMethod: verb.httpMethod,
3722
+ combinedPath: resolvedPath,
3723
+ classAs,
3724
+ methodAs,
3725
+ sourceFile,
3726
+ seenNames,
3727
+ mixin,
3728
+ contractSource: {
3729
+ query: contractDef.query,
3730
+ body: contractDef.body,
3731
+ response: contractDef.response,
3732
+ error: contractDef.error,
3733
+ // Path A: capture both the importable ref and the raw text. The emitter
3734
+ // prefers inlining the text (client-safe — re-exporting from a controller
3735
+ // would drag server-only deps into the client bundle).
3736
+ bodyZodRef,
3737
+ bodyZodText: contractDef.bodyZodText,
3738
+ queryZodRef,
3739
+ queryZodText: contractDef.queryZodText
3740
+ }
3741
+ });
3742
+ }
3743
+ function extractDtoRoute(args) {
3744
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
3745
+ if (!verb) return null;
3746
+ const combined = joinPaths(prefix, verb.handlerPath);
3747
+ const methodName = method.getName();
3748
+ const classAs = readAsDecorator(cls, `class ${className}`);
3749
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3750
+ const dtoContract = extractDtoContract(method, method.getSourceFile(), project, mixin, cls);
3751
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
3752
+ return buildRoute({
3753
+ className,
3754
+ methodName,
3755
+ resolvedMethod: verb.httpMethod,
3756
+ combinedPath: combined,
3757
+ classAs,
3758
+ methodAs,
3759
+ sourceFile,
3760
+ seenNames,
3761
+ mixin,
3762
+ contractSource: {
3763
+ query: dtoContract?.query ?? null,
3764
+ body: dtoContract?.body ?? null,
3765
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
3766
+ error: dtoContract?.error ?? null,
3767
+ paramTypes: dtoContract?.paramTypes ?? null,
3768
+ queryRef: dtoContract?.queryRef ?? null,
3769
+ bodyRef: dtoContract?.bodyRef ?? null,
3770
+ responseRef: dtoContract?.responseRef ?? null,
3771
+ errorRef: dtoContract?.errorRef ?? null,
3772
+ filterFields: dtoContract?.filterFields ?? null,
3773
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3774
+ filterSource: dtoContract?.filterSource ?? null,
3775
+ formWarnings: dtoContract?.formWarnings ?? [],
3776
+ bodySchema: dtoContract?.bodySchema ?? null,
3777
+ querySchema: dtoContract?.querySchema ?? null,
3778
+ stream: dtoContract?.stream ?? false,
3779
+ multipart: dtoContract?.multipart ?? false,
3780
+ multipartBody: dtoContract?.multipartBody ?? null,
3781
+ binaryResponse: dtoContract?.binaryResponse ?? false,
3782
+ asQuery: dtoContract?.asQuery ?? false
3783
+ }
3784
+ });
3785
+ }
3786
+ function extractFromSourceFile(sourceFile, project) {
3787
+ const routes = [];
3788
+ const seenNames = /* @__PURE__ */ new Map();
3789
+ for (const cls of sourceFile.getClasses()) {
3790
+ const controllerDecorator = cls.getDecorator("Controller");
3791
+ if (!controllerDecorator) continue;
3792
+ const firstArg2 = controllerDecorator.getArguments()[0];
3793
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3794
+ const className = cls.getName() ?? "Unknown";
3795
+ const inherited = resolveInheritedMethods(cls);
3796
+ const mixinBinding = inherited ? {
3797
+ factoryName: inherited.factoryName,
3798
+ factoryFilePath: inherited.factoryFilePath,
3799
+ classArgs: inherited.classArgs,
3800
+ namedClassArgs: inherited.namedClassArgs
3801
+ } : void 0;
3802
+ const ownMethods = cls.getMethods();
3803
+ const ownNames = new Set(ownMethods.map((m) => m.getName()));
3804
+ const methods = [
3805
+ ...ownMethods.map((method) => ({ method, mixin: mixinBinding })),
3806
+ ...(inherited?.methods ?? []).filter((method) => !ownNames.has(method.getName())).map((method) => ({ method, mixin: mixinBinding }))
3807
+ ];
3808
+ for (const { method, mixin } of methods) {
3809
+ const verb = resolveVerb(method);
3810
+ const applyContractDecorator = method.getDecorator("ApplyContract");
3811
+ const route = applyContractDecorator ? extractContractRoute({
3812
+ cls,
3813
+ method,
3814
+ applyContractDecorator,
3815
+ verb,
3816
+ prefix,
3817
+ className,
3818
+ sourceFile,
3819
+ project,
3820
+ seenNames,
3821
+ mixin
3822
+ }) : extractDtoRoute({
3823
+ cls,
3824
+ method,
3825
+ verb,
3826
+ prefix,
3827
+ className,
3828
+ sourceFile,
3829
+ project,
3830
+ seenNames,
3831
+ mixin
3832
+ });
3833
+ if (route) routes.push(route);
3834
+ }
3835
+ }
3836
+ return routes;
3837
+ }
3838
+
3839
+ // src/discovery/shared-props.ts
3840
+ function discoverSharedProps(project, moduleEntry) {
3841
+ try {
3842
+ let sourceFile = project.getSourceFile(moduleEntry);
3843
+ if (!sourceFile) {
3844
+ try {
3845
+ sourceFile = project.addSourceFileAtPath(moduleEntry);
3846
+ } catch {
3847
+ return null;
3848
+ }
3849
+ }
3850
+ const forRootCall = findForRootCall(sourceFile);
3851
+ if (!forRootCall) return null;
3852
+ const initializer = findShareInitializer(forRootCall);
3853
+ if (!initializer) return null;
3854
+ return extractShareType(initializer, sourceFile, project);
3855
+ } catch {
3856
+ return null;
3857
+ }
3858
+ }
3859
+ function discoverSharedPropsFromConfig(config) {
3860
+ if (!config.app?.moduleEntry) return null;
3861
+ try {
3862
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path5.join)(config.codegen.cwd, "tsconfig.json");
3863
+ let project;
3864
+ try {
3865
+ project = sharedPropsProject(tsconfigPath);
3866
+ } catch {
3867
+ project = new import_ts_morph13.Project({
3868
+ skipAddingFilesFromTsConfig: true,
3869
+ skipLoadingLibFiles: true,
3870
+ skipFileDependencyResolution: true,
3871
+ compilerOptions: { allowJs: true, strict: false }
3872
+ });
3873
+ }
3874
+ return discoverSharedProps(project, config.app.moduleEntry);
3875
+ } catch {
3876
+ return null;
3877
+ }
3878
+ }
3879
+ var parsedTsconfigs = /* @__PURE__ */ new Map();
3880
+ function readText(file) {
3881
+ try {
3882
+ return (0, import_node_fs3.readFileSync)(file, "utf8");
3883
+ } catch {
3884
+ return "\0missing";
3885
+ }
3886
+ }
3887
+ function sharedPropsProject(tsconfigPath) {
3888
+ const text = loadDiscoveryTsconfig(tsconfigPath).files.map((file) => `${file}\0${readText(file)}`).join("\0");
3889
+ const flags = {
3890
+ skipAddingFilesFromTsConfig: true,
3891
+ skipLoadingLibFiles: true,
3892
+ skipFileDependencyResolution: true
3893
+ };
3894
+ const parsed = parsedTsconfigs.get(tsconfigPath);
3895
+ if (parsed?.text === text) return new import_ts_morph13.Project({ ...flags, compilerOptions: parsed.options });
3896
+ const project = new import_ts_morph13.Project({ ...flags, tsConfigFilePath: tsconfigPath });
3897
+ parsedTsconfigs.set(tsconfigPath, { text, options: project.getCompilerOptions() });
3898
+ return project;
3899
+ }
3900
+ function findForRootCall(sourceFile) {
3901
+ const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph13.SyntaxKind.CallExpression);
3902
+ for (const call of callExpressions) {
3903
+ const expr = call.getExpression();
3904
+ if (!import_ts_morph13.Node.isPropertyAccessExpression(expr)) continue;
3905
+ const methodName = expr.getName();
3906
+ const objectExpr = expr.getExpression();
3907
+ if (methodName === "forRoot" && import_ts_morph13.Node.isIdentifier(objectExpr)) {
3908
+ const name = objectExpr.getText();
3909
+ if (name === "InertiaModule") {
3910
+ return call;
3911
+ }
3912
+ }
3913
+ }
3914
+ return null;
3915
+ }
3916
+ function findShareInitializer(forRootCall) {
3917
+ if (!import_ts_morph13.Node.isCallExpression(forRootCall)) return null;
3918
+ const args = forRootCall.getArguments();
3919
+ const firstArg2 = args[0];
3920
+ if (!firstArg2 || !import_ts_morph13.Node.isObjectLiteralExpression(firstArg2)) return null;
3921
+ for (const prop of firstArg2.getProperties()) {
3922
+ if (import_ts_morph13.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
3923
+ return prop.getInitializer() ?? null;
3924
+ }
3925
+ }
3926
+ return null;
3927
+ }
3928
+ function extractShareType(node, sourceFile, project) {
3929
+ if (import_ts_morph13.Node.isIdentifier(node)) {
3930
+ const ref = resolveIdentifierToImportRef(node, sourceFile, project);
3931
+ if (ref) {
3932
+ return {
3933
+ typeString: `Awaited<ReturnType<typeof import('${ref.importPath}').${ref.exportName}>>`,
3934
+ properties: null,
3935
+ isImportRef: true
3936
+ };
3937
+ }
3938
+ }
3939
+ if (import_ts_morph13.Node.isArrowFunction(node)) {
3940
+ const result = extractFromFunctionLike(node, sourceFile);
3941
+ return result ? { ...result, isImportRef: false } : null;
3942
+ }
3943
+ if (import_ts_morph13.Node.isFunctionExpression(node)) {
3944
+ const result = extractFromFunctionLike(node, sourceFile);
3945
+ return result ? { ...result, isImportRef: false } : null;
3946
+ }
3947
+ if (import_ts_morph13.Node.isObjectLiteralExpression(node)) {
3948
+ const result = extractFromObjectLiteral(node);
3949
+ return result ? { ...result, isImportRef: false } : null;
3950
+ }
3951
+ return null;
3952
+ }
3953
+ function resolveIdentifierToImportRef(id, sourceFile, project) {
3954
+ if (!import_ts_morph13.Node.isIdentifier(id)) return null;
3955
+ const name = id.getText();
3956
+ const localFunc = sourceFile.getFunction(name);
3957
+ if (localFunc?.isExported()) {
3958
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
3959
+ return { importPath: filePath, exportName: name };
3960
+ }
3961
+ const localVar = sourceFile.getVariableDeclaration(name);
3962
+ if (localVar?.isExported()) {
3963
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
3964
+ return { importPath: filePath, exportName: name };
3965
+ }
3966
+ for (const imp of sourceFile.getImportDeclarations()) {
3967
+ for (const named of imp.getNamedImports()) {
3968
+ const importedName = named.getAliasNode()?.getText() ?? named.getName();
3969
+ if (importedName !== name) continue;
3970
+ const resolvedSource = imp.getModuleSpecifierSourceFile();
3971
+ if (!resolvedSource) continue;
3972
+ const originalName = named.getName();
3973
+ const fn = resolvedSource.getFunction(originalName);
3974
+ if (fn?.isExported()) {
3975
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
3976
+ return { importPath: filePath, exportName: originalName };
3977
+ }
3978
+ const v = resolvedSource.getVariableDeclaration(originalName);
3979
+ if (v?.isExported()) {
3980
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
3981
+ return { importPath: filePath, exportName: originalName };
3982
+ }
3983
+ }
3984
+ }
3985
+ return null;
3986
+ }
3987
+ function extractFromFunctionLike(node, _sourceFile) {
3988
+ const returnTypeNode = import_ts_morph13.Node.isArrowFunction(node) || import_ts_morph13.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
3989
+ if (returnTypeNode) {
3990
+ return extractFromReturnTypeAnnotation(returnTypeNode);
3991
+ }
3992
+ if (import_ts_morph13.Node.isArrowFunction(node)) {
3993
+ const body = node.getBody();
3994
+ if (import_ts_morph13.Node.isParenthesizedExpression(body)) {
3995
+ const inner = body.getExpression();
3996
+ if (import_ts_morph13.Node.isObjectLiteralExpression(inner)) {
3997
+ return extractFromObjectLiteral(inner);
3998
+ }
3999
+ }
4000
+ if (import_ts_morph13.Node.isObjectLiteralExpression(body)) {
4001
+ return extractFromObjectLiteral(body);
4002
+ }
4003
+ if (import_ts_morph13.Node.isBlock(body)) {
4004
+ return extractFromBlockReturn(body);
4005
+ }
4006
+ }
4007
+ if (import_ts_morph13.Node.isFunctionExpression(node)) {
4008
+ const body = node.getBody();
4009
+ if (import_ts_morph13.Node.isBlock(body)) {
4010
+ return extractFromBlockReturn(body);
4011
+ }
4012
+ }
4013
+ return null;
4014
+ }
4015
+ function extractFromReturnTypeAnnotation(typeNode) {
4016
+ if (import_ts_morph13.Node.isTypeReference(typeNode)) {
4017
+ const typeName = typeNode.getTypeName();
4018
+ if (import_ts_morph13.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
4019
+ const typeArgs = typeNode.getTypeArguments();
4020
+ const firstArg2 = typeArgs[0];
4021
+ if (firstArg2) {
4022
+ return extractFromReturnTypeAnnotation(firstArg2);
3756
4023
  }
4024
+ return null;
3757
4025
  }
3758
- } else {
3759
- console.warn(
3760
- `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3761
- );
3762
- return null;
3763
4026
  }
3764
- if (!contractDef) return null;
3765
- if (!verb) return null;
3766
- const resolvedPath = joinPaths(prefix, verb.handlerPath);
3767
- const methodName = method.getName();
3768
- const classAs = readAsDecorator(cls, `class ${className}`);
3769
- const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3770
- return buildRoute({
3771
- className,
3772
- methodName,
3773
- resolvedMethod: verb.httpMethod,
3774
- combinedPath: resolvedPath,
3775
- classAs,
3776
- methodAs,
3777
- sourceFile,
3778
- seenNames,
3779
- mixin,
3780
- contractSource: {
3781
- query: contractDef.query,
3782
- body: contractDef.body,
3783
- response: contractDef.response,
3784
- error: contractDef.error,
3785
- // Path A: capture both the importable ref and the raw text. The emitter
3786
- // prefers inlining the text (client-safe — re-exporting from a controller
3787
- // would drag server-only deps into the client bundle).
3788
- bodyZodRef,
3789
- bodyZodText: contractDef.bodyZodText,
3790
- queryZodRef,
3791
- queryZodText: contractDef.queryZodText
4027
+ if (import_ts_morph13.Node.isTypeLiteral(typeNode)) {
4028
+ const properties = [];
4029
+ for (const member of typeNode.getMembers()) {
4030
+ if (import_ts_morph13.Node.isPropertySignature(member)) {
4031
+ const name = member.getName();
4032
+ const memberTypeNode = member.getTypeNode();
4033
+ const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
4034
+ properties.push({ name, type });
4035
+ }
3792
4036
  }
3793
- });
4037
+ if (properties.length === 0) return null;
4038
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
4039
+ return { typeString, properties, isImportRef: false };
4040
+ }
4041
+ return null;
3794
4042
  }
3795
- function extractDtoRoute(args) {
3796
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
3797
- if (!verb) return null;
3798
- const combined = joinPaths(prefix, verb.handlerPath);
3799
- const methodName = method.getName();
3800
- const classAs = readAsDecorator(cls, `class ${className}`);
3801
- const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3802
- const dtoContract = extractDtoContract(method, method.getSourceFile(), project, mixin, cls);
3803
- const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
3804
- return buildRoute({
3805
- className,
3806
- methodName,
3807
- resolvedMethod: verb.httpMethod,
3808
- combinedPath: combined,
3809
- classAs,
3810
- methodAs,
3811
- sourceFile,
3812
- seenNames,
3813
- mixin,
3814
- contractSource: {
3815
- query: dtoContract?.query ?? null,
3816
- body: dtoContract?.body ?? null,
3817
- response: mixinResponse ?? dtoContract?.response ?? "unknown",
3818
- error: dtoContract?.error ?? null,
3819
- paramTypes: dtoContract?.paramTypes ?? null,
3820
- queryRef: dtoContract?.queryRef ?? null,
3821
- bodyRef: dtoContract?.bodyRef ?? null,
3822
- responseRef: dtoContract?.responseRef ?? null,
3823
- errorRef: dtoContract?.errorRef ?? null,
3824
- filterFields: dtoContract?.filterFields ?? null,
3825
- filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3826
- filterSource: dtoContract?.filterSource ?? null,
3827
- formWarnings: dtoContract?.formWarnings ?? [],
3828
- bodySchema: dtoContract?.bodySchema ?? null,
3829
- querySchema: dtoContract?.querySchema ?? null,
3830
- stream: dtoContract?.stream ?? false,
3831
- multipart: dtoContract?.multipart ?? false,
3832
- multipartBody: dtoContract?.multipartBody ?? null,
3833
- binaryResponse: dtoContract?.binaryResponse ?? false,
3834
- asQuery: dtoContract?.asQuery ?? false
4043
+ function extractFromBlockReturn(block) {
4044
+ if (!import_ts_morph13.Node.isBlock(block)) return null;
4045
+ const statements = block.getStatements();
4046
+ for (let i = statements.length - 1; i >= 0; i--) {
4047
+ const stmt = statements[i];
4048
+ if (!import_ts_morph13.Node.isReturnStatement(stmt)) continue;
4049
+ const expr = stmt.getExpression();
4050
+ if (!expr) continue;
4051
+ if (import_ts_morph13.Node.isObjectLiteralExpression(expr)) {
4052
+ return extractFromObjectLiteral(expr);
3835
4053
  }
3836
- });
4054
+ if (import_ts_morph13.Node.isParenthesizedExpression(expr)) {
4055
+ const inner = expr.getExpression();
4056
+ if (import_ts_morph13.Node.isObjectLiteralExpression(inner)) {
4057
+ return extractFromObjectLiteral(inner);
4058
+ }
4059
+ }
4060
+ break;
4061
+ }
4062
+ return null;
3837
4063
  }
3838
- function extractFromSourceFile(sourceFile, project) {
3839
- const routes = [];
3840
- const seenNames = /* @__PURE__ */ new Map();
3841
- for (const cls of sourceFile.getClasses()) {
3842
- const controllerDecorator = cls.getDecorator("Controller");
3843
- if (!controllerDecorator) continue;
3844
- const firstArg2 = controllerDecorator.getArguments()[0];
3845
- const prefix = decoratorStringArg(firstArg2) ?? "";
3846
- const className = cls.getName() ?? "Unknown";
3847
- const inherited = resolveInheritedMethods(cls);
3848
- const mixinBinding = inherited ? {
3849
- factoryName: inherited.factoryName,
3850
- factoryFilePath: inherited.factoryFilePath,
3851
- classArgs: inherited.classArgs,
3852
- namedClassArgs: inherited.namedClassArgs
3853
- } : void 0;
3854
- const ownMethods = cls.getMethods();
3855
- const ownNames = new Set(ownMethods.map((m) => m.getName()));
3856
- const methods = [
3857
- ...ownMethods.map((method) => ({ method, mixin: mixinBinding })),
3858
- ...(inherited?.methods ?? []).filter((method) => !ownNames.has(method.getName())).map((method) => ({ method, mixin: mixinBinding }))
3859
- ];
3860
- for (const { method, mixin } of methods) {
3861
- const verb = resolveVerb(method);
3862
- const applyContractDecorator = method.getDecorator("ApplyContract");
3863
- const route = applyContractDecorator ? extractContractRoute({
3864
- cls,
3865
- method,
3866
- applyContractDecorator,
3867
- verb,
3868
- prefix,
3869
- className,
3870
- sourceFile,
3871
- project,
3872
- seenNames,
3873
- mixin
3874
- }) : extractDtoRoute({
3875
- cls,
3876
- method,
3877
- verb,
3878
- prefix,
3879
- className,
3880
- sourceFile,
3881
- project,
3882
- seenNames,
3883
- mixin
3884
- });
3885
- if (route) routes.push(route);
4064
+ function extractFromObjectLiteral(objLiteral) {
4065
+ if (!import_ts_morph13.Node.isObjectLiteralExpression(objLiteral)) return null;
4066
+ const properties = [];
4067
+ for (const prop of objLiteral.getProperties()) {
4068
+ if (!import_ts_morph13.Node.isPropertyAssignment(prop)) continue;
4069
+ const name = prop.getName();
4070
+ const initializer = prop.getInitializer();
4071
+ if (!initializer) continue;
4072
+ const type = inferExpressionType(initializer);
4073
+ properties.push({ name, type });
4074
+ }
4075
+ if (properties.length === 0) return null;
4076
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
4077
+ return { typeString, properties, isImportRef: false };
4078
+ }
4079
+ function inferExpressionType(node) {
4080
+ if (import_ts_morph13.Node.isStringLiteral(node)) return "string";
4081
+ if (import_ts_morph13.Node.isTemplateExpression(node) || import_ts_morph13.Node.isNoSubstitutionTemplateLiteral(node))
4082
+ return "string";
4083
+ if (import_ts_morph13.Node.isNumericLiteral(node)) return "number";
4084
+ if (node.getKind() === import_ts_morph13.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph13.SyntaxKind.FalseKeyword) {
4085
+ return "boolean";
4086
+ }
4087
+ if (node.getKind() === import_ts_morph13.SyntaxKind.NullKeyword) return "null";
4088
+ if (import_ts_morph13.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
4089
+ if (import_ts_morph13.Node.isObjectLiteralExpression(node)) {
4090
+ const props = node.getProperties();
4091
+ if (props.length === 0) return "Record<string, unknown>";
4092
+ const entries = [];
4093
+ for (const prop of props) {
4094
+ if (!import_ts_morph13.Node.isPropertyAssignment(prop)) continue;
4095
+ const key = prop.getName();
4096
+ const init = prop.getInitializer();
4097
+ if (!init) continue;
4098
+ entries.push(`${key}: ${inferExpressionType(init)}`);
3886
4099
  }
4100
+ if (entries.length === 0) return "Record<string, unknown>";
4101
+ return `{ ${entries.join("; ")} }`;
3887
4102
  }
3888
- return routes;
4103
+ if (import_ts_morph13.Node.isArrayLiteralExpression(node)) {
4104
+ const elements = node.getElements();
4105
+ if (elements.length === 0) return "Array<unknown>";
4106
+ const first = elements[0];
4107
+ if (first) return `Array<${inferExpressionType(first)}>`;
4108
+ return "Array<unknown>";
4109
+ }
4110
+ if (import_ts_morph13.Node.isConditionalExpression(node)) {
4111
+ const whenTrue = inferExpressionType(node.getWhenTrue());
4112
+ const whenFalse = inferExpressionType(node.getWhenFalse());
4113
+ if (whenTrue === whenFalse) return whenTrue;
4114
+ return `${whenTrue} | ${whenFalse}`;
4115
+ }
4116
+ if (import_ts_morph13.Node.isParenthesizedExpression(node)) {
4117
+ return inferExpressionType(node.getExpression());
4118
+ }
4119
+ if (import_ts_morph13.Node.isAsExpression(node)) {
4120
+ const typeNode = node.getTypeNode();
4121
+ if (typeNode) return typeNode.getText();
4122
+ }
4123
+ return "unknown";
3889
4124
  }
3890
4125
 
4126
+ // src/emit/emit-api.ts
4127
+ var import_promises3 = require("fs/promises");
4128
+ var import_node_path7 = require("path");
4129
+
3891
4130
  // src/extension/registry.ts
4131
+ var import_node_path6 = require("path");
4132
+ var import_ts_morph14 = require("ts-morph");
3892
4133
  function resolveApiSlots(extensions) {
3893
4134
  let layer;
3894
4135
  let layerOwner;
@@ -3939,7 +4180,7 @@ function createExtensionContext(config, getRoutes, trackedInputs) {
3939
4180
  },
3940
4181
  project() {
3941
4182
  if (!project) {
3942
- project = new import_ts_morph13.Project({
4183
+ project = new import_ts_morph14.Project({
3943
4184
  skipAddingFilesFromTsConfig: true,
3944
4185
  skipLoadingLibFiles: true,
3945
4186
  skipFileDependencyResolution: true,
@@ -5673,36 +5914,64 @@ async function discoverInputFiles(config) {
5673
5914
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5674
5915
  return [...new Set(matched)].sort();
5675
5916
  }
5676
- async function computeInputsHash(config, extraInputs = []) {
5917
+ var READ_CONCURRENCY = 64;
5918
+ async function readAll(paths, read) {
5919
+ const out = /* @__PURE__ */ new Map();
5920
+ for (let i = 0; i < paths.length; i += READ_CONCURRENCY) {
5921
+ const batch = paths.slice(i, i + READ_CONCURRENCY);
5922
+ const texts = await Promise.all(batch.map(read));
5923
+ batch.forEach((path, j) => out.set(path, texts[j]));
5924
+ }
5925
+ return out;
5926
+ }
5927
+ async function readInputs(config) {
5928
+ const cwd = config.codegen.cwd;
5929
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
5930
+ const extended = loadDiscoveryTsconfig(tsconfigPath).files;
5931
+ const tsconfigFiles = extended.length > 0 ? extended : [tsconfigPath];
5932
+ const files = await discoverInputFiles(config);
5933
+ const contents = /* @__PURE__ */ new Map();
5934
+ const tsconfigs = await readAll(
5935
+ tsconfigFiles,
5936
+ (file) => (0, import_promises11.readFile)(file, "utf8").catch(() => null)
5937
+ );
5938
+ for (const [file, text] of tsconfigs) contents.set(file, text);
5939
+ for (const [file, text] of await readAll(files, (file2) => (0, import_promises11.readFile)(file2, "utf8"))) {
5940
+ contents.set(file, text);
5941
+ }
5942
+ return { tsconfigFiles, files, contents, tracked: /* @__PURE__ */ new Map() };
5943
+ }
5944
+ async function readInput(snapshot, path) {
5945
+ return snapshot.contents.get(path) ?? (0, import_promises11.readFile)(path, "utf8");
5946
+ }
5947
+ async function computeInputsHash(config, extraInputs = [], snapshot) {
5948
+ const inputs = snapshot ?? await readInputs(config);
5677
5949
  const hash = (0, import_node_crypto.createHash)("sha256");
5678
5950
  hash.update(`version:${VERSION}
5679
5951
  `);
5680
5952
  hash.update(`config:${serializeConfig(config)}
5681
5953
  `);
5682
5954
  const cwd = config.codegen.cwd;
5683
- const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
5684
- const tsconfigFiles = loadDiscoveryTsconfig(tsconfigPath).files;
5685
- for (const file of tsconfigFiles.length > 0 ? tsconfigFiles : [tsconfigPath]) {
5686
- const contents = await (0, import_promises11.readFile)(file, "utf8").catch(() => null);
5955
+ for (const file of inputs.tsconfigFiles) {
5687
5956
  hash.update(`tsconfig:${(0, import_node_path15.relative)(cwd, file)}
5688
5957
  `);
5689
- hash.update(contents ?? " missing");
5958
+ hash.update(inputs.contents.get(file) ?? " missing");
5690
5959
  hash.update("\n");
5691
5960
  }
5692
- const globbed = await discoverInputFiles(config);
5693
- const globbedRelative = new Set(globbed.map((file) => (0, import_node_path15.relative)(cwd, file)));
5961
+ const globbedRelative = new Set(inputs.files.map((file) => (0, import_node_path15.relative)(cwd, file)));
5694
5962
  const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
5695
- for (const file of globbed) {
5696
- const contents = await (0, import_promises11.readFile)(file, "utf8");
5963
+ for (const file of inputs.files) {
5697
5964
  hash.update(`file:${(0, import_node_path15.relative)(cwd, file)}
5698
5965
  `);
5699
- hash.update(contents);
5966
+ hash.update(inputs.contents.get(file));
5700
5967
  hash.update("\n");
5701
5968
  }
5969
+ const unread = extra.map((file) => (0, import_node_path15.join)(cwd, file)).filter((abs) => !inputs.tracked.has(abs));
5970
+ for (const [abs, text] of await readAll(unread, readTrackedInput)) inputs.tracked.set(abs, text);
5702
5971
  for (const file of extra) {
5703
5972
  hash.update(`extra:${file}
5704
5973
  `);
5705
- hash.update(await readTrackedInput((0, import_node_path15.join)(cwd, file)));
5974
+ hash.update(inputs.tracked.get((0, import_node_path15.join)(cwd, file)));
5706
5975
  hash.update("\n");
5707
5976
  }
5708
5977
  return hash.digest("hex");
@@ -5783,7 +6052,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
5783
6052
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
5784
6053
  setCodegenDebug(config.debug);
5785
6054
  const manifest = await readManifest(config.codegen.outDir);
5786
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
6055
+ const inputs = await readInputs(config);
6056
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? [], inputs);
5787
6057
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
5788
6058
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
5789
6059
  return;
@@ -5811,7 +6081,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5811
6081
  }
5812
6082
  if (config.pages) {
5813
6083
  const pagesConfig = config.pages;
5814
- const pages = await discoverPageScopes(config);
6084
+ const pages = await discoverPageScopes(config, (path) => readInput(inputs, path));
5815
6085
  const sharedProps = discoverSharedPropsFromConfig(config);
5816
6086
  await emitPages(pages, config.codegen.outDir, {
5817
6087
  propsExport: pagesConfig.propsExport,
@@ -5861,7 +6131,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5861
6131
  }
5862
6132
  const outputFiles = await listOutputFiles(config.codegen.outDir);
5863
6133
  const extraInputs = [...trackedInputs].sort();
5864
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
6134
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs, inputs) : inputsHash;
5865
6135
  await writeManifest(config.codegen.outDir, {
5866
6136
  version: VERSION,
5867
6137
  hash: recordedHash,
@@ -6044,13 +6314,15 @@ async function watch(config, onChange, options = {}) {
6044
6314
  pagesWatcher.on("unlink", schedulePagesRegenerate);
6045
6315
  let contractsDebounceTimer;
6046
6316
  const pendingChangedPaths = /* @__PURE__ */ new Set();
6317
+ let pendingAddedOrRemoved = false;
6047
6318
  const contractsWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, config.contracts.glob), {
6048
6319
  ignoreInitial: true,
6049
6320
  persistent: true,
6050
6321
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
6051
6322
  });
6052
- function scheduleContractsRegenerate(changedPath) {
6323
+ function scheduleContractsRegenerate(changedPath, addedOrRemoved = false) {
6053
6324
  if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
6325
+ if (addedOrRemoved) pendingAddedOrRemoved = true;
6054
6326
  if (contractsDebounceTimer !== void 0) {
6055
6327
  clearTimeout(contractsDebounceTimer);
6056
6328
  }
@@ -6058,8 +6330,12 @@ async function watch(config, onChange, options = {}) {
6058
6330
  contractsDebounceTimer = void 0;
6059
6331
  const changed = [...pendingChangedPaths];
6060
6332
  pendingChangedPaths.clear();
6333
+ const filesAddedOrRemoved = pendingAddedOrRemoved;
6334
+ pendingAddedOrRemoved = false;
6061
6335
  try {
6062
- const routes = await (await getDiscovery()).rediscover(changed);
6336
+ const routes = await (await getDiscovery()).rediscover(changed, {
6337
+ filesAddedOrRemoved
6338
+ });
6063
6339
  lastRoutes = routes;
6064
6340
  await generateAndTrack(routes);
6065
6341
  } catch (err) {
@@ -6071,20 +6347,20 @@ async function watch(config, onChange, options = {}) {
6071
6347
  onChange?.();
6072
6348
  }, config.contracts.debounceMs);
6073
6349
  }
6074
- contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6350
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6075
6351
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6076
- contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6352
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6077
6353
  const formsWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, config.forms.watch), {
6078
6354
  ignoreInitial: true,
6079
6355
  persistent: true,
6080
6356
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
6081
6357
  });
6082
- formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6358
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6083
6359
  formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6084
- formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6085
- trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6360
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6361
+ trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6086
6362
  trackedWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6087
- trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6363
+ trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6088
6364
  return {
6089
6365
  close: async () => {
6090
6366
  if (pagesDebounceTimer !== void 0) {
@@ -6188,7 +6464,7 @@ function createChainModuleRenderer(opts) {
6188
6464
  }
6189
6465
 
6190
6466
  // src/extension/genui-sandbox-kit.ts
6191
- var import_node_fs2 = require("fs");
6467
+ var import_node_fs4 = require("fs");
6192
6468
  var import_node_path19 = require("path");
6193
6469
  var import_fast_glob4 = __toESM(require("fast-glob"), 1);
6194
6470
  var GENUI_KIT_MODULE = "@dudousxd/nestjs-agent-core/genui/kit";
@@ -6200,6 +6476,12 @@ var DEFAULT_KIT_DIRS = [
6200
6476
  "app/components/ui",
6201
6477
  "components/ui"
6202
6478
  ];
6479
+ var LEGACY_OPTIONS = {
6480
+ entry: "`entry: X` is now `components: X`",
6481
+ include: "`include: G` is now `components: G` (globs, files, directories or packages)",
6482
+ css: "`css: G` is now `styles: G` (the app's style entry files)"
6483
+ };
6484
+ var MIN_CORE = "0.53.0";
6203
6485
  async function loadKitModule(specifier = GENUI_KIT_MODULE) {
6204
6486
  let mod;
6205
6487
  try {
@@ -6208,19 +6490,24 @@ async function loadKitModule(specifier = GENUI_KIT_MODULE) {
6208
6490
  const code = err.code;
6209
6491
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
6210
6492
  throw new CodegenError(
6211
- `genuiSandboxKit() needs "${GENUI_KIT_MODULE}" \u2014 install @dudousxd/nestjs-agent-core@>=0.52.0, or pass \`generate\` to genuiSandboxKit().`
6493
+ `genuiSandboxKit() needs "${GENUI_KIT_MODULE}" \u2014 install @dudousxd/nestjs-agent-core@>=${MIN_CORE}, or pass \`generate\` to genuiSandboxKit().`
6212
6494
  );
6213
6495
  }
6214
6496
  if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
6215
6497
  throw new CodegenError(
6216
- `genuiSandboxKit() needs "${GENUI_KIT_MODULE}", which the installed @dudousxd/nestjs-agent-core does not export \u2014 upgrade it to >=0.52.0.`
6498
+ `genuiSandboxKit() needs "${GENUI_KIT_MODULE}", which the installed @dudousxd/nestjs-agent-core does not export \u2014 upgrade it to >=${MIN_CORE}.`
6217
6499
  );
6218
6500
  }
6219
6501
  throw err;
6220
6502
  }
6221
6503
  if (typeof mod.writeSandboxKitDocs !== "function") {
6222
6504
  throw new CodegenError(
6223
- `"${GENUI_KIT_MODULE}" has no writeSandboxKitDocs export \u2014 upgrade @dudousxd/nestjs-agent-core to >=0.52.0.`
6505
+ `"${GENUI_KIT_MODULE}" has no writeSandboxKitDocs export \u2014 upgrade @dudousxd/nestjs-agent-core to >=${MIN_CORE}.`
6506
+ );
6507
+ }
6508
+ if (typeof mod.resolveStyleEntries !== "function") {
6509
+ throw new CodegenError(
6510
+ `The installed @dudousxd/nestjs-agent-core predates genuiSandboxKit({ components, styles, wrapper }) \u2014 upgrade it to >=${MIN_CORE}.`
6224
6511
  );
6225
6512
  }
6226
6513
  return mod;
@@ -6234,52 +6521,92 @@ function globBase(glob) {
6234
6521
  return base.join("/");
6235
6522
  }
6236
6523
  function resolveKitFilesLocally(root, options) {
6237
- if (options.entry !== void 0) {
6238
- const entry = (0, import_node_path19.isAbsolute)(options.entry) ? options.entry : (0, import_node_path19.resolve)(root, options.entry);
6239
- return { entry, files: [entry], dirs: [(0, import_node_path19.dirname)(entry)] };
6240
- }
6241
- const include = options.include ?? DEFAULT_KIT_DIRS.filter((dir) => (0, import_node_fs2.existsSync)((0, import_node_path19.resolve)(root, dir))).map(
6242
- (dir) => `${dir}/*.{tsx,jsx,ts,js}`
6243
- )[0];
6244
- if (include === void 0) return null;
6245
- const globs = typeof include === "string" ? [include] : [...include];
6246
- const files = import_fast_glob4.default.sync(globs, { cwd: root, absolute: true, onlyFiles: true }).sort();
6247
- return { files, dirs: [...new Set(globs.map((glob) => (0, import_node_path19.resolve)(root, globBase(glob))))] };
6524
+ const specs = options.components === void 0 ? DEFAULT_KIT_DIRS.filter((dir) => (0, import_node_fs4.existsSync)((0, import_node_path19.resolve)(root, dir))).slice(0, 1) : typeof options.components === "string" ? [options.components] : [...options.components];
6525
+ const files = [];
6526
+ const dirs = /* @__PURE__ */ new Set();
6527
+ for (const spec of specs) {
6528
+ if (/[*?{]/.test(spec)) {
6529
+ files.push(...import_fast_glob4.default.sync(spec, { cwd: root, absolute: true, onlyFiles: true }));
6530
+ dirs.add((0, import_node_path19.resolve)(root, globBase(spec)));
6531
+ continue;
6532
+ }
6533
+ const path = (0, import_node_path19.isAbsolute)(spec) ? spec : (0, import_node_path19.resolve)(root, spec);
6534
+ if ((0, import_node_fs4.existsSync)(path) && (0, import_node_fs4.statSync)(path).isDirectory()) {
6535
+ files.push(...import_fast_glob4.default.sync("*.{tsx,jsx,ts,js}", { cwd: path, absolute: true, onlyFiles: true }));
6536
+ dirs.add(path);
6537
+ } else if ((0, import_node_fs4.existsSync)(path)) {
6538
+ files.push(path);
6539
+ dirs.add((0, import_node_path19.dirname)(path));
6540
+ }
6541
+ }
6542
+ if (options.wrapper !== void 0) {
6543
+ const wrapper = (0, import_node_path19.isAbsolute)(options.wrapper) ? options.wrapper : (0, import_node_path19.resolve)(root, options.wrapper);
6544
+ if ((0, import_node_fs4.existsSync)(wrapper)) files.push(wrapper);
6545
+ }
6546
+ if (files.length === 0 && dirs.size === 0) return null;
6547
+ return { files: [...new Set(files)].sort(), dirs: [...dirs] };
6248
6548
  }
6249
6549
  function genuiSandboxKit(options = {}) {
6550
+ const legacy = Object.keys(LEGACY_OPTIONS).filter(
6551
+ (key) => options[key] !== void 0
6552
+ );
6553
+ if (legacy.length > 0) {
6554
+ throw new CodegenError(
6555
+ `genuiSandboxKit(): ${legacy.map((key) => `\`${key}\``).join(", ")} ${legacy.length > 1 ? "were" : "was"} removed \u2014 ${legacy.map((key) => LEGACY_OPTIONS[key]).join("; ")}. The options are now the agent's Vite plugin's: components, styles, wrapper, stylesDocs.`
6556
+ );
6557
+ }
6250
6558
  const output = options.output ?? DEFAULT_SANDBOX_KIT_OUTPUT;
6251
6559
  const source = {
6252
- ...options.entry !== void 0 ? { entry: options.entry } : {},
6253
- ...options.include !== void 0 ? { include: options.include } : {}
6560
+ ...options.components !== void 0 ? { components: options.components } : {},
6561
+ ...options.wrapper !== void 0 ? { wrapper: options.wrapper } : {}
6254
6562
  };
6255
6563
  const extension = {
6256
6564
  name: "genui-sandbox-kit",
6257
6565
  // Not read by the host: serialized into the config hash, so changing an option
6258
6566
  // invalidates skip-when-unchanged.
6259
- options: { ...source, css: options.css, tsconfig: options.tsconfig, output },
6567
+ options: {
6568
+ ...source,
6569
+ styles: options.styles,
6570
+ stylesDocs: options.stylesDocs,
6571
+ tsconfig: options.tsconfig,
6572
+ output
6573
+ },
6260
6574
  async emitFiles(ctx) {
6261
6575
  const root = ctx.cwd;
6262
6576
  let generate2 = options.generate;
6263
- let resolveFiles = resolveKitFilesLocally;
6577
+ let mod;
6264
6578
  if (!generate2) {
6265
- const mod = await loadKitModule(options.kitModule);
6579
+ mod = await loadKitModule(options.kitModule);
6266
6580
  generate2 = mod.writeSandboxKitDocs;
6267
- if (typeof mod.resolveSandboxKitFiles === "function") {
6268
- resolveFiles = mod.resolveSandboxKitFiles;
6269
- }
6270
6581
  }
6271
- const kit = resolveFiles(root, source);
6582
+ const resolveFiles = mod?.resolveSandboxKitFiles ?? resolveKitFilesLocally;
6583
+ let kit = null;
6584
+ try {
6585
+ kit = resolveFiles(root, source);
6586
+ } catch {
6587
+ kit = null;
6588
+ }
6272
6589
  if (kit) ctx.trackInput(...kit.files, ...kit.dirs);
6273
- if (options.css !== void 0) {
6274
- const css = typeof options.css === "string" ? [options.css] : [...options.css];
6275
- ctx.trackInput(...import_fast_glob4.default.sync(css, { cwd: root, absolute: true, onlyFiles: true }));
6590
+ if (options.styles !== void 0) {
6591
+ const entries = mod?.resolveStyleEntries ? mod.resolveStyleEntries(root, options.styles).files : import_fast_glob4.default.sync(typeof options.styles === "string" ? [options.styles] : [...options.styles], {
6592
+ cwd: root,
6593
+ absolute: true,
6594
+ onlyFiles: true
6595
+ });
6596
+ const files = mod?.collectStyleSources ? mod.collectStyleSources(entries).map((source2) => source2.file) : entries;
6597
+ ctx.trackInput(.../* @__PURE__ */ new Set([...entries, ...files]));
6598
+ }
6599
+ if (options.stylesDocs !== void 0 && !options.stylesDocs.includes("\n")) {
6600
+ const guide = (0, import_node_path19.isAbsolute)(options.stylesDocs) ? options.stylesDocs : (0, import_node_path19.resolve)(root, options.stylesDocs);
6601
+ if ((0, import_node_fs4.existsSync)(guide)) ctx.trackInput(guide);
6276
6602
  }
6277
6603
  ctx.trackInput((0, import_node_path19.isAbsolute)(output) ? output : (0, import_node_path19.resolve)(root, output));
6278
6604
  await generate2({
6279
6605
  root,
6280
6606
  output,
6281
6607
  ...source,
6282
- ...options.css !== void 0 ? { css: options.css } : {},
6608
+ ...options.styles !== void 0 ? { styles: options.styles } : {},
6609
+ ...options.stylesDocs !== void 0 ? { stylesDocs: options.stylesDocs } : {},
6283
6610
  ...options.tsconfig !== void 0 ? { tsconfig: options.tsconfig } : {}
6284
6611
  });
6285
6612
  return [];
@@ -6289,7 +6616,7 @@ function genuiSandboxKit(options = {}) {
6289
6616
  }
6290
6617
 
6291
6618
  // src/index.ts
6292
- var VERSION = "0.28.0";
6619
+ var VERSION = "0.29.1";
6293
6620
  // Annotate the CommonJS export names for ESM import in node:
6294
6621
  0 && (module.exports = {
6295
6622
  CodegenError,