@dudousxd/nestjs-codegen 0.15.0 → 0.16.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.
@@ -1,2 +1,2 @@
1
- export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-CjIDPMsV.cjs';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-LQNP7Ms3.cjs';
2
2
  import 'ts-morph';
@@ -1,2 +1,2 @@
1
- export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-CjIDPMsV.js';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-LQNP7Ms3.js';
2
2
  import 'ts-morph';
@@ -540,10 +540,33 @@ interface ContractSource {
540
540
  interface ContractDescriptor {
541
541
  contractSource: ContractSource;
542
542
  }
543
+ /**
544
+ * Set when a route was contributed by a base class produced by a factory call
545
+ * (`class X extends createTableController(Entity) {}`). Carries the call-site
546
+ * arguments so downstream consumers (e.g. `@dudousxd/nestjs-filter-codegen`) can
547
+ * resolve what the factory was parameterised with — the decorator arguments
548
+ * inside the factory reference its own parameters, not literal identifiers.
549
+ */
550
+ interface MixinBinding {
551
+ factoryName: string;
552
+ factoryFilePath: string;
553
+ /** Call-site arguments that resolve to a class declaration, in argument order. */
554
+ classArgs: Array<{
555
+ name: string;
556
+ filePath: string;
557
+ }>;
558
+ }
543
559
  interface ControllerRef {
544
560
  className: string;
545
561
  methodName: string;
546
562
  filePath: string;
563
+ /**
564
+ * Present only for routes inherited from a factory-produced base class.
565
+ * `className`/`filePath` still point at the DERIVED class (the call site);
566
+ * `methodName` names a method that exists only on the base, so consumers must
567
+ * branch on this before attempting `controllerClass.getMethod(methodName)`.
568
+ */
569
+ mixin?: MixinBinding;
547
570
  }
548
571
  interface RouteDescriptor {
549
572
  method: string;
@@ -540,10 +540,33 @@ interface ContractSource {
540
540
  interface ContractDescriptor {
541
541
  contractSource: ContractSource;
542
542
  }
543
+ /**
544
+ * Set when a route was contributed by a base class produced by a factory call
545
+ * (`class X extends createTableController(Entity) {}`). Carries the call-site
546
+ * arguments so downstream consumers (e.g. `@dudousxd/nestjs-filter-codegen`) can
547
+ * resolve what the factory was parameterised with — the decorator arguments
548
+ * inside the factory reference its own parameters, not literal identifiers.
549
+ */
550
+ interface MixinBinding {
551
+ factoryName: string;
552
+ factoryFilePath: string;
553
+ /** Call-site arguments that resolve to a class declaration, in argument order. */
554
+ classArgs: Array<{
555
+ name: string;
556
+ filePath: string;
557
+ }>;
558
+ }
543
559
  interface ControllerRef {
544
560
  className: string;
545
561
  methodName: string;
546
562
  filePath: string;
563
+ /**
564
+ * Present only for routes inherited from a factory-produced base class.
565
+ * `className`/`filePath` still point at the DERIVED class (the call site);
566
+ * `methodName` names a method that exists only on the base, so consumers must
567
+ * branch on this before attempting `controllerClass.getMethod(methodName)`.
568
+ */
569
+ mixin?: MixinBinding;
547
570
  }
548
571
  interface RouteDescriptor {
549
572
  method: string;
package/dist/index.cjs CHANGED
@@ -2477,7 +2477,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
2477
2477
  // src/discovery/contracts-fast.ts
2478
2478
  var import_node_path15 = require("path");
2479
2479
  var import_fast_glob3 = __toESM(require("fast-glob"), 1);
2480
- var import_ts_morph9 = require("ts-morph");
2480
+ var import_ts_morph10 = require("ts-morph");
2481
2481
 
2482
2482
  // src/discovery/dto-type-resolver.ts
2483
2483
  var import_ts_morph7 = require("ts-morph");
@@ -3456,6 +3456,12 @@ function toFilterFieldType(name, r) {
3456
3456
  }
3457
3457
 
3458
3458
  // src/discovery/filter-for.ts
3459
+ function resolveMixinEntityClass(mixin, project) {
3460
+ const entityArg = mixin?.classArgs[0];
3461
+ if (!entityArg) return void 0;
3462
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3463
+ return file?.getClass(entityArg.name);
3464
+ }
3459
3465
  function classifyFilterForHint(typeInit) {
3460
3466
  if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
3461
3467
  switch (typeInit.getLiteralValue()) {
@@ -3529,7 +3535,9 @@ function extractFilterForHints(classDecl, project) {
3529
3535
  }
3530
3536
  return hints;
3531
3537
  }
3532
- function extractApplyFilterInfo(method, sourceFile, project) {
3538
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3539
+ const declFile = method.getSourceFile();
3540
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3533
3541
  for (const param of method.getParameters()) {
3534
3542
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3535
3543
  if (!filterDecorator) continue;
@@ -3549,12 +3557,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3549
3557
  }
3550
3558
  }
3551
3559
  const filterClassName = filterClassArg.getText();
3552
- const resolved = findType(filterClassName, sourceFile, project);
3553
- if (resolved && resolved.kind === "class") {
3554
- const classDecl = resolved.decl;
3560
+ const resolved = findType(filterClassName, declFile, project);
3561
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3562
+ if (classDecl) {
3555
3563
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3556
3564
  if (fieldTypes.length === 0) {
3557
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3565
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3558
3566
  }
3559
3567
  const filterForHints = extractFilterForHints(classDecl, project);
3560
3568
  if (filterForHints.size > 0) {
@@ -3639,22 +3647,29 @@ function extractClassPropertyTypes(classDecl, project) {
3639
3647
  }
3640
3648
  return fields;
3641
3649
  }
3642
- function extractFilterableEntityFields(filterClass, project) {
3650
+ function resolveLocalClassDeclaration(identifier) {
3651
+ if (!import_ts_morph6.Node.isIdentifier(identifier)) return void 0;
3652
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && import_ts_morph6.Node.isClassDeclaration(n));
3653
+ }
3654
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3655
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return void 0;
3656
+ const entityProp = optionsArg.getProperty("entity");
3657
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return void 0;
3658
+ const entityInit = entityProp.getInitializer();
3659
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return void 0;
3660
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3661
+ if (!resolved || resolved.kind !== "class") return void 0;
3662
+ return resolved.decl;
3663
+ }
3664
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3643
3665
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3644
3666
  if (!filterableDecorator) return [];
3645
3667
  const args = filterableDecorator.getArguments();
3646
3668
  if (args.length === 0) return [];
3647
3669
  const optionsArg = args[0];
3648
3670
  if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3649
- const entityProp = optionsArg.getProperty("entity");
3650
- if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3651
- const entityInit = entityProp.getInitializer();
3652
- if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3653
- const entityName = entityInit.getText();
3654
- const filterSourceFile = filterClass.getSourceFile();
3655
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3656
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3657
- const entityDecl = resolvedEntity.decl;
3671
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3672
+ if (!entityDecl) return [];
3658
3673
  const fields = collectEntityFields(
3659
3674
  entityDecl,
3660
3675
  entityDecl.getSourceFile(),
@@ -4079,9 +4094,9 @@ function unwrapNamedContainer(node, names) {
4079
4094
  }
4080
4095
  return node;
4081
4096
  }
4082
- function extractDtoContract(method, sourceFile, project) {
4097
+ function extractDtoContract(method, sourceFile, project, mixin) {
4083
4098
  let body = extractBodyType(method, sourceFile, project);
4084
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4099
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4085
4100
  const query = extractQueryType(method, sourceFile, project);
4086
4101
  const uploads = extractUploadedFiles(method);
4087
4102
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4191,12 +4206,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4191
4206
  return null;
4192
4207
  }
4193
4208
 
4194
- // src/discovery/zod-ast-to-ts.ts
4209
+ // src/discovery/heritage.ts
4195
4210
  var import_ts_morph8 = require("ts-morph");
4211
+ function unwrapExpression(node) {
4212
+ let current = node;
4213
+ while (import_ts_morph8.Node.isAsExpression(current) || import_ts_morph8.Node.isSatisfiesExpression(current) || import_ts_morph8.Node.isParenthesizedExpression(current) || import_ts_morph8.Node.isTypeAssertion(current)) {
4214
+ current = current.getExpression();
4215
+ }
4216
+ return current;
4217
+ }
4218
+ function resolveReturnedClass(factoryDecl) {
4219
+ const body = factoryDecl.getBody();
4220
+ if (!body) return void 0;
4221
+ const returns = body.getDescendants().filter((n) => import_ts_morph8.Node.isReturnStatement(n));
4222
+ for (const ret of returns) {
4223
+ const expr = ret.getExpression();
4224
+ if (!expr) continue;
4225
+ const inner = unwrapExpression(expr);
4226
+ if (import_ts_morph8.Node.isClassExpression(inner)) {
4227
+ return inner;
4228
+ }
4229
+ if (import_ts_morph8.Node.isIdentifier(inner)) {
4230
+ const name = inner.getText();
4231
+ const decl = body.getDescendants().find((n) => import_ts_morph8.Node.isClassDeclaration(n) && n.getName() === name);
4232
+ if (decl) return decl;
4233
+ }
4234
+ }
4235
+ return void 0;
4236
+ }
4237
+ function resolveInheritedMethods(cls) {
4238
+ const expr = cls.getExtends()?.getExpression();
4239
+ if (!expr || !import_ts_morph8.Node.isCallExpression(expr)) return void 0;
4240
+ const callee = expr.getExpression();
4241
+ if (!import_ts_morph8.Node.isIdentifier(callee)) return void 0;
4242
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isFunctionDeclaration(n));
4243
+ if (!factoryDecl) return void 0;
4244
+ const returnedClass = resolveReturnedClass(factoryDecl);
4245
+ if (!returnedClass) return void 0;
4246
+ const classArgs = [];
4247
+ for (const arg of expr.getArguments()) {
4248
+ if (!import_ts_morph8.Node.isIdentifier(arg)) continue;
4249
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isClassDeclaration(n));
4250
+ if (decl) {
4251
+ classArgs.push({
4252
+ name: decl.getName() ?? arg.getText(),
4253
+ filePath: decl.getSourceFile().getFilePath()
4254
+ });
4255
+ }
4256
+ }
4257
+ return {
4258
+ methods: returnedClass.getMethods(),
4259
+ factoryName: callee.getText(),
4260
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4261
+ classArgs
4262
+ };
4263
+ }
4264
+ var mixinTypeProject;
4265
+ function getMixinTypeProject() {
4266
+ mixinTypeProject ??= new import_ts_morph8.Project({
4267
+ skipAddingFilesFromTsConfig: true,
4268
+ compilerOptions: { strict: true }
4269
+ });
4270
+ return mixinTypeProject;
4271
+ }
4272
+ function clearMixinTypeProject() {
4273
+ mixinTypeProject = void 0;
4274
+ }
4275
+ function resolveInstantiatedReturnType(cls, methodName) {
4276
+ const filePath = cls.getSourceFile().getFilePath();
4277
+ const className = cls.getName();
4278
+ if (!className) return void 0;
4279
+ const project = getMixinTypeProject();
4280
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4281
+ const typedCls = sourceFile?.getClass(className);
4282
+ if (!typedCls) return void 0;
4283
+ const prop = typedCls.getType().getProperty(methodName);
4284
+ if (!prop) return void 0;
4285
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4286
+ if (!returnType) return void 0;
4287
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4288
+ return unwrapped.getText(typedCls);
4289
+ }
4290
+
4291
+ // src/discovery/zod-ast-to-ts.ts
4292
+ var import_ts_morph9 = require("ts-morph");
4196
4293
  function zodAstToTs(node) {
4197
- if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
4294
+ if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
4198
4295
  const expr = node.getExpression();
4199
- if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
4296
+ if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
4200
4297
  const methodName = expr.getName();
4201
4298
  const receiver = expr.getExpression();
4202
4299
  if (methodName === "optional") {
@@ -4220,17 +4317,17 @@ function zodAstToTs(node) {
4220
4317
  case "literal": {
4221
4318
  const lit = args[0];
4222
4319
  if (!lit) return "unknown";
4223
- if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4224
- if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4225
- if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
4226
- if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
4320
+ if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4321
+ if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4322
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
4323
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
4227
4324
  return "unknown";
4228
4325
  }
4229
4326
  case "enum": {
4230
4327
  const arrArg = args[0];
4231
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4328
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4232
4329
  const members = arrArg.getElements().map(
4233
- (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4330
+ (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4234
4331
  );
4235
4332
  return members.join(" | ");
4236
4333
  }
@@ -4241,10 +4338,10 @@ function zodAstToTs(node) {
4241
4338
  }
4242
4339
  case "object": {
4243
4340
  const objArg = args[0];
4244
- if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
4341
+ if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
4245
4342
  const lines = [];
4246
4343
  for (const prop of objArg.getProperties()) {
4247
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4344
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4248
4345
  const key = prop.getName();
4249
4346
  const valNode = prop.getInitializer();
4250
4347
  if (!valNode) continue;
@@ -4256,7 +4353,7 @@ function zodAstToTs(node) {
4256
4353
  }
4257
4354
  case "union": {
4258
4355
  const arrArg = args[0];
4259
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4356
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4260
4357
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4261
4358
  }
4262
4359
  case "record": {
@@ -4266,7 +4363,7 @@ function zodAstToTs(node) {
4266
4363
  }
4267
4364
  case "tuple": {
4268
4365
  const arrArg = args[0];
4269
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4366
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4270
4367
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4271
4368
  }
4272
4369
  default:
@@ -4276,18 +4373,18 @@ function zodAstToTs(node) {
4276
4373
  return "unknown";
4277
4374
  }
4278
4375
  function isOptionalChain(node) {
4279
- if (!import_ts_morph8.Node.isCallExpression(node)) return false;
4376
+ if (!import_ts_morph9.Node.isCallExpression(node)) return false;
4280
4377
  const expr = node.getExpression();
4281
- return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4378
+ return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4282
4379
  }
4283
4380
  function parseDefineContractCall(callExpr) {
4284
- if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
4381
+ if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
4285
4382
  const callee = callExpr.getExpression();
4286
- const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4383
+ const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4287
4384
  if (calleeName !== "defineContract") return null;
4288
4385
  const args = callExpr.getArguments();
4289
4386
  const optsArg = args[0];
4290
- if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
4387
+ if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
4291
4388
  let query = null;
4292
4389
  let body = null;
4293
4390
  let response = "unknown";
@@ -4295,7 +4392,7 @@ function parseDefineContractCall(callExpr) {
4295
4392
  let bodyZodText = null;
4296
4393
  let queryZodText = null;
4297
4394
  for (const prop of optsArg.getProperties()) {
4298
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4395
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4299
4396
  const propName = prop.getName();
4300
4397
  const val = prop.getInitializer();
4301
4398
  if (!val) continue;
@@ -4324,6 +4421,7 @@ async function discoverContractsFast(opts) {
4324
4421
  project.addSourceFileAtPath(f);
4325
4422
  }
4326
4423
  bindDiscoveryContext(project, cwd, tsconfigPath);
4424
+ clearMixinTypeProject();
4327
4425
  return extractAllRoutes(project);
4328
4426
  }
4329
4427
  function resolveTsconfigPath(cwd, tsconfig) {
@@ -4331,14 +4429,14 @@ function resolveTsconfigPath(cwd, tsconfig) {
4331
4429
  }
4332
4430
  function createDiscoveryProject(tsconfigPath) {
4333
4431
  try {
4334
- return new import_ts_morph9.Project({
4432
+ return new import_ts_morph10.Project({
4335
4433
  tsConfigFilePath: tsconfigPath,
4336
4434
  skipAddingFilesFromTsConfig: true,
4337
4435
  skipLoadingLibFiles: true,
4338
4436
  skipFileDependencyResolution: true
4339
4437
  });
4340
4438
  } catch {
4341
- return new import_ts_morph9.Project({
4439
+ return new import_ts_morph10.Project({
4342
4440
  skipAddingFilesFromTsConfig: true,
4343
4441
  skipLoadingLibFiles: true,
4344
4442
  skipFileDependencyResolution: true,
@@ -4445,15 +4543,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4445
4543
  runExtraction() {
4446
4544
  clearTypeResolutionCaches(this.project);
4447
4545
  clearEnumCache(this.project);
4546
+ clearMixinTypeProject();
4448
4547
  return extractRoutesFrom(this.project, this.controllerPaths);
4449
4548
  }
4450
4549
  };
4451
4550
  function decoratorStringArg(decoratorExpr) {
4452
4551
  if (!decoratorExpr) return void 0;
4453
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4454
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
4552
+ if (import_ts_morph10.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4553
+ if (import_ts_morph10.Node.isArrayLiteralExpression(decoratorExpr)) {
4455
4554
  const first = decoratorExpr.getElements()[0];
4456
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
4555
+ if (first && import_ts_morph10.Node.isStringLiteral(first)) return first.getLiteralValue();
4457
4556
  }
4458
4557
  return void 0;
4459
4558
  }
@@ -4475,7 +4574,8 @@ function joinPaths(prefix, suffix) {
4475
4574
  if (!prefix && !suffix) return "/";
4476
4575
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4477
4576
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4478
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4577
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4578
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4479
4579
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4480
4580
  const combined = p + s;
4481
4581
  return combined === "" ? "/" : combined;
@@ -4529,7 +4629,8 @@ function buildRoute(args) {
4529
4629
  methodAs,
4530
4630
  sourceFile,
4531
4631
  seenNames,
4532
- contractSource
4632
+ contractSource,
4633
+ mixin
4533
4634
  } = args;
4534
4635
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4535
4636
  const qualifiedRef = `${className}.${methodName}`;
@@ -4545,7 +4646,12 @@ function buildRoute(args) {
4545
4646
  path: combinedPath,
4546
4647
  name: routeName,
4547
4648
  params: extractParams(combinedPath),
4548
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4649
+ controllerRef: {
4650
+ className,
4651
+ methodName,
4652
+ filePath: sourceFile.getFilePath(),
4653
+ ...mixin ? { mixin } : {}
4654
+ },
4549
4655
  contract: { contractSource }
4550
4656
  };
4551
4657
  }
@@ -4559,16 +4665,17 @@ function extractContractRoute(args) {
4559
4665
  className,
4560
4666
  sourceFile,
4561
4667
  project,
4562
- seenNames
4668
+ seenNames,
4669
+ mixin
4563
4670
  } = args;
4564
4671
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4565
4672
  if (!firstDecoratorArg) return null;
4566
4673
  let contractDef = null;
4567
4674
  let bodyZodRef = null;
4568
4675
  let queryZodRef = null;
4569
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
4676
+ if (import_ts_morph10.Node.isCallExpression(firstDecoratorArg)) {
4570
4677
  contractDef = parseDefineContractCall(firstDecoratorArg);
4571
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
4678
+ } else if (import_ts_morph10.Node.isIdentifier(firstDecoratorArg)) {
4572
4679
  const identName = firstDecoratorArg.getText();
4573
4680
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4574
4681
  if (!resolvedVar) {
@@ -4611,6 +4718,7 @@ function extractContractRoute(args) {
4611
4718
  methodAs,
4612
4719
  sourceFile,
4613
4720
  seenNames,
4721
+ mixin,
4614
4722
  contractSource: {
4615
4723
  query: contractDef.query,
4616
4724
  body: contractDef.body,
@@ -4627,13 +4735,14 @@ function extractContractRoute(args) {
4627
4735
  });
4628
4736
  }
4629
4737
  function extractDtoRoute(args) {
4630
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4738
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4631
4739
  if (!verb) return null;
4632
4740
  const combined = joinPaths(prefix, verb.handlerPath);
4633
4741
  const methodName = method.getName();
4634
4742
  const classAs = readAsDecorator(cls, `class ${className}`);
4635
4743
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4636
- const dtoContract = extractDtoContract(method, sourceFile, project);
4744
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4745
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4637
4746
  return buildRoute({
4638
4747
  className,
4639
4748
  methodName,
@@ -4643,10 +4752,11 @@ function extractDtoRoute(args) {
4643
4752
  methodAs,
4644
4753
  sourceFile,
4645
4754
  seenNames,
4755
+ mixin,
4646
4756
  contractSource: {
4647
4757
  query: dtoContract?.query ?? null,
4648
4758
  body: dtoContract?.body ?? null,
4649
- response: dtoContract?.response ?? "unknown",
4759
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4650
4760
  error: dtoContract?.error ?? null,
4651
4761
  queryRef: dtoContract?.queryRef ?? null,
4652
4762
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4675,7 +4785,17 @@ function extractFromSourceFile(sourceFile, project) {
4675
4785
  const firstArg2 = controllerDecorator.getArguments()[0];
4676
4786
  const prefix = decoratorStringArg(firstArg2) ?? "";
4677
4787
  const className = cls.getName() ?? "Unknown";
4678
- for (const method of cls.getMethods()) {
4788
+ const inherited = resolveInheritedMethods(cls);
4789
+ const mixinBinding = inherited ? {
4790
+ factoryName: inherited.factoryName,
4791
+ factoryFilePath: inherited.factoryFilePath,
4792
+ classArgs: inherited.classArgs
4793
+ } : void 0;
4794
+ const methods = [
4795
+ ...cls.getMethods().map((method) => ({ method })),
4796
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4797
+ ];
4798
+ for (const { method, mixin } of methods) {
4679
4799
  const verb = resolveVerb(method);
4680
4800
  const applyContractDecorator = method.getDecorator("ApplyContract");
4681
4801
  const route = applyContractDecorator ? extractContractRoute({
@@ -4687,7 +4807,8 @@ function extractFromSourceFile(sourceFile, project) {
4687
4807
  className,
4688
4808
  sourceFile,
4689
4809
  project,
4690
- seenNames
4810
+ seenNames,
4811
+ mixin
4691
4812
  }) : extractDtoRoute({
4692
4813
  cls,
4693
4814
  method,
@@ -4696,7 +4817,8 @@ function extractFromSourceFile(sourceFile, project) {
4696
4817
  className,
4697
4818
  sourceFile,
4698
4819
  project,
4699
- seenNames
4820
+ seenNames,
4821
+ mixin
4700
4822
  });
4701
4823
  if (route) routes.push(route);
4702
4824
  }
@@ -4980,7 +5102,7 @@ function createChainModuleRenderer(opts) {
4980
5102
  }
4981
5103
 
4982
5104
  // src/index.ts
4983
- var VERSION = "0.15.0";
5105
+ var VERSION = "0.16.0";
4984
5106
  // Annotate the CommonJS export names for ESM import in node:
4985
5107
  0 && (module.exports = {
4986
5108
  CodegenError,