@dudousxd/nestjs-codegen 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -87,11 +87,6 @@ function validateUserConfig(userConfig) {
87
87
  );
88
88
  }
89
89
  }
90
- if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
91
- console.warn(
92
- "[nestjs-codegen] The top-level `scopes` option has no effect and is deprecated. For several Inertia apps use `pages.scopes` (one entry per InertiaModule.forFeature scope); for several controller sets use one `contracts.glob` (e.g. a brace glob)."
93
- );
94
- }
95
90
  }
96
91
  function isGlobInput(value) {
97
92
  if (typeof value === "string") return true;
@@ -139,7 +134,6 @@ function applyDefaults(userConfig, cwd) {
139
134
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
140
135
  debounceMs: userConfig.contracts?.debounceMs ?? 500
141
136
  },
142
- scopes: userConfig.scopes ?? {},
143
137
  codegen: {
144
138
  outDir,
145
139
  cwd: resolvedCwd
@@ -180,11 +174,51 @@ import { existsSync } from "fs";
180
174
  import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
181
175
  import fg from "fast-glob";
182
176
  import {
177
+ FileSystemRefreshResult as FileSystemRefreshResult3,
183
178
  Node as Node10,
184
179
  Project as Project3,
185
- ts as ts3
180
+ ts as ts4
186
181
  } from "ts-morph";
187
182
 
183
+ // src/discovery/disk-fingerprints.ts
184
+ import { readFileSync, statSync } from "fs";
185
+ var DiskFingerprints = class {
186
+ stats = /* @__PURE__ */ new Map();
187
+ /** The paths whose disk content differs from `text`, or that no longer exist. */
188
+ changed(files) {
189
+ const changed = [];
190
+ for (const { path, text } of files) {
191
+ let key;
192
+ try {
193
+ const stat2 = statSync(path);
194
+ key = `${stat2.mtimeMs}:${stat2.size}`;
195
+ } catch {
196
+ this.stats.delete(path);
197
+ changed.push(path);
198
+ continue;
199
+ }
200
+ if (this.stats.get(path) === key) continue;
201
+ let disk;
202
+ try {
203
+ disk = readFileSync(path, "utf-8");
204
+ } catch {
205
+ disk = void 0;
206
+ }
207
+ if (disk === text) {
208
+ this.stats.set(path, key);
209
+ } else {
210
+ this.stats.delete(path);
211
+ changed.push(path);
212
+ }
213
+ }
214
+ return changed;
215
+ }
216
+ /** Forget `path`, so its next check compares content again. */
217
+ forget(path) {
218
+ this.stats.delete(path);
219
+ }
220
+ };
221
+
188
222
  // src/discovery/dto-type-resolver.ts
189
223
  import {
190
224
  Node as Node9,
@@ -1199,7 +1233,54 @@ function toFilterFieldType(name, r) {
1199
1233
  }
1200
1234
 
1201
1235
  // src/discovery/heritage.ts
1202
- import { Node as Node4, Project } from "ts-morph";
1236
+ import {
1237
+ FileSystemRefreshResult,
1238
+ Node as Node4,
1239
+ Project
1240
+ } from "ts-morph";
1241
+
1242
+ // src/discovery/unresolved-imports.ts
1243
+ import { ts } from "ts-morph";
1244
+ var UnresolvedImports = class {
1245
+ read;
1246
+ /**
1247
+ * The files with an import that failed to resolve in `program` and now resolves
1248
+ * to a TypeScript file the program does not hold.
1249
+ */
1250
+ nowResolving(program) {
1251
+ if (this.read?.program !== program) {
1252
+ this.read = { program, imports: failedImports(program) };
1253
+ }
1254
+ const options = program.getCompilerOptions();
1255
+ const files = /* @__PURE__ */ new Set();
1256
+ for (const { specifier, from } of this.read.imports) {
1257
+ const file = ts.resolveModuleName(specifier, from, options, ts.sys).resolvedModule?.resolvedFileName;
1258
+ if (file !== void 0 && /\.(m|c)?tsx?$/.test(file) && !program.getSourceFile(file)) {
1259
+ files.add(from);
1260
+ }
1261
+ }
1262
+ return [...files];
1263
+ }
1264
+ };
1265
+ function failedImports(program) {
1266
+ const checker = program.getTypeChecker();
1267
+ const imports = [];
1268
+ for (const file of program.getSourceFiles()) {
1269
+ if (program.isSourceFileDefaultLibrary(file) || file.isDeclarationFile) continue;
1270
+ if (program.isSourceFileFromExternalLibrary(file)) continue;
1271
+ for (const statement of file.statements) {
1272
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) continue;
1273
+ const specifier = statement.moduleSpecifier;
1274
+ if (!specifier || !ts.isStringLiteral(specifier)) continue;
1275
+ if (!checker.getSymbolAtLocation(specifier)) {
1276
+ imports.push({ specifier: specifier.text, from: file.fileName });
1277
+ }
1278
+ }
1279
+ }
1280
+ return imports;
1281
+ }
1282
+
1283
+ // src/discovery/heritage.ts
1203
1284
  function resolveHeritageCall(node) {
1204
1285
  if (!node) return void 0;
1205
1286
  const expr = unwrapExpression(node);
@@ -1399,15 +1480,42 @@ function resolvePropertyClassReference(prop) {
1399
1480
  return { key, ref };
1400
1481
  }
1401
1482
  var mixinTypeProject;
1483
+ var mixinFiles = new DiskFingerprints();
1484
+ var mixinStale = false;
1485
+ var mixinUnresolved = new UnresolvedImports();
1402
1486
  function getMixinTypeProject() {
1403
- mixinTypeProject ??= new Project({
1404
- skipAddingFilesFromTsConfig: true,
1405
- compilerOptions: { strict: true }
1406
- });
1487
+ if (mixinTypeProject && mixinStale && !refreshMixinTypeProject(mixinTypeProject)) {
1488
+ mixinTypeProject = void 0;
1489
+ }
1490
+ mixinStale = false;
1491
+ if (!mixinTypeProject) {
1492
+ mixinTypeProject = new Project({
1493
+ skipAddingFilesFromTsConfig: true,
1494
+ compilerOptions: { strict: true }
1495
+ });
1496
+ mixinFiles = new DiskFingerprints();
1497
+ mixinUnresolved = new UnresolvedImports();
1498
+ }
1407
1499
  return mixinTypeProject;
1408
1500
  }
1409
1501
  function clearMixinTypeProject() {
1410
1502
  mixinTypeProject = void 0;
1503
+ mixinStale = false;
1504
+ }
1505
+ function invalidateMixinTypeProject() {
1506
+ mixinStale = true;
1507
+ }
1508
+ function refreshMixinTypeProject(project) {
1509
+ const program = project.getProgram().compilerObject;
1510
+ const files = program.getSourceFiles().filter((file) => !program.isSourceFileDefaultLibrary(file)).map((file) => ({ path: file.fileName, text: file.text }));
1511
+ const changed = mixinFiles.changed(files);
1512
+ if (changed.length === 0) return mixinUnresolved.nowResolving(program).length === 0;
1513
+ for (const path of changed) {
1514
+ const sf = project.getSourceFile(path);
1515
+ if (sf?.refreshFromFileSystemSync() !== FileSystemRefreshResult.Updated) return false;
1516
+ }
1517
+ project.resolveSourceFileDependencies();
1518
+ return true;
1411
1519
  }
1412
1520
  function resolveInstantiatedReturnType(cls, methodName) {
1413
1521
  const filePath = cls.getSourceFile().getFilePath();
@@ -1738,11 +1846,11 @@ import { Node as Node8 } from "ts-morph";
1738
1846
 
1739
1847
  // src/discovery/schema-type-inference.ts
1740
1848
  import {
1741
- ts as ts2
1849
+ ts as ts3
1742
1850
  } from "ts-morph";
1743
1851
 
1744
1852
  // src/discovery/schema-type-program.ts
1745
- import { Node as Node6, Project as Project2, ts } from "ts-morph";
1853
+ import { FileSystemRefreshResult as FileSystemRefreshResult2, Node as Node6, Project as Project2, ts as ts2 } from "ts-morph";
1746
1854
  var PARAM_DECORATORS = /* @__PURE__ */ new Set(["Body", "Query", "Param"]);
1747
1855
  var SchemaTypeProgram = class {
1748
1856
  constructor(discovery) {
@@ -1752,10 +1860,10 @@ var SchemaTypeProgram = class {
1752
1860
  ...base,
1753
1861
  // A tsconfig-less consumer gets ES5 by default, whose lib has no `Map`/`Set`/
1754
1862
  // `Promise` for the schema libraries' declarations to lean on.
1755
- target: base.target ?? ts.ScriptTarget.ES2022,
1863
+ target: base.target ?? ts2.ScriptTarget.ES2022,
1756
1864
  // With neither set, resolve the way a bundler does: package `exports`
1757
1865
  // (`zod/v4`) and extensionless or `.js` relative imports all resolve.
1758
- ...base.module === void 0 && base.moduleResolution === void 0 ? { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler } : {},
1866
+ ...base.module === void 0 && base.moduleResolution === void 0 ? { module: ts2.ModuleKind.ESNext, moduleResolution: ts2.ModuleResolutionKind.Bundler } : {},
1759
1867
  // Without it `.nullable()` and `.optional()` would erase to the bare type.
1760
1868
  strictNullChecks: true,
1761
1869
  // No ambient `@types/*` (a tsconfig's `types: ['node']`): a schema does not
@@ -1767,8 +1875,7 @@ var SchemaTypeProgram = class {
1767
1875
  skipAddingFilesFromTsConfig: true,
1768
1876
  compilerOptions: this.options
1769
1877
  });
1770
- for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
1771
- this.flush();
1878
+ this.requestDiscoveryFiles();
1772
1879
  }
1773
1880
  project;
1774
1881
  options;
@@ -1781,6 +1888,53 @@ var SchemaTypeProgram = class {
1781
1888
  needs = /* @__PURE__ */ new Map();
1782
1889
  /** Pruned text per file, as last written into {@link project}. */
1783
1890
  written = /* @__PURE__ */ new Map();
1891
+ /** What {@link project} read from disk unpruned (packages, declaration files). */
1892
+ externalFiles = new DiskFingerprints();
1893
+ scratchFiles = new DiskFingerprints();
1894
+ /** The last pruning of each file, with the source text and needs it was pruned for. */
1895
+ pruned = /* @__PURE__ */ new Map();
1896
+ /**
1897
+ * Bring the program up to date with discovery's files and the disk, keeping the
1898
+ * lib and package declarations it has already parsed and checked. Leaves it
1899
+ * holding exactly the files a new program built now would hold, with the same
1900
+ * text. Returns false when a file the program read unpruned has changed on disk
1901
+ * (a package was reinstalled, a `.d.ts` was edited): the caller builds a new
1902
+ * program instead.
1903
+ */
1904
+ refresh() {
1905
+ const program = this.project.getProgram().compilerObject;
1906
+ const external = program.getSourceFiles().filter(
1907
+ (file) => !program.isSourceFileDefaultLibrary(file) && !this.written.has(file.fileName)
1908
+ ).map((file) => ({ path: file.fileName, text: file.text }));
1909
+ if (this.externalFiles.changed(external).length > 0) return false;
1910
+ const scratch = this.scratch.getSourceFiles().map((sf) => ({ path: sf.getFilePath(), text: sf.getFullText() }));
1911
+ for (const path of this.scratchFiles.changed(scratch)) {
1912
+ const sf = this.scratch.getSourceFile(path);
1913
+ if (sf?.refreshFromFileSystemSync() === FileSystemRefreshResult2.Deleted) {
1914
+ this.scratchFiles.forget(path);
1915
+ }
1916
+ }
1917
+ this.needs.clear();
1918
+ this.requestDiscoveryFiles();
1919
+ for (const path of [...this.written.keys()]) {
1920
+ if (this.needs.has(path)) continue;
1921
+ const sf = this.project.getSourceFile(path);
1922
+ if (sf) this.project.removeSourceFile(sf);
1923
+ this.written.delete(path);
1924
+ }
1925
+ return true;
1926
+ }
1927
+ /**
1928
+ * Everything discovery holds now (the controllers) goes in before the first
1929
+ * query: a file added afterwards discards the program and its checker, and
1930
+ * rebuilding them once per controller is exactly the cost this avoids.
1931
+ */
1932
+ requestDiscoveryFiles() {
1933
+ for (const sf of this.discovery.getSourceFiles()) {
1934
+ this.request(sf.getFilePath(), { schema: true });
1935
+ }
1936
+ this.flush();
1937
+ }
1784
1938
  /**
1785
1939
  * The typed copy of a discovery file, with its schema positions kept. Adding one
1786
1940
  * that was not there yet (a contract file discovery reached lazily) rebuilds the
@@ -1821,12 +1975,21 @@ var SchemaTypeProgram = class {
1821
1975
  while (this.queue.length > 0) {
1822
1976
  const path = this.queue.pop();
1823
1977
  const original = this.original(path);
1824
- if (!original) continue;
1825
- const needs = this.needs.get(path);
1826
- const text = this.prune(original, needs);
1978
+ if (!original) {
1979
+ if (this.written.has(path)) changed.set(path, void 0);
1980
+ continue;
1981
+ }
1982
+ const { text, requests } = this.pruneCached(path, original, this.needs.get(path));
1983
+ for (const { path: target, want } of requests) this.request(target, want);
1827
1984
  if (this.written.get(path) !== text) changed.set(path, text);
1828
1985
  }
1829
1986
  for (const [path, text] of changed) {
1987
+ if (text === void 0) {
1988
+ const sf = this.project.getSourceFile(path);
1989
+ if (sf) this.project.removeSourceFile(sf);
1990
+ this.written.delete(path);
1991
+ continue;
1992
+ }
1830
1993
  this.project.createSourceFile(path, text, { overwrite: true });
1831
1994
  this.written.set(path, text);
1832
1995
  }
@@ -1835,11 +1998,29 @@ var SchemaTypeProgram = class {
1835
1998
  original(path) {
1836
1999
  return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
1837
2000
  }
2001
+ /** {@link prune}, reusing the last result while the file's text and needs are the same. */
2002
+ pruneCached(path, sf, needs) {
2003
+ const source = sf.getFullText();
2004
+ const key = `${needs.schema}:${needs.all}:${[...needs.names].sort().join(",")}`;
2005
+ const last = this.pruned.get(path);
2006
+ if (last && last.source === source && last.needs === key) return last;
2007
+ const result = this.prune(sf, needs);
2008
+ if (result.resolved) this.pruned.set(path, { ...result, source, needs: key });
2009
+ else this.pruned.delete(path);
2010
+ return result;
2011
+ }
1838
2012
  /**
1839
2013
  * `sf`'s text with every import/re-export nothing in `needs` references blanked,
1840
- * requesting from each kept one's target the names it brings in.
2014
+ * and the names each kept one brings in from its target.
1841
2015
  */
1842
2016
  prune(sf, needs) {
2017
+ const requests = [];
2018
+ let resolved = true;
2019
+ const resolveFrom = (specifier) => {
2020
+ const target = this.resolve(specifier, sf);
2021
+ if (target === null) resolved = false;
2022
+ return target ?? void 0;
2023
+ };
1843
2024
  const locals = topLevelDeclarations(sf);
1844
2025
  const roots = [];
1845
2026
  if (needs.all) roots.push(...sf.getStatements());
@@ -1873,7 +2054,7 @@ var SchemaTypeProgram = class {
1873
2054
  const seen = new Set(queue);
1874
2055
  while (queue.length > 0) {
1875
2056
  const node = queue.pop();
1876
- const ids = Node6.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts.SyntaxKind.Identifier);
2057
+ const ids = Node6.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts2.SyntaxKind.Identifier);
1877
2058
  for (const id of ids) {
1878
2059
  const name = id.getText();
1879
2060
  if (referenced.has(name)) continue;
@@ -1892,7 +2073,7 @@ var SchemaTypeProgram = class {
1892
2073
  }
1893
2074
  };
1894
2075
  for (const decl of sf.getImportDeclarations()) {
1895
- const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2076
+ const target = resolveFrom(decl.getModuleSpecifierValue());
1896
2077
  const namespace = decl.getNamespaceImport()?.getText();
1897
2078
  const def = decl.getDefaultImport()?.getText();
1898
2079
  const named = decl.getNamedImports().map((n) => ({
@@ -1906,21 +2087,24 @@ var SchemaTypeProgram = class {
1906
2087
  continue;
1907
2088
  }
1908
2089
  if (target) {
1909
- this.request(target, {
1910
- all: wantsNamespace,
1911
- names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2090
+ requests.push({
2091
+ path: target,
2092
+ want: {
2093
+ all: wantsNamespace,
2094
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2095
+ }
1912
2096
  });
1913
2097
  }
1914
2098
  }
1915
2099
  for (const exp of sf.getExportDeclarations()) {
1916
2100
  const spec = exp.getModuleSpecifierValue();
1917
2101
  if (spec === void 0) continue;
1918
- const target = this.resolve(spec, sf);
2102
+ const target = resolveFrom(spec);
1919
2103
  if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
1920
2104
  if (!needs.all && unresolvedNames.length === 0) {
1921
2105
  blank(exp);
1922
2106
  } else if (target) {
1923
- this.request(target, { all: needs.all, names: unresolvedNames });
2107
+ requests.push({ path: target, want: { all: needs.all, names: unresolvedNames } });
1924
2108
  }
1925
2109
  continue;
1926
2110
  }
@@ -1928,22 +2112,24 @@ var SchemaTypeProgram = class {
1928
2112
  (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
1929
2113
  ).map((n) => n.getNameNode().getText());
1930
2114
  if (wanted.length === 0) blank(exp);
1931
- else if (target) this.request(target, { names: wanted });
2115
+ else if (target) requests.push({ path: target, want: { names: wanted } });
1932
2116
  }
1933
- return chars.join("");
2117
+ return { text: chars.join(""), requests, resolved };
1934
2118
  }
1935
2119
  /**
1936
- * The project source file a module specifier resolves to, or undefined for a
1937
- * package (read unpruned by the program itself) or an unresolvable one.
2120
+ * The project source file a module specifier resolves to; undefined for a
2121
+ * package or a declaration file (read unpruned by the program itself), null for
2122
+ * one that does not resolve.
1938
2123
  */
1939
2124
  resolve(specifier, from) {
1940
- const resolved = ts.resolveModuleName(
2125
+ const resolved = ts2.resolveModuleName(
1941
2126
  specifier,
1942
2127
  from.getFilePath(),
1943
2128
  this.options,
1944
- ts.sys
2129
+ ts2.sys
1945
2130
  ).resolvedModule;
1946
- if (!resolved || resolved.isExternalLibraryImport) return void 0;
2131
+ if (!resolved) return null;
2132
+ if (resolved.isExternalLibraryImport) return void 0;
1947
2133
  const file = resolved.resolvedFileName;
1948
2134
  if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
1949
2135
  return file;
@@ -1998,8 +2184,13 @@ function defaultExport(sf) {
1998
2184
 
1999
2185
  // src/discovery/schema-type-inference.ts
2000
2186
  var programs = /* @__PURE__ */ new WeakMap();
2187
+ var stale = /* @__PURE__ */ new WeakSet();
2001
2188
  function programFor(discovery) {
2002
2189
  let program = programs.get(discovery);
2190
+ if (program && stale.has(discovery)) {
2191
+ stale.delete(discovery);
2192
+ if (!program.refresh()) program = void 0;
2193
+ }
2003
2194
  if (!program) {
2004
2195
  program = new SchemaTypeProgram(discovery);
2005
2196
  programs.set(discovery, program);
@@ -2008,6 +2199,10 @@ function programFor(discovery) {
2008
2199
  }
2009
2200
  function clearSchemaTypeProject(discovery) {
2010
2201
  programs.delete(discovery);
2202
+ stale.delete(discovery);
2203
+ }
2204
+ function invalidateSchemaTypeProject(discovery) {
2205
+ if (programs.has(discovery)) stale.add(discovery);
2011
2206
  }
2012
2207
  function counterpart(typedSf, node) {
2013
2208
  const start = node.getStart();
@@ -2071,7 +2266,7 @@ function objectKey(name) {
2071
2266
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2072
2267
  }
2073
2268
  function isOptionalSymbol(sym) {
2074
- return (sym.getFlags() & ts2.SymbolFlags.Optional) !== 0;
2269
+ return (sym.getFlags() & ts3.SymbolFlags.Optional) !== 0;
2075
2270
  }
2076
2271
  function isSymbolKeyed(sym) {
2077
2272
  return sym.getName().startsWith("__@");
@@ -2949,25 +3144,25 @@ var NO_TSCONFIG_OPTIONS = {
2949
3144
  };
2950
3145
  function loadDiscoveryTsconfig(tsconfigPath) {
2951
3146
  if (!existsSync(tsconfigPath)) return { options: null, files: [] };
2952
- const read = ts3.readConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
3147
+ const read = ts4.readConfigFile(tsconfigPath, (path) => ts4.sys.readFile(path));
2953
3148
  if (read.error) {
2954
3149
  return {
2955
3150
  options: null,
2956
3151
  files: [tsconfigPath],
2957
- error: ts3.flattenDiagnosticMessageText(read.error.messageText, " ")
3152
+ error: ts4.flattenDiagnosticMessageText(read.error.messageText, " ")
2958
3153
  };
2959
3154
  }
2960
3155
  const host = {
2961
- useCaseSensitiveFileNames: ts3.sys.useCaseSensitiveFileNames,
3156
+ useCaseSensitiveFileNames: ts4.sys.useCaseSensitiveFileNames,
2962
3157
  // The one line that keeps a tsconfig from being a filesystem walk.
2963
3158
  readDirectory: () => [],
2964
- fileExists: (path) => ts3.sys.fileExists(path),
3159
+ fileExists: (path) => ts4.sys.fileExists(path),
2965
3160
  // `extends` is resolved through the host, so a consumer whose `paths` live
2966
3161
  // in a base tsconfig keeps working.
2967
- readFile: (path) => ts3.sys.readFile(path)
3162
+ readFile: (path) => ts4.sys.readFile(path)
2968
3163
  };
2969
- const sourceFile = ts3.readJsonConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
2970
- const parsed = ts3.parseJsonSourceFileConfigFileContent(
3164
+ const sourceFile = ts4.readJsonConfigFile(tsconfigPath, (path) => ts4.sys.readFile(path));
3165
+ const parsed = ts4.parseJsonSourceFileConfigFileContent(
2971
3166
  sourceFile,
2972
3167
  host,
2973
3168
  dirname2(tsconfigPath),
@@ -3025,6 +3220,9 @@ var PersistentDiscovery = class _PersistentDiscovery {
3025
3220
  glob;
3026
3221
  /** Absolute paths of the controllers currently loaded as extraction roots. */
3027
3222
  controllerPaths = /* @__PURE__ */ new Set();
3223
+ /** Every file in {@link project}, as last read from disk. */
3224
+ files = new DiskFingerprints();
3225
+ unresolved = new UnresolvedImports();
3028
3226
  constructor(project, cwd, glob) {
3029
3227
  this.project = project;
3030
3228
  this.cwd = cwd;
@@ -3050,6 +3248,8 @@ var PersistentDiscovery = class _PersistentDiscovery {
3050
3248
  }
3051
3249
  /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3052
3250
  discover() {
3251
+ clearMixinTypeProject();
3252
+ clearSchemaTypeProject(this.project);
3053
3253
  return this.runExtraction();
3054
3254
  }
3055
3255
  /**
@@ -3057,26 +3257,39 @@ var PersistentDiscovery = class _PersistentDiscovery {
3057
3257
  * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3058
3258
  * to pick up added/removed controllers, clears the per-Project caches, then
3059
3259
  * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3060
- * being exhaustive because re-globbing + refresh-on-presence covers the set.
3260
+ * being exhaustive because re-globbing + comparing every loaded file with the
3261
+ * disk covers the set.
3061
3262
  */
3062
- async rediscover(changedPaths) {
3063
- if (changedPaths) {
3064
- for (const p of changedPaths) {
3065
- const abs = resolve3(p);
3066
- const sf = this.project.getSourceFile(abs);
3067
- if (sf) {
3068
- await sf.refreshFromFileSystem();
3069
- }
3263
+ async rediscover(changedPaths, options = {}) {
3264
+ const hinted = new Set([...changedPaths ?? []].map((p) => resolve3(p)));
3265
+ const program = this.project.getProgram().compilerObject;
3266
+ const sources = program.getSourceFiles().filter(
3267
+ (file) => !program.isSourceFileDefaultLibrary(file) && !program.isSourceFileFromExternalLibrary(file)
3268
+ ).map((file) => ({ path: file.fileName, text: file.text }));
3269
+ for (const path of /* @__PURE__ */ new Set([...hinted, ...this.files.changed(sources)])) {
3270
+ const sf = this.project.getSourceFile(path);
3271
+ if (sf && await sf.refreshFromFileSystem() === FileSystemRefreshResult3.Deleted) {
3272
+ this.files.forget(path);
3070
3273
  }
3071
3274
  }
3275
+ const inProject = new Set(this.project.getSourceFiles().map((sf) => sf.getFilePath()));
3276
+ const definitions = this.project.getLanguageService().compilerObject.getProgram();
3277
+ for (const path of definitions ? this.unresolved.nowResolving(definitions) : []) {
3278
+ const sf = this.project.getSourceFile(path);
3279
+ if (!sf) continue;
3280
+ this.project.removeSourceFile(sf);
3281
+ if (inProject.has(path)) this.project.addSourceFileAtPathIfExists(path);
3282
+ }
3072
3283
  const globbed = new Set(
3073
3284
  await fg(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3074
3285
  );
3286
+ let controllersChanged = false;
3075
3287
  for (const f of globbed) {
3076
3288
  if (!this.controllerPaths.has(f)) {
3077
3289
  try {
3078
3290
  this.project.addSourceFileAtPath(f);
3079
3291
  this.controllerPaths.add(f);
3292
+ controllersChanged = true;
3080
3293
  } catch {
3081
3294
  }
3082
3295
  }
@@ -3086,16 +3299,22 @@ var PersistentDiscovery = class _PersistentDiscovery {
3086
3299
  const sf = this.project.getSourceFile(f);
3087
3300
  if (sf) this.project.removeSourceFile(sf);
3088
3301
  this.controllerPaths.delete(f);
3302
+ controllersChanged = true;
3089
3303
  }
3090
3304
  }
3305
+ if (controllersChanged || options.filesAddedOrRemoved) {
3306
+ clearMixinTypeProject();
3307
+ clearSchemaTypeProject(this.project);
3308
+ } else {
3309
+ invalidateMixinTypeProject();
3310
+ invalidateSchemaTypeProject(this.project);
3311
+ }
3091
3312
  return this.runExtraction();
3092
3313
  }
3093
3314
  /** Clear stale per-Project caches, then extract over the controller set. */
3094
3315
  runExtraction() {
3095
3316
  clearTypeResolutionCaches(this.project);
3096
3317
  clearEnumCache(this.project);
3097
- clearMixinTypeProject();
3098
- clearSchemaTypeProject(this.project);
3099
3318
  return extractRoutesFrom(this.project, this.controllerPaths);
3100
3319
  }
3101
3320
  };
@@ -3411,7 +3630,7 @@ async function discoverPages(opts) {
3411
3630
  const rel = relative2(opts.cwd, file);
3412
3631
  const nameRel = relative2(pagesBase, file).split(sep2).join("/");
3413
3632
  const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
3414
- const source = await readFile(file, "utf8");
3633
+ const source = await (opts.read ? opts.read(file) : readFile(file, "utf8"));
3415
3634
  const propsSource = extractPropsSource(source, opts.propsExport);
3416
3635
  out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
3417
3636
  }
@@ -3419,10 +3638,14 @@ async function discoverPages(opts) {
3419
3638
  assertUniqueNames(out);
3420
3639
  return out;
3421
3640
  }
3422
- async function discoverPageScopes(config) {
3641
+ async function discoverPageScopes(config, read) {
3423
3642
  const pages = config.pages;
3424
3643
  if (!pages) return [];
3425
- const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
3644
+ const common = {
3645
+ cwd: config.codegen.cwd,
3646
+ propsExport: pages.propsExport,
3647
+ ...read ? { read } : {}
3648
+ };
3426
3649
  const out = await discoverPages({
3427
3650
  ...common,
3428
3651
  glob: pages.glob,
@@ -3497,6 +3720,7 @@ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
3497
3720
  import { dirname as dirname4, join as join13 } from "path";
3498
3721
 
3499
3722
  // src/discovery/shared-props.ts
3723
+ import { readFileSync as readFileSync2 } from "fs";
3500
3724
  import { join as join4 } from "path";
3501
3725
  import { Node as Node11, Project as Project4, SyntaxKind as SyntaxKind4 } from "ts-morph";
3502
3726
  function discoverSharedProps(project, moduleEntry) {
@@ -3524,12 +3748,7 @@ function discoverSharedPropsFromConfig(config) {
3524
3748
  const tsconfigPath = config.app.tsconfig ?? join4(config.codegen.cwd, "tsconfig.json");
3525
3749
  let project;
3526
3750
  try {
3527
- project = new Project4({
3528
- tsConfigFilePath: tsconfigPath,
3529
- skipAddingFilesFromTsConfig: true,
3530
- skipLoadingLibFiles: true,
3531
- skipFileDependencyResolution: true
3532
- });
3751
+ project = sharedPropsProject(tsconfigPath);
3533
3752
  } catch {
3534
3753
  project = new Project4({
3535
3754
  skipAddingFilesFromTsConfig: true,
@@ -3543,6 +3762,27 @@ function discoverSharedPropsFromConfig(config) {
3543
3762
  return null;
3544
3763
  }
3545
3764
  }
3765
+ var parsedTsconfigs = /* @__PURE__ */ new Map();
3766
+ function readText(file) {
3767
+ try {
3768
+ return readFileSync2(file, "utf8");
3769
+ } catch {
3770
+ return "\0missing";
3771
+ }
3772
+ }
3773
+ function sharedPropsProject(tsconfigPath) {
3774
+ const text = loadDiscoveryTsconfig(tsconfigPath).files.map((file) => `${file}\0${readText(file)}`).join("\0");
3775
+ const flags = {
3776
+ skipAddingFilesFromTsConfig: true,
3777
+ skipLoadingLibFiles: true,
3778
+ skipFileDependencyResolution: true
3779
+ };
3780
+ const parsed = parsedTsconfigs.get(tsconfigPath);
3781
+ if (parsed?.text === text) return new Project4({ ...flags, compilerOptions: parsed.options });
3782
+ const project = new Project4({ ...flags, tsConfigFilePath: tsconfigPath });
3783
+ parsedTsconfigs.set(tsconfigPath, { text, options: project.getCompilerOptions() });
3784
+ return project;
3785
+ }
3546
3786
  function findForRootCall(sourceFile) {
3547
3787
  const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
3548
3788
  for (const call of callExpressions) {
@@ -5498,14 +5738,15 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
5498
5738
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
5499
5739
  setCodegenDebug(config.debug);
5500
5740
  const manifest = await readManifest(config.codegen.outDir);
5501
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
5741
+ const inputs = await readInputs(config);
5742
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? [], inputs);
5502
5743
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
5503
5744
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
5504
5745
  return;
5505
5746
  }
5506
5747
  const configHash = computeConfigHash(config);
5507
5748
  const configKeyHashes = computeConfigKeyHashes(config);
5508
- if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
5749
+ if (config.driftGuard && manifest?.version === VERSION && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
5509
5750
  throw new DriftGuardError(
5510
5751
  driftGuardMessage(
5511
5752
  config.codegen.outDir,
@@ -5526,7 +5767,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5526
5767
  }
5527
5768
  if (config.pages) {
5528
5769
  const pagesConfig = config.pages;
5529
- const pages = await discoverPageScopes(config);
5770
+ const pages = await discoverPageScopes(config, (path) => readInput(inputs, path));
5530
5771
  const sharedProps = discoverSharedPropsFromConfig(config);
5531
5772
  await emitPages(pages, config.codegen.outDir, {
5532
5773
  propsExport: pagesConfig.propsExport,
@@ -5576,7 +5817,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5576
5817
  }
5577
5818
  const outputFiles = await listOutputFiles(config.codegen.outDir);
5578
5819
  const extraInputs = [...trackedInputs].sort();
5579
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
5820
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs, inputs) : inputsHash;
5580
5821
  await writeManifest(config.codegen.outDir, {
5581
5822
  version: VERSION,
5582
5823
  hash: recordedHash,
@@ -5759,13 +6000,15 @@ async function watch(config, onChange, options = {}) {
5759
6000
  pagesWatcher.on("unlink", schedulePagesRegenerate);
5760
6001
  let contractsDebounceTimer;
5761
6002
  const pendingChangedPaths = /* @__PURE__ */ new Set();
6003
+ let pendingAddedOrRemoved = false;
5762
6004
  const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
5763
6005
  ignoreInitial: true,
5764
6006
  persistent: true,
5765
6007
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5766
6008
  });
5767
- function scheduleContractsRegenerate(changedPath) {
6009
+ function scheduleContractsRegenerate(changedPath, addedOrRemoved = false) {
5768
6010
  if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
6011
+ if (addedOrRemoved) pendingAddedOrRemoved = true;
5769
6012
  if (contractsDebounceTimer !== void 0) {
5770
6013
  clearTimeout(contractsDebounceTimer);
5771
6014
  }
@@ -5773,8 +6016,12 @@ async function watch(config, onChange, options = {}) {
5773
6016
  contractsDebounceTimer = void 0;
5774
6017
  const changed = [...pendingChangedPaths];
5775
6018
  pendingChangedPaths.clear();
6019
+ const filesAddedOrRemoved = pendingAddedOrRemoved;
6020
+ pendingAddedOrRemoved = false;
5776
6021
  try {
5777
- const routes = await (await getDiscovery()).rediscover(changed);
6022
+ const routes = await (await getDiscovery()).rediscover(changed, {
6023
+ filesAddedOrRemoved
6024
+ });
5778
6025
  lastRoutes = routes;
5779
6026
  await generateAndTrack(routes);
5780
6027
  } catch (err) {
@@ -5786,20 +6033,25 @@ async function watch(config, onChange, options = {}) {
5786
6033
  onChange?.();
5787
6034
  }, config.contracts.debounceMs);
5788
6035
  }
5789
- contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6036
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5790
6037
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5791
- contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6038
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
5792
6039
  const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
5793
6040
  ignoreInitial: true,
5794
6041
  persistent: true,
5795
6042
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5796
6043
  });
5797
- formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6044
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5798
6045
  formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5799
- formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
5800
- trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6046
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6047
+ trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5801
6048
  trackedWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5802
- trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6049
+ trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6050
+ await Promise.all(
6051
+ [pagesWatcher, contractsWatcher, formsWatcher].map(
6052
+ (w) => new Promise((resolveReady) => w.once("ready", () => resolveReady()))
6053
+ )
6054
+ );
5803
6055
  return {
5804
6056
  close: async () => {
5805
6057
  if (pagesDebounceTimer !== void 0) {
@@ -5820,7 +6072,7 @@ async function watch(config, onChange, options = {}) {
5820
6072
  }
5821
6073
 
5822
6074
  // src/index.ts
5823
- var VERSION = "0.29.0";
6075
+ var VERSION = "0.30.0";
5824
6076
 
5825
6077
  // src/generate-manifest.ts
5826
6078
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -5885,36 +6137,64 @@ async function discoverInputFiles(config) {
5885
6137
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
5886
6138
  return [...new Set(matched)].sort();
5887
6139
  }
5888
- async function computeInputsHash(config, extraInputs = []) {
6140
+ var READ_CONCURRENCY = 64;
6141
+ async function readAll(paths, read) {
6142
+ const out = /* @__PURE__ */ new Map();
6143
+ for (let i = 0; i < paths.length; i += READ_CONCURRENCY) {
6144
+ const batch = paths.slice(i, i + READ_CONCURRENCY);
6145
+ const texts = await Promise.all(batch.map(read));
6146
+ batch.forEach((path, j) => out.set(path, texts[j]));
6147
+ }
6148
+ return out;
6149
+ }
6150
+ async function readInputs(config) {
6151
+ const cwd = config.codegen.cwd;
6152
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
6153
+ const extended = loadDiscoveryTsconfig(tsconfigPath).files;
6154
+ const tsconfigFiles = extended.length > 0 ? extended : [tsconfigPath];
6155
+ const files = await discoverInputFiles(config);
6156
+ const contents = /* @__PURE__ */ new Map();
6157
+ const tsconfigs = await readAll(
6158
+ tsconfigFiles,
6159
+ (file) => readFile4(file, "utf8").catch(() => null)
6160
+ );
6161
+ for (const [file, text] of tsconfigs) contents.set(file, text);
6162
+ for (const [file, text] of await readAll(files, (file2) => readFile4(file2, "utf8"))) {
6163
+ contents.set(file, text);
6164
+ }
6165
+ return { tsconfigFiles, files, contents, tracked: /* @__PURE__ */ new Map() };
6166
+ }
6167
+ async function readInput(snapshot, path) {
6168
+ return snapshot.contents.get(path) ?? readFile4(path, "utf8");
6169
+ }
6170
+ async function computeInputsHash(config, extraInputs = [], snapshot) {
6171
+ const inputs = snapshot ?? await readInputs(config);
5889
6172
  const hash = createHash("sha256");
5890
6173
  hash.update(`version:${VERSION}
5891
6174
  `);
5892
6175
  hash.update(`config:${serializeConfig(config)}
5893
6176
  `);
5894
6177
  const cwd = config.codegen.cwd;
5895
- const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
5896
- const tsconfigFiles = loadDiscoveryTsconfig(tsconfigPath).files;
5897
- for (const file of tsconfigFiles.length > 0 ? tsconfigFiles : [tsconfigPath]) {
5898
- const contents = await readFile4(file, "utf8").catch(() => null);
6178
+ for (const file of inputs.tsconfigFiles) {
5899
6179
  hash.update(`tsconfig:${relative7(cwd, file)}
5900
6180
  `);
5901
- hash.update(contents ?? " missing");
6181
+ hash.update(inputs.contents.get(file) ?? " missing");
5902
6182
  hash.update("\n");
5903
6183
  }
5904
- const globbed = await discoverInputFiles(config);
5905
- const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
6184
+ const globbedRelative = new Set(inputs.files.map((file) => relative7(cwd, file)));
5906
6185
  const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
5907
- for (const file of globbed) {
5908
- const contents = await readFile4(file, "utf8");
6186
+ for (const file of inputs.files) {
5909
6187
  hash.update(`file:${relative7(cwd, file)}
5910
6188
  `);
5911
- hash.update(contents);
6189
+ hash.update(inputs.contents.get(file));
5912
6190
  hash.update("\n");
5913
6191
  }
6192
+ const unread = extra.map((file) => join16(cwd, file)).filter((abs) => !inputs.tracked.has(abs));
6193
+ for (const [abs, text] of await readAll(unread, readTrackedInput)) inputs.tracked.set(abs, text);
5914
6194
  for (const file of extra) {
5915
6195
  hash.update(`extra:${file}
5916
6196
  `);
5917
- hash.update(await readTrackedInput(join16(cwd, file)));
6197
+ hash.update(inputs.tracked.get(join16(cwd, file)));
5918
6198
  hash.update("\n");
5919
6199
  }
5920
6200
  return hash.digest("hex");