@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/cli/main.js CHANGED
@@ -185,7 +185,7 @@ Run \`nestjs-codegen init\` to create a starter config.`
185
185
 
186
186
  // src/generate.ts
187
187
  import { mkdir as mkdir9, writeFile as writeFile10 } from "fs/promises";
188
- import { dirname as dirname2, join as join13 } from "path";
188
+ import { dirname as dirname4, join as join14 } from "path";
189
189
 
190
190
  // src/discovery/pages.ts
191
191
  import { readFile } from "fs/promises";
@@ -2172,275 +2172,17 @@ function buildEmpty() {
2172
2172
  // src/generate-manifest.ts
2173
2173
  import { createHash } from "crypto";
2174
2174
  import { readFile as readFile2, readdir, writeFile as writeFile9 } from "fs/promises";
2175
- import { join as join12, relative as relative7 } from "path";
2176
- import fg2 from "fast-glob";
2177
- var MANIFEST_FILE = ".codegen-manifest.json";
2178
- var LOCK_FILE = ".watcher.lock";
2179
- var DriftGuardError = class extends Error {
2180
- constructor(message) {
2181
- super(message);
2182
- this.name = "DriftGuardError";
2183
- }
2184
- };
2185
- function isEntryPoint(value) {
2186
- return value === "cli" || value === "module";
2187
- }
2188
- function isManifestShape(value) {
2189
- if (typeof value !== "object" || value === null) return false;
2190
- const candidate = value;
2191
- if (typeof candidate.version !== "string") return false;
2192
- if (typeof candidate.hash !== "string") return false;
2193
- if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
2194
- if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
2195
- if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
2196
- return false;
2197
- }
2198
- if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
2199
- return false;
2200
- }
2201
- if (!Array.isArray(candidate.files)) return false;
2202
- return candidate.files.every((entry) => typeof entry === "string");
2203
- }
2204
- function isStringRecord(value) {
2205
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2206
- return Object.values(value).every((entry) => typeof entry === "string");
2207
- }
2208
- function serializeConfig(config) {
2209
- return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
2210
- }
2211
- function serializeConfigValue(value, unserializableMarker) {
2212
- try {
2213
- return JSON.stringify(value, (_key, entry) => {
2214
- if (typeof entry === "function") return `[fn:${entry.name}]`;
2215
- return entry;
2216
- });
2217
- } catch {
2218
- return unserializableMarker;
2219
- }
2220
- }
2221
- function computeConfigKeyHashes(config) {
2222
- const hashes = {};
2223
- for (const [key, value] of Object.entries(config)) {
2224
- if (value === void 0) continue;
2225
- hashes[key] = createHash("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
2226
- }
2227
- return hashes;
2228
- }
2229
- function diffConfigKeyHashes(previous, current) {
2230
- const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
2231
- return [...keys].filter((key) => previous[key] !== current[key]).sort();
2232
- }
2233
- async function discoverInputFiles(config) {
2234
- const globs = [config.contracts.glob, config.forms.watch];
2235
- if (config.pages) globs.push(config.pages.glob);
2236
- const cwd = config.codegen.cwd;
2237
- const matched = await fg2(globs, { cwd, absolute: true, onlyFiles: true });
2238
- return [...new Set(matched)].sort();
2239
- }
2240
- async function computeInputsHash(config, extraInputs = []) {
2241
- const hash = createHash("sha256");
2242
- hash.update(`version:${VERSION}
2243
- `);
2244
- hash.update(`config:${serializeConfig(config)}
2245
- `);
2246
- const cwd = config.codegen.cwd;
2247
- const globbed = await discoverInputFiles(config);
2248
- const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
2249
- const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
2250
- for (const file of globbed) {
2251
- const contents = await readFile2(file, "utf8");
2252
- hash.update(`file:${relative7(cwd, file)}
2253
- `);
2254
- hash.update(contents);
2255
- hash.update("\n");
2256
- }
2257
- for (const file of extra) {
2258
- const contents = await readFile2(join12(cwd, file), "utf8").catch(() => null);
2259
- hash.update(`extra:${file}
2260
- `);
2261
- hash.update(contents ?? "\0missing");
2262
- hash.update("\n");
2263
- }
2264
- return hash.digest("hex");
2265
- }
2266
- async function readManifest(outDir) {
2267
- try {
2268
- const raw = await readFile2(join12(outDir, MANIFEST_FILE), "utf8");
2269
- const parsed = JSON.parse(raw);
2270
- if (!isManifestShape(parsed)) return null;
2271
- return {
2272
- version: parsed.version,
2273
- hash: parsed.hash,
2274
- ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2275
- ...parsed.configHash ? { configHash: parsed.configHash } : {},
2276
- ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
2277
- ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
2278
- files: parsed.files
2279
- };
2280
- } catch {
2281
- return null;
2282
- }
2283
- }
2284
- function computeConfigHash(config) {
2285
- return createHash("sha256").update(serializeConfig(config)).digest("hex");
2286
- }
2287
- async function writeManifest(outDir, manifest) {
2288
- await writeFile9(join12(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
2289
- `, "utf8");
2290
- }
2291
- async function listOutputFiles(outDir) {
2292
- const found = [];
2293
- async function walk(dir) {
2294
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
2295
- for (const entry of entries) {
2296
- const abs = join12(dir, entry.name);
2297
- if (entry.isDirectory()) {
2298
- await walk(abs);
2299
- } else if (entry.isFile()) {
2300
- const rel = relative7(outDir, abs);
2301
- if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
2302
- found.push(rel);
2303
- }
2304
- }
2305
- }
2306
- await walk(outDir);
2307
- return found.sort();
2308
- }
2309
- async function allOutputsExist(outDir, files) {
2310
- const present = new Set(await listOutputFiles(outDir));
2311
- return files.every((file) => present.has(file));
2312
- }
2313
- async function isManifestFresh(outDir, manifest, inputsHash) {
2314
- if (manifest === null) return false;
2315
- if (manifest.version !== VERSION) return false;
2316
- if (manifest.hash !== inputsHash) return false;
2317
- if (manifest.files.length === 0) return false;
2318
- return allOutputsExist(outDir, manifest.files);
2319
- }
2320
-
2321
- // src/util/debug-log.ts
2322
- var debugEnabled = false;
2323
- function setCodegenDebug(enabled) {
2324
- debugEnabled = enabled;
2325
- }
2326
- function debugWarn(message) {
2327
- if (debugEnabled) console.warn(`[nestjs-codegen] ${message}`);
2328
- }
2329
-
2330
- // src/generate.ts
2331
- function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
2332
- 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)";
2333
- 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.`;
2334
- }
2335
- async function generate(config, inputRoutes = [], entryPoint = "cli") {
2336
- setCodegenDebug(config.debug);
2337
- const manifest = await readManifest(config.codegen.outDir);
2338
- const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2339
- if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2340
- console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2341
- return;
2342
- }
2343
- const configHash = computeConfigHash(config);
2344
- const configKeyHashes = computeConfigKeyHashes(config);
2345
- if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
2346
- throw new DriftGuardError(
2347
- driftGuardMessage(
2348
- config.codegen.outDir,
2349
- manifest.entryPoint,
2350
- entryPoint,
2351
- // A pre-key-hash manifest can't tell us WHICH keys differ — pass none
2352
- // rather than diffing against {} (which would name every key).
2353
- manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
2354
- )
2355
- );
2356
- }
2357
- const extensions = config.extensions ?? [];
2358
- let routes = inputRoutes;
2359
- const trackedInputs = /* @__PURE__ */ new Set();
2360
- const ctx = createExtensionContext(config, () => routes, trackedInputs);
2361
- if (extensions.length > 0) {
2362
- routes = await applyTransformRoutes(routes, extensions, ctx);
2363
- }
2364
- if (config.pages) {
2365
- const pagesConfig = config.pages;
2366
- const pages = await discoverPages({
2367
- glob: pagesConfig.glob,
2368
- cwd: config.codegen.cwd,
2369
- propsExport: pagesConfig.propsExport,
2370
- componentNameStrategy: pagesConfig.componentNameStrategy
2371
- });
2372
- const sharedProps = discoverSharedPropsFromConfig(config);
2373
- await emitPages(pages, config.codegen.outDir, {
2374
- propsExport: pagesConfig.propsExport,
2375
- sharedProps
2376
- });
2377
- await emitCache(pages, config.codegen.outDir);
2378
- }
2379
- const hasRoutes = routes.length > 0;
2380
- const hasContracts = routes.some((r) => r.contract);
2381
- if (hasRoutes) {
2382
- await emitRoutes(routes, config.codegen.outDir);
2383
- }
2384
- if (hasContracts) {
2385
- await emitApi(routes, config.codegen.outDir, {
2386
- ...config.fetcher?.importPath ? { fetcherImportPath: config.fetcher.importPath } : {},
2387
- serialization: config.serialization,
2388
- extensions,
2389
- ctx
2390
- });
2391
- }
2392
- const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2393
- if (hasContracts && config.openapi.enabled) {
2394
- await emitOpenApi(routes, config.codegen.outDir, {
2395
- fileName: config.openapi.fileName,
2396
- info: {
2397
- title: config.openapi.title,
2398
- version: config.openapi.version,
2399
- ...config.openapi.description ? { description: config.openapi.description } : {}
2400
- }
2401
- });
2402
- }
2403
- if (hasContracts && config.mocks.enabled) {
2404
- await emitMocks(routes, config.codegen.outDir, {
2405
- fileName: config.mocks.fileName,
2406
- seed: config.mocks.seed,
2407
- baseUrl: config.mocks.baseUrl
2408
- });
2409
- }
2410
- await emitIndex(config.codegen.outDir, hasContracts, hasForms);
2411
- if (extensions.length > 0) {
2412
- const extraFiles = await collectEmittedFiles(extensions, ctx);
2413
- for (const file of extraFiles) {
2414
- const dest = join13(config.codegen.outDir, file.path);
2415
- await mkdir9(dirname2(dest), { recursive: true });
2416
- await writeFile10(dest, file.contents, "utf8");
2417
- }
2418
- }
2419
- const outputFiles = await listOutputFiles(config.codegen.outDir);
2420
- const extraInputs = [...trackedInputs].sort();
2421
- const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2422
- await writeManifest(config.codegen.outDir, {
2423
- version: VERSION,
2424
- hash: recordedHash,
2425
- entryPoint,
2426
- configHash,
2427
- configKeyHashes,
2428
- files: outputFiles,
2429
- ...extraInputs.length > 0 ? { extraInputs } : {}
2430
- });
2431
- }
2432
-
2433
- // src/watch/watcher.ts
2434
- import { readFile as readFile4 } from "fs/promises";
2435
- import { join as join16 } from "path";
2436
- import chokidar from "chokidar";
2175
+ import { join as join13, relative as relative7 } from "path";
2176
+ import fg3 from "fast-glob";
2437
2177
 
2438
2178
  // src/discovery/contracts-fast.ts
2439
- import { join as join14, resolve as resolve4 } from "path";
2440
- import fg3 from "fast-glob";
2179
+ import { existsSync } from "fs";
2180
+ import { dirname as dirname3, join as join12, resolve as resolve4 } from "path";
2181
+ import fg2 from "fast-glob";
2441
2182
  import {
2442
2183
  Node as Node9,
2443
- Project as Project4
2184
+ Project as Project4,
2185
+ ts
2444
2186
  } from "ts-morph";
2445
2187
 
2446
2188
  // src/discovery/dto-type-resolver.ts
@@ -2454,9 +2196,17 @@ import {
2454
2196
  Node as Node3
2455
2197
  } from "ts-morph";
2456
2198
 
2199
+ // src/util/debug-log.ts
2200
+ var debugEnabled = false;
2201
+ function setCodegenDebug(enabled) {
2202
+ debugEnabled = enabled;
2203
+ }
2204
+ function debugWarn(message) {
2205
+ if (debugEnabled) console.warn(`[nestjs-codegen] ${message}`);
2206
+ }
2207
+
2457
2208
  // src/discovery/type-ref-resolution.ts
2458
- import { readFileSync } from "fs";
2459
- import { dirname as dirname3, resolve as resolve3 } from "path";
2209
+ import { dirname as dirname2, resolve as resolve3 } from "path";
2460
2210
  import {
2461
2211
  Node as Node2
2462
2212
  } from "ts-morph";
@@ -2472,16 +2222,6 @@ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
2472
2222
  function dbg(...args) {
2473
2223
  if (_debug) console.log("[codegen:debug]", ...args);
2474
2224
  }
2475
- function loadTsconfigPaths(tsconfigPath) {
2476
- try {
2477
- const raw = readFileSync(tsconfigPath, "utf8");
2478
- const stripped = raw.replace(/\/\/.*$/gm, "");
2479
- const parsed = JSON.parse(stripped);
2480
- return parsed.compilerOptions?.paths ?? null;
2481
- } catch {
2482
- return null;
2483
- }
2484
- }
2485
2225
  function findTypeInFile(name, file) {
2486
2226
  const cls = file.getClass(name);
2487
2227
  if (cls) return { kind: "class", decl: cls, file };
@@ -2510,7 +2250,7 @@ function findTypeInFile(name, file) {
2510
2250
  }
2511
2251
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2512
2252
  if (moduleSpecifier.startsWith(".")) {
2513
- const dir = dirname3(sourceFile.getFilePath());
2253
+ const dir = dirname2(sourceFile.getFilePath());
2514
2254
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2515
2255
  return [
2516
2256
  resolve3(dir, `${noExt}.ts`),
@@ -2519,7 +2259,7 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2519
2259
  ];
2520
2260
  }
2521
2261
  const ctx = _ctxFor(project);
2522
- const baseUrl = ctx.projectRoot;
2262
+ const baseUrl = ctx.pathsBase ?? ctx.projectRoot;
2523
2263
  const tsconfigPaths = ctx.tsconfigPaths;
2524
2264
  dbg(
2525
2265
  "resolveModuleSpecifier",
@@ -3717,7 +3457,7 @@ function extractFilterForHints(classDecl, project) {
3717
3457
  }
3718
3458
  return hints;
3719
3459
  }
3720
- function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3460
+ function extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass) {
3721
3461
  const declFile = method.getSourceFile();
3722
3462
  const mixinEntity = resolveMixinEntityClass(mixin, project);
3723
3463
  for (const param of method.getParameters()) {
@@ -3743,7 +3483,7 @@ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3743
3483
  const filterClassName = filterClassArg.getText();
3744
3484
  const resolved = factoryStaticClass ? void 0 : findType(filterClassName, declFile, project);
3745
3485
  const declaredClass = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
3746
- const ownRoute = !mixin || declFile.getFilePath() !== mixin.factoryFilePath;
3486
+ const ownRoute = !mixin || (controllerClass ? method.getParent() === controllerClass : declFile.getFilePath() !== mixin.factoryFilePath);
3747
3487
  const namedByRoute = ownRoute && Node6.isIdentifier(filterClassArg) ? declaredClass : void 0;
3748
3488
  for (const candidate of orderFilterCandidates({
3749
3489
  namedByRoute,
@@ -4334,9 +4074,9 @@ function unwrapNamedContainer(node, names) {
4334
4074
  }
4335
4075
  return node;
4336
4076
  }
4337
- function extractDtoContract(method, sourceFile, project, mixin) {
4077
+ function extractDtoContract(method, sourceFile, project, mixin, controllerClass) {
4338
4078
  let body = extractBodyType(method, sourceFile, project);
4339
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4079
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass);
4340
4080
  const query = extractQueryType(method, sourceFile, project);
4341
4081
  const uploads = extractUploadedFiles(method);
4342
4082
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4573,43 +4313,80 @@ function parseDefineContractCall(callExpr) {
4573
4313
  async function discoverContractsFast(opts) {
4574
4314
  const { cwd, glob, tsconfig } = opts;
4575
4315
  const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
4576
- const project = createDiscoveryProject(tsconfigPath);
4577
- const files = await fg3(glob, { cwd, absolute: true, onlyFiles: true });
4316
+ const loaded = loadDiscoveryTsconfig(tsconfigPath);
4317
+ const project = createDiscoveryProject(tsconfigPath, loaded);
4318
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
4578
4319
  for (const f of files) {
4579
4320
  project.addSourceFileAtPath(f);
4580
4321
  }
4581
- bindDiscoveryContext(project, cwd, tsconfigPath);
4322
+ bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
4582
4323
  clearMixinTypeProject();
4583
4324
  return extractAllRoutes(project);
4584
4325
  }
4585
4326
  function resolveTsconfigPath(cwd, tsconfig) {
4586
- return tsconfig ? resolve4(tsconfig) : join14(cwd, "tsconfig.json");
4327
+ return tsconfig ? resolve4(tsconfig) : join12(cwd, "tsconfig.json");
4587
4328
  }
4588
- function createDiscoveryProject(tsconfigPath) {
4589
- try {
4590
- return new Project4({
4591
- tsConfigFilePath: tsconfigPath,
4592
- skipAddingFilesFromTsConfig: true,
4593
- skipLoadingLibFiles: true,
4594
- skipFileDependencyResolution: true
4595
- });
4596
- } catch {
4597
- return new Project4({
4598
- skipAddingFilesFromTsConfig: true,
4599
- skipLoadingLibFiles: true,
4600
- skipFileDependencyResolution: true,
4601
- compilerOptions: {
4602
- allowJs: true,
4603
- resolveJsonModule: false,
4604
- strict: false
4605
- }
4606
- });
4329
+ var NO_TSCONFIG_OPTIONS = {
4330
+ allowJs: true,
4331
+ resolveJsonModule: false,
4332
+ strict: false
4333
+ };
4334
+ function loadDiscoveryTsconfig(tsconfigPath) {
4335
+ if (!existsSync(tsconfigPath)) return { options: null, files: [] };
4336
+ const read = ts.readConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
4337
+ if (read.error) {
4338
+ return {
4339
+ options: null,
4340
+ files: [tsconfigPath],
4341
+ error: ts.flattenDiagnosticMessageText(read.error.messageText, " ")
4342
+ };
4343
+ }
4344
+ const host = {
4345
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
4346
+ // The one line that keeps a tsconfig from being a filesystem walk.
4347
+ readDirectory: () => [],
4348
+ fileExists: (path) => ts.sys.fileExists(path),
4349
+ // `extends` is resolved through the host, so a consumer whose `paths` live
4350
+ // in a base tsconfig keeps working.
4351
+ readFile: (path) => ts.sys.readFile(path)
4352
+ };
4353
+ const sourceFile = ts.readJsonConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
4354
+ const parsed = ts.parseJsonSourceFileConfigFileContent(
4355
+ sourceFile,
4356
+ host,
4357
+ dirname3(tsconfigPath),
4358
+ void 0,
4359
+ tsconfigPath
4360
+ );
4361
+ return {
4362
+ options: parsed.options,
4363
+ files: [tsconfigPath, ...sourceFile.extendedSourceFiles ?? []]
4364
+ };
4365
+ }
4366
+ function pathsBaseDir(options, fallback) {
4367
+ if (typeof options.baseUrl === "string") return options.baseUrl;
4368
+ if (typeof options.pathsBasePath === "string") return options.pathsBasePath;
4369
+ return fallback;
4370
+ }
4371
+ function createDiscoveryProject(tsconfigPath, tsconfig = loadDiscoveryTsconfig(tsconfigPath)) {
4372
+ if (tsconfig.error) {
4373
+ console.warn(
4374
+ `[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.`
4375
+ );
4607
4376
  }
4377
+ return new Project4({
4378
+ compilerOptions: tsconfig.options ?? { ...NO_TSCONFIG_OPTIONS },
4379
+ skipAddingFilesFromTsConfig: true,
4380
+ skipLoadingLibFiles: true,
4381
+ skipFileDependencyResolution: true
4382
+ });
4608
4383
  }
4609
- function bindDiscoveryContext(project, cwd, tsconfigPath) {
4384
+ function bindDiscoveryContext(project, cwd, tsconfigPath, tsconfig = loadDiscoveryTsconfig(tsconfigPath)) {
4385
+ const { options } = tsconfig;
4610
4386
  setDiscoveryContext(project, {
4611
4387
  projectRoot: cwd,
4612
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
4388
+ tsconfigPaths: options?.paths ?? null,
4389
+ pathsBase: options ? pathsBaseDir(options, cwd) : cwd
4613
4390
  });
4614
4391
  }
4615
4392
  function extractRoutesFrom(project, controllerPaths) {
@@ -4645,10 +4422,11 @@ var PersistentDiscovery = class _PersistentDiscovery {
4645
4422
  static async create(opts) {
4646
4423
  const { cwd, glob, tsconfig } = opts;
4647
4424
  const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
4648
- const project = createDiscoveryProject(tsconfigPath);
4649
- bindDiscoveryContext(project, cwd, tsconfigPath);
4425
+ const loaded = loadDiscoveryTsconfig(tsconfigPath);
4426
+ const project = createDiscoveryProject(tsconfigPath, loaded);
4427
+ bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
4650
4428
  const instance = new _PersistentDiscovery(project, cwd, glob);
4651
- const files = await fg3(glob, { cwd, absolute: true, onlyFiles: true });
4429
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
4652
4430
  for (const f of files) {
4653
4431
  project.addSourceFileAtPath(f);
4654
4432
  instance.controllerPaths.add(f);
@@ -4677,7 +4455,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4677
4455
  }
4678
4456
  }
4679
4457
  const globbed = new Set(
4680
- await fg3(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
4458
+ await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
4681
4459
  );
4682
4460
  for (const f of globbed) {
4683
4461
  if (!this.controllerPaths.has(f)) {
@@ -4899,7 +4677,7 @@ function extractDtoRoute(args) {
4899
4677
  const methodName = method.getName();
4900
4678
  const classAs = readAsDecorator(cls, `class ${className}`);
4901
4679
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4902
- const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4680
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin, cls);
4903
4681
  const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4904
4682
  return buildRoute({
4905
4683
  className,
@@ -4987,6 +4765,268 @@ function extractFromSourceFile(sourceFile, project) {
4987
4765
  return routes;
4988
4766
  }
4989
4767
 
4768
+ // src/generate-manifest.ts
4769
+ var MANIFEST_FILE = ".codegen-manifest.json";
4770
+ var LOCK_FILE = ".watcher.lock";
4771
+ var DriftGuardError = class extends Error {
4772
+ constructor(message) {
4773
+ super(message);
4774
+ this.name = "DriftGuardError";
4775
+ }
4776
+ };
4777
+ function isEntryPoint(value) {
4778
+ return value === "cli" || value === "module";
4779
+ }
4780
+ function isManifestShape(value) {
4781
+ if (typeof value !== "object" || value === null) return false;
4782
+ const candidate = value;
4783
+ if (typeof candidate.version !== "string") return false;
4784
+ if (typeof candidate.hash !== "string") return false;
4785
+ if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
4786
+ if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
4787
+ if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
4788
+ return false;
4789
+ }
4790
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
4791
+ return false;
4792
+ }
4793
+ if (!Array.isArray(candidate.files)) return false;
4794
+ return candidate.files.every((entry) => typeof entry === "string");
4795
+ }
4796
+ function isStringRecord(value) {
4797
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4798
+ return Object.values(value).every((entry) => typeof entry === "string");
4799
+ }
4800
+ function serializeConfig(config) {
4801
+ return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
4802
+ }
4803
+ function serializeConfigValue(value, unserializableMarker) {
4804
+ try {
4805
+ return JSON.stringify(value, (_key, entry) => {
4806
+ if (typeof entry === "function") return `[fn:${entry.name}]`;
4807
+ return entry;
4808
+ });
4809
+ } catch {
4810
+ return unserializableMarker;
4811
+ }
4812
+ }
4813
+ function computeConfigKeyHashes(config) {
4814
+ const hashes = {};
4815
+ for (const [key, value] of Object.entries(config)) {
4816
+ if (value === void 0) continue;
4817
+ hashes[key] = createHash("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
4818
+ }
4819
+ return hashes;
4820
+ }
4821
+ function diffConfigKeyHashes(previous, current) {
4822
+ const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
4823
+ return [...keys].filter((key) => previous[key] !== current[key]).sort();
4824
+ }
4825
+ async function discoverInputFiles(config) {
4826
+ const globs = [config.contracts.glob, config.forms.watch];
4827
+ if (config.pages) globs.push(config.pages.glob);
4828
+ const cwd = config.codegen.cwd;
4829
+ const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4830
+ return [...new Set(matched)].sort();
4831
+ }
4832
+ async function computeInputsHash(config, extraInputs = []) {
4833
+ const hash = createHash("sha256");
4834
+ hash.update(`version:${VERSION}
4835
+ `);
4836
+ hash.update(`config:${serializeConfig(config)}
4837
+ `);
4838
+ const cwd = config.codegen.cwd;
4839
+ const tsconfigPath = resolveTsconfigPath(cwd, config.app?.tsconfig ?? void 0);
4840
+ const tsconfigFiles = loadDiscoveryTsconfig(tsconfigPath).files;
4841
+ for (const file of tsconfigFiles.length > 0 ? tsconfigFiles : [tsconfigPath]) {
4842
+ const contents = await readFile2(file, "utf8").catch(() => null);
4843
+ hash.update(`tsconfig:${relative7(cwd, file)}
4844
+ `);
4845
+ hash.update(contents ?? " missing");
4846
+ hash.update("\n");
4847
+ }
4848
+ const globbed = await discoverInputFiles(config);
4849
+ const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
4850
+ const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
4851
+ for (const file of globbed) {
4852
+ const contents = await readFile2(file, "utf8");
4853
+ hash.update(`file:${relative7(cwd, file)}
4854
+ `);
4855
+ hash.update(contents);
4856
+ hash.update("\n");
4857
+ }
4858
+ for (const file of extra) {
4859
+ const contents = await readFile2(join13(cwd, file), "utf8").catch(() => null);
4860
+ hash.update(`extra:${file}
4861
+ `);
4862
+ hash.update(contents ?? "\0missing");
4863
+ hash.update("\n");
4864
+ }
4865
+ return hash.digest("hex");
4866
+ }
4867
+ async function readManifest(outDir) {
4868
+ try {
4869
+ const raw = await readFile2(join13(outDir, MANIFEST_FILE), "utf8");
4870
+ const parsed = JSON.parse(raw);
4871
+ if (!isManifestShape(parsed)) return null;
4872
+ return {
4873
+ version: parsed.version,
4874
+ hash: parsed.hash,
4875
+ ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
4876
+ ...parsed.configHash ? { configHash: parsed.configHash } : {},
4877
+ ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
4878
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
4879
+ files: parsed.files
4880
+ };
4881
+ } catch {
4882
+ return null;
4883
+ }
4884
+ }
4885
+ function computeConfigHash(config) {
4886
+ return createHash("sha256").update(serializeConfig(config)).digest("hex");
4887
+ }
4888
+ async function writeManifest(outDir, manifest) {
4889
+ await writeFile9(join13(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4890
+ `, "utf8");
4891
+ }
4892
+ async function listOutputFiles(outDir) {
4893
+ const found = [];
4894
+ async function walk(dir) {
4895
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
4896
+ for (const entry of entries) {
4897
+ const abs = join13(dir, entry.name);
4898
+ if (entry.isDirectory()) {
4899
+ await walk(abs);
4900
+ } else if (entry.isFile()) {
4901
+ const rel = relative7(outDir, abs);
4902
+ if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
4903
+ found.push(rel);
4904
+ }
4905
+ }
4906
+ }
4907
+ await walk(outDir);
4908
+ return found.sort();
4909
+ }
4910
+ async function allOutputsExist(outDir, files) {
4911
+ const present = new Set(await listOutputFiles(outDir));
4912
+ return files.every((file) => present.has(file));
4913
+ }
4914
+ async function isManifestFresh(outDir, manifest, inputsHash) {
4915
+ if (manifest === null) return false;
4916
+ if (manifest.version !== VERSION) return false;
4917
+ if (manifest.hash !== inputsHash) return false;
4918
+ if (manifest.files.length === 0) return false;
4919
+ return allOutputsExist(outDir, manifest.files);
4920
+ }
4921
+
4922
+ // src/generate.ts
4923
+ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
4924
+ 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)";
4925
+ 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.`;
4926
+ }
4927
+ async function generate(config, inputRoutes = [], entryPoint = "cli") {
4928
+ setCodegenDebug(config.debug);
4929
+ const manifest = await readManifest(config.codegen.outDir);
4930
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
4931
+ if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
4932
+ console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
4933
+ return;
4934
+ }
4935
+ const configHash = computeConfigHash(config);
4936
+ const configKeyHashes = computeConfigKeyHashes(config);
4937
+ if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
4938
+ throw new DriftGuardError(
4939
+ driftGuardMessage(
4940
+ config.codegen.outDir,
4941
+ manifest.entryPoint,
4942
+ entryPoint,
4943
+ // A pre-key-hash manifest can't tell us WHICH keys differ — pass none
4944
+ // rather than diffing against {} (which would name every key).
4945
+ manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
4946
+ )
4947
+ );
4948
+ }
4949
+ const extensions = config.extensions ?? [];
4950
+ let routes = inputRoutes;
4951
+ const trackedInputs = /* @__PURE__ */ new Set();
4952
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
4953
+ if (extensions.length > 0) {
4954
+ routes = await applyTransformRoutes(routes, extensions, ctx);
4955
+ }
4956
+ if (config.pages) {
4957
+ const pagesConfig = config.pages;
4958
+ const pages = await discoverPages({
4959
+ glob: pagesConfig.glob,
4960
+ cwd: config.codegen.cwd,
4961
+ propsExport: pagesConfig.propsExport,
4962
+ componentNameStrategy: pagesConfig.componentNameStrategy
4963
+ });
4964
+ const sharedProps = discoverSharedPropsFromConfig(config);
4965
+ await emitPages(pages, config.codegen.outDir, {
4966
+ propsExport: pagesConfig.propsExport,
4967
+ sharedProps
4968
+ });
4969
+ await emitCache(pages, config.codegen.outDir);
4970
+ }
4971
+ const hasRoutes = routes.length > 0;
4972
+ const hasContracts = routes.some((r) => r.contract);
4973
+ if (hasRoutes) {
4974
+ await emitRoutes(routes, config.codegen.outDir);
4975
+ }
4976
+ if (hasContracts) {
4977
+ await emitApi(routes, config.codegen.outDir, {
4978
+ ...config.fetcher?.importPath ? { fetcherImportPath: config.fetcher.importPath } : {},
4979
+ serialization: config.serialization,
4980
+ extensions,
4981
+ ctx
4982
+ });
4983
+ }
4984
+ const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
4985
+ if (hasContracts && config.openapi.enabled) {
4986
+ await emitOpenApi(routes, config.codegen.outDir, {
4987
+ fileName: config.openapi.fileName,
4988
+ info: {
4989
+ title: config.openapi.title,
4990
+ version: config.openapi.version,
4991
+ ...config.openapi.description ? { description: config.openapi.description } : {}
4992
+ }
4993
+ });
4994
+ }
4995
+ if (hasContracts && config.mocks.enabled) {
4996
+ await emitMocks(routes, config.codegen.outDir, {
4997
+ fileName: config.mocks.fileName,
4998
+ seed: config.mocks.seed,
4999
+ baseUrl: config.mocks.baseUrl
5000
+ });
5001
+ }
5002
+ await emitIndex(config.codegen.outDir, hasContracts, hasForms);
5003
+ if (extensions.length > 0) {
5004
+ const extraFiles = await collectEmittedFiles(extensions, ctx);
5005
+ for (const file of extraFiles) {
5006
+ const dest = join14(config.codegen.outDir, file.path);
5007
+ await mkdir9(dirname4(dest), { recursive: true });
5008
+ await writeFile10(dest, file.contents, "utf8");
5009
+ }
5010
+ }
5011
+ const outputFiles = await listOutputFiles(config.codegen.outDir);
5012
+ const extraInputs = [...trackedInputs].sort();
5013
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
5014
+ await writeManifest(config.codegen.outDir, {
5015
+ version: VERSION,
5016
+ hash: recordedHash,
5017
+ entryPoint,
5018
+ configHash,
5019
+ configKeyHashes,
5020
+ files: outputFiles,
5021
+ ...extraInputs.length > 0 ? { extraInputs } : {}
5022
+ });
5023
+ }
5024
+
5025
+ // src/watch/watcher.ts
5026
+ import { readFile as readFile4 } from "fs/promises";
5027
+ import { join as join16 } from "path";
5028
+ import chokidar from "chokidar";
5029
+
4990
5030
  // src/watch/lock-file.ts
4991
5031
  import { open } from "fs/promises";
4992
5032
  import { mkdir as mkdir10, readFile as readFile3, unlink } from "fs/promises";
@@ -5180,7 +5220,7 @@ async function watch(config, onChange, options = {}) {
5180
5220
  }
5181
5221
 
5182
5222
  // src/index.ts
5183
- var VERSION = "0.20.0";
5223
+ var VERSION = "0.21.1";
5184
5224
 
5185
5225
  // src/cli/codegen.ts
5186
5226
  async function runCodegen(opts = {}) {
@@ -5208,22 +5248,22 @@ async function runCodegen(opts = {}) {
5208
5248
 
5209
5249
  // src/cli/doctor.ts
5210
5250
  import { execFileSync as execFileSync2 } from "child_process";
5211
- import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5251
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
5212
5252
  import { join as join18 } from "path";
5213
5253
 
5214
5254
  // src/cli/init.ts
5215
5255
  import { execFileSync } from "child_process";
5216
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5256
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5217
5257
  import { access as access2, mkdir as mkdir11, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
5218
5258
  import { join as join17 } from "path";
5219
5259
  import { createInterface } from "readline";
5220
5260
 
5221
5261
  // src/cli/patch-utils.ts
5222
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
5262
+ import { readFileSync, writeFileSync } from "fs";
5223
5263
  function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
5224
5264
  let raw;
5225
5265
  try {
5226
- raw = readFileSync2(filePath, "utf8");
5266
+ raw = readFileSync(filePath, "utf8");
5227
5267
  } catch {
5228
5268
  return "skipped";
5229
5269
  }
@@ -5441,7 +5481,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
5441
5481
  function patchAppModule(filePath, rootView) {
5442
5482
  let content;
5443
5483
  try {
5444
- content = readFileSync3(filePath, "utf8");
5484
+ content = readFileSync2(filePath, "utf8");
5445
5485
  } catch {
5446
5486
  return "skipped";
5447
5487
  }
@@ -5482,7 +5522,7 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
5482
5522
  function patchMainTs(filePath) {
5483
5523
  let content;
5484
5524
  try {
5485
- content = readFileSync3(filePath, "utf8");
5525
+ content = readFileSync2(filePath, "utf8");
5486
5526
  } catch {
5487
5527
  return "skipped";
5488
5528
  }
@@ -5962,18 +6002,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
5962
6002
 
5963
6003
  // src/cli/doctor.ts
5964
6004
  function checkFileExists(cwd, file) {
5965
- return existsSync(join18(cwd, file));
6005
+ return existsSync2(join18(cwd, file));
5966
6006
  }
5967
6007
  function readJson(path) {
5968
6008
  try {
5969
- const raw = readFileSync4(path, "utf8").replace(/\/\/.*$/gm, "");
6009
+ const raw = readFileSync3(path, "utf8").replace(/\/\/.*$/gm, "");
5970
6010
  return JSON.parse(raw);
5971
6011
  } catch {
5972
6012
  return null;
5973
6013
  }
5974
6014
  }
5975
6015
  function writeJsonField(filePath, dotPath, value) {
5976
- const raw = readFileSync4(filePath, "utf8");
6016
+ const raw = readFileSync3(filePath, "utf8");
5977
6017
  const stripped = raw.replace(/\/\/.*$/gm, "");
5978
6018
  const obj = JSON.parse(stripped);
5979
6019
  let target = obj;
@@ -6005,8 +6045,8 @@ function getPackageVersion(cwd, pkg) {
6005
6045
  }
6006
6046
  }
6007
6047
  function detectPkgManager(cwd) {
6008
- if (existsSync(join18(cwd, "pnpm-lock.yaml"))) return "pnpm";
6009
- if (existsSync(join18(cwd, "yarn.lock"))) return "yarn";
6048
+ if (existsSync2(join18(cwd, "pnpm-lock.yaml"))) return "pnpm";
6049
+ if (existsSync2(join18(cwd, "yarn.lock"))) return "yarn";
6010
6050
  return "npm";
6011
6051
  }
6012
6052
  async function runDoctor(opts) {
@@ -6175,14 +6215,14 @@ async function runDoctor(opts) {
6175
6215
  const innerTsconfigPath = join18(cwd, "inertia", "tsconfig.json");
6176
6216
  checks.push({
6177
6217
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
6178
- pass: existsSync(innerTsconfigPath),
6218
+ pass: existsSync2(innerTsconfigPath),
6179
6219
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
6180
6220
  autoFix: () => {
6181
6221
  writeFileSync3(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
6182
6222
  }
6183
6223
  });
6184
6224
  if (checkFileExists(cwd, "vite.config.ts")) {
6185
- const viteContent = readFileSync4(join18(cwd, "vite.config.ts"), "utf8");
6225
+ const viteContent = readFileSync3(join18(cwd, "vite.config.ts"), "utf8");
6186
6226
  checks.push({
6187
6227
  name: "vite.config.ts has resolve.alias",
6188
6228
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -6248,7 +6288,7 @@ async function runDoctor(opts) {
6248
6288
  }
6249
6289
  if (checkFileExists(cwd, ".gitignore")) {
6250
6290
  const gitignorePath = join18(cwd, ".gitignore");
6251
- const gitignore = readFileSync4(gitignorePath, "utf8");
6291
+ const gitignore = readFileSync3(gitignorePath, "utf8");
6252
6292
  checks.push({
6253
6293
  name: ".gitignore includes .nestjs-inertia/",
6254
6294
  pass: gitignore.includes(".nestjs-inertia"),