@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/index.js CHANGED
@@ -519,9 +519,10 @@ function inferExpressionType(node) {
519
519
 
520
520
  // src/emit/emit-api.ts
521
521
  import { mkdir, writeFile } from "fs/promises";
522
- import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
522
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative4 } from "path";
523
523
 
524
524
  // src/extension/registry.ts
525
+ import { relative as relative3, resolve as resolve2 } from "path";
525
526
  import { Project as Project2 } from "ts-morph";
526
527
  function resolveApiSlots(extensions) {
527
528
  let layer;
@@ -554,7 +555,7 @@ function mergeExclusive(target, incoming, {
554
555
  target.set(key, { value, owner });
555
556
  }
556
557
  }
557
- function createExtensionContext(config, getRoutes) {
558
+ function createExtensionContext(config, getRoutes, trackedInputs) {
558
559
  let project;
559
560
  return {
560
561
  cwd: config.codegen.cwd,
@@ -563,6 +564,13 @@ function createExtensionContext(config, getRoutes) {
563
564
  get routes() {
564
565
  return getRoutes();
565
566
  },
567
+ trackInput(...paths) {
568
+ if (!trackedInputs) return;
569
+ for (const path of paths) {
570
+ if (!path) continue;
571
+ trackedInputs.add(relative3(config.codegen.cwd, resolve2(config.codegen.cwd, path)));
572
+ }
573
+ },
566
574
  project() {
567
575
  if (!project) {
568
576
  project = new Project2({
@@ -730,7 +738,7 @@ function rawResponseType(c, outDir) {
730
738
  return c.contractSource.response;
731
739
  }
732
740
  if (c.controllerRef) {
733
- let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
741
+ let relPath = relative4(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
734
742
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
735
743
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
736
744
  }
@@ -1056,6 +1064,11 @@ function buildApiFile(routes, outDir, opts = {}) {
1056
1064
  config: {},
1057
1065
  project: () => {
1058
1066
  throw new Error("ExtensionContext.project() is unavailable in standalone emitApi.");
1067
+ },
1068
+ // No manifest is written on the standalone path, so there is nothing for
1069
+ // tracked dependencies to be recorded into — accept and discard them
1070
+ // rather than making every caller supply a sink.
1071
+ trackInput: () => {
1059
1072
  }
1060
1073
  };
1061
1074
  const importsByFile = /* @__PURE__ */ new Map();
@@ -1112,7 +1125,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1112
1125
  for (const [filePath, names] of importsByFile) {
1113
1126
  let relPath;
1114
1127
  if (isAbsolute2(filePath)) {
1115
- relPath = relative3(outDir, filePath).replace(/\.ts$/, "");
1128
+ relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1116
1129
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1117
1130
  } else {
1118
1131
  relPath = filePath;
@@ -1226,7 +1239,7 @@ async function emitCache(pages, outDir) {
1226
1239
 
1227
1240
  // src/emit/emit-forms.ts
1228
1241
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1229
- import { join as join6, relative as relative4 } from "path";
1242
+ import { join as join6, relative as relative5 } from "path";
1230
1243
  async function emitForms(routes, outDir, config, adapter) {
1231
1244
  if (config && config.enabled === false) return false;
1232
1245
  const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
@@ -1245,7 +1258,7 @@ function deriveBaseName(routeName) {
1245
1258
  return { method, full };
1246
1259
  }
1247
1260
  function relImport(outDir, filePath) {
1248
- let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1261
+ let relPath = relative5(outDir, filePath).replace(/\.ts$/, "");
1249
1262
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1250
1263
  return relPath;
1251
1264
  }
@@ -1996,7 +2009,7 @@ async function emitOpenApi(routes, outDir, opts = {}) {
1996
2009
 
1997
2010
  // src/emit/emit-pages.ts
1998
2011
  import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1999
- import { join as join10, relative as relative5 } from "path";
2012
+ import { join as join10, relative as relative6 } from "path";
2000
2013
  async function emitPages(pages, outDir, _options = {}) {
2001
2014
  await mkdir7(outDir, { recursive: true });
2002
2015
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -2036,7 +2049,7 @@ ${propsBody}
2036
2049
  `;
2037
2050
  }
2038
2051
  function buildAugmentationType(page, outDir) {
2039
- let importPath = relative5(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2052
+ let importPath = relative6(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2040
2053
  if (!importPath.startsWith(".")) {
2041
2054
  importPath = `./${importPath}`;
2042
2055
  }
@@ -2167,7 +2180,7 @@ function buildEmpty() {
2167
2180
  // src/generate-manifest.ts
2168
2181
  import { createHash } from "crypto";
2169
2182
  import { readFile as readFile2, readdir, writeFile as writeFile9 } from "fs/promises";
2170
- import { join as join12, relative as relative6 } from "path";
2183
+ import { join as join12, relative as relative7 } from "path";
2171
2184
  import fg2 from "fast-glob";
2172
2185
  var MANIFEST_FILE = ".codegen-manifest.json";
2173
2186
  var LOCK_FILE = ".watcher.lock";
@@ -2190,6 +2203,9 @@ function isManifestShape(value) {
2190
2203
  if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
2191
2204
  return false;
2192
2205
  }
2206
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
2207
+ return false;
2208
+ }
2193
2209
  if (!Array.isArray(candidate.files)) return false;
2194
2210
  return candidate.files.every((entry) => typeof entry === "string");
2195
2211
  }
@@ -2229,21 +2245,30 @@ async function discoverInputFiles(config) {
2229
2245
  const matched = await fg2(globs, { cwd, absolute: true, onlyFiles: true });
2230
2246
  return [...new Set(matched)].sort();
2231
2247
  }
2232
- async function computeInputsHash(config) {
2248
+ async function computeInputsHash(config, extraInputs = []) {
2233
2249
  const hash = createHash("sha256");
2234
2250
  hash.update(`version:${VERSION}
2235
2251
  `);
2236
2252
  hash.update(`config:${serializeConfig(config)}
2237
2253
  `);
2238
- const inputFiles = await discoverInputFiles(config);
2239
2254
  const cwd = config.codegen.cwd;
2240
- for (const file of inputFiles) {
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) {
2241
2259
  const contents = await readFile2(file, "utf8");
2242
- hash.update(`file:${relative6(cwd, file)}
2260
+ hash.update(`file:${relative7(cwd, file)}
2243
2261
  `);
2244
2262
  hash.update(contents);
2245
2263
  hash.update("\n");
2246
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
+ }
2247
2272
  return hash.digest("hex");
2248
2273
  }
2249
2274
  async function readManifest(outDir) {
@@ -2257,6 +2282,7 @@ async function readManifest(outDir) {
2257
2282
  ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2258
2283
  ...parsed.configHash ? { configHash: parsed.configHash } : {},
2259
2284
  ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
2285
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
2260
2286
  files: parsed.files
2261
2287
  };
2262
2288
  } catch {
@@ -2279,7 +2305,7 @@ async function listOutputFiles(outDir) {
2279
2305
  if (entry.isDirectory()) {
2280
2306
  await walk(abs);
2281
2307
  } else if (entry.isFile()) {
2282
- const rel = relative6(outDir, abs);
2308
+ const rel = relative7(outDir, abs);
2283
2309
  if (rel === MANIFEST_FILE || rel === LOCK_FILE) continue;
2284
2310
  found.push(rel);
2285
2311
  }
@@ -2316,8 +2342,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
2316
2342
  }
2317
2343
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
2318
2344
  setCodegenDebug(config.debug);
2319
- const inputsHash = await computeInputsHash(config);
2320
2345
  const manifest = await readManifest(config.codegen.outDir);
2346
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2321
2347
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2322
2348
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2323
2349
  return;
@@ -2338,7 +2364,8 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2338
2364
  }
2339
2365
  const extensions = config.extensions ?? [];
2340
2366
  let routes = inputRoutes;
2341
- const ctx = createExtensionContext(config, () => routes);
2367
+ const trackedInputs = /* @__PURE__ */ new Set();
2368
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
2342
2369
  if (extensions.length > 0) {
2343
2370
  routes = await applyTransformRoutes(routes, extensions, ctx);
2344
2371
  }
@@ -2398,13 +2425,16 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2398
2425
  }
2399
2426
  }
2400
2427
  const outputFiles = await listOutputFiles(config.codegen.outDir);
2428
+ const extraInputs = [...trackedInputs].sort();
2429
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2401
2430
  await writeManifest(config.codegen.outDir, {
2402
2431
  version: VERSION,
2403
- hash: inputsHash,
2432
+ hash: recordedHash,
2404
2433
  entryPoint,
2405
2434
  configHash,
2406
2435
  configKeyHashes,
2407
- files: outputFiles
2436
+ files: outputFiles,
2437
+ ...extraInputs.length > 0 ? { extraInputs } : {}
2408
2438
  });
2409
2439
  }
2410
2440
 
@@ -2414,11 +2444,11 @@ import { join as join16 } from "path";
2414
2444
  import chokidar from "chokidar";
2415
2445
 
2416
2446
  // src/discovery/contracts-fast.ts
2417
- import { join as join14, resolve as resolve3 } from "path";
2447
+ import { join as join14, resolve as resolve4 } from "path";
2418
2448
  import fg3 from "fast-glob";
2419
2449
  import {
2420
- Node as Node8,
2421
- Project as Project3
2450
+ Node as Node9,
2451
+ Project as Project4
2422
2452
  } from "ts-morph";
2423
2453
 
2424
2454
  // src/discovery/dto-type-resolver.ts
@@ -2434,7 +2464,7 @@ import {
2434
2464
 
2435
2465
  // src/discovery/type-ref-resolution.ts
2436
2466
  import { readFileSync } from "fs";
2437
- import { dirname as dirname3, resolve as resolve2 } from "path";
2467
+ import { dirname as dirname3, resolve as resolve3 } from "path";
2438
2468
  import {
2439
2469
  Node as Node2
2440
2470
  } from "ts-morph";
@@ -2491,9 +2521,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2491
2521
  const dir = dirname3(sourceFile.getFilePath());
2492
2522
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2493
2523
  return [
2494
- resolve2(dir, `${noExt}.ts`),
2495
- resolve2(dir, `${moduleSpecifier}.ts`),
2496
- resolve2(dir, moduleSpecifier, "index.ts")
2524
+ resolve3(dir, `${noExt}.ts`),
2525
+ resolve3(dir, `${moduleSpecifier}.ts`),
2526
+ resolve3(dir, moduleSpecifier, "index.ts")
2497
2527
  ];
2498
2528
  }
2499
2529
  const ctx = _ctxFor(project);
@@ -2514,8 +2544,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2514
2544
  const rest = moduleSpecifier.slice(prefix.length);
2515
2545
  const candidates = [];
2516
2546
  for (const mapping of mappings) {
2517
- const resolved = resolve2(baseUrl, mapping.replace("*", rest));
2518
- candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
2547
+ const resolved = resolve3(baseUrl, mapping.replace("*", rest));
2548
+ candidates.push(`${resolved}.ts`, resolve3(resolved, "index.ts"));
2519
2549
  }
2520
2550
  dbg(" resolved candidates:", candidates);
2521
2551
  return candidates;
@@ -3410,6 +3440,12 @@ function toFilterFieldType(name, r) {
3410
3440
  }
3411
3441
 
3412
3442
  // src/discovery/filter-for.ts
3443
+ function resolveMixinEntityClass(mixin, project) {
3444
+ const entityArg = mixin?.classArgs[0];
3445
+ if (!entityArg) return void 0;
3446
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3447
+ return file?.getClass(entityArg.name);
3448
+ }
3413
3449
  function classifyFilterForHint(typeInit) {
3414
3450
  if (Node5.isStringLiteral(typeInit)) {
3415
3451
  switch (typeInit.getLiteralValue()) {
@@ -3483,7 +3519,9 @@ function extractFilterForHints(classDecl, project) {
3483
3519
  }
3484
3520
  return hints;
3485
3521
  }
3486
- function extractApplyFilterInfo(method, sourceFile, project) {
3522
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3523
+ const declFile = method.getSourceFile();
3524
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3487
3525
  for (const param of method.getParameters()) {
3488
3526
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3489
3527
  if (!filterDecorator) continue;
@@ -3503,12 +3541,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3503
3541
  }
3504
3542
  }
3505
3543
  const filterClassName = filterClassArg.getText();
3506
- const resolved = findType(filterClassName, sourceFile, project);
3507
- if (resolved && resolved.kind === "class") {
3508
- const classDecl = resolved.decl;
3544
+ const resolved = findType(filterClassName, declFile, project);
3545
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3546
+ if (classDecl) {
3509
3547
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3510
3548
  if (fieldTypes.length === 0) {
3511
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3549
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3512
3550
  }
3513
3551
  const filterForHints = extractFilterForHints(classDecl, project);
3514
3552
  if (filterForHints.size > 0) {
@@ -3593,22 +3631,29 @@ function extractClassPropertyTypes(classDecl, project) {
3593
3631
  }
3594
3632
  return fields;
3595
3633
  }
3596
- function extractFilterableEntityFields(filterClass, project) {
3634
+ function resolveLocalClassDeclaration(identifier) {
3635
+ if (!Node5.isIdentifier(identifier)) return void 0;
3636
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && Node5.isClassDeclaration(n));
3637
+ }
3638
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3639
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return void 0;
3640
+ const entityProp = optionsArg.getProperty("entity");
3641
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return void 0;
3642
+ const entityInit = entityProp.getInitializer();
3643
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return void 0;
3644
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3645
+ if (!resolved || resolved.kind !== "class") return void 0;
3646
+ return resolved.decl;
3647
+ }
3648
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3597
3649
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3598
3650
  if (!filterableDecorator) return [];
3599
3651
  const args = filterableDecorator.getArguments();
3600
3652
  if (args.length === 0) return [];
3601
3653
  const optionsArg = args[0];
3602
3654
  if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3603
- const entityProp = optionsArg.getProperty("entity");
3604
- if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3605
- const entityInit = entityProp.getInitializer();
3606
- if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3607
- const entityName = entityInit.getText();
3608
- const filterSourceFile = filterClass.getSourceFile();
3609
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3610
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3611
- const entityDecl = resolvedEntity.decl;
3655
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3656
+ if (!entityDecl) return [];
3612
3657
  const fields = collectEntityFields(
3613
3658
  entityDecl,
3614
3659
  entityDecl.getSourceFile(),
@@ -4033,9 +4078,9 @@ function unwrapNamedContainer(node, names) {
4033
4078
  }
4034
4079
  return node;
4035
4080
  }
4036
- function extractDtoContract(method, sourceFile, project) {
4081
+ function extractDtoContract(method, sourceFile, project, mixin) {
4037
4082
  let body = extractBodyType(method, sourceFile, project);
4038
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4083
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4039
4084
  const query = extractQueryType(method, sourceFile, project);
4040
4085
  const uploads = extractUploadedFiles(method);
4041
4086
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4145,12 +4190,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4145
4190
  return null;
4146
4191
  }
4147
4192
 
4193
+ // src/discovery/heritage.ts
4194
+ import { Node as Node7, Project as Project3 } from "ts-morph";
4195
+ function unwrapExpression(node) {
4196
+ let current = node;
4197
+ while (Node7.isAsExpression(current) || Node7.isSatisfiesExpression(current) || Node7.isParenthesizedExpression(current) || Node7.isTypeAssertion(current)) {
4198
+ current = current.getExpression();
4199
+ }
4200
+ return current;
4201
+ }
4202
+ function resolveReturnedClass(factoryDecl) {
4203
+ const body = factoryDecl.getBody();
4204
+ if (!body) return void 0;
4205
+ const returns = body.getDescendants().filter((n) => Node7.isReturnStatement(n));
4206
+ for (const ret of returns) {
4207
+ const expr = ret.getExpression();
4208
+ if (!expr) continue;
4209
+ const inner = unwrapExpression(expr);
4210
+ if (Node7.isClassExpression(inner)) {
4211
+ return inner;
4212
+ }
4213
+ if (Node7.isIdentifier(inner)) {
4214
+ const name = inner.getText();
4215
+ const decl = body.getDescendants().find((n) => Node7.isClassDeclaration(n) && n.getName() === name);
4216
+ if (decl) return decl;
4217
+ }
4218
+ }
4219
+ return void 0;
4220
+ }
4221
+ function resolveInheritedMethods(cls) {
4222
+ const expr = cls.getExtends()?.getExpression();
4223
+ if (!expr || !Node7.isCallExpression(expr)) return void 0;
4224
+ const callee = expr.getExpression();
4225
+ if (!Node7.isIdentifier(callee)) return void 0;
4226
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isFunctionDeclaration(n));
4227
+ if (!factoryDecl) return void 0;
4228
+ const returnedClass = resolveReturnedClass(factoryDecl);
4229
+ if (!returnedClass) return void 0;
4230
+ const classArgs = [];
4231
+ for (const arg of expr.getArguments()) {
4232
+ if (!Node7.isIdentifier(arg)) continue;
4233
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isClassDeclaration(n));
4234
+ if (decl) {
4235
+ classArgs.push({
4236
+ name: decl.getName() ?? arg.getText(),
4237
+ filePath: decl.getSourceFile().getFilePath()
4238
+ });
4239
+ }
4240
+ }
4241
+ return {
4242
+ methods: returnedClass.getMethods(),
4243
+ factoryName: callee.getText(),
4244
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4245
+ classArgs
4246
+ };
4247
+ }
4248
+ var mixinTypeProject;
4249
+ function getMixinTypeProject() {
4250
+ mixinTypeProject ??= new Project3({
4251
+ skipAddingFilesFromTsConfig: true,
4252
+ compilerOptions: { strict: true }
4253
+ });
4254
+ return mixinTypeProject;
4255
+ }
4256
+ function clearMixinTypeProject() {
4257
+ mixinTypeProject = void 0;
4258
+ }
4259
+ function resolveInstantiatedReturnType(cls, methodName) {
4260
+ const filePath = cls.getSourceFile().getFilePath();
4261
+ const className = cls.getName();
4262
+ if (!className) return void 0;
4263
+ const project = getMixinTypeProject();
4264
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4265
+ const typedCls = sourceFile?.getClass(className);
4266
+ if (!typedCls) return void 0;
4267
+ const prop = typedCls.getType().getProperty(methodName);
4268
+ if (!prop) return void 0;
4269
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4270
+ if (!returnType) return void 0;
4271
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4272
+ return unwrapped.getText(typedCls);
4273
+ }
4274
+
4148
4275
  // src/discovery/zod-ast-to-ts.ts
4149
- import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
4276
+ import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
4150
4277
  function zodAstToTs(node) {
4151
- if (!Node7.isCallExpression(node)) return "unknown";
4278
+ if (!Node8.isCallExpression(node)) return "unknown";
4152
4279
  const expr = node.getExpression();
4153
- if (Node7.isPropertyAccessExpression(expr)) {
4280
+ if (Node8.isPropertyAccessExpression(expr)) {
4154
4281
  const methodName = expr.getName();
4155
4282
  const receiver = expr.getExpression();
4156
4283
  if (methodName === "optional") {
@@ -4174,17 +4301,17 @@ function zodAstToTs(node) {
4174
4301
  case "literal": {
4175
4302
  const lit = args[0];
4176
4303
  if (!lit) return "unknown";
4177
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4178
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4304
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4305
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4179
4306
  if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
4180
4307
  if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
4181
4308
  return "unknown";
4182
4309
  }
4183
4310
  case "enum": {
4184
4311
  const arrArg = args[0];
4185
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4312
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4186
4313
  const members = arrArg.getElements().map(
4187
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4314
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4188
4315
  );
4189
4316
  return members.join(" | ");
4190
4317
  }
@@ -4195,10 +4322,10 @@ function zodAstToTs(node) {
4195
4322
  }
4196
4323
  case "object": {
4197
4324
  const objArg = args[0];
4198
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
4325
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
4199
4326
  const lines = [];
4200
4327
  for (const prop of objArg.getProperties()) {
4201
- if (!Node7.isPropertyAssignment(prop)) continue;
4328
+ if (!Node8.isPropertyAssignment(prop)) continue;
4202
4329
  const key = prop.getName();
4203
4330
  const valNode = prop.getInitializer();
4204
4331
  if (!valNode) continue;
@@ -4210,7 +4337,7 @@ function zodAstToTs(node) {
4210
4337
  }
4211
4338
  case "union": {
4212
4339
  const arrArg = args[0];
4213
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4340
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4214
4341
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4215
4342
  }
4216
4343
  case "record": {
@@ -4220,7 +4347,7 @@ function zodAstToTs(node) {
4220
4347
  }
4221
4348
  case "tuple": {
4222
4349
  const arrArg = args[0];
4223
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4350
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4224
4351
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4225
4352
  }
4226
4353
  default:
@@ -4230,18 +4357,18 @@ function zodAstToTs(node) {
4230
4357
  return "unknown";
4231
4358
  }
4232
4359
  function isOptionalChain(node) {
4233
- if (!Node7.isCallExpression(node)) return false;
4360
+ if (!Node8.isCallExpression(node)) return false;
4234
4361
  const expr = node.getExpression();
4235
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4362
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4236
4363
  }
4237
4364
  function parseDefineContractCall(callExpr) {
4238
- if (!Node7.isCallExpression(callExpr)) return null;
4365
+ if (!Node8.isCallExpression(callExpr)) return null;
4239
4366
  const callee = callExpr.getExpression();
4240
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
4367
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
4241
4368
  if (calleeName !== "defineContract") return null;
4242
4369
  const args = callExpr.getArguments();
4243
4370
  const optsArg = args[0];
4244
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
4371
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
4245
4372
  let query = null;
4246
4373
  let body = null;
4247
4374
  let response = "unknown";
@@ -4249,7 +4376,7 @@ function parseDefineContractCall(callExpr) {
4249
4376
  let bodyZodText = null;
4250
4377
  let queryZodText = null;
4251
4378
  for (const prop of optsArg.getProperties()) {
4252
- if (!Node7.isPropertyAssignment(prop)) continue;
4379
+ if (!Node8.isPropertyAssignment(prop)) continue;
4253
4380
  const propName = prop.getName();
4254
4381
  const val = prop.getInitializer();
4255
4382
  if (!val) continue;
@@ -4278,21 +4405,22 @@ async function discoverContractsFast(opts) {
4278
4405
  project.addSourceFileAtPath(f);
4279
4406
  }
4280
4407
  bindDiscoveryContext(project, cwd, tsconfigPath);
4408
+ clearMixinTypeProject();
4281
4409
  return extractAllRoutes(project);
4282
4410
  }
4283
4411
  function resolveTsconfigPath(cwd, tsconfig) {
4284
- return tsconfig ? resolve3(tsconfig) : join14(cwd, "tsconfig.json");
4412
+ return tsconfig ? resolve4(tsconfig) : join14(cwd, "tsconfig.json");
4285
4413
  }
4286
4414
  function createDiscoveryProject(tsconfigPath) {
4287
4415
  try {
4288
- return new Project3({
4416
+ return new Project4({
4289
4417
  tsConfigFilePath: tsconfigPath,
4290
4418
  skipAddingFilesFromTsConfig: true,
4291
4419
  skipLoadingLibFiles: true,
4292
4420
  skipFileDependencyResolution: true
4293
4421
  });
4294
4422
  } catch {
4295
- return new Project3({
4423
+ return new Project4({
4296
4424
  skipAddingFilesFromTsConfig: true,
4297
4425
  skipLoadingLibFiles: true,
4298
4426
  skipFileDependencyResolution: true,
@@ -4367,7 +4495,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4367
4495
  async rediscover(changedPaths) {
4368
4496
  if (changedPaths) {
4369
4497
  for (const p of changedPaths) {
4370
- const abs = resolve3(p);
4498
+ const abs = resolve4(p);
4371
4499
  const sf = this.project.getSourceFile(abs);
4372
4500
  if (sf) {
4373
4501
  await sf.refreshFromFileSystem();
@@ -4399,15 +4527,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4399
4527
  runExtraction() {
4400
4528
  clearTypeResolutionCaches(this.project);
4401
4529
  clearEnumCache(this.project);
4530
+ clearMixinTypeProject();
4402
4531
  return extractRoutesFrom(this.project, this.controllerPaths);
4403
4532
  }
4404
4533
  };
4405
4534
  function decoratorStringArg(decoratorExpr) {
4406
4535
  if (!decoratorExpr) return void 0;
4407
- if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4408
- if (Node8.isArrayLiteralExpression(decoratorExpr)) {
4536
+ if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4537
+ if (Node9.isArrayLiteralExpression(decoratorExpr)) {
4409
4538
  const first = decoratorExpr.getElements()[0];
4410
- if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
4539
+ if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
4411
4540
  }
4412
4541
  return void 0;
4413
4542
  }
@@ -4429,7 +4558,8 @@ function joinPaths(prefix, suffix) {
4429
4558
  if (!prefix && !suffix) return "/";
4430
4559
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4431
4560
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4432
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4561
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4562
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4433
4563
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4434
4564
  const combined = p + s;
4435
4565
  return combined === "" ? "/" : combined;
@@ -4483,7 +4613,8 @@ function buildRoute(args) {
4483
4613
  methodAs,
4484
4614
  sourceFile,
4485
4615
  seenNames,
4486
- contractSource
4616
+ contractSource,
4617
+ mixin
4487
4618
  } = args;
4488
4619
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4489
4620
  const qualifiedRef = `${className}.${methodName}`;
@@ -4499,7 +4630,12 @@ function buildRoute(args) {
4499
4630
  path: combinedPath,
4500
4631
  name: routeName,
4501
4632
  params: extractParams(combinedPath),
4502
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4633
+ controllerRef: {
4634
+ className,
4635
+ methodName,
4636
+ filePath: sourceFile.getFilePath(),
4637
+ ...mixin ? { mixin } : {}
4638
+ },
4503
4639
  contract: { contractSource }
4504
4640
  };
4505
4641
  }
@@ -4513,16 +4649,17 @@ function extractContractRoute(args) {
4513
4649
  className,
4514
4650
  sourceFile,
4515
4651
  project,
4516
- seenNames
4652
+ seenNames,
4653
+ mixin
4517
4654
  } = args;
4518
4655
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4519
4656
  if (!firstDecoratorArg) return null;
4520
4657
  let contractDef = null;
4521
4658
  let bodyZodRef = null;
4522
4659
  let queryZodRef = null;
4523
- if (Node8.isCallExpression(firstDecoratorArg)) {
4660
+ if (Node9.isCallExpression(firstDecoratorArg)) {
4524
4661
  contractDef = parseDefineContractCall(firstDecoratorArg);
4525
- } else if (Node8.isIdentifier(firstDecoratorArg)) {
4662
+ } else if (Node9.isIdentifier(firstDecoratorArg)) {
4526
4663
  const identName = firstDecoratorArg.getText();
4527
4664
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4528
4665
  if (!resolvedVar) {
@@ -4565,6 +4702,7 @@ function extractContractRoute(args) {
4565
4702
  methodAs,
4566
4703
  sourceFile,
4567
4704
  seenNames,
4705
+ mixin,
4568
4706
  contractSource: {
4569
4707
  query: contractDef.query,
4570
4708
  body: contractDef.body,
@@ -4581,13 +4719,14 @@ function extractContractRoute(args) {
4581
4719
  });
4582
4720
  }
4583
4721
  function extractDtoRoute(args) {
4584
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4722
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4585
4723
  if (!verb) return null;
4586
4724
  const combined = joinPaths(prefix, verb.handlerPath);
4587
4725
  const methodName = method.getName();
4588
4726
  const classAs = readAsDecorator(cls, `class ${className}`);
4589
4727
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4590
- const dtoContract = extractDtoContract(method, sourceFile, project);
4728
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4729
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4591
4730
  return buildRoute({
4592
4731
  className,
4593
4732
  methodName,
@@ -4597,10 +4736,11 @@ function extractDtoRoute(args) {
4597
4736
  methodAs,
4598
4737
  sourceFile,
4599
4738
  seenNames,
4739
+ mixin,
4600
4740
  contractSource: {
4601
4741
  query: dtoContract?.query ?? null,
4602
4742
  body: dtoContract?.body ?? null,
4603
- response: dtoContract?.response ?? "unknown",
4743
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4604
4744
  error: dtoContract?.error ?? null,
4605
4745
  queryRef: dtoContract?.queryRef ?? null,
4606
4746
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4629,7 +4769,17 @@ function extractFromSourceFile(sourceFile, project) {
4629
4769
  const firstArg2 = controllerDecorator.getArguments()[0];
4630
4770
  const prefix = decoratorStringArg(firstArg2) ?? "";
4631
4771
  const className = cls.getName() ?? "Unknown";
4632
- for (const method of cls.getMethods()) {
4772
+ const inherited = resolveInheritedMethods(cls);
4773
+ const mixinBinding = inherited ? {
4774
+ factoryName: inherited.factoryName,
4775
+ factoryFilePath: inherited.factoryFilePath,
4776
+ classArgs: inherited.classArgs
4777
+ } : void 0;
4778
+ const methods = [
4779
+ ...cls.getMethods().map((method) => ({ method })),
4780
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4781
+ ];
4782
+ for (const { method, mixin } of methods) {
4633
4783
  const verb = resolveVerb(method);
4634
4784
  const applyContractDecorator = method.getDecorator("ApplyContract");
4635
4785
  const route = applyContractDecorator ? extractContractRoute({
@@ -4641,7 +4791,8 @@ function extractFromSourceFile(sourceFile, project) {
4641
4791
  className,
4642
4792
  sourceFile,
4643
4793
  project,
4644
- seenNames
4794
+ seenNames,
4795
+ mixin
4645
4796
  }) : extractDtoRoute({
4646
4797
  cls,
4647
4798
  method,
@@ -4650,7 +4801,8 @@ function extractFromSourceFile(sourceFile, project) {
4650
4801
  className,
4651
4802
  sourceFile,
4652
4803
  project,
4653
- seenNames
4804
+ seenNames,
4805
+ mixin
4654
4806
  });
4655
4807
  if (route) routes.push(route);
4656
4808
  }
@@ -4934,7 +5086,7 @@ function createChainModuleRenderer(opts) {
4934
5086
  }
4935
5087
 
4936
5088
  // src/index.ts
4937
- var VERSION = "0.15.0";
5089
+ var VERSION = "0.17.0";
4938
5090
  export {
4939
5091
  CodegenError,
4940
5092
  ConfigError,