@dudousxd/nestjs-codegen 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -514,9 +514,10 @@ function inferExpressionType(node) {
514
514
 
515
515
  // src/emit/emit-api.ts
516
516
  import { mkdir, writeFile } from "fs/promises";
517
- import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
517
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative4 } from "path";
518
518
 
519
519
  // src/extension/registry.ts
520
+ import { relative as relative3, resolve as resolve2 } from "path";
520
521
  import { Project as Project2 } from "ts-morph";
521
522
  function resolveApiSlots(extensions) {
522
523
  let layer;
@@ -549,7 +550,7 @@ function mergeExclusive(target, incoming, {
549
550
  target.set(key, { value, owner });
550
551
  }
551
552
  }
552
- function createExtensionContext(config, getRoutes) {
553
+ function createExtensionContext(config, getRoutes, trackedInputs) {
553
554
  let project;
554
555
  return {
555
556
  cwd: config.codegen.cwd,
@@ -558,6 +559,13 @@ function createExtensionContext(config, getRoutes) {
558
559
  get routes() {
559
560
  return getRoutes();
560
561
  },
562
+ trackInput(...paths) {
563
+ if (!trackedInputs) return;
564
+ for (const path of paths) {
565
+ if (!path) continue;
566
+ trackedInputs.add(relative3(config.codegen.cwd, resolve2(config.codegen.cwd, path)));
567
+ }
568
+ },
561
569
  project() {
562
570
  if (!project) {
563
571
  project = new Project2({
@@ -725,7 +733,7 @@ function rawResponseType(c, outDir) {
725
733
  return c.contractSource.response;
726
734
  }
727
735
  if (c.controllerRef) {
728
- let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
736
+ let relPath = relative4(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
729
737
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
730
738
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
731
739
  }
@@ -1051,6 +1059,11 @@ function buildApiFile(routes, outDir, opts = {}) {
1051
1059
  config: {},
1052
1060
  project: () => {
1053
1061
  throw new Error("ExtensionContext.project() is unavailable in standalone emitApi.");
1062
+ },
1063
+ // No manifest is written on the standalone path, so there is nothing for
1064
+ // tracked dependencies to be recorded into — accept and discard them
1065
+ // rather than making every caller supply a sink.
1066
+ trackInput: () => {
1054
1067
  }
1055
1068
  };
1056
1069
  const importsByFile = /* @__PURE__ */ new Map();
@@ -1107,7 +1120,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1107
1120
  for (const [filePath, names] of importsByFile) {
1108
1121
  let relPath;
1109
1122
  if (isAbsolute2(filePath)) {
1110
- relPath = relative3(outDir, filePath).replace(/\.ts$/, "");
1123
+ relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1111
1124
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1112
1125
  } else {
1113
1126
  relPath = filePath;
@@ -1221,7 +1234,7 @@ async function emitCache(pages, outDir) {
1221
1234
 
1222
1235
  // src/emit/emit-forms.ts
1223
1236
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1224
- import { join as join6, relative as relative4 } from "path";
1237
+ import { join as join6, relative as relative5 } from "path";
1225
1238
  async function emitForms(routes, outDir, config, adapter) {
1226
1239
  if (config && config.enabled === false) return false;
1227
1240
  const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
@@ -1240,7 +1253,7 @@ function deriveBaseName(routeName) {
1240
1253
  return { method, full };
1241
1254
  }
1242
1255
  function relImport(outDir, filePath) {
1243
- let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1256
+ let relPath = relative5(outDir, filePath).replace(/\.ts$/, "");
1244
1257
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1245
1258
  return relPath;
1246
1259
  }
@@ -1988,7 +2001,7 @@ async function emitOpenApi(routes, outDir, opts = {}) {
1988
2001
 
1989
2002
  // src/emit/emit-pages.ts
1990
2003
  import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1991
- import { join as join10, relative as relative5 } from "path";
2004
+ import { join as join10, relative as relative6 } from "path";
1992
2005
  async function emitPages(pages, outDir, _options = {}) {
1993
2006
  await mkdir7(outDir, { recursive: true });
1994
2007
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -2028,7 +2041,7 @@ ${propsBody}
2028
2041
  `;
2029
2042
  }
2030
2043
  function buildAugmentationType(page, outDir) {
2031
- let importPath = relative5(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2044
+ let importPath = relative6(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2032
2045
  if (!importPath.startsWith(".")) {
2033
2046
  importPath = `./${importPath}`;
2034
2047
  }
@@ -2159,7 +2172,7 @@ function buildEmpty() {
2159
2172
  // src/generate-manifest.ts
2160
2173
  import { createHash } from "crypto";
2161
2174
  import { readFile as readFile2, readdir, writeFile as writeFile9 } from "fs/promises";
2162
- import { join as join12, relative as relative6 } from "path";
2175
+ import { join as join12, relative as relative7 } from "path";
2163
2176
  import fg2 from "fast-glob";
2164
2177
  var MANIFEST_FILE = ".codegen-manifest.json";
2165
2178
  var LOCK_FILE = ".watcher.lock";
@@ -2182,6 +2195,9 @@ function isManifestShape(value) {
2182
2195
  if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
2183
2196
  return false;
2184
2197
  }
2198
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
2199
+ return false;
2200
+ }
2185
2201
  if (!Array.isArray(candidate.files)) return false;
2186
2202
  return candidate.files.every((entry) => typeof entry === "string");
2187
2203
  }
@@ -2221,21 +2237,30 @@ async function discoverInputFiles(config) {
2221
2237
  const matched = await fg2(globs, { cwd, absolute: true, onlyFiles: true });
2222
2238
  return [...new Set(matched)].sort();
2223
2239
  }
2224
- async function computeInputsHash(config) {
2240
+ async function computeInputsHash(config, extraInputs = []) {
2225
2241
  const hash = createHash("sha256");
2226
2242
  hash.update(`version:${VERSION}
2227
2243
  `);
2228
2244
  hash.update(`config:${serializeConfig(config)}
2229
2245
  `);
2230
- const inputFiles = await discoverInputFiles(config);
2231
2246
  const cwd = config.codegen.cwd;
2232
- for (const file of inputFiles) {
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) {
2233
2251
  const contents = await readFile2(file, "utf8");
2234
- hash.update(`file:${relative6(cwd, file)}
2252
+ hash.update(`file:${relative7(cwd, file)}
2235
2253
  `);
2236
2254
  hash.update(contents);
2237
2255
  hash.update("\n");
2238
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
+ }
2239
2264
  return hash.digest("hex");
2240
2265
  }
2241
2266
  async function readManifest(outDir) {
@@ -2249,6 +2274,7 @@ async function readManifest(outDir) {
2249
2274
  ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2250
2275
  ...parsed.configHash ? { configHash: parsed.configHash } : {},
2251
2276
  ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
2277
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
2252
2278
  files: parsed.files
2253
2279
  };
2254
2280
  } catch {
@@ -2271,7 +2297,7 @@ async function listOutputFiles(outDir) {
2271
2297
  if (entry.isDirectory()) {
2272
2298
  await walk(abs);
2273
2299
  } else if (entry.isFile()) {
2274
- const rel = relative6(outDir, abs);
2300
+ const rel = relative7(outDir, abs);
2275
2301
  if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
2276
2302
  found.push(rel);
2277
2303
  }
@@ -2308,8 +2334,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
2308
2334
  }
2309
2335
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
2310
2336
  setCodegenDebug(config.debug);
2311
- const inputsHash = await computeInputsHash(config);
2312
2337
  const manifest = await readManifest(config.codegen.outDir);
2338
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2313
2339
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2314
2340
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2315
2341
  return;
@@ -2330,7 +2356,8 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2330
2356
  }
2331
2357
  const extensions = config.extensions ?? [];
2332
2358
  let routes = inputRoutes;
2333
- const ctx = createExtensionContext(config, () => routes);
2359
+ const trackedInputs = /* @__PURE__ */ new Set();
2360
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
2334
2361
  if (extensions.length > 0) {
2335
2362
  routes = await applyTransformRoutes(routes, extensions, ctx);
2336
2363
  }
@@ -2390,13 +2417,16 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2390
2417
  }
2391
2418
  }
2392
2419
  const outputFiles = await listOutputFiles(config.codegen.outDir);
2420
+ const extraInputs = [...trackedInputs].sort();
2421
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2393
2422
  await writeManifest(config.codegen.outDir, {
2394
2423
  version: VERSION,
2395
- hash: inputsHash,
2424
+ hash: recordedHash,
2396
2425
  entryPoint,
2397
2426
  configHash,
2398
2427
  configKeyHashes,
2399
- files: outputFiles
2428
+ files: outputFiles,
2429
+ ...extraInputs.length > 0 ? { extraInputs } : {}
2400
2430
  });
2401
2431
  }
2402
2432
 
@@ -2406,11 +2436,11 @@ import { join as join16 } from "path";
2406
2436
  import chokidar from "chokidar";
2407
2437
 
2408
2438
  // src/discovery/contracts-fast.ts
2409
- import { join as join14, resolve as resolve3 } from "path";
2439
+ import { join as join14, resolve as resolve4 } from "path";
2410
2440
  import fg3 from "fast-glob";
2411
2441
  import {
2412
- Node as Node8,
2413
- Project as Project3
2442
+ Node as Node9,
2443
+ Project as Project4
2414
2444
  } from "ts-morph";
2415
2445
 
2416
2446
  // src/discovery/dto-type-resolver.ts
@@ -2426,7 +2456,7 @@ import {
2426
2456
 
2427
2457
  // src/discovery/type-ref-resolution.ts
2428
2458
  import { readFileSync } from "fs";
2429
- import { dirname as dirname3, resolve as resolve2 } from "path";
2459
+ import { dirname as dirname3, resolve as resolve3 } from "path";
2430
2460
  import {
2431
2461
  Node as Node2
2432
2462
  } from "ts-morph";
@@ -2483,9 +2513,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2483
2513
  const dir = dirname3(sourceFile.getFilePath());
2484
2514
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2485
2515
  return [
2486
- resolve2(dir, `${noExt}.ts`),
2487
- resolve2(dir, `${moduleSpecifier}.ts`),
2488
- resolve2(dir, moduleSpecifier, "index.ts")
2516
+ resolve3(dir, `${noExt}.ts`),
2517
+ resolve3(dir, `${moduleSpecifier}.ts`),
2518
+ resolve3(dir, moduleSpecifier, "index.ts")
2489
2519
  ];
2490
2520
  }
2491
2521
  const ctx = _ctxFor(project);
@@ -2506,8 +2536,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2506
2536
  const rest = moduleSpecifier.slice(prefix.length);
2507
2537
  const candidates = [];
2508
2538
  for (const mapping of mappings) {
2509
- const resolved = resolve2(baseUrl, mapping.replace("*", rest));
2510
- candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
2539
+ const resolved = resolve3(baseUrl, mapping.replace("*", rest));
2540
+ candidates.push(`${resolved}.ts`, resolve3(resolved, "index.ts"));
2511
2541
  }
2512
2542
  dbg(" resolved candidates:", candidates);
2513
2543
  return candidates;
@@ -3402,6 +3432,12 @@ function toFilterFieldType(name, r) {
3402
3432
  }
3403
3433
 
3404
3434
  // src/discovery/filter-for.ts
3435
+ function resolveMixinEntityClass(mixin, project) {
3436
+ const entityArg = mixin?.classArgs[0];
3437
+ if (!entityArg) return void 0;
3438
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3439
+ return file?.getClass(entityArg.name);
3440
+ }
3405
3441
  function classifyFilterForHint(typeInit) {
3406
3442
  if (Node5.isStringLiteral(typeInit)) {
3407
3443
  switch (typeInit.getLiteralValue()) {
@@ -3475,7 +3511,9 @@ function extractFilterForHints(classDecl, project) {
3475
3511
  }
3476
3512
  return hints;
3477
3513
  }
3478
- function extractApplyFilterInfo(method, sourceFile, project) {
3514
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3515
+ const declFile = method.getSourceFile();
3516
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3479
3517
  for (const param of method.getParameters()) {
3480
3518
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3481
3519
  if (!filterDecorator) continue;
@@ -3495,12 +3533,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3495
3533
  }
3496
3534
  }
3497
3535
  const filterClassName = filterClassArg.getText();
3498
- const resolved = findType(filterClassName, sourceFile, project);
3499
- if (resolved && resolved.kind === "class") {
3500
- const classDecl = resolved.decl;
3536
+ const resolved = findType(filterClassName, declFile, project);
3537
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3538
+ if (classDecl) {
3501
3539
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3502
3540
  if (fieldTypes.length === 0) {
3503
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3541
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3504
3542
  }
3505
3543
  const filterForHints = extractFilterForHints(classDecl, project);
3506
3544
  if (filterForHints.size > 0) {
@@ -3585,22 +3623,29 @@ function extractClassPropertyTypes(classDecl, project) {
3585
3623
  }
3586
3624
  return fields;
3587
3625
  }
3588
- function extractFilterableEntityFields(filterClass, project) {
3626
+ function resolveLocalClassDeclaration(identifier) {
3627
+ if (!Node5.isIdentifier(identifier)) return void 0;
3628
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && Node5.isClassDeclaration(n));
3629
+ }
3630
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3631
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return void 0;
3632
+ const entityProp = optionsArg.getProperty("entity");
3633
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return void 0;
3634
+ const entityInit = entityProp.getInitializer();
3635
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return void 0;
3636
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3637
+ if (!resolved || resolved.kind !== "class") return void 0;
3638
+ return resolved.decl;
3639
+ }
3640
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3589
3641
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3590
3642
  if (!filterableDecorator) return [];
3591
3643
  const args = filterableDecorator.getArguments();
3592
3644
  if (args.length === 0) return [];
3593
3645
  const optionsArg = args[0];
3594
3646
  if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3595
- const entityProp = optionsArg.getProperty("entity");
3596
- if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3597
- const entityInit = entityProp.getInitializer();
3598
- if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3599
- const entityName = entityInit.getText();
3600
- const filterSourceFile = filterClass.getSourceFile();
3601
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3602
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3603
- const entityDecl = resolvedEntity.decl;
3647
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3648
+ if (!entityDecl) return [];
3604
3649
  const fields = collectEntityFields(
3605
3650
  entityDecl,
3606
3651
  entityDecl.getSourceFile(),
@@ -4025,9 +4070,9 @@ function unwrapNamedContainer(node, names) {
4025
4070
  }
4026
4071
  return node;
4027
4072
  }
4028
- function extractDtoContract(method, sourceFile, project) {
4073
+ function extractDtoContract(method, sourceFile, project, mixin) {
4029
4074
  let body = extractBodyType(method, sourceFile, project);
4030
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4075
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4031
4076
  const query = extractQueryType(method, sourceFile, project);
4032
4077
  const uploads = extractUploadedFiles(method);
4033
4078
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4137,12 +4182,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4137
4182
  return null;
4138
4183
  }
4139
4184
 
4185
+ // src/discovery/heritage.ts
4186
+ import { Node as Node7, Project as Project3 } from "ts-morph";
4187
+ function unwrapExpression(node) {
4188
+ let current = node;
4189
+ while (Node7.isAsExpression(current) || Node7.isSatisfiesExpression(current) || Node7.isParenthesizedExpression(current) || Node7.isTypeAssertion(current)) {
4190
+ current = current.getExpression();
4191
+ }
4192
+ return current;
4193
+ }
4194
+ function resolveReturnedClass(factoryDecl) {
4195
+ const body = factoryDecl.getBody();
4196
+ if (!body) return void 0;
4197
+ const returns = body.getDescendants().filter((n) => Node7.isReturnStatement(n));
4198
+ for (const ret of returns) {
4199
+ const expr = ret.getExpression();
4200
+ if (!expr) continue;
4201
+ const inner = unwrapExpression(expr);
4202
+ if (Node7.isClassExpression(inner)) {
4203
+ return inner;
4204
+ }
4205
+ if (Node7.isIdentifier(inner)) {
4206
+ const name = inner.getText();
4207
+ const decl = body.getDescendants().find((n) => Node7.isClassDeclaration(n) && n.getName() === name);
4208
+ if (decl) return decl;
4209
+ }
4210
+ }
4211
+ return void 0;
4212
+ }
4213
+ function resolveInheritedMethods(cls) {
4214
+ const expr = cls.getExtends()?.getExpression();
4215
+ if (!expr || !Node7.isCallExpression(expr)) return void 0;
4216
+ const callee = expr.getExpression();
4217
+ if (!Node7.isIdentifier(callee)) return void 0;
4218
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isFunctionDeclaration(n));
4219
+ if (!factoryDecl) return void 0;
4220
+ const returnedClass = resolveReturnedClass(factoryDecl);
4221
+ if (!returnedClass) return void 0;
4222
+ const classArgs = [];
4223
+ for (const arg of expr.getArguments()) {
4224
+ if (!Node7.isIdentifier(arg)) continue;
4225
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isClassDeclaration(n));
4226
+ if (decl) {
4227
+ classArgs.push({
4228
+ name: decl.getName() ?? arg.getText(),
4229
+ filePath: decl.getSourceFile().getFilePath()
4230
+ });
4231
+ }
4232
+ }
4233
+ return {
4234
+ methods: returnedClass.getMethods(),
4235
+ factoryName: callee.getText(),
4236
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4237
+ classArgs
4238
+ };
4239
+ }
4240
+ var mixinTypeProject;
4241
+ function getMixinTypeProject() {
4242
+ mixinTypeProject ??= new Project3({
4243
+ skipAddingFilesFromTsConfig: true,
4244
+ compilerOptions: { strict: true }
4245
+ });
4246
+ return mixinTypeProject;
4247
+ }
4248
+ function clearMixinTypeProject() {
4249
+ mixinTypeProject = void 0;
4250
+ }
4251
+ function resolveInstantiatedReturnType(cls, methodName) {
4252
+ const filePath = cls.getSourceFile().getFilePath();
4253
+ const className = cls.getName();
4254
+ if (!className) return void 0;
4255
+ const project = getMixinTypeProject();
4256
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4257
+ const typedCls = sourceFile?.getClass(className);
4258
+ if (!typedCls) return void 0;
4259
+ const prop = typedCls.getType().getProperty(methodName);
4260
+ if (!prop) return void 0;
4261
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4262
+ if (!returnType) return void 0;
4263
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4264
+ return unwrapped.getText(typedCls);
4265
+ }
4266
+
4140
4267
  // src/discovery/zod-ast-to-ts.ts
4141
- import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
4268
+ import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
4142
4269
  function zodAstToTs(node) {
4143
- if (!Node7.isCallExpression(node)) return "unknown";
4270
+ if (!Node8.isCallExpression(node)) return "unknown";
4144
4271
  const expr = node.getExpression();
4145
- if (Node7.isPropertyAccessExpression(expr)) {
4272
+ if (Node8.isPropertyAccessExpression(expr)) {
4146
4273
  const methodName = expr.getName();
4147
4274
  const receiver = expr.getExpression();
4148
4275
  if (methodName === "optional") {
@@ -4166,17 +4293,17 @@ function zodAstToTs(node) {
4166
4293
  case "literal": {
4167
4294
  const lit = args[0];
4168
4295
  if (!lit) return "unknown";
4169
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4170
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4296
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4297
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4171
4298
  if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
4172
4299
  if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
4173
4300
  return "unknown";
4174
4301
  }
4175
4302
  case "enum": {
4176
4303
  const arrArg = args[0];
4177
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4304
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4178
4305
  const members = arrArg.getElements().map(
4179
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4306
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4180
4307
  );
4181
4308
  return members.join(" | ");
4182
4309
  }
@@ -4187,10 +4314,10 @@ function zodAstToTs(node) {
4187
4314
  }
4188
4315
  case "object": {
4189
4316
  const objArg = args[0];
4190
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
4317
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
4191
4318
  const lines = [];
4192
4319
  for (const prop of objArg.getProperties()) {
4193
- if (!Node7.isPropertyAssignment(prop)) continue;
4320
+ if (!Node8.isPropertyAssignment(prop)) continue;
4194
4321
  const key = prop.getName();
4195
4322
  const valNode = prop.getInitializer();
4196
4323
  if (!valNode) continue;
@@ -4202,7 +4329,7 @@ function zodAstToTs(node) {
4202
4329
  }
4203
4330
  case "union": {
4204
4331
  const arrArg = args[0];
4205
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4332
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4206
4333
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4207
4334
  }
4208
4335
  case "record": {
@@ -4212,7 +4339,7 @@ function zodAstToTs(node) {
4212
4339
  }
4213
4340
  case "tuple": {
4214
4341
  const arrArg = args[0];
4215
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4342
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4216
4343
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4217
4344
  }
4218
4345
  default:
@@ -4222,18 +4349,18 @@ function zodAstToTs(node) {
4222
4349
  return "unknown";
4223
4350
  }
4224
4351
  function isOptionalChain(node) {
4225
- if (!Node7.isCallExpression(node)) return false;
4352
+ if (!Node8.isCallExpression(node)) return false;
4226
4353
  const expr = node.getExpression();
4227
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4354
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4228
4355
  }
4229
4356
  function parseDefineContractCall(callExpr) {
4230
- if (!Node7.isCallExpression(callExpr)) return null;
4357
+ if (!Node8.isCallExpression(callExpr)) return null;
4231
4358
  const callee = callExpr.getExpression();
4232
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
4359
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
4233
4360
  if (calleeName !== "defineContract") return null;
4234
4361
  const args = callExpr.getArguments();
4235
4362
  const optsArg = args[0];
4236
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
4363
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
4237
4364
  let query = null;
4238
4365
  let body = null;
4239
4366
  let response = "unknown";
@@ -4241,7 +4368,7 @@ function parseDefineContractCall(callExpr) {
4241
4368
  let bodyZodText = null;
4242
4369
  let queryZodText = null;
4243
4370
  for (const prop of optsArg.getProperties()) {
4244
- if (!Node7.isPropertyAssignment(prop)) continue;
4371
+ if (!Node8.isPropertyAssignment(prop)) continue;
4245
4372
  const propName = prop.getName();
4246
4373
  const val = prop.getInitializer();
4247
4374
  if (!val) continue;
@@ -4270,21 +4397,22 @@ async function discoverContractsFast(opts) {
4270
4397
  project.addSourceFileAtPath(f);
4271
4398
  }
4272
4399
  bindDiscoveryContext(project, cwd, tsconfigPath);
4400
+ clearMixinTypeProject();
4273
4401
  return extractAllRoutes(project);
4274
4402
  }
4275
4403
  function resolveTsconfigPath(cwd, tsconfig) {
4276
- return tsconfig ? resolve3(tsconfig) : join14(cwd, "tsconfig.json");
4404
+ return tsconfig ? resolve4(tsconfig) : join14(cwd, "tsconfig.json");
4277
4405
  }
4278
4406
  function createDiscoveryProject(tsconfigPath) {
4279
4407
  try {
4280
- return new Project3({
4408
+ return new Project4({
4281
4409
  tsConfigFilePath: tsconfigPath,
4282
4410
  skipAddingFilesFromTsConfig: true,
4283
4411
  skipLoadingLibFiles: true,
4284
4412
  skipFileDependencyResolution: true
4285
4413
  });
4286
4414
  } catch {
4287
- return new Project3({
4415
+ return new Project4({
4288
4416
  skipAddingFilesFromTsConfig: true,
4289
4417
  skipLoadingLibFiles: true,
4290
4418
  skipFileDependencyResolution: true,
@@ -4359,7 +4487,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4359
4487
  async rediscover(changedPaths) {
4360
4488
  if (changedPaths) {
4361
4489
  for (const p of changedPaths) {
4362
- const abs = resolve3(p);
4490
+ const abs = resolve4(p);
4363
4491
  const sf = this.project.getSourceFile(abs);
4364
4492
  if (sf) {
4365
4493
  await sf.refreshFromFileSystem();
@@ -4391,15 +4519,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4391
4519
  runExtraction() {
4392
4520
  clearTypeResolutionCaches(this.project);
4393
4521
  clearEnumCache(this.project);
4522
+ clearMixinTypeProject();
4394
4523
  return extractRoutesFrom(this.project, this.controllerPaths);
4395
4524
  }
4396
4525
  };
4397
4526
  function decoratorStringArg(decoratorExpr) {
4398
4527
  if (!decoratorExpr) return void 0;
4399
- if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4400
- if (Node8.isArrayLiteralExpression(decoratorExpr)) {
4528
+ if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4529
+ if (Node9.isArrayLiteralExpression(decoratorExpr)) {
4401
4530
  const first = decoratorExpr.getElements()[0];
4402
- if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
4531
+ if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
4403
4532
  }
4404
4533
  return void 0;
4405
4534
  }
@@ -4421,7 +4550,8 @@ function joinPaths(prefix, suffix) {
4421
4550
  if (!prefix && !suffix) return "/";
4422
4551
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4423
4552
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4424
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4553
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4554
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4425
4555
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4426
4556
  const combined = p + s;
4427
4557
  return combined === "" ? "/" : combined;
@@ -4475,7 +4605,8 @@ function buildRoute(args) {
4475
4605
  methodAs,
4476
4606
  sourceFile,
4477
4607
  seenNames,
4478
- contractSource
4608
+ contractSource,
4609
+ mixin
4479
4610
  } = args;
4480
4611
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4481
4612
  const qualifiedRef = `${className}.${methodName}`;
@@ -4491,7 +4622,12 @@ function buildRoute(args) {
4491
4622
  path: combinedPath,
4492
4623
  name: routeName,
4493
4624
  params: extractParams(combinedPath),
4494
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4625
+ controllerRef: {
4626
+ className,
4627
+ methodName,
4628
+ filePath: sourceFile.getFilePath(),
4629
+ ...mixin ? { mixin } : {}
4630
+ },
4495
4631
  contract: { contractSource }
4496
4632
  };
4497
4633
  }
@@ -4505,16 +4641,17 @@ function extractContractRoute(args) {
4505
4641
  className,
4506
4642
  sourceFile,
4507
4643
  project,
4508
- seenNames
4644
+ seenNames,
4645
+ mixin
4509
4646
  } = args;
4510
4647
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4511
4648
  if (!firstDecoratorArg) return null;
4512
4649
  let contractDef = null;
4513
4650
  let bodyZodRef = null;
4514
4651
  let queryZodRef = null;
4515
- if (Node8.isCallExpression(firstDecoratorArg)) {
4652
+ if (Node9.isCallExpression(firstDecoratorArg)) {
4516
4653
  contractDef = parseDefineContractCall(firstDecoratorArg);
4517
- } else if (Node8.isIdentifier(firstDecoratorArg)) {
4654
+ } else if (Node9.isIdentifier(firstDecoratorArg)) {
4518
4655
  const identName = firstDecoratorArg.getText();
4519
4656
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4520
4657
  if (!resolvedVar) {
@@ -4557,6 +4694,7 @@ function extractContractRoute(args) {
4557
4694
  methodAs,
4558
4695
  sourceFile,
4559
4696
  seenNames,
4697
+ mixin,
4560
4698
  contractSource: {
4561
4699
  query: contractDef.query,
4562
4700
  body: contractDef.body,
@@ -4573,13 +4711,14 @@ function extractContractRoute(args) {
4573
4711
  });
4574
4712
  }
4575
4713
  function extractDtoRoute(args) {
4576
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4714
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4577
4715
  if (!verb) return null;
4578
4716
  const combined = joinPaths(prefix, verb.handlerPath);
4579
4717
  const methodName = method.getName();
4580
4718
  const classAs = readAsDecorator(cls, `class ${className}`);
4581
4719
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4582
- const dtoContract = extractDtoContract(method, sourceFile, project);
4720
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4721
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4583
4722
  return buildRoute({
4584
4723
  className,
4585
4724
  methodName,
@@ -4589,10 +4728,11 @@ function extractDtoRoute(args) {
4589
4728
  methodAs,
4590
4729
  sourceFile,
4591
4730
  seenNames,
4731
+ mixin,
4592
4732
  contractSource: {
4593
4733
  query: dtoContract?.query ?? null,
4594
4734
  body: dtoContract?.body ?? null,
4595
- response: dtoContract?.response ?? "unknown",
4735
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4596
4736
  error: dtoContract?.error ?? null,
4597
4737
  queryRef: dtoContract?.queryRef ?? null,
4598
4738
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4621,7 +4761,17 @@ function extractFromSourceFile(sourceFile, project) {
4621
4761
  const firstArg2 = controllerDecorator.getArguments()[0];
4622
4762
  const prefix = decoratorStringArg(firstArg2) ?? "";
4623
4763
  const className = cls.getName() ?? "Unknown";
4624
- for (const method of cls.getMethods()) {
4764
+ const inherited = resolveInheritedMethods(cls);
4765
+ const mixinBinding = inherited ? {
4766
+ factoryName: inherited.factoryName,
4767
+ factoryFilePath: inherited.factoryFilePath,
4768
+ classArgs: inherited.classArgs
4769
+ } : void 0;
4770
+ const methods = [
4771
+ ...cls.getMethods().map((method) => ({ method })),
4772
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4773
+ ];
4774
+ for (const { method, mixin } of methods) {
4625
4775
  const verb = resolveVerb(method);
4626
4776
  const applyContractDecorator = method.getDecorator("ApplyContract");
4627
4777
  const route = applyContractDecorator ? extractContractRoute({
@@ -4633,7 +4783,8 @@ function extractFromSourceFile(sourceFile, project) {
4633
4783
  className,
4634
4784
  sourceFile,
4635
4785
  project,
4636
- seenNames
4786
+ seenNames,
4787
+ mixin
4637
4788
  }) : extractDtoRoute({
4638
4789
  cls,
4639
4790
  method,
@@ -4642,7 +4793,8 @@ function extractFromSourceFile(sourceFile, project) {
4642
4793
  className,
4643
4794
  sourceFile,
4644
4795
  project,
4645
- seenNames
4796
+ seenNames,
4797
+ mixin
4646
4798
  });
4647
4799
  if (route) routes.push(route);
4648
4800
  }
@@ -4843,7 +4995,7 @@ async function watch(config, onChange, options = {}) {
4843
4995
  }
4844
4996
 
4845
4997
  // src/index.ts
4846
- var VERSION = "0.15.0";
4998
+ var VERSION = "0.17.0";
4847
4999
 
4848
5000
  // src/cli/codegen.ts
4849
5001
  async function runCodegen(opts = {}) {
@@ -4851,9 +5003,9 @@ async function runCodegen(opts = {}) {
4851
5003
  const config = await loadConfig(cwd);
4852
5004
  if (opts.watch) {
4853
5005
  const watcher = await watch(config, void 0, { entryPoint: "cli" });
4854
- await new Promise((resolve4) => {
5006
+ await new Promise((resolve5) => {
4855
5007
  function onSignal() {
4856
- watcher.close().then(resolve4).catch(resolve4);
5008
+ watcher.close().then(resolve5).catch(resolve5);
4857
5009
  }
4858
5010
  process.once("SIGINT", onSignal);
4859
5011
  process.once("SIGTERM", onSignal);
@@ -4983,16 +5135,16 @@ async function detectPackageManager(cwd) {
4983
5135
  }
4984
5136
  async function promptFramework() {
4985
5137
  if (!process.stdin.isTTY) return "react";
4986
- return new Promise((resolve4) => {
5138
+ return new Promise((resolve5) => {
4987
5139
  const rl = createInterface({ input: process.stdin, output: process.stdout });
4988
5140
  rl.question(
4989
5141
  "[nestjs-codegen] Which frontend framework? (react/vue/svelte) [react]: ",
4990
5142
  (answer) => {
4991
5143
  rl.close();
4992
5144
  const trimmed = answer.trim().toLowerCase();
4993
- if (trimmed === "vue") resolve4("vue");
4994
- else if (trimmed === "svelte") resolve4("svelte");
4995
- else resolve4("react");
5145
+ if (trimmed === "vue") resolve5("vue");
5146
+ else if (trimmed === "svelte") resolve5("svelte");
5147
+ else resolve5("react");
4996
5148
  }
4997
5149
  );
4998
5150
  });