@dudousxd/nestjs-codegen 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -190,7 +190,7 @@ Run \`nestjs-codegen init\` to create a starter config.`
190
190
 
191
191
  // src/generate.ts
192
192
  import { mkdir as mkdir9, writeFile as writeFile10 } from "fs/promises";
193
- import { dirname as dirname2, join as join13 } from "path";
193
+ import { dirname as dirname4, join as join14 } from "path";
194
194
 
195
195
  // src/discovery/pages.ts
196
196
  import { readFile } from "fs/promises";
@@ -2180,275 +2180,17 @@ function buildEmpty() {
2180
2180
  // src/generate-manifest.ts
2181
2181
  import { createHash } from "crypto";
2182
2182
  import { readFile as readFile2, readdir, writeFile as writeFile9 } from "fs/promises";
2183
- import { join as join12, relative as relative7 } from "path";
2184
- import fg2 from "fast-glob";
2185
- var MANIFEST_FILE = ".codegen-manifest.json";
2186
- var LOCK_FILE = ".watcher.lock";
2187
- var DriftGuardError = class extends Error {
2188
- constructor(message) {
2189
- super(message);
2190
- this.name = "DriftGuardError";
2191
- }
2192
- };
2193
- function isEntryPoint(value) {
2194
- return value === "cli" || value === "module";
2195
- }
2196
- function isManifestShape(value) {
2197
- if (typeof value !== "object" || value === null) return false;
2198
- const candidate = value;
2199
- if (typeof candidate.version !== "string") return false;
2200
- if (typeof candidate.hash !== "string") return false;
2201
- if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
2202
- if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
2203
- if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
2204
- return false;
2205
- }
2206
- if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
2207
- return false;
2208
- }
2209
- if (!Array.isArray(candidate.files)) return false;
2210
- return candidate.files.every((entry) => typeof entry === "string");
2211
- }
2212
- function isStringRecord(value) {
2213
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2214
- return Object.values(value).every((entry) => typeof entry === "string");
2215
- }
2216
- function serializeConfig(config) {
2217
- return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
2218
- }
2219
- function serializeConfigValue(value, unserializableMarker) {
2220
- try {
2221
- return JSON.stringify(value, (_key, entry) => {
2222
- if (typeof entry === "function") return `[fn:${entry.name}]`;
2223
- return entry;
2224
- });
2225
- } catch {
2226
- return unserializableMarker;
2227
- }
2228
- }
2229
- function computeConfigKeyHashes(config) {
2230
- const hashes = {};
2231
- for (const [key, value] of Object.entries(config)) {
2232
- if (value === void 0) continue;
2233
- hashes[key] = createHash("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
2234
- }
2235
- return hashes;
2236
- }
2237
- function diffConfigKeyHashes(previous, current) {
2238
- const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
2239
- return [...keys].filter((key) => previous[key] !== current[key]).sort();
2240
- }
2241
- async function discoverInputFiles(config) {
2242
- const globs = [config.contracts.glob, config.forms.watch];
2243
- if (config.pages) globs.push(config.pages.glob);
2244
- const cwd = config.codegen.cwd;
2245
- const matched = await fg2(globs, { cwd, absolute: true, onlyFiles: true });
2246
- return [...new Set(matched)].sort();
2247
- }
2248
- async function computeInputsHash(config, extraInputs = []) {
2249
- const hash = createHash("sha256");
2250
- hash.update(`version:${VERSION}
2251
- `);
2252
- hash.update(`config:${serializeConfig(config)}
2253
- `);
2254
- const cwd = config.codegen.cwd;
2255
- const globbed = await discoverInputFiles(config);
2256
- const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
2257
- const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
2258
- for (const file of globbed) {
2259
- const contents = await readFile2(file, "utf8");
2260
- hash.update(`file:${relative7(cwd, file)}
2261
- `);
2262
- hash.update(contents);
2263
- hash.update("\n");
2264
- }
2265
- for (const file of extra) {
2266
- const contents = await readFile2(join12(cwd, file), "utf8").catch(() => null);
2267
- hash.update(`extra:${file}
2268
- `);
2269
- hash.update(contents ?? "\0missing");
2270
- hash.update("\n");
2271
- }
2272
- return hash.digest("hex");
2273
- }
2274
- async function readManifest(outDir) {
2275
- try {
2276
- const raw = await readFile2(join12(outDir, MANIFEST_FILE), "utf8");
2277
- const parsed = JSON.parse(raw);
2278
- if (!isManifestShape(parsed)) return null;
2279
- return {
2280
- version: parsed.version,
2281
- hash: parsed.hash,
2282
- ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2283
- ...parsed.configHash ? { configHash: parsed.configHash } : {},
2284
- ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
2285
- ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
2286
- files: parsed.files
2287
- };
2288
- } catch {
2289
- return null;
2290
- }
2291
- }
2292
- function computeConfigHash(config) {
2293
- return createHash("sha256").update(serializeConfig(config)).digest("hex");
2294
- }
2295
- async function writeManifest(outDir, manifest) {
2296
- await writeFile9(join12(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
2297
- `, "utf8");
2298
- }
2299
- async function listOutputFiles(outDir) {
2300
- const found = [];
2301
- async function walk(dir) {
2302
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
2303
- for (const entry of entries) {
2304
- const abs = join12(dir, entry.name);
2305
- if (entry.isDirectory()) {
2306
- await walk(abs);
2307
- } else if (entry.isFile()) {
2308
- const rel = relative7(outDir, abs);
2309
- if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
2310
- found.push(rel);
2311
- }
2312
- }
2313
- }
2314
- await walk(outDir);
2315
- return found.sort();
2316
- }
2317
- async function allOutputsExist(outDir, files) {
2318
- const present = new Set(await listOutputFiles(outDir));
2319
- return files.every((file) => present.has(file));
2320
- }
2321
- async function isManifestFresh(outDir, manifest, inputsHash) {
2322
- if (manifest === null) return false;
2323
- if (manifest.version !== VERSION) return false;
2324
- if (manifest.hash !== inputsHash) return false;
2325
- if (manifest.files.length === 0) return false;
2326
- return allOutputsExist(outDir, manifest.files);
2327
- }
2328
-
2329
- // src/util/debug-log.ts
2330
- var debugEnabled = false;
2331
- function setCodegenDebug(enabled) {
2332
- debugEnabled = enabled;
2333
- }
2334
- function debugWarn(message) {
2335
- if (debugEnabled) console.warn(`[nestjs-codegen] ${message}`);
2336
- }
2337
-
2338
- // src/generate.ts
2339
- function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
2340
- const differ = differingKeys.length > 0 ? `their resolved configs differ at: ${differingKeys.map((key) => `\`${key}\``).join(", ")}` : "their resolved configs differ (re-run after this generate records per-key hashes to see which keys)";
2341
- return `[nestjs-codegen] Config drift detected in "${outDir}": the last generate ran from the "${previousEntryPoint}" entry point, this run is from the "${currentEntryPoint}" entry point, and ${differ}. Both entry points must read the SAME config \u2014 export a shared config object (e.g. codegen.config.ts) and import it from BOTH nestjs-codegen.config.ts (CLI) and NestjsCodegenModule.forRoot() (Nest module), or set \`driftGuard: false\` on either config to opt out of this check.`;
2342
- }
2343
- async function generate(config, inputRoutes = [], entryPoint = "cli") {
2344
- setCodegenDebug(config.debug);
2345
- const manifest = await readManifest(config.codegen.outDir);
2346
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2347
- if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2348
- console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2349
- return;
2350
- }
2351
- const configHash = computeConfigHash(config);
2352
- const configKeyHashes = computeConfigKeyHashes(config);
2353
- if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
2354
- throw new DriftGuardError(
2355
- driftGuardMessage(
2356
- config.codegen.outDir,
2357
- manifest.entryPoint,
2358
- entryPoint,
2359
- // A pre-key-hash manifest can't tell us WHICH keys differ — pass none
2360
- // rather than diffing against {} (which would name every key).
2361
- manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
2362
- )
2363
- );
2364
- }
2365
- const extensions = config.extensions ?? [];
2366
- let routes = inputRoutes;
2367
- const trackedInputs = /* @__PURE__ */ new Set();
2368
- const ctx = createExtensionContext(config, () => routes, trackedInputs);
2369
- if (extensions.length > 0) {
2370
- routes = await applyTransformRoutes(routes, extensions, ctx);
2371
- }
2372
- if (config.pages) {
2373
- const pagesConfig = config.pages;
2374
- const pages = await discoverPages({
2375
- glob: pagesConfig.glob,
2376
- cwd: config.codegen.cwd,
2377
- propsExport: pagesConfig.propsExport,
2378
- componentNameStrategy: pagesConfig.componentNameStrategy
2379
- });
2380
- const sharedProps = discoverSharedPropsFromConfig(config);
2381
- await emitPages(pages, config.codegen.outDir, {
2382
- propsExport: pagesConfig.propsExport,
2383
- sharedProps
2384
- });
2385
- await emitCache(pages, config.codegen.outDir);
2386
- }
2387
- const hasRoutes = routes.length > 0;
2388
- const hasContracts = routes.some((r) => r.contract);
2389
- if (hasRoutes) {
2390
- await emitRoutes(routes, config.codegen.outDir);
2391
- }
2392
- if (hasContracts) {
2393
- await emitApi(routes, config.codegen.outDir, {
2394
- ...config.fetcher?.importPath ? { fetcherImportPath: config.fetcher.importPath } : {},
2395
- serialization: config.serialization,
2396
- extensions,
2397
- ctx
2398
- });
2399
- }
2400
- const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2401
- if (hasContracts && config.openapi.enabled) {
2402
- await emitOpenApi(routes, config.codegen.outDir, {
2403
- fileName: config.openapi.fileName,
2404
- info: {
2405
- title: config.openapi.title,
2406
- version: config.openapi.version,
2407
- ...config.openapi.description ? { description: config.openapi.description } : {}
2408
- }
2409
- });
2410
- }
2411
- if (hasContracts && config.mocks.enabled) {
2412
- await emitMocks(routes, config.codegen.outDir, {
2413
- fileName: config.mocks.fileName,
2414
- seed: config.mocks.seed,
2415
- baseUrl: config.mocks.baseUrl
2416
- });
2417
- }
2418
- await emitIndex(config.codegen.outDir, hasContracts, hasForms);
2419
- if (extensions.length > 0) {
2420
- const extraFiles = await collectEmittedFiles(extensions, ctx);
2421
- for (const file of extraFiles) {
2422
- const dest = join13(config.codegen.outDir, file.path);
2423
- await mkdir9(dirname2(dest), { recursive: true });
2424
- await writeFile10(dest, file.contents, "utf8");
2425
- }
2426
- }
2427
- const outputFiles = await listOutputFiles(config.codegen.outDir);
2428
- const extraInputs = [...trackedInputs].sort();
2429
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2430
- await writeManifest(config.codegen.outDir, {
2431
- version: VERSION,
2432
- hash: recordedHash,
2433
- entryPoint,
2434
- configHash,
2435
- configKeyHashes,
2436
- files: outputFiles,
2437
- ...extraInputs.length > 0 ? { extraInputs } : {}
2438
- });
2439
- }
2440
-
2441
- // src/watch/watcher.ts
2442
- import { readFile as readFile4 } from "fs/promises";
2443
- import { join as join16 } from "path";
2444
- import chokidar from "chokidar";
2183
+ import { join as join13, relative as relative7 } from "path";
2184
+ import fg3 from "fast-glob";
2445
2185
 
2446
2186
  // src/discovery/contracts-fast.ts
2447
- import { join as join14, resolve as resolve4 } from "path";
2448
- import fg3 from "fast-glob";
2187
+ import { existsSync } from "fs";
2188
+ import { dirname as dirname3, join as join12, resolve as resolve4 } from "path";
2189
+ import fg2 from "fast-glob";
2449
2190
  import {
2450
2191
  Node as Node9,
2451
- Project as Project4
2192
+ Project as Project4,
2193
+ ts
2452
2194
  } from "ts-morph";
2453
2195
 
2454
2196
  // src/discovery/dto-type-resolver.ts
@@ -2462,9 +2204,17 @@ import {
2462
2204
  Node as Node3
2463
2205
  } from "ts-morph";
2464
2206
 
2207
+ // src/util/debug-log.ts
2208
+ var debugEnabled = false;
2209
+ function setCodegenDebug(enabled) {
2210
+ debugEnabled = enabled;
2211
+ }
2212
+ function debugWarn(message) {
2213
+ if (debugEnabled) console.warn(`[nestjs-codegen] ${message}`);
2214
+ }
2215
+
2465
2216
  // src/discovery/type-ref-resolution.ts
2466
- import { readFileSync } from "fs";
2467
- import { dirname as dirname3, resolve as resolve3 } from "path";
2217
+ import { dirname as dirname2, resolve as resolve3 } from "path";
2468
2218
  import {
2469
2219
  Node as Node2
2470
2220
  } from "ts-morph";
@@ -2480,16 +2230,6 @@ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
2480
2230
  function dbg(...args) {
2481
2231
  if (_debug) console.log("[codegen:debug]", ...args);
2482
2232
  }
2483
- function loadTsconfigPaths(tsconfigPath) {
2484
- try {
2485
- const raw = readFileSync(tsconfigPath, "utf8");
2486
- const stripped = raw.replace(/\/\/.*$/gm, "");
2487
- const parsed = JSON.parse(stripped);
2488
- return parsed.compilerOptions?.paths ?? null;
2489
- } catch {
2490
- return null;
2491
- }
2492
- }
2493
2233
  function findTypeInFile(name, file) {
2494
2234
  const cls = file.getClass(name);
2495
2235
  if (cls) return { kind: "class", decl: cls, file };
@@ -2518,7 +2258,7 @@ function findTypeInFile(name, file) {
2518
2258
  }
2519
2259
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2520
2260
  if (moduleSpecifier.startsWith(".")) {
2521
- const dir = dirname3(sourceFile.getFilePath());
2261
+ const dir = dirname2(sourceFile.getFilePath());
2522
2262
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2523
2263
  return [
2524
2264
  resolve3(dir, `${noExt}.ts`),
@@ -2527,7 +2267,7 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2527
2267
  ];
2528
2268
  }
2529
2269
  const ctx = _ctxFor(project);
2530
- const baseUrl = ctx.projectRoot;
2270
+ const baseUrl = ctx.pathsBase ?? ctx.projectRoot;
2531
2271
  const tsconfigPaths = ctx.tsconfigPaths;
2532
2272
  dbg(
2533
2273
  "resolveModuleSpecifier",
@@ -3725,7 +3465,7 @@ function extractFilterForHints(classDecl, project) {
3725
3465
  }
3726
3466
  return hints;
3727
3467
  }
3728
- function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3468
+ function extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass) {
3729
3469
  const declFile = method.getSourceFile();
3730
3470
  const mixinEntity = resolveMixinEntityClass(mixin, project);
3731
3471
  for (const param of method.getParameters()) {
@@ -3751,7 +3491,7 @@ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3751
3491
  const filterClassName = filterClassArg.getText();
3752
3492
  const resolved = factoryStaticClass ? void 0 : findType(filterClassName, declFile, project);
3753
3493
  const declaredClass = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
3754
- const ownRoute = !mixin || declFile.getFilePath() !== mixin.factoryFilePath;
3494
+ const ownRoute = !mixin || (controllerClass ? method.getParent() === controllerClass : declFile.getFilePath() !== mixin.factoryFilePath);
3755
3495
  const namedByRoute = ownRoute && Node6.isIdentifier(filterClassArg) ? declaredClass : void 0;
3756
3496
  for (const candidate of orderFilterCandidates({
3757
3497
  namedByRoute,
@@ -4342,9 +4082,9 @@ function unwrapNamedContainer(node, names) {
4342
4082
  }
4343
4083
  return node;
4344
4084
  }
4345
- function extractDtoContract(method, sourceFile, project, mixin) {
4085
+ function extractDtoContract(method, sourceFile, project, mixin, controllerClass) {
4346
4086
  let body = extractBodyType(method, sourceFile, project);
4347
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4087
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass);
4348
4088
  const query = extractQueryType(method, sourceFile, project);
4349
4089
  const uploads = extractUploadedFiles(method);
4350
4090
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4581,43 +4321,80 @@ function parseDefineContractCall(callExpr) {
4581
4321
  async function discoverContractsFast(opts) {
4582
4322
  const { cwd, glob, tsconfig } = opts;
4583
4323
  const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
4584
- const project = createDiscoveryProject(tsconfigPath);
4585
- const files = await fg3(glob, { cwd, absolute: true, onlyFiles: true });
4324
+ const loaded = loadDiscoveryTsconfig(tsconfigPath);
4325
+ const project = createDiscoveryProject(tsconfigPath, loaded);
4326
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
4586
4327
  for (const f of files) {
4587
4328
  project.addSourceFileAtPath(f);
4588
4329
  }
4589
- bindDiscoveryContext(project, cwd, tsconfigPath);
4330
+ bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
4590
4331
  clearMixinTypeProject();
4591
4332
  return extractAllRoutes(project);
4592
4333
  }
4593
4334
  function resolveTsconfigPath(cwd, tsconfig) {
4594
- return tsconfig ? resolve4(tsconfig) : join14(cwd, "tsconfig.json");
4335
+ return tsconfig ? resolve4(tsconfig) : join12(cwd, "tsconfig.json");
4595
4336
  }
4596
- function createDiscoveryProject(tsconfigPath) {
4597
- try {
4598
- return new Project4({
4599
- tsConfigFilePath: tsconfigPath,
4600
- skipAddingFilesFromTsConfig: true,
4601
- skipLoadingLibFiles: true,
4602
- skipFileDependencyResolution: true
4603
- });
4604
- } catch {
4605
- return new Project4({
4606
- skipAddingFilesFromTsConfig: true,
4607
- skipLoadingLibFiles: true,
4608
- skipFileDependencyResolution: true,
4609
- compilerOptions: {
4610
- allowJs: true,
4611
- resolveJsonModule: false,
4612
- strict: false
4613
- }
4614
- });
4337
+ var NO_TSCONFIG_OPTIONS = {
4338
+ allowJs: true,
4339
+ resolveJsonModule: false,
4340
+ strict: false
4341
+ };
4342
+ function loadDiscoveryTsconfig(tsconfigPath) {
4343
+ if (!existsSync(tsconfigPath)) return { options: null, files: [] };
4344
+ const read = ts.readConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
4345
+ if (read.error) {
4346
+ return {
4347
+ options: null,
4348
+ files: [tsconfigPath],
4349
+ error: ts.flattenDiagnosticMessageText(read.error.messageText, " ")
4350
+ };
4351
+ }
4352
+ const host = {
4353
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
4354
+ // The one line that keeps a tsconfig from being a filesystem walk.
4355
+ readDirectory: () => [],
4356
+ fileExists: (path) => ts.sys.fileExists(path),
4357
+ // `extends` is resolved through the host, so a consumer whose `paths` live
4358
+ // in a base tsconfig keeps working.
4359
+ readFile: (path) => ts.sys.readFile(path)
4360
+ };
4361
+ const sourceFile = ts.readJsonConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
4362
+ const parsed = ts.parseJsonSourceFileConfigFileContent(
4363
+ sourceFile,
4364
+ host,
4365
+ dirname3(tsconfigPath),
4366
+ void 0,
4367
+ tsconfigPath
4368
+ );
4369
+ return {
4370
+ options: parsed.options,
4371
+ files: [tsconfigPath, ...sourceFile.extendedSourceFiles ?? []]
4372
+ };
4373
+ }
4374
+ function pathsBaseDir(options, fallback) {
4375
+ if (typeof options.baseUrl === "string") return options.baseUrl;
4376
+ if (typeof options.pathsBasePath === "string") return options.pathsBasePath;
4377
+ return fallback;
4378
+ }
4379
+ function createDiscoveryProject(tsconfigPath, tsconfig = loadDiscoveryTsconfig(tsconfigPath)) {
4380
+ if (tsconfig.error) {
4381
+ console.warn(
4382
+ `[nestjs-codegen/fast] Could not load ${tsconfigPath}: ${tsconfig.error} \u2014 continuing WITHOUT its compiler options, so path alias imports (e.g. '@/...') will not resolve and controllers extending an alias-imported factory will contribute NO routes to the generated client.`
4383
+ );
4615
4384
  }
4385
+ return new Project4({
4386
+ compilerOptions: tsconfig.options ?? { ...NO_TSCONFIG_OPTIONS },
4387
+ skipAddingFilesFromTsConfig: true,
4388
+ skipLoadingLibFiles: true,
4389
+ skipFileDependencyResolution: true
4390
+ });
4616
4391
  }
4617
- function bindDiscoveryContext(project, cwd, tsconfigPath) {
4392
+ function bindDiscoveryContext(project, cwd, tsconfigPath, tsconfig = loadDiscoveryTsconfig(tsconfigPath)) {
4393
+ const { options } = tsconfig;
4618
4394
  setDiscoveryContext(project, {
4619
4395
  projectRoot: cwd,
4620
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
4396
+ tsconfigPaths: options?.paths ?? null,
4397
+ pathsBase: options ? pathsBaseDir(options, cwd) : cwd
4621
4398
  });
4622
4399
  }
4623
4400
  function extractRoutesFrom(project, controllerPaths) {
@@ -4653,10 +4430,11 @@ var PersistentDiscovery = class _PersistentDiscovery {
4653
4430
  static async create(opts) {
4654
4431
  const { cwd, glob, tsconfig } = opts;
4655
4432
  const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
4656
- const project = createDiscoveryProject(tsconfigPath);
4657
- bindDiscoveryContext(project, cwd, tsconfigPath);
4433
+ const loaded = loadDiscoveryTsconfig(tsconfigPath);
4434
+ const project = createDiscoveryProject(tsconfigPath, loaded);
4435
+ bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
4658
4436
  const instance = new _PersistentDiscovery(project, cwd, glob);
4659
- const files = await fg3(glob, { cwd, absolute: true, onlyFiles: true });
4437
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
4660
4438
  for (const f of files) {
4661
4439
  project.addSourceFileAtPath(f);
4662
4440
  instance.controllerPaths.add(f);
@@ -4685,7 +4463,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4685
4463
  }
4686
4464
  }
4687
4465
  const globbed = new Set(
4688
- await fg3(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
4466
+ await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
4689
4467
  );
4690
4468
  for (const f of globbed) {
4691
4469
  if (!this.controllerPaths.has(f)) {
@@ -4907,7 +4685,7 @@ function extractDtoRoute(args) {
4907
4685
  const methodName = method.getName();
4908
4686
  const classAs = readAsDecorator(cls, `class ${className}`);
4909
4687
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4910
- const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4688
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin, cls);
4911
4689
  const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4912
4690
  return buildRoute({
4913
4691
  className,
@@ -4995,6 +4773,268 @@ function extractFromSourceFile(sourceFile, project) {
4995
4773
  return routes;
4996
4774
  }
4997
4775
 
4776
+ // src/generate-manifest.ts
4777
+ var MANIFEST_FILE = ".codegen-manifest.json";
4778
+ var LOCK_FILE = ".watcher.lock";
4779
+ var DriftGuardError = class extends Error {
4780
+ constructor(message) {
4781
+ super(message);
4782
+ this.name = "DriftGuardError";
4783
+ }
4784
+ };
4785
+ function isEntryPoint(value) {
4786
+ return value === "cli" || value === "module";
4787
+ }
4788
+ function isManifestShape(value) {
4789
+ if (typeof value !== "object" || value === null) return false;
4790
+ const candidate = value;
4791
+ if (typeof candidate.version !== "string") return false;
4792
+ if (typeof candidate.hash !== "string") return false;
4793
+ if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
4794
+ if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
4795
+ if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
4796
+ return false;
4797
+ }
4798
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
4799
+ return false;
4800
+ }
4801
+ if (!Array.isArray(candidate.files)) return false;
4802
+ return candidate.files.every((entry) => typeof entry === "string");
4803
+ }
4804
+ function isStringRecord(value) {
4805
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4806
+ return Object.values(value).every((entry) => typeof entry === "string");
4807
+ }
4808
+ function serializeConfig(config) {
4809
+ return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
4810
+ }
4811
+ function serializeConfigValue(value, unserializableMarker) {
4812
+ try {
4813
+ return JSON.stringify(value, (_key, entry) => {
4814
+ if (typeof entry === "function") return `[fn:${entry.name}]`;
4815
+ return entry;
4816
+ });
4817
+ } catch {
4818
+ return unserializableMarker;
4819
+ }
4820
+ }
4821
+ function computeConfigKeyHashes(config) {
4822
+ const hashes = {};
4823
+ for (const [key, value] of Object.entries(config)) {
4824
+ if (value === void 0) continue;
4825
+ hashes[key] = createHash("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
4826
+ }
4827
+ return hashes;
4828
+ }
4829
+ function diffConfigKeyHashes(previous, current) {
4830
+ const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
4831
+ return [...keys].filter((key) => previous[key] !== current[key]).sort();
4832
+ }
4833
+ async function discoverInputFiles(config) {
4834
+ const globs = [config.contracts.glob, config.forms.watch];
4835
+ if (config.pages) globs.push(config.pages.glob);
4836
+ const cwd = config.codegen.cwd;
4837
+ const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4838
+ return [...new Set(matched)].sort();
4839
+ }
4840
+ async function computeInputsHash(config, extraInputs = []) {
4841
+ const hash = createHash("sha256");
4842
+ hash.update(`version:${VERSION}
4843
+ `);
4844
+ hash.update(`config:${serializeConfig(config)}
4845
+ `);
4846
+ const cwd = config.codegen.cwd;
4847
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
4848
+ const tsconfigFiles = loadDiscoveryTsconfig(tsconfigPath).files;
4849
+ for (const file of tsconfigFiles.length > 0 ? tsconfigFiles : [tsconfigPath]) {
4850
+ const contents = await readFile2(file, "utf8").catch(() => null);
4851
+ hash.update(`tsconfig:${relative7(cwd, file)}
4852
+ `);
4853
+ hash.update(contents ?? " missing");
4854
+ hash.update("\n");
4855
+ }
4856
+ const globbed = await discoverInputFiles(config);
4857
+ const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
4858
+ const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
4859
+ for (const file of globbed) {
4860
+ const contents = await readFile2(file, "utf8");
4861
+ hash.update(`file:${relative7(cwd, file)}
4862
+ `);
4863
+ hash.update(contents);
4864
+ hash.update("\n");
4865
+ }
4866
+ for (const file of extra) {
4867
+ const contents = await readFile2(join13(cwd, file), "utf8").catch(() => null);
4868
+ hash.update(`extra:${file}
4869
+ `);
4870
+ hash.update(contents ?? "\0missing");
4871
+ hash.update("\n");
4872
+ }
4873
+ return hash.digest("hex");
4874
+ }
4875
+ async function readManifest(outDir) {
4876
+ try {
4877
+ const raw = await readFile2(join13(outDir, MANIFEST_FILE), "utf8");
4878
+ const parsed = JSON.parse(raw);
4879
+ if (!isManifestShape(parsed)) return null;
4880
+ return {
4881
+ version: parsed.version,
4882
+ hash: parsed.hash,
4883
+ ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
4884
+ ...parsed.configHash ? { configHash: parsed.configHash } : {},
4885
+ ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
4886
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
4887
+ files: parsed.files
4888
+ };
4889
+ } catch {
4890
+ return null;
4891
+ }
4892
+ }
4893
+ function computeConfigHash(config) {
4894
+ return createHash("sha256").update(serializeConfig(config)).digest("hex");
4895
+ }
4896
+ async function writeManifest(outDir, manifest) {
4897
+ await writeFile9(join13(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4898
+ `, "utf8");
4899
+ }
4900
+ async function listOutputFiles(outDir) {
4901
+ const found = [];
4902
+ async function walk(dir) {
4903
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
4904
+ for (const entry of entries) {
4905
+ const abs = join13(dir, entry.name);
4906
+ if (entry.isDirectory()) {
4907
+ await walk(abs);
4908
+ } else if (entry.isFile()) {
4909
+ const rel = relative7(outDir, abs);
4910
+ if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
4911
+ found.push(rel);
4912
+ }
4913
+ }
4914
+ }
4915
+ await walk(outDir);
4916
+ return found.sort();
4917
+ }
4918
+ async function allOutputsExist(outDir, files) {
4919
+ const present = new Set(await listOutputFiles(outDir));
4920
+ return files.every((file) => present.has(file));
4921
+ }
4922
+ async function isManifestFresh(outDir, manifest, inputsHash) {
4923
+ if (manifest === null) return false;
4924
+ if (manifest.version !== VERSION) return false;
4925
+ if (manifest.hash !== inputsHash) return false;
4926
+ if (manifest.files.length === 0) return false;
4927
+ return allOutputsExist(outDir, manifest.files);
4928
+ }
4929
+
4930
+ // src/generate.ts
4931
+ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
4932
+ const differ = differingKeys.length > 0 ? `their resolved configs differ at: ${differingKeys.map((key) => `\`${key}\``).join(", ")}` : "their resolved configs differ (re-run after this generate records per-key hashes to see which keys)";
4933
+ return `[nestjs-codegen] Config drift detected in "${outDir}": the last generate ran from the "${previousEntryPoint}" entry point, this run is from the "${currentEntryPoint}" entry point, and ${differ}. Both entry points must read the SAME config \u2014 export a shared config object (e.g. codegen.config.ts) and import it from BOTH nestjs-codegen.config.ts (CLI) and NestjsCodegenModule.forRoot() (Nest module), or set \`driftGuard: false\` on either config to opt out of this check.`;
4934
+ }
4935
+ async function generate(config, inputRoutes = [], entryPoint = "cli") {
4936
+ setCodegenDebug(config.debug);
4937
+ const manifest = await readManifest(config.codegen.outDir);
4938
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
4939
+ if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
4940
+ console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
4941
+ return;
4942
+ }
4943
+ const configHash = computeConfigHash(config);
4944
+ const configKeyHashes = computeConfigKeyHashes(config);
4945
+ if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
4946
+ throw new DriftGuardError(
4947
+ driftGuardMessage(
4948
+ config.codegen.outDir,
4949
+ manifest.entryPoint,
4950
+ entryPoint,
4951
+ // A pre-key-hash manifest can't tell us WHICH keys differ — pass none
4952
+ // rather than diffing against {} (which would name every key).
4953
+ manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
4954
+ )
4955
+ );
4956
+ }
4957
+ const extensions = config.extensions ?? [];
4958
+ let routes = inputRoutes;
4959
+ const trackedInputs = /* @__PURE__ */ new Set();
4960
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
4961
+ if (extensions.length > 0) {
4962
+ routes = await applyTransformRoutes(routes, extensions, ctx);
4963
+ }
4964
+ if (config.pages) {
4965
+ const pagesConfig = config.pages;
4966
+ const pages = await discoverPages({
4967
+ glob: pagesConfig.glob,
4968
+ cwd: config.codegen.cwd,
4969
+ propsExport: pagesConfig.propsExport,
4970
+ componentNameStrategy: pagesConfig.componentNameStrategy
4971
+ });
4972
+ const sharedProps = discoverSharedPropsFromConfig(config);
4973
+ await emitPages(pages, config.codegen.outDir, {
4974
+ propsExport: pagesConfig.propsExport,
4975
+ sharedProps
4976
+ });
4977
+ await emitCache(pages, config.codegen.outDir);
4978
+ }
4979
+ const hasRoutes = routes.length > 0;
4980
+ const hasContracts = routes.some((r) => r.contract);
4981
+ if (hasRoutes) {
4982
+ await emitRoutes(routes, config.codegen.outDir);
4983
+ }
4984
+ if (hasContracts) {
4985
+ await emitApi(routes, config.codegen.outDir, {
4986
+ ...config.fetcher?.importPath ? { fetcherImportPath: config.fetcher.importPath } : {},
4987
+ serialization: config.serialization,
4988
+ extensions,
4989
+ ctx
4990
+ });
4991
+ }
4992
+ const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
4993
+ if (hasContracts && config.openapi.enabled) {
4994
+ await emitOpenApi(routes, config.codegen.outDir, {
4995
+ fileName: config.openapi.fileName,
4996
+ info: {
4997
+ title: config.openapi.title,
4998
+ version: config.openapi.version,
4999
+ ...config.openapi.description ? { description: config.openapi.description } : {}
5000
+ }
5001
+ });
5002
+ }
5003
+ if (hasContracts && config.mocks.enabled) {
5004
+ await emitMocks(routes, config.codegen.outDir, {
5005
+ fileName: config.mocks.fileName,
5006
+ seed: config.mocks.seed,
5007
+ baseUrl: config.mocks.baseUrl
5008
+ });
5009
+ }
5010
+ await emitIndex(config.codegen.outDir, hasContracts, hasForms);
5011
+ if (extensions.length > 0) {
5012
+ const extraFiles = await collectEmittedFiles(extensions, ctx);
5013
+ for (const file of extraFiles) {
5014
+ const dest = join14(config.codegen.outDir, file.path);
5015
+ await mkdir9(dirname4(dest), { recursive: true });
5016
+ await writeFile10(dest, file.contents, "utf8");
5017
+ }
5018
+ }
5019
+ const outputFiles = await listOutputFiles(config.codegen.outDir);
5020
+ const extraInputs = [...trackedInputs].sort();
5021
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
5022
+ await writeManifest(config.codegen.outDir, {
5023
+ version: VERSION,
5024
+ hash: recordedHash,
5025
+ entryPoint,
5026
+ configHash,
5027
+ configKeyHashes,
5028
+ files: outputFiles,
5029
+ ...extraInputs.length > 0 ? { extraInputs } : {}
5030
+ });
5031
+ }
5032
+
5033
+ // src/watch/watcher.ts
5034
+ import { readFile as readFile4 } from "fs/promises";
5035
+ import { join as join16 } from "path";
5036
+ import chokidar from "chokidar";
5037
+
4998
5038
  // src/watch/lock-file.ts
4999
5039
  import { open } from "fs/promises";
5000
5040
  import { mkdir as mkdir10, readFile as readFile3, unlink } from "fs/promises";
@@ -5271,7 +5311,7 @@ function createChainModuleRenderer(opts) {
5271
5311
  }
5272
5312
 
5273
5313
  // src/index.ts
5274
- var VERSION = "0.20.0";
5314
+ var VERSION = "0.21.1";
5275
5315
  export {
5276
5316
  CodegenError,
5277
5317
  ConfigError,