@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/cli/main.cjs CHANGED
@@ -281,7 +281,7 @@ async function discoverPages(opts) {
281
281
  const rel = (0, import_node_path2.relative)(opts.cwd, file);
282
282
  const nameRel = (0, import_node_path2.relative)(pagesBase, file).split(import_node_path2.sep).join("/");
283
283
  const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
284
- const source = await (0, import_promises2.readFile)(file, "utf8");
284
+ const source = await (opts.read ? opts.read(file) : (0, import_promises2.readFile)(file, "utf8"));
285
285
  const propsSource = extractPropsSource(source, opts.propsExport);
286
286
  out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
287
287
  }
@@ -289,10 +289,14 @@ async function discoverPages(opts) {
289
289
  assertUniqueNames(out);
290
290
  return out;
291
291
  }
292
- async function discoverPageScopes(config) {
292
+ async function discoverPageScopes(config, read) {
293
293
  const pages = config.pages;
294
294
  if (!pages) return [];
295
- const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
295
+ const common = {
296
+ cwd: config.codegen.cwd,
297
+ propsExport: pages.propsExport,
298
+ ...read ? { read } : {}
299
+ };
296
300
  const out = await discoverPages({
297
301
  ...common,
298
302
  glob: pages.glob,
@@ -363,297 +367,60 @@ function extractPropsSource(source, exportName) {
363
367
  }
364
368
 
365
369
  // src/discovery/shared-props.ts
366
- var import_node_path3 = require("path");
367
- var import_ts_morph = require("ts-morph");
368
- function discoverSharedProps(project, moduleEntry) {
369
- try {
370
- let sourceFile = project.getSourceFile(moduleEntry);
371
- if (!sourceFile) {
370
+ var import_node_fs3 = require("fs");
371
+ var import_node_path5 = require("path");
372
+ var import_ts_morph13 = require("ts-morph");
373
+
374
+ // src/discovery/contracts-fast.ts
375
+ var import_node_fs2 = require("fs");
376
+ var import_node_path4 = require("path");
377
+ var import_fast_glob2 = __toESM(require("fast-glob"), 1);
378
+ var import_ts_morph12 = require("ts-morph");
379
+
380
+ // src/discovery/disk-fingerprints.ts
381
+ var import_node_fs = require("fs");
382
+ var DiskFingerprints = class {
383
+ stats = /* @__PURE__ */ new Map();
384
+ /** The paths whose disk content differs from `text`, or that no longer exist. */
385
+ changed(files) {
386
+ const changed = [];
387
+ for (const { path, text } of files) {
388
+ let key;
372
389
  try {
373
- sourceFile = project.addSourceFileAtPath(moduleEntry);
390
+ const stat2 = (0, import_node_fs.statSync)(path);
391
+ key = `${stat2.mtimeMs}:${stat2.size}`;
374
392
  } catch {
375
- return null;
376
- }
377
- }
378
- const forRootCall = findForRootCall(sourceFile);
379
- if (!forRootCall) return null;
380
- const initializer = findShareInitializer(forRootCall);
381
- if (!initializer) return null;
382
- return extractShareType(initializer, sourceFile, project);
383
- } catch {
384
- return null;
385
- }
386
- }
387
- function discoverSharedPropsFromConfig(config) {
388
- if (!config.app?.moduleEntry) return null;
389
- try {
390
- const tsconfigPath = config.app.tsconfig ?? (0, import_node_path3.join)(config.codegen.cwd, "tsconfig.json");
391
- let project;
392
- try {
393
- project = new import_ts_morph.Project({
394
- tsConfigFilePath: tsconfigPath,
395
- skipAddingFilesFromTsConfig: true,
396
- skipLoadingLibFiles: true,
397
- skipFileDependencyResolution: true
398
- });
399
- } catch {
400
- project = new import_ts_morph.Project({
401
- skipAddingFilesFromTsConfig: true,
402
- skipLoadingLibFiles: true,
403
- skipFileDependencyResolution: true,
404
- compilerOptions: { allowJs: true, strict: false }
405
- });
406
- }
407
- return discoverSharedProps(project, config.app.moduleEntry);
408
- } catch {
409
- return null;
410
- }
411
- }
412
- function findForRootCall(sourceFile) {
413
- const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
414
- for (const call of callExpressions) {
415
- const expr = call.getExpression();
416
- if (!import_ts_morph.Node.isPropertyAccessExpression(expr)) continue;
417
- const methodName = expr.getName();
418
- const objectExpr = expr.getExpression();
419
- if (methodName === "forRoot" && import_ts_morph.Node.isIdentifier(objectExpr)) {
420
- const name = objectExpr.getText();
421
- if (name === "InertiaModule") {
422
- return call;
423
- }
424
- }
425
- }
426
- return null;
427
- }
428
- function findShareInitializer(forRootCall) {
429
- if (!import_ts_morph.Node.isCallExpression(forRootCall)) return null;
430
- const args = forRootCall.getArguments();
431
- const firstArg2 = args[0];
432
- if (!firstArg2 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg2)) return null;
433
- for (const prop of firstArg2.getProperties()) {
434
- if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
435
- return prop.getInitializer() ?? null;
436
- }
437
- }
438
- return null;
439
- }
440
- function extractShareType(node, sourceFile, project) {
441
- if (import_ts_morph.Node.isIdentifier(node)) {
442
- const ref = resolveIdentifierToImportRef(node, sourceFile, project);
443
- if (ref) {
444
- return {
445
- typeString: `Awaited<ReturnType<typeof import('${ref.importPath}').${ref.exportName}>>`,
446
- properties: null,
447
- isImportRef: true
448
- };
449
- }
450
- }
451
- if (import_ts_morph.Node.isArrowFunction(node)) {
452
- const result = extractFromFunctionLike(node, sourceFile);
453
- return result ? { ...result, isImportRef: false } : null;
454
- }
455
- if (import_ts_morph.Node.isFunctionExpression(node)) {
456
- const result = extractFromFunctionLike(node, sourceFile);
457
- return result ? { ...result, isImportRef: false } : null;
458
- }
459
- if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
460
- const result = extractFromObjectLiteral(node);
461
- return result ? { ...result, isImportRef: false } : null;
462
- }
463
- return null;
464
- }
465
- function resolveIdentifierToImportRef(id, sourceFile, project) {
466
- if (!import_ts_morph.Node.isIdentifier(id)) return null;
467
- const name = id.getText();
468
- const localFunc = sourceFile.getFunction(name);
469
- if (localFunc?.isExported()) {
470
- const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
471
- return { importPath: filePath, exportName: name };
472
- }
473
- const localVar = sourceFile.getVariableDeclaration(name);
474
- if (localVar?.isExported()) {
475
- const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
476
- return { importPath: filePath, exportName: name };
477
- }
478
- for (const imp of sourceFile.getImportDeclarations()) {
479
- for (const named of imp.getNamedImports()) {
480
- const importedName = named.getAliasNode()?.getText() ?? named.getName();
481
- if (importedName !== name) continue;
482
- const resolvedSource = imp.getModuleSpecifierSourceFile();
483
- if (!resolvedSource) continue;
484
- const originalName = named.getName();
485
- const fn = resolvedSource.getFunction(originalName);
486
- if (fn?.isExported()) {
487
- const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
488
- return { importPath: filePath, exportName: originalName };
489
- }
490
- const v = resolvedSource.getVariableDeclaration(originalName);
491
- if (v?.isExported()) {
492
- const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
493
- return { importPath: filePath, exportName: originalName };
494
- }
495
- }
496
- }
497
- return null;
498
- }
499
- function extractFromFunctionLike(node, _sourceFile) {
500
- const returnTypeNode = import_ts_morph.Node.isArrowFunction(node) || import_ts_morph.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
501
- if (returnTypeNode) {
502
- return extractFromReturnTypeAnnotation(returnTypeNode);
503
- }
504
- if (import_ts_morph.Node.isArrowFunction(node)) {
505
- const body = node.getBody();
506
- if (import_ts_morph.Node.isParenthesizedExpression(body)) {
507
- const inner = body.getExpression();
508
- if (import_ts_morph.Node.isObjectLiteralExpression(inner)) {
509
- return extractFromObjectLiteral(inner);
510
- }
511
- }
512
- if (import_ts_morph.Node.isObjectLiteralExpression(body)) {
513
- return extractFromObjectLiteral(body);
514
- }
515
- if (import_ts_morph.Node.isBlock(body)) {
516
- return extractFromBlockReturn(body);
517
- }
518
- }
519
- if (import_ts_morph.Node.isFunctionExpression(node)) {
520
- const body = node.getBody();
521
- if (import_ts_morph.Node.isBlock(body)) {
522
- return extractFromBlockReturn(body);
523
- }
524
- }
525
- return null;
526
- }
527
- function extractFromReturnTypeAnnotation(typeNode) {
528
- if (import_ts_morph.Node.isTypeReference(typeNode)) {
529
- const typeName = typeNode.getTypeName();
530
- if (import_ts_morph.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
531
- const typeArgs = typeNode.getTypeArguments();
532
- const firstArg2 = typeArgs[0];
533
- if (firstArg2) {
534
- return extractFromReturnTypeAnnotation(firstArg2);
393
+ this.stats.delete(path);
394
+ changed.push(path);
395
+ continue;
535
396
  }
536
- return null;
537
- }
538
- }
539
- if (import_ts_morph.Node.isTypeLiteral(typeNode)) {
540
- const properties = [];
541
- for (const member of typeNode.getMembers()) {
542
- if (import_ts_morph.Node.isPropertySignature(member)) {
543
- const name = member.getName();
544
- const memberTypeNode = member.getTypeNode();
545
- const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
546
- properties.push({ name, type });
397
+ if (this.stats.get(path) === key) continue;
398
+ let disk;
399
+ try {
400
+ disk = (0, import_node_fs.readFileSync)(path, "utf-8");
401
+ } catch {
402
+ disk = void 0;
547
403
  }
548
- }
549
- if (properties.length === 0) return null;
550
- const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
551
- return { typeString, properties, isImportRef: false };
552
- }
553
- return null;
554
- }
555
- function extractFromBlockReturn(block) {
556
- if (!import_ts_morph.Node.isBlock(block)) return null;
557
- const statements = block.getStatements();
558
- for (let i = statements.length - 1; i >= 0; i--) {
559
- const stmt = statements[i];
560
- if (!import_ts_morph.Node.isReturnStatement(stmt)) continue;
561
- const expr = stmt.getExpression();
562
- if (!expr) continue;
563
- if (import_ts_morph.Node.isObjectLiteralExpression(expr)) {
564
- return extractFromObjectLiteral(expr);
565
- }
566
- if (import_ts_morph.Node.isParenthesizedExpression(expr)) {
567
- const inner = expr.getExpression();
568
- if (import_ts_morph.Node.isObjectLiteralExpression(inner)) {
569
- return extractFromObjectLiteral(inner);
404
+ if (disk === text) {
405
+ this.stats.set(path, key);
406
+ } else {
407
+ this.stats.delete(path);
408
+ changed.push(path);
570
409
  }
571
410
  }
572
- break;
411
+ return changed;
573
412
  }
574
- return null;
575
- }
576
- function extractFromObjectLiteral(objLiteral) {
577
- if (!import_ts_morph.Node.isObjectLiteralExpression(objLiteral)) return null;
578
- const properties = [];
579
- for (const prop of objLiteral.getProperties()) {
580
- if (!import_ts_morph.Node.isPropertyAssignment(prop)) continue;
581
- const name = prop.getName();
582
- const initializer = prop.getInitializer();
583
- if (!initializer) continue;
584
- const type = inferExpressionType(initializer);
585
- properties.push({ name, type });
586
- }
587
- if (properties.length === 0) return null;
588
- const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
589
- return { typeString, properties, isImportRef: false };
590
- }
591
- function inferExpressionType(node) {
592
- if (import_ts_morph.Node.isStringLiteral(node)) return "string";
593
- if (import_ts_morph.Node.isTemplateExpression(node) || import_ts_morph.Node.isNoSubstitutionTemplateLiteral(node))
594
- return "string";
595
- if (import_ts_morph.Node.isNumericLiteral(node)) return "number";
596
- if (node.getKind() === import_ts_morph.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph.SyntaxKind.FalseKeyword) {
597
- return "boolean";
598
- }
599
- if (node.getKind() === import_ts_morph.SyntaxKind.NullKeyword) return "null";
600
- if (import_ts_morph.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
601
- if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
602
- const props = node.getProperties();
603
- if (props.length === 0) return "Record<string, unknown>";
604
- const entries = [];
605
- for (const prop of props) {
606
- if (!import_ts_morph.Node.isPropertyAssignment(prop)) continue;
607
- const key = prop.getName();
608
- const init = prop.getInitializer();
609
- if (!init) continue;
610
- entries.push(`${key}: ${inferExpressionType(init)}`);
611
- }
612
- if (entries.length === 0) return "Record<string, unknown>";
613
- return `{ ${entries.join("; ")} }`;
614
- }
615
- if (import_ts_morph.Node.isArrayLiteralExpression(node)) {
616
- const elements = node.getElements();
617
- if (elements.length === 0) return "Array<unknown>";
618
- const first = elements[0];
619
- if (first) return `Array<${inferExpressionType(first)}>`;
620
- return "Array<unknown>";
621
- }
622
- if (import_ts_morph.Node.isConditionalExpression(node)) {
623
- const whenTrue = inferExpressionType(node.getWhenTrue());
624
- const whenFalse = inferExpressionType(node.getWhenFalse());
625
- if (whenTrue === whenFalse) return whenTrue;
626
- return `${whenTrue} | ${whenFalse}`;
627
- }
628
- if (import_ts_morph.Node.isParenthesizedExpression(node)) {
629
- return inferExpressionType(node.getExpression());
630
- }
631
- if (import_ts_morph.Node.isAsExpression(node)) {
632
- const typeNode = node.getTypeNode();
633
- if (typeNode) return typeNode.getText();
413
+ /** Forget `path`, so its next check compares content again. */
414
+ forget(path) {
415
+ this.stats.delete(path);
634
416
  }
635
- return "unknown";
636
- }
637
-
638
- // src/emit/emit-api.ts
639
- var import_promises3 = require("fs/promises");
640
- var import_node_path7 = require("path");
641
-
642
- // src/extension/registry.ts
643
- var import_node_path6 = require("path");
644
- var import_ts_morph13 = require("ts-morph");
645
-
646
- // src/discovery/contracts-fast.ts
647
- var import_node_fs = require("fs");
648
- var import_node_path5 = require("path");
649
- var import_fast_glob2 = __toESM(require("fast-glob"), 1);
650
- var import_ts_morph12 = require("ts-morph");
417
+ };
651
418
 
652
419
  // src/discovery/dto-type-resolver.ts
653
420
  var import_ts_morph11 = require("ts-morph");
654
421
 
655
422
  // src/discovery/dto-to-ir.ts
656
- var import_ts_morph3 = require("ts-morph");
423
+ var import_ts_morph2 = require("ts-morph");
657
424
 
658
425
  // src/util/debug-log.ts
659
426
  var debugEnabled = false;
@@ -665,8 +432,8 @@ function debugWarn(message) {
665
432
  }
666
433
 
667
434
  // src/discovery/type-ref-resolution.ts
668
- var import_node_path4 = require("path");
669
- var import_ts_morph2 = require("ts-morph");
435
+ var import_node_path3 = require("path");
436
+ var import_ts_morph = require("ts-morph");
670
437
  var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
671
438
  var _ctxByProject = /* @__PURE__ */ new WeakMap();
672
439
  function setDiscoveryContext(project, ctx) {
@@ -707,17 +474,17 @@ function findTypeInFile(name, file) {
707
474
  }
708
475
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
709
476
  if (moduleSpecifier.startsWith(".")) {
710
- const dir = (0, import_node_path4.dirname)(sourceFile.getFilePath());
477
+ const dir = (0, import_node_path3.dirname)(sourceFile.getFilePath());
711
478
  const ext = /\.(c|m)?(j|t)sx?$/.exec(moduleSpecifier)?.[0];
712
479
  const base = ext ? moduleSpecifier.slice(0, -ext.length) : moduleSpecifier;
713
480
  const flavour = ext?.startsWith(".c") ? "c" : ext?.startsWith(".m") ? "m" : "";
714
481
  const candidates = [
715
- (0, import_node_path4.resolve)(dir, `${base}.${flavour}ts`),
716
- ...flavour === "" ? [(0, import_node_path4.resolve)(dir, `${base}.tsx`)] : [],
717
- (0, import_node_path4.resolve)(dir, `${moduleSpecifier}.ts`),
718
- (0, import_node_path4.resolve)(dir, moduleSpecifier, "index.ts"),
719
- (0, import_node_path4.resolve)(dir, `${base}.d.${flavour}ts`),
720
- ...ext === void 0 ? [(0, import_node_path4.resolve)(dir, moduleSpecifier, "index.d.ts")] : []
482
+ (0, import_node_path3.resolve)(dir, `${base}.${flavour}ts`),
483
+ ...flavour === "" ? [(0, import_node_path3.resolve)(dir, `${base}.tsx`)] : [],
484
+ (0, import_node_path3.resolve)(dir, `${moduleSpecifier}.ts`),
485
+ (0, import_node_path3.resolve)(dir, moduleSpecifier, "index.ts"),
486
+ (0, import_node_path3.resolve)(dir, `${base}.d.${flavour}ts`),
487
+ ...ext === void 0 ? [(0, import_node_path3.resolve)(dir, moduleSpecifier, "index.d.ts")] : []
721
488
  ];
722
489
  return [...new Set(candidates)];
723
490
  }
@@ -739,8 +506,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
739
506
  const rest = moduleSpecifier.slice(prefix.length);
740
507
  const candidates = [];
741
508
  for (const mapping of mappings) {
742
- const resolved = (0, import_node_path4.resolve)(baseUrl, mapping.replace("*", rest));
743
- candidates.push(`${resolved}.ts`, (0, import_node_path4.resolve)(resolved, "index.ts"));
509
+ const resolved = (0, import_node_path3.resolve)(baseUrl, mapping.replace("*", rest));
510
+ candidates.push(`${resolved}.ts`, (0, import_node_path3.resolve)(resolved, "index.ts"));
744
511
  }
745
512
  dbg(" resolved candidates:", candidates);
746
513
  return candidates;
@@ -931,13 +698,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
931
698
  name = nodeOrName;
932
699
  } else {
933
700
  const typeNode = nodeOrName;
934
- if (opts.unwrapContainers && import_ts_morph2.Node.isArrayTypeNode(typeNode)) {
701
+ if (opts.unwrapContainers && import_ts_morph.Node.isArrayTypeNode(typeNode)) {
935
702
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
936
703
  return inner ? { ...inner, isArray: true } : null;
937
704
  }
938
- if (!import_ts_morph2.Node.isTypeReference(typeNode)) return null;
705
+ if (!import_ts_morph.Node.isTypeReference(typeNode)) return null;
939
706
  const typeName = typeNode.getTypeName();
940
- const refName = import_ts_morph2.Node.isIdentifier(typeName) ? typeName.getText() : null;
707
+ const refName = import_ts_morph.Node.isIdentifier(typeName) ? typeName.getText() : null;
941
708
  if (!refName) return null;
942
709
  if (opts.unwrapContainers && refName === "Promise") {
943
710
  const first = typeNode.getTypeArguments()[0];
@@ -1071,7 +838,7 @@ function buildProperty(prop, classFile, ctx) {
1071
838
  const dec = (n) => decorators.get(n);
1072
839
  const typeNode = prop.getTypeNode();
1073
840
  const typeText = typeNode?.getText() ?? "unknown";
1074
- const isArrayType = !!typeNode && import_ts_morph3.Node.isArrayTypeNode(typeNode);
841
+ const isArrayType = !!typeNode && import_ts_morph2.Node.isArrayTypeNode(typeNode);
1075
842
  const asField = (child) => applyPresence(
1076
843
  has("IsArray") || isArrayType ? { kind: "array", element: child } : child,
1077
844
  decorators
@@ -1285,22 +1052,22 @@ function firstArgText(decorator) {
1285
1052
  }
1286
1053
  function numericArg(decorator) {
1287
1054
  const arg = firstArg(decorator);
1288
- if (arg && import_ts_morph3.Node.isNumericLiteral(arg)) return arg.getText();
1055
+ if (arg && import_ts_morph2.Node.isNumericLiteral(arg)) return arg.getText();
1289
1056
  return null;
1290
1057
  }
1291
1058
  function numericArgs(decorator) {
1292
1059
  const args = decorator?.getArguments() ?? [];
1293
- const num = (n) => n && import_ts_morph3.Node.isNumericLiteral(n) ? n.getText() : null;
1060
+ const num = (n) => n && import_ts_morph2.Node.isNumericLiteral(n) ? n.getText() : null;
1294
1061
  return [num(args[0]), num(args[1])];
1295
1062
  }
1296
1063
  function messageRaw(decorator) {
1297
1064
  const args = decorator?.getArguments() ?? [];
1298
1065
  for (const arg of args) {
1299
- if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1066
+ if (import_ts_morph2.Node.isObjectLiteralExpression(arg)) {
1300
1067
  for (const prop of arg.getProperties()) {
1301
- if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
1068
+ if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
1302
1069
  const init = prop.getInitializer();
1303
- if (init && import_ts_morph3.Node.isStringLiteral(init)) return init.getText();
1070
+ if (init && import_ts_morph2.Node.isStringLiteral(init)) return init.getText();
1304
1071
  }
1305
1072
  }
1306
1073
  }
@@ -1309,30 +1076,30 @@ function messageRaw(decorator) {
1309
1076
  }
1310
1077
  function resolveDiscriminator(decorator) {
1311
1078
  const optsArg = decorator?.getArguments()[1];
1312
- if (!optsArg || !import_ts_morph3.Node.isObjectLiteralExpression(optsArg)) return null;
1079
+ if (!optsArg || !import_ts_morph2.Node.isObjectLiteralExpression(optsArg)) return null;
1313
1080
  let discProp;
1314
1081
  for (const prop of optsArg.getProperties()) {
1315
- if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
1082
+ if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
1316
1083
  discProp = prop.getInitializer();
1317
1084
  }
1318
1085
  }
1319
- if (!discProp || !import_ts_morph3.Node.isObjectLiteralExpression(discProp)) return null;
1086
+ if (!discProp || !import_ts_morph2.Node.isObjectLiteralExpression(discProp)) return null;
1320
1087
  let property = null;
1321
1088
  const subTypes = [];
1322
1089
  for (const prop of discProp.getProperties()) {
1323
- if (!import_ts_morph3.Node.isPropertyAssignment(prop)) continue;
1090
+ if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
1324
1091
  const name = prop.getName();
1325
1092
  const init = prop.getInitializer();
1326
1093
  if (!init) continue;
1327
- if (name === "property" && import_ts_morph3.Node.isStringLiteral(init)) {
1094
+ if (name === "property" && import_ts_morph2.Node.isStringLiteral(init)) {
1328
1095
  property = init.getLiteralValue();
1329
- } else if (name === "subTypes" && import_ts_morph3.Node.isArrayLiteralExpression(init)) {
1096
+ } else if (name === "subTypes" && import_ts_morph2.Node.isArrayLiteralExpression(init)) {
1330
1097
  for (const el of init.getElements()) {
1331
- if (!import_ts_morph3.Node.isObjectLiteralExpression(el)) continue;
1098
+ if (!import_ts_morph2.Node.isObjectLiteralExpression(el)) continue;
1332
1099
  for (const p of el.getProperties()) {
1333
- if (!import_ts_morph3.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
1100
+ if (!import_ts_morph2.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
1334
1101
  const nameInit = p.getInitializer();
1335
- if (nameInit && import_ts_morph3.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
1102
+ if (nameInit && import_ts_morph2.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
1336
1103
  }
1337
1104
  }
1338
1105
  }
@@ -1343,9 +1110,9 @@ function resolveDiscriminator(decorator) {
1343
1110
  function resolveTypeFactoryName(decorator) {
1344
1111
  const arg = firstArg(decorator);
1345
1112
  if (!arg) return null;
1346
- if (import_ts_morph3.Node.isArrowFunction(arg)) {
1113
+ if (import_ts_morph2.Node.isArrowFunction(arg)) {
1347
1114
  const body = arg.getBody();
1348
- if (import_ts_morph3.Node.isIdentifier(body)) return body.getText();
1115
+ if (import_ts_morph2.Node.isIdentifier(body)) return body.getText();
1349
1116
  }
1350
1117
  return null;
1351
1118
  }
@@ -1354,12 +1121,12 @@ function singularClassName(typeText) {
1354
1121
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
1355
1122
  }
1356
1123
  function genericTypeArgNames(typeNode) {
1357
- if (!typeNode || !import_ts_morph3.Node.isTypeReference(typeNode)) return [];
1124
+ if (!typeNode || !import_ts_morph2.Node.isTypeReference(typeNode)) return [];
1358
1125
  const names = [];
1359
1126
  for (const arg of typeNode.getTypeArguments()) {
1360
- if (!import_ts_morph3.Node.isTypeReference(arg)) return [];
1127
+ if (!import_ts_morph2.Node.isTypeReference(arg)) return [];
1361
1128
  const tn = arg.getTypeName();
1362
- if (!import_ts_morph3.Node.isIdentifier(tn)) return [];
1129
+ if (!import_ts_morph2.Node.isIdentifier(tn)) return [];
1363
1130
  names.push(tn.getText());
1364
1131
  }
1365
1132
  return names;
@@ -1367,7 +1134,7 @@ function genericTypeArgNames(typeNode) {
1367
1134
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
1368
1135
  const arg = firstArg(decorator);
1369
1136
  if (!arg) return null;
1370
- if (import_ts_morph3.Node.isIdentifier(arg)) {
1137
+ if (import_ts_morph2.Node.isIdentifier(arg)) {
1371
1138
  const name = arg.getText();
1372
1139
  const resolved = findType(name, classFile, ctx.project);
1373
1140
  if (resolved && resolved.kind === "enum") {
@@ -1381,12 +1148,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
1381
1148
  }
1382
1149
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
1383
1150
  }
1384
- if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1151
+ if (import_ts_morph2.Node.isObjectLiteralExpression(arg)) {
1385
1152
  const values = [];
1386
1153
  for (const p of arg.getProperties()) {
1387
- if (!import_ts_morph3.Node.isPropertyAssignment(p)) continue;
1154
+ if (!import_ts_morph2.Node.isPropertyAssignment(p)) continue;
1388
1155
  const init = p.getInitializer();
1389
- if (init && import_ts_morph3.Node.isStringLiteral(init)) values.push(init.getText());
1156
+ if (init && import_ts_morph2.Node.isStringLiteral(init)) values.push(init.getText());
1390
1157
  }
1391
1158
  if (values.length > 0) return { kind: "enum", literals: values };
1392
1159
  }
@@ -1394,9 +1161,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
1394
1161
  }
1395
1162
  function inSchemaFromDecorator(decorator) {
1396
1163
  const arg = firstArg(decorator);
1397
- if (arg && import_ts_morph3.Node.isArrayLiteralExpression(arg)) {
1164
+ if (arg && import_ts_morph2.Node.isArrayLiteralExpression(arg)) {
1398
1165
  const elements = arg.getElements();
1399
- const allStrings = elements.every((e) => import_ts_morph3.Node.isStringLiteral(e));
1166
+ const allStrings = elements.every((e) => import_ts_morph2.Node.isStringLiteral(e));
1400
1167
  if (allStrings && elements.length > 0) {
1401
1168
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
1402
1169
  }
@@ -1414,7 +1181,7 @@ function inSchemaFromDecorator(decorator) {
1414
1181
  var import_ts_morph6 = require("ts-morph");
1415
1182
 
1416
1183
  // src/discovery/filter-field-types.ts
1417
- var import_ts_morph4 = require("ts-morph");
1184
+ var import_ts_morph3 = require("ts-morph");
1418
1185
 
1419
1186
  // src/discovery/enum-resolution.ts
1420
1187
  var _enumCache = /* @__PURE__ */ new WeakMap();
@@ -1467,28 +1234,28 @@ function markNullable(r, nullable) {
1467
1234
  }
1468
1235
  var TRANSPARENT_TYPE_BRANDS = /* @__PURE__ */ new Set(["Opt", "Hidden"]);
1469
1236
  function classifyTypeNode(typeNode, sourceFile, project, opts) {
1470
- if (import_ts_morph4.Node.isUnionTypeNode(typeNode)) {
1237
+ if (import_ts_morph3.Node.isUnionTypeNode(typeNode)) {
1471
1238
  let nullable = false;
1472
1239
  const stringLits = [];
1473
1240
  const numberLits = [];
1474
1241
  const others = [];
1475
1242
  for (const member of typeNode.getTypeNodes()) {
1476
1243
  const kind = member.getKind();
1477
- if (kind === import_ts_morph4.SyntaxKind.NullKeyword || kind === import_ts_morph4.SyntaxKind.UndefinedKeyword) {
1244
+ if (kind === import_ts_morph3.SyntaxKind.NullKeyword || kind === import_ts_morph3.SyntaxKind.UndefinedKeyword) {
1478
1245
  nullable = true;
1479
1246
  continue;
1480
1247
  }
1481
- if (import_ts_morph4.Node.isLiteralTypeNode(member)) {
1248
+ if (import_ts_morph3.Node.isLiteralTypeNode(member)) {
1482
1249
  const lit = member.getLiteral();
1483
- if (import_ts_morph4.Node.isStringLiteral(lit)) {
1250
+ if (import_ts_morph3.Node.isStringLiteral(lit)) {
1484
1251
  stringLits.push(lit.getLiteralValue());
1485
1252
  continue;
1486
1253
  }
1487
- if (import_ts_morph4.Node.isNumericLiteral(lit)) {
1254
+ if (import_ts_morph3.Node.isNumericLiteral(lit)) {
1488
1255
  numberLits.push(lit.getText());
1489
1256
  continue;
1490
1257
  }
1491
- if (lit.getKind() === import_ts_morph4.SyntaxKind.NullKeyword) {
1258
+ if (lit.getKind() === import_ts_morph3.SyntaxKind.NullKeyword) {
1492
1259
  nullable = true;
1493
1260
  continue;
1494
1261
  }
@@ -1508,19 +1275,19 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1508
1275
  return markNullable({ kind: "unknown" }, nullable);
1509
1276
  }
1510
1277
  switch (typeNode.getKind()) {
1511
- case import_ts_morph4.SyntaxKind.StringKeyword:
1278
+ case import_ts_morph3.SyntaxKind.StringKeyword:
1512
1279
  return { kind: "string" };
1513
- case import_ts_morph4.SyntaxKind.NumberKeyword:
1280
+ case import_ts_morph3.SyntaxKind.NumberKeyword:
1514
1281
  return { kind: "number" };
1515
- case import_ts_morph4.SyntaxKind.BooleanKeyword:
1282
+ case import_ts_morph3.SyntaxKind.BooleanKeyword:
1516
1283
  return { kind: "boolean" };
1517
- case import_ts_morph4.SyntaxKind.AnyKeyword:
1518
- case import_ts_morph4.SyntaxKind.UnknownKeyword:
1284
+ case import_ts_morph3.SyntaxKind.AnyKeyword:
1285
+ case import_ts_morph3.SyntaxKind.UnknownKeyword:
1519
1286
  return { kind: "unknown" };
1520
1287
  default:
1521
1288
  break;
1522
1289
  }
1523
- if (import_ts_morph4.Node.isTypeReference(typeNode)) {
1290
+ if (import_ts_morph3.Node.isTypeReference(typeNode)) {
1524
1291
  const refName = typeNode.getTypeName().getText();
1525
1292
  if (refName === "Date") return { kind: "date" };
1526
1293
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -1538,25 +1305,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1538
1305
  if (typeRef) return { kind: "unknown", typeRef };
1539
1306
  return { kind: "unknown" };
1540
1307
  }
1541
- if (import_ts_morph4.Node.isTypeLiteral(typeNode)) return { kind: "json" };
1308
+ if (import_ts_morph3.Node.isTypeLiteral(typeNode)) return { kind: "json" };
1542
1309
  return { kind: "unknown" };
1543
1310
  }
1544
1311
  function enumFromDecoratorArgs(args, sourceFile, project) {
1545
1312
  for (const arg of args) {
1546
- if (import_ts_morph4.Node.isArrowFunction(arg)) {
1313
+ if (import_ts_morph3.Node.isArrowFunction(arg)) {
1547
1314
  const body = arg.getBody();
1548
- if (import_ts_morph4.Node.isIdentifier(body)) {
1315
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1549
1316
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1550
1317
  if (en) return en;
1551
1318
  }
1552
1319
  }
1553
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1320
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1554
1321
  const itemsProp = arg.getProperty("items");
1555
- if (itemsProp && import_ts_morph4.Node.isPropertyAssignment(itemsProp)) {
1322
+ if (itemsProp && import_ts_morph3.Node.isPropertyAssignment(itemsProp)) {
1556
1323
  const init = itemsProp.getInitializer();
1557
- if (init && import_ts_morph4.Node.isArrowFunction(init)) {
1324
+ if (init && import_ts_morph3.Node.isArrowFunction(init)) {
1558
1325
  const body = init.getBody();
1559
- if (import_ts_morph4.Node.isIdentifier(body)) {
1326
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1560
1327
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1561
1328
  if (en) return en;
1562
1329
  }
@@ -1579,7 +1346,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1579
1346
  return { kind: "string" };
1580
1347
  }
1581
1348
  for (const arg of args) {
1582
- if (import_ts_morph4.Node.isStringLiteral(arg)) {
1349
+ if (import_ts_morph3.Node.isStringLiteral(arg)) {
1583
1350
  const raw = arg.getLiteralValue();
1584
1351
  const kind = classifyTypeKeyword(raw);
1585
1352
  if (kind) {
@@ -1587,11 +1354,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1587
1354
  return { kind };
1588
1355
  }
1589
1356
  }
1590
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1357
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1591
1358
  const enumProp = arg.getProperty("enum");
1592
- if (enumProp && import_ts_morph4.Node.isPropertyAssignment(enumProp)) {
1359
+ if (enumProp && import_ts_morph3.Node.isPropertyAssignment(enumProp)) {
1593
1360
  const init = enumProp.getInitializer();
1594
- if (init && import_ts_morph4.Node.isIdentifier(init)) {
1361
+ if (init && import_ts_morph3.Node.isIdentifier(init)) {
1595
1362
  const en = resolveEnumValues(init.getText(), sourceFile, project);
1596
1363
  if (en) {
1597
1364
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -1601,15 +1368,15 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1601
1368
  }
1602
1369
  for (const key of ["columnType", "type"]) {
1603
1370
  const typeProp = arg.getProperty(key);
1604
- if (!typeProp || !import_ts_morph4.Node.isPropertyAssignment(typeProp)) continue;
1371
+ if (!typeProp || !import_ts_morph3.Node.isPropertyAssignment(typeProp)) continue;
1605
1372
  const init = typeProp.getInitializer();
1606
1373
  if (!init) continue;
1607
- if (import_ts_morph4.Node.isStringLiteral(init)) {
1374
+ if (import_ts_morph3.Node.isStringLiteral(init)) {
1608
1375
  const kind = classifyTypeKeyword(init.getLiteralValue());
1609
1376
  if (kind) return { kind };
1610
1377
  continue;
1611
1378
  }
1612
- if (import_ts_morph4.Node.isIdentifier(init)) {
1379
+ if (import_ts_morph3.Node.isIdentifier(init)) {
1613
1380
  const kind = classifyTypeKeyword(init.getText());
1614
1381
  if (kind) return { kind };
1615
1382
  }
@@ -1652,6 +1419,49 @@ function toFilterFieldType(name, r) {
1652
1419
 
1653
1420
  // src/discovery/heritage.ts
1654
1421
  var import_ts_morph5 = require("ts-morph");
1422
+
1423
+ // src/discovery/unresolved-imports.ts
1424
+ var import_ts_morph4 = require("ts-morph");
1425
+ var UnresolvedImports = class {
1426
+ read;
1427
+ /**
1428
+ * The files with an import that failed to resolve in `program` and now resolves
1429
+ * to a TypeScript file the program does not hold.
1430
+ */
1431
+ nowResolving(program) {
1432
+ if (this.read?.program !== program) {
1433
+ this.read = { program, imports: failedImports(program) };
1434
+ }
1435
+ const options = program.getCompilerOptions();
1436
+ const files = /* @__PURE__ */ new Set();
1437
+ for (const { specifier, from } of this.read.imports) {
1438
+ const file = import_ts_morph4.ts.resolveModuleName(specifier, from, options, import_ts_morph4.ts.sys).resolvedModule?.resolvedFileName;
1439
+ if (file !== void 0 && /\.(m|c)?tsx?$/.test(file) && !program.getSourceFile(file)) {
1440
+ files.add(from);
1441
+ }
1442
+ }
1443
+ return [...files];
1444
+ }
1445
+ };
1446
+ function failedImports(program) {
1447
+ const checker = program.getTypeChecker();
1448
+ const imports = [];
1449
+ for (const file of program.getSourceFiles()) {
1450
+ if (program.isSourceFileDefaultLibrary(file) || file.isDeclarationFile) continue;
1451
+ if (program.isSourceFileFromExternalLibrary(file)) continue;
1452
+ for (const statement of file.statements) {
1453
+ if (!import_ts_morph4.ts.isImportDeclaration(statement) && !import_ts_morph4.ts.isExportDeclaration(statement)) continue;
1454
+ const specifier = statement.moduleSpecifier;
1455
+ if (!specifier || !import_ts_morph4.ts.isStringLiteral(specifier)) continue;
1456
+ if (!checker.getSymbolAtLocation(specifier)) {
1457
+ imports.push({ specifier: specifier.text, from: file.fileName });
1458
+ }
1459
+ }
1460
+ }
1461
+ return imports;
1462
+ }
1463
+
1464
+ // src/discovery/heritage.ts
1655
1465
  function resolveHeritageCall(node) {
1656
1466
  if (!node) return void 0;
1657
1467
  const expr = unwrapExpression(node);
@@ -1851,15 +1661,42 @@ function resolvePropertyClassReference(prop) {
1851
1661
  return { key, ref };
1852
1662
  }
1853
1663
  var mixinTypeProject;
1664
+ var mixinFiles = new DiskFingerprints();
1665
+ var mixinStale = false;
1666
+ var mixinUnresolved = new UnresolvedImports();
1854
1667
  function getMixinTypeProject() {
1855
- mixinTypeProject ??= new import_ts_morph5.Project({
1856
- skipAddingFilesFromTsConfig: true,
1857
- compilerOptions: { strict: true }
1858
- });
1668
+ if (mixinTypeProject && mixinStale && !refreshMixinTypeProject(mixinTypeProject)) {
1669
+ mixinTypeProject = void 0;
1670
+ }
1671
+ mixinStale = false;
1672
+ if (!mixinTypeProject) {
1673
+ mixinTypeProject = new import_ts_morph5.Project({
1674
+ skipAddingFilesFromTsConfig: true,
1675
+ compilerOptions: { strict: true }
1676
+ });
1677
+ mixinFiles = new DiskFingerprints();
1678
+ mixinUnresolved = new UnresolvedImports();
1679
+ }
1859
1680
  return mixinTypeProject;
1860
1681
  }
1861
1682
  function clearMixinTypeProject() {
1862
1683
  mixinTypeProject = void 0;
1684
+ mixinStale = false;
1685
+ }
1686
+ function invalidateMixinTypeProject() {
1687
+ mixinStale = true;
1688
+ }
1689
+ function refreshMixinTypeProject(project) {
1690
+ const program = project.getProgram().compilerObject;
1691
+ const files = program.getSourceFiles().filter((file) => !program.isSourceFileDefaultLibrary(file)).map((file) => ({ path: file.fileName, text: file.text }));
1692
+ const changed = mixinFiles.changed(files);
1693
+ if (changed.length === 0) return mixinUnresolved.nowResolving(program).length === 0;
1694
+ for (const path of changed) {
1695
+ const sf = project.getSourceFile(path);
1696
+ if (sf?.refreshFromFileSystemSync() !== import_ts_morph5.FileSystemRefreshResult.Updated) return false;
1697
+ }
1698
+ project.resolveSourceFileDependencies();
1699
+ return true;
1863
1700
  }
1864
1701
  function resolveInstantiatedReturnType(cls, methodName) {
1865
1702
  const filePath = cls.getSourceFile().getFilePath();
@@ -2217,8 +2054,7 @@ var SchemaTypeProgram = class {
2217
2054
  skipAddingFilesFromTsConfig: true,
2218
2055
  compilerOptions: this.options
2219
2056
  });
2220
- for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
2221
- this.flush();
2057
+ this.requestDiscoveryFiles();
2222
2058
  }
2223
2059
  project;
2224
2060
  options;
@@ -2231,6 +2067,53 @@ var SchemaTypeProgram = class {
2231
2067
  needs = /* @__PURE__ */ new Map();
2232
2068
  /** Pruned text per file, as last written into {@link project}. */
2233
2069
  written = /* @__PURE__ */ new Map();
2070
+ /** What {@link project} read from disk unpruned (packages, declaration files). */
2071
+ externalFiles = new DiskFingerprints();
2072
+ scratchFiles = new DiskFingerprints();
2073
+ /** The last pruning of each file, with the source text and needs it was pruned for. */
2074
+ pruned = /* @__PURE__ */ new Map();
2075
+ /**
2076
+ * Bring the program up to date with discovery's files and the disk, keeping the
2077
+ * lib and package declarations it has already parsed and checked. Leaves it
2078
+ * holding exactly the files a new program built now would hold, with the same
2079
+ * text. Returns false when a file the program read unpruned has changed on disk
2080
+ * (a package was reinstalled, a `.d.ts` was edited): the caller builds a new
2081
+ * program instead.
2082
+ */
2083
+ refresh() {
2084
+ const program = this.project.getProgram().compilerObject;
2085
+ const external = program.getSourceFiles().filter(
2086
+ (file) => !program.isSourceFileDefaultLibrary(file) && !this.written.has(file.fileName)
2087
+ ).map((file) => ({ path: file.fileName, text: file.text }));
2088
+ if (this.externalFiles.changed(external).length > 0) return false;
2089
+ const scratch = this.scratch.getSourceFiles().map((sf) => ({ path: sf.getFilePath(), text: sf.getFullText() }));
2090
+ for (const path of this.scratchFiles.changed(scratch)) {
2091
+ const sf = this.scratch.getSourceFile(path);
2092
+ if (sf?.refreshFromFileSystemSync() === import_ts_morph7.FileSystemRefreshResult.Deleted) {
2093
+ this.scratchFiles.forget(path);
2094
+ }
2095
+ }
2096
+ this.needs.clear();
2097
+ this.requestDiscoveryFiles();
2098
+ for (const path of [...this.written.keys()]) {
2099
+ if (this.needs.has(path)) continue;
2100
+ const sf = this.project.getSourceFile(path);
2101
+ if (sf) this.project.removeSourceFile(sf);
2102
+ this.written.delete(path);
2103
+ }
2104
+ return true;
2105
+ }
2106
+ /**
2107
+ * Everything discovery holds now (the controllers) goes in before the first
2108
+ * query: a file added afterwards discards the program and its checker, and
2109
+ * rebuilding them once per controller is exactly the cost this avoids.
2110
+ */
2111
+ requestDiscoveryFiles() {
2112
+ for (const sf of this.discovery.getSourceFiles()) {
2113
+ this.request(sf.getFilePath(), { schema: true });
2114
+ }
2115
+ this.flush();
2116
+ }
2234
2117
  /**
2235
2118
  * The typed copy of a discovery file, with its schema positions kept. Adding one
2236
2119
  * that was not there yet (a contract file discovery reached lazily) rebuilds the
@@ -2271,12 +2154,21 @@ var SchemaTypeProgram = class {
2271
2154
  while (this.queue.length > 0) {
2272
2155
  const path = this.queue.pop();
2273
2156
  const original = this.original(path);
2274
- if (!original) continue;
2275
- const needs = this.needs.get(path);
2276
- const text = this.prune(original, needs);
2157
+ if (!original) {
2158
+ if (this.written.has(path)) changed.set(path, void 0);
2159
+ continue;
2160
+ }
2161
+ const { text, requests } = this.pruneCached(path, original, this.needs.get(path));
2162
+ for (const { path: target, want } of requests) this.request(target, want);
2277
2163
  if (this.written.get(path) !== text) changed.set(path, text);
2278
2164
  }
2279
2165
  for (const [path, text] of changed) {
2166
+ if (text === void 0) {
2167
+ const sf = this.project.getSourceFile(path);
2168
+ if (sf) this.project.removeSourceFile(sf);
2169
+ this.written.delete(path);
2170
+ continue;
2171
+ }
2280
2172
  this.project.createSourceFile(path, text, { overwrite: true });
2281
2173
  this.written.set(path, text);
2282
2174
  }
@@ -2285,11 +2177,29 @@ var SchemaTypeProgram = class {
2285
2177
  original(path) {
2286
2178
  return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
2287
2179
  }
2180
+ /** {@link prune}, reusing the last result while the file's text and needs are the same. */
2181
+ pruneCached(path, sf, needs) {
2182
+ const source = sf.getFullText();
2183
+ const key = `${needs.schema}:${needs.all}:${[...needs.names].sort().join(",")}`;
2184
+ const last = this.pruned.get(path);
2185
+ if (last && last.source === source && last.needs === key) return last;
2186
+ const result = this.prune(sf, needs);
2187
+ if (result.resolved) this.pruned.set(path, { ...result, source, needs: key });
2188
+ else this.pruned.delete(path);
2189
+ return result;
2190
+ }
2288
2191
  /**
2289
2192
  * `sf`'s text with every import/re-export nothing in `needs` references blanked,
2290
- * requesting from each kept one's target the names it brings in.
2193
+ * and the names each kept one brings in from its target.
2291
2194
  */
2292
2195
  prune(sf, needs) {
2196
+ const requests = [];
2197
+ let resolved = true;
2198
+ const resolveFrom = (specifier) => {
2199
+ const target = this.resolve(specifier, sf);
2200
+ if (target === null) resolved = false;
2201
+ return target ?? void 0;
2202
+ };
2293
2203
  const locals = topLevelDeclarations(sf);
2294
2204
  const roots = [];
2295
2205
  if (needs.all) roots.push(...sf.getStatements());
@@ -2342,7 +2252,7 @@ var SchemaTypeProgram = class {
2342
2252
  }
2343
2253
  };
2344
2254
  for (const decl of sf.getImportDeclarations()) {
2345
- const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2255
+ const target = resolveFrom(decl.getModuleSpecifierValue());
2346
2256
  const namespace = decl.getNamespaceImport()?.getText();
2347
2257
  const def = decl.getDefaultImport()?.getText();
2348
2258
  const named = decl.getNamedImports().map((n) => ({
@@ -2356,21 +2266,24 @@ var SchemaTypeProgram = class {
2356
2266
  continue;
2357
2267
  }
2358
2268
  if (target) {
2359
- this.request(target, {
2360
- all: wantsNamespace,
2361
- names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2269
+ requests.push({
2270
+ path: target,
2271
+ want: {
2272
+ all: wantsNamespace,
2273
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2274
+ }
2362
2275
  });
2363
2276
  }
2364
2277
  }
2365
2278
  for (const exp of sf.getExportDeclarations()) {
2366
2279
  const spec = exp.getModuleSpecifierValue();
2367
2280
  if (spec === void 0) continue;
2368
- const target = this.resolve(spec, sf);
2281
+ const target = resolveFrom(spec);
2369
2282
  if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
2370
2283
  if (!needs.all && unresolvedNames.length === 0) {
2371
2284
  blank(exp);
2372
2285
  } else if (target) {
2373
- this.request(target, { all: needs.all, names: unresolvedNames });
2286
+ requests.push({ path: target, want: { all: needs.all, names: unresolvedNames } });
2374
2287
  }
2375
2288
  continue;
2376
2289
  }
@@ -2378,13 +2291,14 @@ var SchemaTypeProgram = class {
2378
2291
  (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
2379
2292
  ).map((n) => n.getNameNode().getText());
2380
2293
  if (wanted.length === 0) blank(exp);
2381
- else if (target) this.request(target, { names: wanted });
2294
+ else if (target) requests.push({ path: target, want: { names: wanted } });
2382
2295
  }
2383
- return chars.join("");
2296
+ return { text: chars.join(""), requests, resolved };
2384
2297
  }
2385
2298
  /**
2386
- * The project source file a module specifier resolves to, or undefined for a
2387
- * package (read unpruned by the program itself) or an unresolvable one.
2299
+ * The project source file a module specifier resolves to; undefined for a
2300
+ * package or a declaration file (read unpruned by the program itself), null for
2301
+ * one that does not resolve.
2388
2302
  */
2389
2303
  resolve(specifier, from) {
2390
2304
  const resolved = import_ts_morph7.ts.resolveModuleName(
@@ -2393,7 +2307,8 @@ var SchemaTypeProgram = class {
2393
2307
  this.options,
2394
2308
  import_ts_morph7.ts.sys
2395
2309
  ).resolvedModule;
2396
- if (!resolved || resolved.isExternalLibraryImport) return void 0;
2310
+ if (!resolved) return null;
2311
+ if (resolved.isExternalLibraryImport) return void 0;
2397
2312
  const file = resolved.resolvedFileName;
2398
2313
  if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
2399
2314
  return file;
@@ -2448,8 +2363,13 @@ function defaultExport(sf) {
2448
2363
 
2449
2364
  // src/discovery/schema-type-inference.ts
2450
2365
  var programs = /* @__PURE__ */ new WeakMap();
2366
+ var stale = /* @__PURE__ */ new WeakSet();
2451
2367
  function programFor(discovery) {
2452
2368
  let program = programs.get(discovery);
2369
+ if (program && stale.has(discovery)) {
2370
+ stale.delete(discovery);
2371
+ if (!program.refresh()) program = void 0;
2372
+ }
2453
2373
  if (!program) {
2454
2374
  program = new SchemaTypeProgram(discovery);
2455
2375
  programs.set(discovery, program);
@@ -2458,6 +2378,10 @@ function programFor(discovery) {
2458
2378
  }
2459
2379
  function clearSchemaTypeProject(discovery) {
2460
2380
  programs.delete(discovery);
2381
+ stale.delete(discovery);
2382
+ }
2383
+ function invalidateSchemaTypeProject(discovery) {
2384
+ if (programs.has(discovery)) stale.add(discovery);
2461
2385
  }
2462
2386
  function counterpart(typedSf, node) {
2463
2387
  const start = node.getStart();
@@ -3405,7 +3329,7 @@ async function discoverContractsFast(opts) {
3405
3329
  return routes;
3406
3330
  }
3407
3331
  function resolveTsconfigPath(cwd, tsconfig) {
3408
- return tsconfig ? (0, import_node_path5.resolve)(tsconfig) : (0, import_node_path5.join)(cwd, "tsconfig.json");
3332
+ return tsconfig ? (0, import_node_path4.resolve)(tsconfig) : (0, import_node_path4.join)(cwd, "tsconfig.json");
3409
3333
  }
3410
3334
  var NO_TSCONFIG_OPTIONS = {
3411
3335
  allowJs: true,
@@ -3413,7 +3337,7 @@ var NO_TSCONFIG_OPTIONS = {
3413
3337
  strict: false
3414
3338
  };
3415
3339
  function loadDiscoveryTsconfig(tsconfigPath) {
3416
- if (!(0, import_node_fs.existsSync)(tsconfigPath)) return { options: null, files: [] };
3340
+ if (!(0, import_node_fs2.existsSync)(tsconfigPath)) return { options: null, files: [] };
3417
3341
  const read = import_ts_morph12.ts.readConfigFile(tsconfigPath, (path) => import_ts_morph12.ts.sys.readFile(path));
3418
3342
  if (read.error) {
3419
3343
  return {
@@ -3435,7 +3359,7 @@ function loadDiscoveryTsconfig(tsconfigPath) {
3435
3359
  const parsed = import_ts_morph12.ts.parseJsonSourceFileConfigFileContent(
3436
3360
  sourceFile,
3437
3361
  host,
3438
- (0, import_node_path5.dirname)(tsconfigPath),
3362
+ (0, import_node_path4.dirname)(tsconfigPath),
3439
3363
  void 0,
3440
3364
  tsconfigPath
3441
3365
  );
@@ -3497,6 +3421,9 @@ var PersistentDiscovery = class _PersistentDiscovery {
3497
3421
  glob;
3498
3422
  /** Absolute paths of the controllers currently loaded as extraction roots. */
3499
3423
  controllerPaths = /* @__PURE__ */ new Set();
3424
+ /** Every file in {@link project}, as last read from disk. */
3425
+ files = new DiskFingerprints();
3426
+ unresolved = new UnresolvedImports();
3500
3427
  constructor(project, cwd, glob) {
3501
3428
  this.project = project;
3502
3429
  this.cwd = cwd;
@@ -3522,6 +3449,8 @@ var PersistentDiscovery = class _PersistentDiscovery {
3522
3449
  }
3523
3450
  /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3524
3451
  discover() {
3452
+ clearMixinTypeProject();
3453
+ clearSchemaTypeProject(this.project);
3525
3454
  return this.runExtraction();
3526
3455
  }
3527
3456
  /**
@@ -3529,26 +3458,39 @@ var PersistentDiscovery = class _PersistentDiscovery {
3529
3458
  * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3530
3459
  * to pick up added/removed controllers, clears the per-Project caches, then
3531
3460
  * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3532
- * being exhaustive because re-globbing + refresh-on-presence covers the set.
3461
+ * being exhaustive because re-globbing + comparing every loaded file with the
3462
+ * disk covers the set.
3533
3463
  */
3534
- async rediscover(changedPaths) {
3535
- if (changedPaths) {
3536
- for (const p of changedPaths) {
3537
- const abs = (0, import_node_path5.resolve)(p);
3538
- const sf = this.project.getSourceFile(abs);
3539
- if (sf) {
3540
- await sf.refreshFromFileSystem();
3541
- }
3464
+ async rediscover(changedPaths, options = {}) {
3465
+ const hinted = new Set([...changedPaths ?? []].map((p) => (0, import_node_path4.resolve)(p)));
3466
+ const program = this.project.getProgram().compilerObject;
3467
+ const sources = program.getSourceFiles().filter(
3468
+ (file) => !program.isSourceFileDefaultLibrary(file) && !program.isSourceFileFromExternalLibrary(file)
3469
+ ).map((file) => ({ path: file.fileName, text: file.text }));
3470
+ for (const path of /* @__PURE__ */ new Set([...hinted, ...this.files.changed(sources)])) {
3471
+ const sf = this.project.getSourceFile(path);
3472
+ if (sf && await sf.refreshFromFileSystem() === import_ts_morph12.FileSystemRefreshResult.Deleted) {
3473
+ this.files.forget(path);
3542
3474
  }
3543
3475
  }
3476
+ const inProject = new Set(this.project.getSourceFiles().map((sf) => sf.getFilePath()));
3477
+ const definitions = this.project.getLanguageService().compilerObject.getProgram();
3478
+ for (const path of definitions ? this.unresolved.nowResolving(definitions) : []) {
3479
+ const sf = this.project.getSourceFile(path);
3480
+ if (!sf) continue;
3481
+ this.project.removeSourceFile(sf);
3482
+ if (inProject.has(path)) this.project.addSourceFileAtPathIfExists(path);
3483
+ }
3544
3484
  const globbed = new Set(
3545
3485
  await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3546
3486
  );
3487
+ let controllersChanged = false;
3547
3488
  for (const f of globbed) {
3548
3489
  if (!this.controllerPaths.has(f)) {
3549
3490
  try {
3550
3491
  this.project.addSourceFileAtPath(f);
3551
3492
  this.controllerPaths.add(f);
3493
+ controllersChanged = true;
3552
3494
  } catch {
3553
3495
  }
3554
3496
  }
@@ -3558,16 +3500,22 @@ var PersistentDiscovery = class _PersistentDiscovery {
3558
3500
  const sf = this.project.getSourceFile(f);
3559
3501
  if (sf) this.project.removeSourceFile(sf);
3560
3502
  this.controllerPaths.delete(f);
3503
+ controllersChanged = true;
3561
3504
  }
3562
3505
  }
3506
+ if (controllersChanged || options.filesAddedOrRemoved) {
3507
+ clearMixinTypeProject();
3508
+ clearSchemaTypeProject(this.project);
3509
+ } else {
3510
+ invalidateMixinTypeProject();
3511
+ invalidateSchemaTypeProject(this.project);
3512
+ }
3563
3513
  return this.runExtraction();
3564
3514
  }
3565
3515
  /** Clear stale per-Project caches, then extract over the controller set. */
3566
3516
  runExtraction() {
3567
3517
  clearTypeResolutionCaches(this.project);
3568
3518
  clearEnumCache(this.project);
3569
- clearMixinTypeProject();
3570
- clearSchemaTypeProject(this.project);
3571
3519
  return extractRoutesFrom(this.project, this.controllerPaths);
3572
3520
  }
3573
3521
  };
@@ -3854,7 +3802,300 @@ function extractFromSourceFile(sourceFile, project) {
3854
3802
  return routes;
3855
3803
  }
3856
3804
 
3805
+ // src/discovery/shared-props.ts
3806
+ function discoverSharedProps(project, moduleEntry) {
3807
+ try {
3808
+ let sourceFile = project.getSourceFile(moduleEntry);
3809
+ if (!sourceFile) {
3810
+ try {
3811
+ sourceFile = project.addSourceFileAtPath(moduleEntry);
3812
+ } catch {
3813
+ return null;
3814
+ }
3815
+ }
3816
+ const forRootCall = findForRootCall(sourceFile);
3817
+ if (!forRootCall) return null;
3818
+ const initializer = findShareInitializer(forRootCall);
3819
+ if (!initializer) return null;
3820
+ return extractShareType(initializer, sourceFile, project);
3821
+ } catch {
3822
+ return null;
3823
+ }
3824
+ }
3825
+ function discoverSharedPropsFromConfig(config) {
3826
+ if (!config.app?.moduleEntry) return null;
3827
+ try {
3828
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path5.join)(config.codegen.cwd, "tsconfig.json");
3829
+ let project;
3830
+ try {
3831
+ project = sharedPropsProject(tsconfigPath);
3832
+ } catch {
3833
+ project = new import_ts_morph13.Project({
3834
+ skipAddingFilesFromTsConfig: true,
3835
+ skipLoadingLibFiles: true,
3836
+ skipFileDependencyResolution: true,
3837
+ compilerOptions: { allowJs: true, strict: false }
3838
+ });
3839
+ }
3840
+ return discoverSharedProps(project, config.app.moduleEntry);
3841
+ } catch {
3842
+ return null;
3843
+ }
3844
+ }
3845
+ var parsedTsconfigs = /* @__PURE__ */ new Map();
3846
+ function readText(file) {
3847
+ try {
3848
+ return (0, import_node_fs3.readFileSync)(file, "utf8");
3849
+ } catch {
3850
+ return "\0missing";
3851
+ }
3852
+ }
3853
+ function sharedPropsProject(tsconfigPath) {
3854
+ const text = loadDiscoveryTsconfig(tsconfigPath).files.map((file) => `${file}\0${readText(file)}`).join("\0");
3855
+ const flags = {
3856
+ skipAddingFilesFromTsConfig: true,
3857
+ skipLoadingLibFiles: true,
3858
+ skipFileDependencyResolution: true
3859
+ };
3860
+ const parsed = parsedTsconfigs.get(tsconfigPath);
3861
+ if (parsed?.text === text) return new import_ts_morph13.Project({ ...flags, compilerOptions: parsed.options });
3862
+ const project = new import_ts_morph13.Project({ ...flags, tsConfigFilePath: tsconfigPath });
3863
+ parsedTsconfigs.set(tsconfigPath, { text, options: project.getCompilerOptions() });
3864
+ return project;
3865
+ }
3866
+ function findForRootCall(sourceFile) {
3867
+ const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph13.SyntaxKind.CallExpression);
3868
+ for (const call of callExpressions) {
3869
+ const expr = call.getExpression();
3870
+ if (!import_ts_morph13.Node.isPropertyAccessExpression(expr)) continue;
3871
+ const methodName = expr.getName();
3872
+ const objectExpr = expr.getExpression();
3873
+ if (methodName === "forRoot" && import_ts_morph13.Node.isIdentifier(objectExpr)) {
3874
+ const name = objectExpr.getText();
3875
+ if (name === "InertiaModule") {
3876
+ return call;
3877
+ }
3878
+ }
3879
+ }
3880
+ return null;
3881
+ }
3882
+ function findShareInitializer(forRootCall) {
3883
+ if (!import_ts_morph13.Node.isCallExpression(forRootCall)) return null;
3884
+ const args = forRootCall.getArguments();
3885
+ const firstArg2 = args[0];
3886
+ if (!firstArg2 || !import_ts_morph13.Node.isObjectLiteralExpression(firstArg2)) return null;
3887
+ for (const prop of firstArg2.getProperties()) {
3888
+ if (import_ts_morph13.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
3889
+ return prop.getInitializer() ?? null;
3890
+ }
3891
+ }
3892
+ return null;
3893
+ }
3894
+ function extractShareType(node, sourceFile, project) {
3895
+ if (import_ts_morph13.Node.isIdentifier(node)) {
3896
+ const ref = resolveIdentifierToImportRef(node, sourceFile, project);
3897
+ if (ref) {
3898
+ return {
3899
+ typeString: `Awaited<ReturnType<typeof import('${ref.importPath}').${ref.exportName}>>`,
3900
+ properties: null,
3901
+ isImportRef: true
3902
+ };
3903
+ }
3904
+ }
3905
+ if (import_ts_morph13.Node.isArrowFunction(node)) {
3906
+ const result = extractFromFunctionLike(node, sourceFile);
3907
+ return result ? { ...result, isImportRef: false } : null;
3908
+ }
3909
+ if (import_ts_morph13.Node.isFunctionExpression(node)) {
3910
+ const result = extractFromFunctionLike(node, sourceFile);
3911
+ return result ? { ...result, isImportRef: false } : null;
3912
+ }
3913
+ if (import_ts_morph13.Node.isObjectLiteralExpression(node)) {
3914
+ const result = extractFromObjectLiteral(node);
3915
+ return result ? { ...result, isImportRef: false } : null;
3916
+ }
3917
+ return null;
3918
+ }
3919
+ function resolveIdentifierToImportRef(id, sourceFile, project) {
3920
+ if (!import_ts_morph13.Node.isIdentifier(id)) return null;
3921
+ const name = id.getText();
3922
+ const localFunc = sourceFile.getFunction(name);
3923
+ if (localFunc?.isExported()) {
3924
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
3925
+ return { importPath: filePath, exportName: name };
3926
+ }
3927
+ const localVar = sourceFile.getVariableDeclaration(name);
3928
+ if (localVar?.isExported()) {
3929
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
3930
+ return { importPath: filePath, exportName: name };
3931
+ }
3932
+ for (const imp of sourceFile.getImportDeclarations()) {
3933
+ for (const named of imp.getNamedImports()) {
3934
+ const importedName = named.getAliasNode()?.getText() ?? named.getName();
3935
+ if (importedName !== name) continue;
3936
+ const resolvedSource = imp.getModuleSpecifierSourceFile();
3937
+ if (!resolvedSource) continue;
3938
+ const originalName = named.getName();
3939
+ const fn = resolvedSource.getFunction(originalName);
3940
+ if (fn?.isExported()) {
3941
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
3942
+ return { importPath: filePath, exportName: originalName };
3943
+ }
3944
+ const v = resolvedSource.getVariableDeclaration(originalName);
3945
+ if (v?.isExported()) {
3946
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
3947
+ return { importPath: filePath, exportName: originalName };
3948
+ }
3949
+ }
3950
+ }
3951
+ return null;
3952
+ }
3953
+ function extractFromFunctionLike(node, _sourceFile) {
3954
+ const returnTypeNode = import_ts_morph13.Node.isArrowFunction(node) || import_ts_morph13.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
3955
+ if (returnTypeNode) {
3956
+ return extractFromReturnTypeAnnotation(returnTypeNode);
3957
+ }
3958
+ if (import_ts_morph13.Node.isArrowFunction(node)) {
3959
+ const body = node.getBody();
3960
+ if (import_ts_morph13.Node.isParenthesizedExpression(body)) {
3961
+ const inner = body.getExpression();
3962
+ if (import_ts_morph13.Node.isObjectLiteralExpression(inner)) {
3963
+ return extractFromObjectLiteral(inner);
3964
+ }
3965
+ }
3966
+ if (import_ts_morph13.Node.isObjectLiteralExpression(body)) {
3967
+ return extractFromObjectLiteral(body);
3968
+ }
3969
+ if (import_ts_morph13.Node.isBlock(body)) {
3970
+ return extractFromBlockReturn(body);
3971
+ }
3972
+ }
3973
+ if (import_ts_morph13.Node.isFunctionExpression(node)) {
3974
+ const body = node.getBody();
3975
+ if (import_ts_morph13.Node.isBlock(body)) {
3976
+ return extractFromBlockReturn(body);
3977
+ }
3978
+ }
3979
+ return null;
3980
+ }
3981
+ function extractFromReturnTypeAnnotation(typeNode) {
3982
+ if (import_ts_morph13.Node.isTypeReference(typeNode)) {
3983
+ const typeName = typeNode.getTypeName();
3984
+ if (import_ts_morph13.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
3985
+ const typeArgs = typeNode.getTypeArguments();
3986
+ const firstArg2 = typeArgs[0];
3987
+ if (firstArg2) {
3988
+ return extractFromReturnTypeAnnotation(firstArg2);
3989
+ }
3990
+ return null;
3991
+ }
3992
+ }
3993
+ if (import_ts_morph13.Node.isTypeLiteral(typeNode)) {
3994
+ const properties = [];
3995
+ for (const member of typeNode.getMembers()) {
3996
+ if (import_ts_morph13.Node.isPropertySignature(member)) {
3997
+ const name = member.getName();
3998
+ const memberTypeNode = member.getTypeNode();
3999
+ const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
4000
+ properties.push({ name, type });
4001
+ }
4002
+ }
4003
+ if (properties.length === 0) return null;
4004
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
4005
+ return { typeString, properties, isImportRef: false };
4006
+ }
4007
+ return null;
4008
+ }
4009
+ function extractFromBlockReturn(block) {
4010
+ if (!import_ts_morph13.Node.isBlock(block)) return null;
4011
+ const statements = block.getStatements();
4012
+ for (let i = statements.length - 1; i >= 0; i--) {
4013
+ const stmt = statements[i];
4014
+ if (!import_ts_morph13.Node.isReturnStatement(stmt)) continue;
4015
+ const expr = stmt.getExpression();
4016
+ if (!expr) continue;
4017
+ if (import_ts_morph13.Node.isObjectLiteralExpression(expr)) {
4018
+ return extractFromObjectLiteral(expr);
4019
+ }
4020
+ if (import_ts_morph13.Node.isParenthesizedExpression(expr)) {
4021
+ const inner = expr.getExpression();
4022
+ if (import_ts_morph13.Node.isObjectLiteralExpression(inner)) {
4023
+ return extractFromObjectLiteral(inner);
4024
+ }
4025
+ }
4026
+ break;
4027
+ }
4028
+ return null;
4029
+ }
4030
+ function extractFromObjectLiteral(objLiteral) {
4031
+ if (!import_ts_morph13.Node.isObjectLiteralExpression(objLiteral)) return null;
4032
+ const properties = [];
4033
+ for (const prop of objLiteral.getProperties()) {
4034
+ if (!import_ts_morph13.Node.isPropertyAssignment(prop)) continue;
4035
+ const name = prop.getName();
4036
+ const initializer = prop.getInitializer();
4037
+ if (!initializer) continue;
4038
+ const type = inferExpressionType(initializer);
4039
+ properties.push({ name, type });
4040
+ }
4041
+ if (properties.length === 0) return null;
4042
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
4043
+ return { typeString, properties, isImportRef: false };
4044
+ }
4045
+ function inferExpressionType(node) {
4046
+ if (import_ts_morph13.Node.isStringLiteral(node)) return "string";
4047
+ if (import_ts_morph13.Node.isTemplateExpression(node) || import_ts_morph13.Node.isNoSubstitutionTemplateLiteral(node))
4048
+ return "string";
4049
+ if (import_ts_morph13.Node.isNumericLiteral(node)) return "number";
4050
+ if (node.getKind() === import_ts_morph13.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph13.SyntaxKind.FalseKeyword) {
4051
+ return "boolean";
4052
+ }
4053
+ if (node.getKind() === import_ts_morph13.SyntaxKind.NullKeyword) return "null";
4054
+ if (import_ts_morph13.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
4055
+ if (import_ts_morph13.Node.isObjectLiteralExpression(node)) {
4056
+ const props = node.getProperties();
4057
+ if (props.length === 0) return "Record<string, unknown>";
4058
+ const entries = [];
4059
+ for (const prop of props) {
4060
+ if (!import_ts_morph13.Node.isPropertyAssignment(prop)) continue;
4061
+ const key = prop.getName();
4062
+ const init = prop.getInitializer();
4063
+ if (!init) continue;
4064
+ entries.push(`${key}: ${inferExpressionType(init)}`);
4065
+ }
4066
+ if (entries.length === 0) return "Record<string, unknown>";
4067
+ return `{ ${entries.join("; ")} }`;
4068
+ }
4069
+ if (import_ts_morph13.Node.isArrayLiteralExpression(node)) {
4070
+ const elements = node.getElements();
4071
+ if (elements.length === 0) return "Array<unknown>";
4072
+ const first = elements[0];
4073
+ if (first) return `Array<${inferExpressionType(first)}>`;
4074
+ return "Array<unknown>";
4075
+ }
4076
+ if (import_ts_morph13.Node.isConditionalExpression(node)) {
4077
+ const whenTrue = inferExpressionType(node.getWhenTrue());
4078
+ const whenFalse = inferExpressionType(node.getWhenFalse());
4079
+ if (whenTrue === whenFalse) return whenTrue;
4080
+ return `${whenTrue} | ${whenFalse}`;
4081
+ }
4082
+ if (import_ts_morph13.Node.isParenthesizedExpression(node)) {
4083
+ return inferExpressionType(node.getExpression());
4084
+ }
4085
+ if (import_ts_morph13.Node.isAsExpression(node)) {
4086
+ const typeNode = node.getTypeNode();
4087
+ if (typeNode) return typeNode.getText();
4088
+ }
4089
+ return "unknown";
4090
+ }
4091
+
4092
+ // src/emit/emit-api.ts
4093
+ var import_promises3 = require("fs/promises");
4094
+ var import_node_path7 = require("path");
4095
+
3857
4096
  // src/extension/registry.ts
4097
+ var import_node_path6 = require("path");
4098
+ var import_ts_morph14 = require("ts-morph");
3858
4099
  function resolveApiSlots(extensions) {
3859
4100
  let layer;
3860
4101
  let layerOwner;
@@ -3905,7 +4146,7 @@ function createExtensionContext(config, getRoutes, trackedInputs) {
3905
4146
  },
3906
4147
  project() {
3907
4148
  if (!project) {
3908
- project = new import_ts_morph13.Project({
4149
+ project = new import_ts_morph14.Project({
3909
4150
  skipAddingFilesFromTsConfig: true,
3910
4151
  skipLoadingLibFiles: true,
3911
4152
  skipFileDependencyResolution: true,
@@ -5636,36 +5877,64 @@ async function discoverInputFiles(config) {
5636
5877
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5637
5878
  return [...new Set(matched)].sort();
5638
5879
  }
5639
- async function computeInputsHash(config, extraInputs = []) {
5880
+ var READ_CONCURRENCY = 64;
5881
+ async function readAll(paths, read) {
5882
+ const out = /* @__PURE__ */ new Map();
5883
+ for (let i = 0; i < paths.length; i += READ_CONCURRENCY) {
5884
+ const batch = paths.slice(i, i + READ_CONCURRENCY);
5885
+ const texts = await Promise.all(batch.map(read));
5886
+ batch.forEach((path, j) => out.set(path, texts[j]));
5887
+ }
5888
+ return out;
5889
+ }
5890
+ async function readInputs(config) {
5891
+ const cwd = config.codegen.cwd;
5892
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
5893
+ const extended = loadDiscoveryTsconfig(tsconfigPath).files;
5894
+ const tsconfigFiles = extended.length > 0 ? extended : [tsconfigPath];
5895
+ const files = await discoverInputFiles(config);
5896
+ const contents = /* @__PURE__ */ new Map();
5897
+ const tsconfigs = await readAll(
5898
+ tsconfigFiles,
5899
+ (file) => (0, import_promises11.readFile)(file, "utf8").catch(() => null)
5900
+ );
5901
+ for (const [file, text] of tsconfigs) contents.set(file, text);
5902
+ for (const [file, text] of await readAll(files, (file2) => (0, import_promises11.readFile)(file2, "utf8"))) {
5903
+ contents.set(file, text);
5904
+ }
5905
+ return { tsconfigFiles, files, contents, tracked: /* @__PURE__ */ new Map() };
5906
+ }
5907
+ async function readInput(snapshot, path) {
5908
+ return snapshot.contents.get(path) ?? (0, import_promises11.readFile)(path, "utf8");
5909
+ }
5910
+ async function computeInputsHash(config, extraInputs = [], snapshot) {
5911
+ const inputs = snapshot ?? await readInputs(config);
5640
5912
  const hash = (0, import_node_crypto.createHash)("sha256");
5641
5913
  hash.update(`version:${VERSION}
5642
5914
  `);
5643
5915
  hash.update(`config:${serializeConfig(config)}
5644
5916
  `);
5645
5917
  const cwd = config.codegen.cwd;
5646
- const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
5647
- const tsconfigFiles = loadDiscoveryTsconfig(tsconfigPath).files;
5648
- for (const file of tsconfigFiles.length > 0 ? tsconfigFiles : [tsconfigPath]) {
5649
- const contents = await (0, import_promises11.readFile)(file, "utf8").catch(() => null);
5918
+ for (const file of inputs.tsconfigFiles) {
5650
5919
  hash.update(`tsconfig:${(0, import_node_path15.relative)(cwd, file)}
5651
5920
  `);
5652
- hash.update(contents ?? " missing");
5921
+ hash.update(inputs.contents.get(file) ?? " missing");
5653
5922
  hash.update("\n");
5654
5923
  }
5655
- const globbed = await discoverInputFiles(config);
5656
- const globbedRelative = new Set(globbed.map((file) => (0, import_node_path15.relative)(cwd, file)));
5924
+ const globbedRelative = new Set(inputs.files.map((file) => (0, import_node_path15.relative)(cwd, file)));
5657
5925
  const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
5658
- for (const file of globbed) {
5659
- const contents = await (0, import_promises11.readFile)(file, "utf8");
5926
+ for (const file of inputs.files) {
5660
5927
  hash.update(`file:${(0, import_node_path15.relative)(cwd, file)}
5661
5928
  `);
5662
- hash.update(contents);
5929
+ hash.update(inputs.contents.get(file));
5663
5930
  hash.update("\n");
5664
5931
  }
5932
+ const unread = extra.map((file) => (0, import_node_path15.join)(cwd, file)).filter((abs) => !inputs.tracked.has(abs));
5933
+ for (const [abs, text] of await readAll(unread, readTrackedInput)) inputs.tracked.set(abs, text);
5665
5934
  for (const file of extra) {
5666
5935
  hash.update(`extra:${file}
5667
5936
  `);
5668
- hash.update(await readTrackedInput((0, import_node_path15.join)(cwd, file)));
5937
+ hash.update(inputs.tracked.get((0, import_node_path15.join)(cwd, file)));
5669
5938
  hash.update("\n");
5670
5939
  }
5671
5940
  return hash.digest("hex");
@@ -5746,7 +6015,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
5746
6015
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
5747
6016
  setCodegenDebug(config.debug);
5748
6017
  const manifest = await readManifest(config.codegen.outDir);
5749
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
6018
+ const inputs = await readInputs(config);
6019
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? [], inputs);
5750
6020
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
5751
6021
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
5752
6022
  return;
@@ -5774,7 +6044,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5774
6044
  }
5775
6045
  if (config.pages) {
5776
6046
  const pagesConfig = config.pages;
5777
- const pages = await discoverPageScopes(config);
6047
+ const pages = await discoverPageScopes(config, (path) => readInput(inputs, path));
5778
6048
  const sharedProps = discoverSharedPropsFromConfig(config);
5779
6049
  await emitPages(pages, config.codegen.outDir, {
5780
6050
  propsExport: pagesConfig.propsExport,
@@ -5824,7 +6094,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5824
6094
  }
5825
6095
  const outputFiles = await listOutputFiles(config.codegen.outDir);
5826
6096
  const extraInputs = [...trackedInputs].sort();
5827
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
6097
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs, inputs) : inputsHash;
5828
6098
  await writeManifest(config.codegen.outDir, {
5829
6099
  version: VERSION,
5830
6100
  hash: recordedHash,
@@ -6007,13 +6277,15 @@ async function watch(config, onChange, options = {}) {
6007
6277
  pagesWatcher.on("unlink", schedulePagesRegenerate);
6008
6278
  let contractsDebounceTimer;
6009
6279
  const pendingChangedPaths = /* @__PURE__ */ new Set();
6280
+ let pendingAddedOrRemoved = false;
6010
6281
  const contractsWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, config.contracts.glob), {
6011
6282
  ignoreInitial: true,
6012
6283
  persistent: true,
6013
6284
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
6014
6285
  });
6015
- function scheduleContractsRegenerate(changedPath) {
6286
+ function scheduleContractsRegenerate(changedPath, addedOrRemoved = false) {
6016
6287
  if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
6288
+ if (addedOrRemoved) pendingAddedOrRemoved = true;
6017
6289
  if (contractsDebounceTimer !== void 0) {
6018
6290
  clearTimeout(contractsDebounceTimer);
6019
6291
  }
@@ -6021,8 +6293,12 @@ async function watch(config, onChange, options = {}) {
6021
6293
  contractsDebounceTimer = void 0;
6022
6294
  const changed = [...pendingChangedPaths];
6023
6295
  pendingChangedPaths.clear();
6296
+ const filesAddedOrRemoved = pendingAddedOrRemoved;
6297
+ pendingAddedOrRemoved = false;
6024
6298
  try {
6025
- const routes = await (await getDiscovery()).rediscover(changed);
6299
+ const routes = await (await getDiscovery()).rediscover(changed, {
6300
+ filesAddedOrRemoved
6301
+ });
6026
6302
  lastRoutes = routes;
6027
6303
  await generateAndTrack(routes);
6028
6304
  } catch (err) {
@@ -6034,20 +6310,20 @@ async function watch(config, onChange, options = {}) {
6034
6310
  onChange?.();
6035
6311
  }, config.contracts.debounceMs);
6036
6312
  }
6037
- contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6313
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6038
6314
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6039
- contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6315
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6040
6316
  const formsWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, config.forms.watch), {
6041
6317
  ignoreInitial: true,
6042
6318
  persistent: true,
6043
6319
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
6044
6320
  });
6045
- formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6321
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6046
6322
  formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6047
- formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6048
- trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6323
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6324
+ trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
6049
6325
  trackedWatcher.on("change", (p) => scheduleContractsRegenerate(p));
6050
- trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6326
+ trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6051
6327
  return {
6052
6328
  close: async () => {
6053
6329
  if (pagesDebounceTimer !== void 0) {
@@ -6068,7 +6344,7 @@ async function watch(config, onChange, options = {}) {
6068
6344
  }
6069
6345
 
6070
6346
  // src/index.ts
6071
- var VERSION = "0.28.0";
6347
+ var VERSION = "0.29.1";
6072
6348
 
6073
6349
  // src/cli/codegen.ts
6074
6350
  async function runCodegen(opts = {}) {
@@ -6096,28 +6372,28 @@ async function runCodegen(opts = {}) {
6096
6372
 
6097
6373
  // src/cli/doctor.ts
6098
6374
  var import_node_child_process2 = require("child_process");
6099
- var import_node_fs4 = require("fs");
6375
+ var import_node_fs6 = require("fs");
6100
6376
  var import_node_path20 = require("path");
6101
6377
 
6102
6378
  // src/cli/init.ts
6103
6379
  var import_node_child_process = require("child_process");
6104
- var import_node_fs3 = require("fs");
6380
+ var import_node_fs5 = require("fs");
6105
6381
  var import_promises16 = require("fs/promises");
6106
6382
  var import_node_path19 = require("path");
6107
6383
  var import_node_readline = require("readline");
6108
6384
 
6109
6385
  // src/cli/patch-utils.ts
6110
- var import_node_fs2 = require("fs");
6386
+ var import_node_fs4 = require("fs");
6111
6387
  function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
6112
6388
  let raw;
6113
6389
  try {
6114
- raw = (0, import_node_fs2.readFileSync)(filePath, "utf8");
6390
+ raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
6115
6391
  } catch {
6116
6392
  return "skipped";
6117
6393
  }
6118
6394
  const json = JSON.parse(parse(raw));
6119
6395
  if (!mutator(json)) return "already";
6120
- (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
6396
+ (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
6121
6397
  `, "utf8");
6122
6398
  return "patched";
6123
6399
  }
@@ -6329,7 +6605,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
6329
6605
  function patchAppModule(filePath, rootView) {
6330
6606
  let content;
6331
6607
  try {
6332
- content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
6608
+ content = (0, import_node_fs5.readFileSync)(filePath, "utf8");
6333
6609
  } catch {
6334
6610
  return "skipped";
6335
6611
  }
@@ -6364,13 +6640,13 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
6364
6640
  }
6365
6641
  }
6366
6642
  if (!changed) return "already";
6367
- (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
6643
+ (0, import_node_fs5.writeFileSync)(filePath, content, "utf8");
6368
6644
  return "patched";
6369
6645
  }
6370
6646
  function patchMainTs(filePath) {
6371
6647
  let content;
6372
6648
  try {
6373
- content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
6649
+ content = (0, import_node_fs5.readFileSync)(filePath, "utf8");
6374
6650
  } catch {
6375
6651
  return "skipped";
6376
6652
  }
@@ -6395,7 +6671,7 @@ function patchMainTs(filePath) {
6395
6671
  });`;
6396
6672
  content = `${content.slice(0, insertAfterPos)}
6397
6673
  ${viteSetup}${content.slice(insertAfterPos)}`;
6398
- (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
6674
+ (0, import_node_fs5.writeFileSync)(filePath, content, "utf8");
6399
6675
  return "patched";
6400
6676
  }
6401
6677
  function configTemplate(framework) {
@@ -6850,18 +7126,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
6850
7126
 
6851
7127
  // src/cli/doctor.ts
6852
7128
  function checkFileExists(cwd, file) {
6853
- return (0, import_node_fs4.existsSync)((0, import_node_path20.join)(cwd, file));
7129
+ return (0, import_node_fs6.existsSync)((0, import_node_path20.join)(cwd, file));
6854
7130
  }
6855
7131
  function readJson(path) {
6856
7132
  try {
6857
- const raw = (0, import_node_fs4.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
7133
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
6858
7134
  return JSON.parse(raw);
6859
7135
  } catch {
6860
7136
  return null;
6861
7137
  }
6862
7138
  }
6863
7139
  function writeJsonField(filePath, dotPath, value) {
6864
- const raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
7140
+ const raw = (0, import_node_fs6.readFileSync)(filePath, "utf8");
6865
7141
  const stripped = raw.replace(/\/\/.*$/gm, "");
6866
7142
  const obj = JSON.parse(stripped);
6867
7143
  let target = obj;
@@ -6881,7 +7157,7 @@ function writeJsonField(filePath, dotPath, value) {
6881
7157
  } else {
6882
7158
  target[lastKey] = value;
6883
7159
  }
6884
- (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
7160
+ (0, import_node_fs6.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
6885
7161
  `, "utf8");
6886
7162
  }
6887
7163
  function getPackageVersion(cwd, pkg) {
@@ -6893,8 +7169,8 @@ function getPackageVersion(cwd, pkg) {
6893
7169
  }
6894
7170
  }
6895
7171
  function detectPkgManager(cwd) {
6896
- if ((0, import_node_fs4.existsSync)((0, import_node_path20.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
6897
- if ((0, import_node_fs4.existsSync)((0, import_node_path20.join)(cwd, "yarn.lock"))) return "yarn";
7172
+ if ((0, import_node_fs6.existsSync)((0, import_node_path20.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
7173
+ if ((0, import_node_fs6.existsSync)((0, import_node_path20.join)(cwd, "yarn.lock"))) return "yarn";
6898
7174
  return "npm";
6899
7175
  }
6900
7176
  async function runDoctor(opts) {
@@ -7015,7 +7291,7 @@ async function runDoctor(opts) {
7015
7291
  pass: !!inertiaTsconfig,
7016
7292
  fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
7017
7293
  autoFix: () => {
7018
- (0, import_node_fs4.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
7294
+ (0, import_node_fs6.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
7019
7295
  }
7020
7296
  });
7021
7297
  if (inertiaTsconfig) {
@@ -7063,14 +7339,14 @@ async function runDoctor(opts) {
7063
7339
  const innerTsconfigPath = (0, import_node_path20.join)(cwd, "inertia", "tsconfig.json");
7064
7340
  checks.push({
7065
7341
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
7066
- pass: (0, import_node_fs4.existsSync)(innerTsconfigPath),
7342
+ pass: (0, import_node_fs6.existsSync)(innerTsconfigPath),
7067
7343
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
7068
7344
  autoFix: () => {
7069
- (0, import_node_fs4.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
7345
+ (0, import_node_fs6.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
7070
7346
  }
7071
7347
  });
7072
7348
  if (checkFileExists(cwd, "vite.config.ts")) {
7073
- const viteContent = (0, import_node_fs4.readFileSync)((0, import_node_path20.join)(cwd, "vite.config.ts"), "utf8");
7349
+ const viteContent = (0, import_node_fs6.readFileSync)((0, import_node_path20.join)(cwd, "vite.config.ts"), "utf8");
7074
7350
  checks.push({
7075
7351
  name: "vite.config.ts has resolve.alias",
7076
7352
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -7136,12 +7412,12 @@ async function runDoctor(opts) {
7136
7412
  }
7137
7413
  if (checkFileExists(cwd, ".gitignore")) {
7138
7414
  const gitignorePath = (0, import_node_path20.join)(cwd, ".gitignore");
7139
- const gitignore = (0, import_node_fs4.readFileSync)(gitignorePath, "utf8");
7415
+ const gitignore = (0, import_node_fs6.readFileSync)(gitignorePath, "utf8");
7140
7416
  checks.push({
7141
7417
  name: ".gitignore includes .nestjs-inertia/",
7142
7418
  pass: gitignore.includes(".nestjs-inertia"),
7143
7419
  fix: "Add .nestjs-inertia/ to .gitignore",
7144
- autoFix: () => (0, import_node_fs4.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
7420
+ autoFix: () => (0, import_node_fs6.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
7145
7421
  });
7146
7422
  }
7147
7423
  const pkgJsonPath = (0, import_node_path20.join)(cwd, "package.json");