@dudousxd/nestjs-codegen 0.29.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.
@@ -180,11 +180,51 @@ import { existsSync } from "fs";
180
180
  import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
181
181
  import fg from "fast-glob";
182
182
  import {
183
+ FileSystemRefreshResult as FileSystemRefreshResult3,
183
184
  Node as Node10,
184
185
  Project as Project3,
185
- ts as ts3
186
+ ts as ts4
186
187
  } from "ts-morph";
187
188
 
189
+ // src/discovery/disk-fingerprints.ts
190
+ import { readFileSync, statSync } from "fs";
191
+ var DiskFingerprints = class {
192
+ stats = /* @__PURE__ */ new Map();
193
+ /** The paths whose disk content differs from `text`, or that no longer exist. */
194
+ changed(files) {
195
+ const changed = [];
196
+ for (const { path, text } of files) {
197
+ let key;
198
+ try {
199
+ const stat2 = statSync(path);
200
+ key = `${stat2.mtimeMs}:${stat2.size}`;
201
+ } catch {
202
+ this.stats.delete(path);
203
+ changed.push(path);
204
+ continue;
205
+ }
206
+ if (this.stats.get(path) === key) continue;
207
+ let disk;
208
+ try {
209
+ disk = readFileSync(path, "utf-8");
210
+ } catch {
211
+ disk = void 0;
212
+ }
213
+ if (disk === text) {
214
+ this.stats.set(path, key);
215
+ } else {
216
+ this.stats.delete(path);
217
+ changed.push(path);
218
+ }
219
+ }
220
+ return changed;
221
+ }
222
+ /** Forget `path`, so its next check compares content again. */
223
+ forget(path) {
224
+ this.stats.delete(path);
225
+ }
226
+ };
227
+
188
228
  // src/discovery/dto-type-resolver.ts
189
229
  import {
190
230
  Node as Node9,
@@ -1199,7 +1239,54 @@ function toFilterFieldType(name, r) {
1199
1239
  }
1200
1240
 
1201
1241
  // src/discovery/heritage.ts
1202
- import { Node as Node4, Project } from "ts-morph";
1242
+ import {
1243
+ FileSystemRefreshResult,
1244
+ Node as Node4,
1245
+ Project
1246
+ } from "ts-morph";
1247
+
1248
+ // src/discovery/unresolved-imports.ts
1249
+ import { ts } from "ts-morph";
1250
+ var UnresolvedImports = class {
1251
+ read;
1252
+ /**
1253
+ * The files with an import that failed to resolve in `program` and now resolves
1254
+ * to a TypeScript file the program does not hold.
1255
+ */
1256
+ nowResolving(program) {
1257
+ if (this.read?.program !== program) {
1258
+ this.read = { program, imports: failedImports(program) };
1259
+ }
1260
+ const options = program.getCompilerOptions();
1261
+ const files = /* @__PURE__ */ new Set();
1262
+ for (const { specifier, from } of this.read.imports) {
1263
+ const file = ts.resolveModuleName(specifier, from, options, ts.sys).resolvedModule?.resolvedFileName;
1264
+ if (file !== void 0 && /\.(m|c)?tsx?$/.test(file) && !program.getSourceFile(file)) {
1265
+ files.add(from);
1266
+ }
1267
+ }
1268
+ return [...files];
1269
+ }
1270
+ };
1271
+ function failedImports(program) {
1272
+ const checker = program.getTypeChecker();
1273
+ const imports = [];
1274
+ for (const file of program.getSourceFiles()) {
1275
+ if (program.isSourceFileDefaultLibrary(file) || file.isDeclarationFile) continue;
1276
+ if (program.isSourceFileFromExternalLibrary(file)) continue;
1277
+ for (const statement of file.statements) {
1278
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) continue;
1279
+ const specifier = statement.moduleSpecifier;
1280
+ if (!specifier || !ts.isStringLiteral(specifier)) continue;
1281
+ if (!checker.getSymbolAtLocation(specifier)) {
1282
+ imports.push({ specifier: specifier.text, from: file.fileName });
1283
+ }
1284
+ }
1285
+ }
1286
+ return imports;
1287
+ }
1288
+
1289
+ // src/discovery/heritage.ts
1203
1290
  function resolveHeritageCall(node) {
1204
1291
  if (!node) return void 0;
1205
1292
  const expr = unwrapExpression(node);
@@ -1399,15 +1486,42 @@ function resolvePropertyClassReference(prop) {
1399
1486
  return { key, ref };
1400
1487
  }
1401
1488
  var mixinTypeProject;
1489
+ var mixinFiles = new DiskFingerprints();
1490
+ var mixinStale = false;
1491
+ var mixinUnresolved = new UnresolvedImports();
1402
1492
  function getMixinTypeProject() {
1403
- mixinTypeProject ??= new Project({
1404
- skipAddingFilesFromTsConfig: true,
1405
- compilerOptions: { strict: true }
1406
- });
1493
+ if (mixinTypeProject && mixinStale && !refreshMixinTypeProject(mixinTypeProject)) {
1494
+ mixinTypeProject = void 0;
1495
+ }
1496
+ mixinStale = false;
1497
+ if (!mixinTypeProject) {
1498
+ mixinTypeProject = new Project({
1499
+ skipAddingFilesFromTsConfig: true,
1500
+ compilerOptions: { strict: true }
1501
+ });
1502
+ mixinFiles = new DiskFingerprints();
1503
+ mixinUnresolved = new UnresolvedImports();
1504
+ }
1407
1505
  return mixinTypeProject;
1408
1506
  }
1409
1507
  function clearMixinTypeProject() {
1410
1508
  mixinTypeProject = void 0;
1509
+ mixinStale = false;
1510
+ }
1511
+ function invalidateMixinTypeProject() {
1512
+ mixinStale = true;
1513
+ }
1514
+ function refreshMixinTypeProject(project) {
1515
+ const program = project.getProgram().compilerObject;
1516
+ const files = program.getSourceFiles().filter((file) => !program.isSourceFileDefaultLibrary(file)).map((file) => ({ path: file.fileName, text: file.text }));
1517
+ const changed = mixinFiles.changed(files);
1518
+ if (changed.length === 0) return mixinUnresolved.nowResolving(program).length === 0;
1519
+ for (const path of changed) {
1520
+ const sf = project.getSourceFile(path);
1521
+ if (sf?.refreshFromFileSystemSync() !== FileSystemRefreshResult.Updated) return false;
1522
+ }
1523
+ project.resolveSourceFileDependencies();
1524
+ return true;
1411
1525
  }
1412
1526
  function resolveInstantiatedReturnType(cls, methodName) {
1413
1527
  const filePath = cls.getSourceFile().getFilePath();
@@ -1738,11 +1852,11 @@ import { Node as Node8 } from "ts-morph";
1738
1852
 
1739
1853
  // src/discovery/schema-type-inference.ts
1740
1854
  import {
1741
- ts as ts2
1855
+ ts as ts3
1742
1856
  } from "ts-morph";
1743
1857
 
1744
1858
  // src/discovery/schema-type-program.ts
1745
- import { Node as Node6, Project as Project2, ts } from "ts-morph";
1859
+ import { FileSystemRefreshResult as FileSystemRefreshResult2, Node as Node6, Project as Project2, ts as ts2 } from "ts-morph";
1746
1860
  var PARAM_DECORATORS = /* @__PURE__ */ new Set(["Body", "Query", "Param"]);
1747
1861
  var SchemaTypeProgram = class {
1748
1862
  constructor(discovery) {
@@ -1752,10 +1866,10 @@ var SchemaTypeProgram = class {
1752
1866
  ...base,
1753
1867
  // A tsconfig-less consumer gets ES5 by default, whose lib has no `Map`/`Set`/
1754
1868
  // `Promise` for the schema libraries' declarations to lean on.
1755
- target: base.target ?? ts.ScriptTarget.ES2022,
1869
+ target: base.target ?? ts2.ScriptTarget.ES2022,
1756
1870
  // With neither set, resolve the way a bundler does: package `exports`
1757
1871
  // (`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 } : {},
1872
+ ...base.module === void 0 && base.moduleResolution === void 0 ? { module: ts2.ModuleKind.ESNext, moduleResolution: ts2.ModuleResolutionKind.Bundler } : {},
1759
1873
  // Without it `.nullable()` and `.optional()` would erase to the bare type.
1760
1874
  strictNullChecks: true,
1761
1875
  // No ambient `@types/*` (a tsconfig's `types: ['node']`): a schema does not
@@ -1767,8 +1881,7 @@ var SchemaTypeProgram = class {
1767
1881
  skipAddingFilesFromTsConfig: true,
1768
1882
  compilerOptions: this.options
1769
1883
  });
1770
- for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
1771
- this.flush();
1884
+ this.requestDiscoveryFiles();
1772
1885
  }
1773
1886
  project;
1774
1887
  options;
@@ -1781,6 +1894,53 @@ var SchemaTypeProgram = class {
1781
1894
  needs = /* @__PURE__ */ new Map();
1782
1895
  /** Pruned text per file, as last written into {@link project}. */
1783
1896
  written = /* @__PURE__ */ new Map();
1897
+ /** What {@link project} read from disk unpruned (packages, declaration files). */
1898
+ externalFiles = new DiskFingerprints();
1899
+ scratchFiles = new DiskFingerprints();
1900
+ /** The last pruning of each file, with the source text and needs it was pruned for. */
1901
+ pruned = /* @__PURE__ */ new Map();
1902
+ /**
1903
+ * Bring the program up to date with discovery's files and the disk, keeping the
1904
+ * lib and package declarations it has already parsed and checked. Leaves it
1905
+ * holding exactly the files a new program built now would hold, with the same
1906
+ * text. Returns false when a file the program read unpruned has changed on disk
1907
+ * (a package was reinstalled, a `.d.ts` was edited): the caller builds a new
1908
+ * program instead.
1909
+ */
1910
+ refresh() {
1911
+ const program = this.project.getProgram().compilerObject;
1912
+ const external = program.getSourceFiles().filter(
1913
+ (file) => !program.isSourceFileDefaultLibrary(file) && !this.written.has(file.fileName)
1914
+ ).map((file) => ({ path: file.fileName, text: file.text }));
1915
+ if (this.externalFiles.changed(external).length > 0) return false;
1916
+ const scratch = this.scratch.getSourceFiles().map((sf) => ({ path: sf.getFilePath(), text: sf.getFullText() }));
1917
+ for (const path of this.scratchFiles.changed(scratch)) {
1918
+ const sf = this.scratch.getSourceFile(path);
1919
+ if (sf?.refreshFromFileSystemSync() === FileSystemRefreshResult2.Deleted) {
1920
+ this.scratchFiles.forget(path);
1921
+ }
1922
+ }
1923
+ this.needs.clear();
1924
+ this.requestDiscoveryFiles();
1925
+ for (const path of [...this.written.keys()]) {
1926
+ if (this.needs.has(path)) continue;
1927
+ const sf = this.project.getSourceFile(path);
1928
+ if (sf) this.project.removeSourceFile(sf);
1929
+ this.written.delete(path);
1930
+ }
1931
+ return true;
1932
+ }
1933
+ /**
1934
+ * Everything discovery holds now (the controllers) goes in before the first
1935
+ * query: a file added afterwards discards the program and its checker, and
1936
+ * rebuilding them once per controller is exactly the cost this avoids.
1937
+ */
1938
+ requestDiscoveryFiles() {
1939
+ for (const sf of this.discovery.getSourceFiles()) {
1940
+ this.request(sf.getFilePath(), { schema: true });
1941
+ }
1942
+ this.flush();
1943
+ }
1784
1944
  /**
1785
1945
  * The typed copy of a discovery file, with its schema positions kept. Adding one
1786
1946
  * that was not there yet (a contract file discovery reached lazily) rebuilds the
@@ -1821,12 +1981,21 @@ var SchemaTypeProgram = class {
1821
1981
  while (this.queue.length > 0) {
1822
1982
  const path = this.queue.pop();
1823
1983
  const original = this.original(path);
1824
- if (!original) continue;
1825
- const needs = this.needs.get(path);
1826
- const text = this.prune(original, needs);
1984
+ if (!original) {
1985
+ if (this.written.has(path)) changed.set(path, void 0);
1986
+ continue;
1987
+ }
1988
+ const { text, requests } = this.pruneCached(path, original, this.needs.get(path));
1989
+ for (const { path: target, want } of requests) this.request(target, want);
1827
1990
  if (this.written.get(path) !== text) changed.set(path, text);
1828
1991
  }
1829
1992
  for (const [path, text] of changed) {
1993
+ if (text === void 0) {
1994
+ const sf = this.project.getSourceFile(path);
1995
+ if (sf) this.project.removeSourceFile(sf);
1996
+ this.written.delete(path);
1997
+ continue;
1998
+ }
1830
1999
  this.project.createSourceFile(path, text, { overwrite: true });
1831
2000
  this.written.set(path, text);
1832
2001
  }
@@ -1835,11 +2004,29 @@ var SchemaTypeProgram = class {
1835
2004
  original(path) {
1836
2005
  return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
1837
2006
  }
2007
+ /** {@link prune}, reusing the last result while the file's text and needs are the same. */
2008
+ pruneCached(path, sf, needs) {
2009
+ const source = sf.getFullText();
2010
+ const key = `${needs.schema}:${needs.all}:${[...needs.names].sort().join(",")}`;
2011
+ const last = this.pruned.get(path);
2012
+ if (last && last.source === source && last.needs === key) return last;
2013
+ const result = this.prune(sf, needs);
2014
+ if (result.resolved) this.pruned.set(path, { ...result, source, needs: key });
2015
+ else this.pruned.delete(path);
2016
+ return result;
2017
+ }
1838
2018
  /**
1839
2019
  * `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.
2020
+ * and the names each kept one brings in from its target.
1841
2021
  */
1842
2022
  prune(sf, needs) {
2023
+ const requests = [];
2024
+ let resolved = true;
2025
+ const resolveFrom = (specifier) => {
2026
+ const target = this.resolve(specifier, sf);
2027
+ if (target === null) resolved = false;
2028
+ return target ?? void 0;
2029
+ };
1843
2030
  const locals = topLevelDeclarations(sf);
1844
2031
  const roots = [];
1845
2032
  if (needs.all) roots.push(...sf.getStatements());
@@ -1873,7 +2060,7 @@ var SchemaTypeProgram = class {
1873
2060
  const seen = new Set(queue);
1874
2061
  while (queue.length > 0) {
1875
2062
  const node = queue.pop();
1876
- const ids = Node6.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts.SyntaxKind.Identifier);
2063
+ const ids = Node6.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts2.SyntaxKind.Identifier);
1877
2064
  for (const id of ids) {
1878
2065
  const name = id.getText();
1879
2066
  if (referenced.has(name)) continue;
@@ -1892,7 +2079,7 @@ var SchemaTypeProgram = class {
1892
2079
  }
1893
2080
  };
1894
2081
  for (const decl of sf.getImportDeclarations()) {
1895
- const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2082
+ const target = resolveFrom(decl.getModuleSpecifierValue());
1896
2083
  const namespace = decl.getNamespaceImport()?.getText();
1897
2084
  const def = decl.getDefaultImport()?.getText();
1898
2085
  const named = decl.getNamedImports().map((n) => ({
@@ -1906,21 +2093,24 @@ var SchemaTypeProgram = class {
1906
2093
  continue;
1907
2094
  }
1908
2095
  if (target) {
1909
- this.request(target, {
1910
- all: wantsNamespace,
1911
- names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2096
+ requests.push({
2097
+ path: target,
2098
+ want: {
2099
+ all: wantsNamespace,
2100
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2101
+ }
1912
2102
  });
1913
2103
  }
1914
2104
  }
1915
2105
  for (const exp of sf.getExportDeclarations()) {
1916
2106
  const spec = exp.getModuleSpecifierValue();
1917
2107
  if (spec === void 0) continue;
1918
- const target = this.resolve(spec, sf);
2108
+ const target = resolveFrom(spec);
1919
2109
  if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
1920
2110
  if (!needs.all && unresolvedNames.length === 0) {
1921
2111
  blank(exp);
1922
2112
  } else if (target) {
1923
- this.request(target, { all: needs.all, names: unresolvedNames });
2113
+ requests.push({ path: target, want: { all: needs.all, names: unresolvedNames } });
1924
2114
  }
1925
2115
  continue;
1926
2116
  }
@@ -1928,22 +2118,24 @@ var SchemaTypeProgram = class {
1928
2118
  (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
1929
2119
  ).map((n) => n.getNameNode().getText());
1930
2120
  if (wanted.length === 0) blank(exp);
1931
- else if (target) this.request(target, { names: wanted });
2121
+ else if (target) requests.push({ path: target, want: { names: wanted } });
1932
2122
  }
1933
- return chars.join("");
2123
+ return { text: chars.join(""), requests, resolved };
1934
2124
  }
1935
2125
  /**
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.
2126
+ * The project source file a module specifier resolves to; undefined for a
2127
+ * package or a declaration file (read unpruned by the program itself), null for
2128
+ * one that does not resolve.
1938
2129
  */
1939
2130
  resolve(specifier, from) {
1940
- const resolved = ts.resolveModuleName(
2131
+ const resolved = ts2.resolveModuleName(
1941
2132
  specifier,
1942
2133
  from.getFilePath(),
1943
2134
  this.options,
1944
- ts.sys
2135
+ ts2.sys
1945
2136
  ).resolvedModule;
1946
- if (!resolved || resolved.isExternalLibraryImport) return void 0;
2137
+ if (!resolved) return null;
2138
+ if (resolved.isExternalLibraryImport) return void 0;
1947
2139
  const file = resolved.resolvedFileName;
1948
2140
  if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
1949
2141
  return file;
@@ -1998,8 +2190,13 @@ function defaultExport(sf) {
1998
2190
 
1999
2191
  // src/discovery/schema-type-inference.ts
2000
2192
  var programs = /* @__PURE__ */ new WeakMap();
2193
+ var stale = /* @__PURE__ */ new WeakSet();
2001
2194
  function programFor(discovery) {
2002
2195
  let program = programs.get(discovery);
2196
+ if (program && stale.has(discovery)) {
2197
+ stale.delete(discovery);
2198
+ if (!program.refresh()) program = void 0;
2199
+ }
2003
2200
  if (!program) {
2004
2201
  program = new SchemaTypeProgram(discovery);
2005
2202
  programs.set(discovery, program);
@@ -2008,6 +2205,10 @@ function programFor(discovery) {
2008
2205
  }
2009
2206
  function clearSchemaTypeProject(discovery) {
2010
2207
  programs.delete(discovery);
2208
+ stale.delete(discovery);
2209
+ }
2210
+ function invalidateSchemaTypeProject(discovery) {
2211
+ if (programs.has(discovery)) stale.add(discovery);
2011
2212
  }
2012
2213
  function counterpart(typedSf, node) {
2013
2214
  const start = node.getStart();
@@ -2071,7 +2272,7 @@ function objectKey(name) {
2071
2272
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2072
2273
  }
2073
2274
  function isOptionalSymbol(sym) {
2074
- return (sym.getFlags() & ts2.SymbolFlags.Optional) !== 0;
2275
+ return (sym.getFlags() & ts3.SymbolFlags.Optional) !== 0;
2075
2276
  }
2076
2277
  function isSymbolKeyed(sym) {
2077
2278
  return sym.getName().startsWith("__@");
@@ -2949,25 +3150,25 @@ var NO_TSCONFIG_OPTIONS = {
2949
3150
  };
2950
3151
  function loadDiscoveryTsconfig(tsconfigPath) {
2951
3152
  if (!existsSync(tsconfigPath)) return { options: null, files: [] };
2952
- const read = ts3.readConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
3153
+ const read = ts4.readConfigFile(tsconfigPath, (path) => ts4.sys.readFile(path));
2953
3154
  if (read.error) {
2954
3155
  return {
2955
3156
  options: null,
2956
3157
  files: [tsconfigPath],
2957
- error: ts3.flattenDiagnosticMessageText(read.error.messageText, " ")
3158
+ error: ts4.flattenDiagnosticMessageText(read.error.messageText, " ")
2958
3159
  };
2959
3160
  }
2960
3161
  const host = {
2961
- useCaseSensitiveFileNames: ts3.sys.useCaseSensitiveFileNames,
3162
+ useCaseSensitiveFileNames: ts4.sys.useCaseSensitiveFileNames,
2962
3163
  // The one line that keeps a tsconfig from being a filesystem walk.
2963
3164
  readDirectory: () => [],
2964
- fileExists: (path) => ts3.sys.fileExists(path),
3165
+ fileExists: (path) => ts4.sys.fileExists(path),
2965
3166
  // `extends` is resolved through the host, so a consumer whose `paths` live
2966
3167
  // in a base tsconfig keeps working.
2967
- readFile: (path) => ts3.sys.readFile(path)
3168
+ readFile: (path) => ts4.sys.readFile(path)
2968
3169
  };
2969
- const sourceFile = ts3.readJsonConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
2970
- const parsed = ts3.parseJsonSourceFileConfigFileContent(
3170
+ const sourceFile = ts4.readJsonConfigFile(tsconfigPath, (path) => ts4.sys.readFile(path));
3171
+ const parsed = ts4.parseJsonSourceFileConfigFileContent(
2971
3172
  sourceFile,
2972
3173
  host,
2973
3174
  dirname2(tsconfigPath),
@@ -3025,6 +3226,9 @@ var PersistentDiscovery = class _PersistentDiscovery {
3025
3226
  glob;
3026
3227
  /** Absolute paths of the controllers currently loaded as extraction roots. */
3027
3228
  controllerPaths = /* @__PURE__ */ new Set();
3229
+ /** Every file in {@link project}, as last read from disk. */
3230
+ files = new DiskFingerprints();
3231
+ unresolved = new UnresolvedImports();
3028
3232
  constructor(project, cwd, glob) {
3029
3233
  this.project = project;
3030
3234
  this.cwd = cwd;
@@ -3050,6 +3254,8 @@ var PersistentDiscovery = class _PersistentDiscovery {
3050
3254
  }
3051
3255
  /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3052
3256
  discover() {
3257
+ clearMixinTypeProject();
3258
+ clearSchemaTypeProject(this.project);
3053
3259
  return this.runExtraction();
3054
3260
  }
3055
3261
  /**
@@ -3057,26 +3263,39 @@ var PersistentDiscovery = class _PersistentDiscovery {
3057
3263
  * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3058
3264
  * to pick up added/removed controllers, clears the per-Project caches, then
3059
3265
  * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3060
- * being exhaustive because re-globbing + refresh-on-presence covers the set.
3266
+ * being exhaustive because re-globbing + comparing every loaded file with the
3267
+ * disk covers the set.
3061
3268
  */
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
- }
3269
+ async rediscover(changedPaths, options = {}) {
3270
+ const hinted = new Set([...changedPaths ?? []].map((p) => resolve3(p)));
3271
+ const program = this.project.getProgram().compilerObject;
3272
+ const sources = program.getSourceFiles().filter(
3273
+ (file) => !program.isSourceFileDefaultLibrary(file) && !program.isSourceFileFromExternalLibrary(file)
3274
+ ).map((file) => ({ path: file.fileName, text: file.text }));
3275
+ for (const path of /* @__PURE__ */ new Set([...hinted, ...this.files.changed(sources)])) {
3276
+ const sf = this.project.getSourceFile(path);
3277
+ if (sf && await sf.refreshFromFileSystem() === FileSystemRefreshResult3.Deleted) {
3278
+ this.files.forget(path);
3070
3279
  }
3071
3280
  }
3281
+ const inProject = new Set(this.project.getSourceFiles().map((sf) => sf.getFilePath()));
3282
+ const definitions = this.project.getLanguageService().compilerObject.getProgram();
3283
+ for (const path of definitions ? this.unresolved.nowResolving(definitions) : []) {
3284
+ const sf = this.project.getSourceFile(path);
3285
+ if (!sf) continue;
3286
+ this.project.removeSourceFile(sf);
3287
+ if (inProject.has(path)) this.project.addSourceFileAtPathIfExists(path);
3288
+ }
3072
3289
  const globbed = new Set(
3073
3290
  await fg(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3074
3291
  );
3292
+ let controllersChanged = false;
3075
3293
  for (const f of globbed) {
3076
3294
  if (!this.controllerPaths.has(f)) {
3077
3295
  try {
3078
3296
  this.project.addSourceFileAtPath(f);
3079
3297
  this.controllerPaths.add(f);
3298
+ controllersChanged = true;
3080
3299
  } catch {
3081
3300
  }
3082
3301
  }
@@ -3086,16 +3305,22 @@ var PersistentDiscovery = class _PersistentDiscovery {
3086
3305
  const sf = this.project.getSourceFile(f);
3087
3306
  if (sf) this.project.removeSourceFile(sf);
3088
3307
  this.controllerPaths.delete(f);
3308
+ controllersChanged = true;
3089
3309
  }
3090
3310
  }
3311
+ if (controllersChanged || options.filesAddedOrRemoved) {
3312
+ clearMixinTypeProject();
3313
+ clearSchemaTypeProject(this.project);
3314
+ } else {
3315
+ invalidateMixinTypeProject();
3316
+ invalidateSchemaTypeProject(this.project);
3317
+ }
3091
3318
  return this.runExtraction();
3092
3319
  }
3093
3320
  /** Clear stale per-Project caches, then extract over the controller set. */
3094
3321
  runExtraction() {
3095
3322
  clearTypeResolutionCaches(this.project);
3096
3323
  clearEnumCache(this.project);
3097
- clearMixinTypeProject();
3098
- clearSchemaTypeProject(this.project);
3099
3324
  return extractRoutesFrom(this.project, this.controllerPaths);
3100
3325
  }
3101
3326
  };
@@ -3411,7 +3636,7 @@ async function discoverPages(opts) {
3411
3636
  const rel = relative2(opts.cwd, file);
3412
3637
  const nameRel = relative2(pagesBase, file).split(sep2).join("/");
3413
3638
  const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
3414
- const source = await readFile(file, "utf8");
3639
+ const source = await (opts.read ? opts.read(file) : readFile(file, "utf8"));
3415
3640
  const propsSource = extractPropsSource(source, opts.propsExport);
3416
3641
  out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
3417
3642
  }
@@ -3419,10 +3644,14 @@ async function discoverPages(opts) {
3419
3644
  assertUniqueNames(out);
3420
3645
  return out;
3421
3646
  }
3422
- async function discoverPageScopes(config) {
3647
+ async function discoverPageScopes(config, read) {
3423
3648
  const pages = config.pages;
3424
3649
  if (!pages) return [];
3425
- const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
3650
+ const common = {
3651
+ cwd: config.codegen.cwd,
3652
+ propsExport: pages.propsExport,
3653
+ ...read ? { read } : {}
3654
+ };
3426
3655
  const out = await discoverPages({
3427
3656
  ...common,
3428
3657
  glob: pages.glob,
@@ -3497,6 +3726,7 @@ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
3497
3726
  import { dirname as dirname4, join as join13 } from "path";
3498
3727
 
3499
3728
  // src/discovery/shared-props.ts
3729
+ import { readFileSync as readFileSync2 } from "fs";
3500
3730
  import { join as join4 } from "path";
3501
3731
  import { Node as Node11, Project as Project4, SyntaxKind as SyntaxKind4 } from "ts-morph";
3502
3732
  function discoverSharedProps(project, moduleEntry) {
@@ -3524,12 +3754,7 @@ function discoverSharedPropsFromConfig(config) {
3524
3754
  const tsconfigPath = config.app.tsconfig ?? join4(config.codegen.cwd, "tsconfig.json");
3525
3755
  let project;
3526
3756
  try {
3527
- project = new Project4({
3528
- tsConfigFilePath: tsconfigPath,
3529
- skipAddingFilesFromTsConfig: true,
3530
- skipLoadingLibFiles: true,
3531
- skipFileDependencyResolution: true
3532
- });
3757
+ project = sharedPropsProject(tsconfigPath);
3533
3758
  } catch {
3534
3759
  project = new Project4({
3535
3760
  skipAddingFilesFromTsConfig: true,
@@ -3543,6 +3768,27 @@ function discoverSharedPropsFromConfig(config) {
3543
3768
  return null;
3544
3769
  }
3545
3770
  }
3771
+ var parsedTsconfigs = /* @__PURE__ */ new Map();
3772
+ function readText(file) {
3773
+ try {
3774
+ return readFileSync2(file, "utf8");
3775
+ } catch {
3776
+ return "\0missing";
3777
+ }
3778
+ }
3779
+ function sharedPropsProject(tsconfigPath) {
3780
+ const text = loadDiscoveryTsconfig(tsconfigPath).files.map((file) => `${file}\0${readText(file)}`).join("\0");
3781
+ const flags = {
3782
+ skipAddingFilesFromTsConfig: true,
3783
+ skipLoadingLibFiles: true,
3784
+ skipFileDependencyResolution: true
3785
+ };
3786
+ const parsed = parsedTsconfigs.get(tsconfigPath);
3787
+ if (parsed?.text === text) return new Project4({ ...flags, compilerOptions: parsed.options });
3788
+ const project = new Project4({ ...flags, tsConfigFilePath: tsconfigPath });
3789
+ parsedTsconfigs.set(tsconfigPath, { text, options: project.getCompilerOptions() });
3790
+ return project;
3791
+ }
3546
3792
  function findForRootCall(sourceFile) {
3547
3793
  const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
3548
3794
  for (const call of callExpressions) {
@@ -5498,7 +5744,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
5498
5744
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
5499
5745
  setCodegenDebug(config.debug);
5500
5746
  const manifest = await readManifest(config.codegen.outDir);
5501
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
5747
+ const inputs = await readInputs(config);
5748
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? [], inputs);
5502
5749
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
5503
5750
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
5504
5751
  return;
@@ -5526,7 +5773,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5526
5773
  }
5527
5774
  if (config.pages) {
5528
5775
  const pagesConfig = config.pages;
5529
- const pages = await discoverPageScopes(config);
5776
+ const pages = await discoverPageScopes(config, (path) => readInput(inputs, path));
5530
5777
  const sharedProps = discoverSharedPropsFromConfig(config);
5531
5778
  await emitPages(pages, config.codegen.outDir, {
5532
5779
  propsExport: pagesConfig.propsExport,
@@ -5576,7 +5823,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5576
5823
  }
5577
5824
  const outputFiles = await listOutputFiles(config.codegen.outDir);
5578
5825
  const extraInputs = [...trackedInputs].sort();
5579
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
5826
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs, inputs) : inputsHash;
5580
5827
  await writeManifest(config.codegen.outDir, {
5581
5828
  version: VERSION,
5582
5829
  hash: recordedHash,
@@ -5759,13 +6006,15 @@ async function watch(config, onChange, options = {}) {
5759
6006
  pagesWatcher.on("unlink", schedulePagesRegenerate);
5760
6007
  let contractsDebounceTimer;
5761
6008
  const pendingChangedPaths = /* @__PURE__ */ new Set();
6009
+ let pendingAddedOrRemoved = false;
5762
6010
  const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
5763
6011
  ignoreInitial: true,
5764
6012
  persistent: true,
5765
6013
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5766
6014
  });
5767
- function scheduleContractsRegenerate(changedPath) {
6015
+ function scheduleContractsRegenerate(changedPath, addedOrRemoved = false) {
5768
6016
  if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
6017
+ if (addedOrRemoved) pendingAddedOrRemoved = true;
5769
6018
  if (contractsDebounceTimer !== void 0) {
5770
6019
  clearTimeout(contractsDebounceTimer);
5771
6020
  }
@@ -5773,8 +6022,12 @@ async function watch(config, onChange, options = {}) {
5773
6022
  contractsDebounceTimer = void 0;
5774
6023
  const changed = [...pendingChangedPaths];
5775
6024
  pendingChangedPaths.clear();
6025
+ const filesAddedOrRemoved = pendingAddedOrRemoved;
6026
+ pendingAddedOrRemoved = false;
5776
6027
  try {
5777
- const routes = await (await getDiscovery()).rediscover(changed);
6028
+ const routes = await (await getDiscovery()).rediscover(changed, {
6029
+ filesAddedOrRemoved
6030
+ });
5778
6031
  lastRoutes = routes;
5779
6032
  await generateAndTrack(routes);
5780
6033
  } catch (err) {
@@ -5786,20 +6039,20 @@ async function watch(config, onChange, options = {}) {
5786
6039
  onChange?.();
5787
6040
  }, config.contracts.debounceMs);
5788
6041
  }
5789
- contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6042
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5790
6043
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5791
- contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6044
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
5792
6045
  const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
5793
6046
  ignoreInitial: true,
5794
6047
  persistent: true,
5795
6048
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5796
6049
  });
5797
- formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6050
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5798
6051
  formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5799
- formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
5800
- trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p));
6052
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
6053
+ trackedWatcher.on("add", (p) => scheduleContractsRegenerate(p, true));
5801
6054
  trackedWatcher.on("change", (p) => scheduleContractsRegenerate(p));
5802
- trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
6055
+ trackedWatcher.on("unlink", (p) => scheduleContractsRegenerate(p, true));
5803
6056
  return {
5804
6057
  close: async () => {
5805
6058
  if (pagesDebounceTimer !== void 0) {
@@ -5820,7 +6073,7 @@ async function watch(config, onChange, options = {}) {
5820
6073
  }
5821
6074
 
5822
6075
  // src/index.ts
5823
- var VERSION = "0.29.0";
6076
+ var VERSION = "0.29.1";
5824
6077
 
5825
6078
  // src/generate-manifest.ts
5826
6079
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -5885,36 +6138,64 @@ async function discoverInputFiles(config) {
5885
6138
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
5886
6139
  return [...new Set(matched)].sort();
5887
6140
  }
5888
- async function computeInputsHash(config, extraInputs = []) {
6141
+ var READ_CONCURRENCY = 64;
6142
+ async function readAll(paths, read) {
6143
+ const out = /* @__PURE__ */ new Map();
6144
+ for (let i = 0; i < paths.length; i += READ_CONCURRENCY) {
6145
+ const batch = paths.slice(i, i + READ_CONCURRENCY);
6146
+ const texts = await Promise.all(batch.map(read));
6147
+ batch.forEach((path, j) => out.set(path, texts[j]));
6148
+ }
6149
+ return out;
6150
+ }
6151
+ async function readInputs(config) {
6152
+ const cwd = config.codegen.cwd;
6153
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
6154
+ const extended = loadDiscoveryTsconfig(tsconfigPath).files;
6155
+ const tsconfigFiles = extended.length > 0 ? extended : [tsconfigPath];
6156
+ const files = await discoverInputFiles(config);
6157
+ const contents = /* @__PURE__ */ new Map();
6158
+ const tsconfigs = await readAll(
6159
+ tsconfigFiles,
6160
+ (file) => readFile4(file, "utf8").catch(() => null)
6161
+ );
6162
+ for (const [file, text] of tsconfigs) contents.set(file, text);
6163
+ for (const [file, text] of await readAll(files, (file2) => readFile4(file2, "utf8"))) {
6164
+ contents.set(file, text);
6165
+ }
6166
+ return { tsconfigFiles, files, contents, tracked: /* @__PURE__ */ new Map() };
6167
+ }
6168
+ async function readInput(snapshot, path) {
6169
+ return snapshot.contents.get(path) ?? readFile4(path, "utf8");
6170
+ }
6171
+ async function computeInputsHash(config, extraInputs = [], snapshot) {
6172
+ const inputs = snapshot ?? await readInputs(config);
5889
6173
  const hash = createHash("sha256");
5890
6174
  hash.update(`version:${VERSION}
5891
6175
  `);
5892
6176
  hash.update(`config:${serializeConfig(config)}
5893
6177
  `);
5894
6178
  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);
6179
+ for (const file of inputs.tsconfigFiles) {
5899
6180
  hash.update(`tsconfig:${relative7(cwd, file)}
5900
6181
  `);
5901
- hash.update(contents ?? " missing");
6182
+ hash.update(inputs.contents.get(file) ?? " missing");
5902
6183
  hash.update("\n");
5903
6184
  }
5904
- const globbed = await discoverInputFiles(config);
5905
- const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
6185
+ const globbedRelative = new Set(inputs.files.map((file) => relative7(cwd, file)));
5906
6186
  const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
5907
- for (const file of globbed) {
5908
- const contents = await readFile4(file, "utf8");
6187
+ for (const file of inputs.files) {
5909
6188
  hash.update(`file:${relative7(cwd, file)}
5910
6189
  `);
5911
- hash.update(contents);
6190
+ hash.update(inputs.contents.get(file));
5912
6191
  hash.update("\n");
5913
6192
  }
6193
+ const unread = extra.map((file) => join16(cwd, file)).filter((abs) => !inputs.tracked.has(abs));
6194
+ for (const [abs, text] of await readAll(unread, readTrackedInput)) inputs.tracked.set(abs, text);
5914
6195
  for (const file of extra) {
5915
6196
  hash.update(`extra:${file}
5916
6197
  `);
5917
- hash.update(await readTrackedInput(join16(cwd, file)));
6198
+ hash.update(inputs.tracked.get(join16(cwd, file)));
5918
6199
  hash.update("\n");
5919
6200
  }
5920
6201
  return hash.digest("hex");