@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.
@@ -141,7 +141,7 @@ function applyDefaults(userConfig, cwd) {
141
141
  // src/generate-manifest.ts
142
142
  import { createHash } from "crypto";
143
143
  import { readFile as readFile4, readdir, writeFile as writeFile10 } from "fs/promises";
144
- import { join as join16, relative as relative6 } from "path";
144
+ import { join as join16, relative as relative7 } from "path";
145
145
  import fg3 from "fast-glob";
146
146
 
147
147
  // src/generate.ts
@@ -475,9 +475,10 @@ function inferExpressionType(node) {
475
475
 
476
476
  // src/emit/emit-api.ts
477
477
  import { mkdir, writeFile } from "fs/promises";
478
- import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
478
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative4 } from "path";
479
479
 
480
480
  // src/extension/registry.ts
481
+ import { relative as relative3, resolve as resolve2 } from "path";
481
482
  import { Project as Project2 } from "ts-morph";
482
483
  function resolveApiSlots(extensions) {
483
484
  let layer;
@@ -510,7 +511,7 @@ function mergeExclusive(target, incoming, {
510
511
  target.set(key, { value, owner });
511
512
  }
512
513
  }
513
- function createExtensionContext(config, getRoutes) {
514
+ function createExtensionContext(config, getRoutes, trackedInputs) {
514
515
  let project;
515
516
  return {
516
517
  cwd: config.codegen.cwd,
@@ -519,6 +520,13 @@ function createExtensionContext(config, getRoutes) {
519
520
  get routes() {
520
521
  return getRoutes();
521
522
  },
523
+ trackInput(...paths) {
524
+ if (!trackedInputs) return;
525
+ for (const path of paths) {
526
+ if (!path) continue;
527
+ trackedInputs.add(relative3(config.codegen.cwd, resolve2(config.codegen.cwd, path)));
528
+ }
529
+ },
522
530
  project() {
523
531
  if (!project) {
524
532
  project = new Project2({
@@ -686,7 +694,7 @@ function rawResponseType(c, outDir) {
686
694
  return c.contractSource.response;
687
695
  }
688
696
  if (c.controllerRef) {
689
- let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
697
+ let relPath = relative4(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
690
698
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
691
699
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
692
700
  }
@@ -1012,6 +1020,11 @@ function buildApiFile(routes, outDir, opts = {}) {
1012
1020
  config: {},
1013
1021
  project: () => {
1014
1022
  throw new Error("ExtensionContext.project() is unavailable in standalone emitApi.");
1023
+ },
1024
+ // No manifest is written on the standalone path, so there is nothing for
1025
+ // tracked dependencies to be recorded into — accept and discard them
1026
+ // rather than making every caller supply a sink.
1027
+ trackInput: () => {
1015
1028
  }
1016
1029
  };
1017
1030
  const importsByFile = /* @__PURE__ */ new Map();
@@ -1068,7 +1081,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1068
1081
  for (const [filePath, names] of importsByFile) {
1069
1082
  let relPath;
1070
1083
  if (isAbsolute2(filePath)) {
1071
- relPath = relative3(outDir, filePath).replace(/\.ts$/, "");
1084
+ relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1072
1085
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1073
1086
  } else {
1074
1087
  relPath = filePath;
@@ -1182,7 +1195,7 @@ async function emitCache(pages, outDir) {
1182
1195
 
1183
1196
  // src/emit/emit-forms.ts
1184
1197
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1185
- import { join as join6, relative as relative4 } from "path";
1198
+ import { join as join6, relative as relative5 } from "path";
1186
1199
  async function emitForms(routes, outDir, config, adapter) {
1187
1200
  if (config && config.enabled === false) return false;
1188
1201
  const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
@@ -1201,7 +1214,7 @@ function deriveBaseName(routeName) {
1201
1214
  return { method, full };
1202
1215
  }
1203
1216
  function relImport(outDir, filePath) {
1204
- let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1217
+ let relPath = relative5(outDir, filePath).replace(/\.ts$/, "");
1205
1218
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1206
1219
  return relPath;
1207
1220
  }
@@ -1949,7 +1962,7 @@ async function emitOpenApi(routes, outDir, opts = {}) {
1949
1962
 
1950
1963
  // src/emit/emit-pages.ts
1951
1964
  import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1952
- import { join as join10, relative as relative5 } from "path";
1965
+ import { join as join10, relative as relative6 } from "path";
1953
1966
  async function emitPages(pages, outDir, _options = {}) {
1954
1967
  await mkdir7(outDir, { recursive: true });
1955
1968
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -1989,7 +2002,7 @@ ${propsBody}
1989
2002
  `;
1990
2003
  }
1991
2004
  function buildAugmentationType(page, outDir) {
1992
- let importPath = relative5(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2005
+ let importPath = relative6(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1993
2006
  if (!importPath.startsWith(".")) {
1994
2007
  importPath = `./${importPath}`;
1995
2008
  }
@@ -2133,8 +2146,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
2133
2146
  }
2134
2147
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
2135
2148
  setCodegenDebug(config.debug);
2136
- const inputsHash = await computeInputsHash(config);
2137
2149
  const manifest = await readManifest(config.codegen.outDir);
2150
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2138
2151
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2139
2152
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2140
2153
  return;
@@ -2155,7 +2168,8 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2155
2168
  }
2156
2169
  const extensions = config.extensions ?? [];
2157
2170
  let routes = inputRoutes;
2158
- const ctx = createExtensionContext(config, () => routes);
2171
+ const trackedInputs = /* @__PURE__ */ new Set();
2172
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
2159
2173
  if (extensions.length > 0) {
2160
2174
  routes = await applyTransformRoutes(routes, extensions, ctx);
2161
2175
  }
@@ -2215,13 +2229,16 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2215
2229
  }
2216
2230
  }
2217
2231
  const outputFiles = await listOutputFiles(config.codegen.outDir);
2232
+ const extraInputs = [...trackedInputs].sort();
2233
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2218
2234
  await writeManifest(config.codegen.outDir, {
2219
2235
  version: VERSION,
2220
- hash: inputsHash,
2236
+ hash: recordedHash,
2221
2237
  entryPoint,
2222
2238
  configHash,
2223
2239
  configKeyHashes,
2224
- files: outputFiles
2240
+ files: outputFiles,
2241
+ ...extraInputs.length > 0 ? { extraInputs } : {}
2225
2242
  });
2226
2243
  }
2227
2244
 
@@ -2231,11 +2248,11 @@ import { join as join15 } from "path";
2231
2248
  import chokidar from "chokidar";
2232
2249
 
2233
2250
  // src/discovery/contracts-fast.ts
2234
- import { join as join13, resolve as resolve3 } from "path";
2251
+ import { join as join13, resolve as resolve4 } from "path";
2235
2252
  import fg2 from "fast-glob";
2236
2253
  import {
2237
- Node as Node8,
2238
- Project as Project3
2254
+ Node as Node9,
2255
+ Project as Project4
2239
2256
  } from "ts-morph";
2240
2257
 
2241
2258
  // src/discovery/dto-type-resolver.ts
@@ -2251,7 +2268,7 @@ import {
2251
2268
 
2252
2269
  // src/discovery/type-ref-resolution.ts
2253
2270
  import { readFileSync } from "fs";
2254
- import { dirname as dirname3, resolve as resolve2 } from "path";
2271
+ import { dirname as dirname3, resolve as resolve3 } from "path";
2255
2272
  import {
2256
2273
  Node as Node2
2257
2274
  } from "ts-morph";
@@ -2308,9 +2325,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2308
2325
  const dir = dirname3(sourceFile.getFilePath());
2309
2326
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2310
2327
  return [
2311
- resolve2(dir, `${noExt}.ts`),
2312
- resolve2(dir, `${moduleSpecifier}.ts`),
2313
- resolve2(dir, moduleSpecifier, "index.ts")
2328
+ resolve3(dir, `${noExt}.ts`),
2329
+ resolve3(dir, `${moduleSpecifier}.ts`),
2330
+ resolve3(dir, moduleSpecifier, "index.ts")
2314
2331
  ];
2315
2332
  }
2316
2333
  const ctx = _ctxFor(project);
@@ -2331,8 +2348,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2331
2348
  const rest = moduleSpecifier.slice(prefix.length);
2332
2349
  const candidates = [];
2333
2350
  for (const mapping of mappings) {
2334
- const resolved = resolve2(baseUrl, mapping.replace("*", rest));
2335
- candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
2351
+ const resolved = resolve3(baseUrl, mapping.replace("*", rest));
2352
+ candidates.push(`${resolved}.ts`, resolve3(resolved, "index.ts"));
2336
2353
  }
2337
2354
  dbg(" resolved candidates:", candidates);
2338
2355
  return candidates;
@@ -3227,6 +3244,12 @@ function toFilterFieldType(name, r) {
3227
3244
  }
3228
3245
 
3229
3246
  // src/discovery/filter-for.ts
3247
+ function resolveMixinEntityClass(mixin, project) {
3248
+ const entityArg = mixin?.classArgs[0];
3249
+ if (!entityArg) return void 0;
3250
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3251
+ return file?.getClass(entityArg.name);
3252
+ }
3230
3253
  function classifyFilterForHint(typeInit) {
3231
3254
  if (Node5.isStringLiteral(typeInit)) {
3232
3255
  switch (typeInit.getLiteralValue()) {
@@ -3300,7 +3323,9 @@ function extractFilterForHints(classDecl, project) {
3300
3323
  }
3301
3324
  return hints;
3302
3325
  }
3303
- function extractApplyFilterInfo(method, sourceFile, project) {
3326
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3327
+ const declFile = method.getSourceFile();
3328
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3304
3329
  for (const param of method.getParameters()) {
3305
3330
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3306
3331
  if (!filterDecorator) continue;
@@ -3320,12 +3345,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3320
3345
  }
3321
3346
  }
3322
3347
  const filterClassName = filterClassArg.getText();
3323
- const resolved = findType(filterClassName, sourceFile, project);
3324
- if (resolved && resolved.kind === "class") {
3325
- const classDecl = resolved.decl;
3348
+ const resolved = findType(filterClassName, declFile, project);
3349
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3350
+ if (classDecl) {
3326
3351
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3327
3352
  if (fieldTypes.length === 0) {
3328
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3353
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3329
3354
  }
3330
3355
  const filterForHints = extractFilterForHints(classDecl, project);
3331
3356
  if (filterForHints.size > 0) {
@@ -3410,22 +3435,29 @@ function extractClassPropertyTypes(classDecl, project) {
3410
3435
  }
3411
3436
  return fields;
3412
3437
  }
3413
- function extractFilterableEntityFields(filterClass, project) {
3438
+ function resolveLocalClassDeclaration(identifier) {
3439
+ if (!Node5.isIdentifier(identifier)) return void 0;
3440
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && Node5.isClassDeclaration(n));
3441
+ }
3442
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3443
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return void 0;
3444
+ const entityProp = optionsArg.getProperty("entity");
3445
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return void 0;
3446
+ const entityInit = entityProp.getInitializer();
3447
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return void 0;
3448
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3449
+ if (!resolved || resolved.kind !== "class") return void 0;
3450
+ return resolved.decl;
3451
+ }
3452
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3414
3453
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3415
3454
  if (!filterableDecorator) return [];
3416
3455
  const args = filterableDecorator.getArguments();
3417
3456
  if (args.length === 0) return [];
3418
3457
  const optionsArg = args[0];
3419
3458
  if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3420
- const entityProp = optionsArg.getProperty("entity");
3421
- if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3422
- const entityInit = entityProp.getInitializer();
3423
- if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3424
- const entityName = entityInit.getText();
3425
- const filterSourceFile = filterClass.getSourceFile();
3426
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3427
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3428
- const entityDecl = resolvedEntity.decl;
3459
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3460
+ if (!entityDecl) return [];
3429
3461
  const fields = collectEntityFields(
3430
3462
  entityDecl,
3431
3463
  entityDecl.getSourceFile(),
@@ -3850,9 +3882,9 @@ function unwrapNamedContainer(node, names) {
3850
3882
  }
3851
3883
  return node;
3852
3884
  }
3853
- function extractDtoContract(method, sourceFile, project) {
3885
+ function extractDtoContract(method, sourceFile, project, mixin) {
3854
3886
  let body = extractBodyType(method, sourceFile, project);
3855
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
3887
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
3856
3888
  const query = extractQueryType(method, sourceFile, project);
3857
3889
  const uploads = extractUploadedFiles(method);
3858
3890
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -3962,12 +3994,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
3962
3994
  return null;
3963
3995
  }
3964
3996
 
3997
+ // src/discovery/heritage.ts
3998
+ import { Node as Node7, Project as Project3 } from "ts-morph";
3999
+ function unwrapExpression(node) {
4000
+ let current = node;
4001
+ while (Node7.isAsExpression(current) || Node7.isSatisfiesExpression(current) || Node7.isParenthesizedExpression(current) || Node7.isTypeAssertion(current)) {
4002
+ current = current.getExpression();
4003
+ }
4004
+ return current;
4005
+ }
4006
+ function resolveReturnedClass(factoryDecl) {
4007
+ const body = factoryDecl.getBody();
4008
+ if (!body) return void 0;
4009
+ const returns = body.getDescendants().filter((n) => Node7.isReturnStatement(n));
4010
+ for (const ret of returns) {
4011
+ const expr = ret.getExpression();
4012
+ if (!expr) continue;
4013
+ const inner = unwrapExpression(expr);
4014
+ if (Node7.isClassExpression(inner)) {
4015
+ return inner;
4016
+ }
4017
+ if (Node7.isIdentifier(inner)) {
4018
+ const name = inner.getText();
4019
+ const decl = body.getDescendants().find((n) => Node7.isClassDeclaration(n) && n.getName() === name);
4020
+ if (decl) return decl;
4021
+ }
4022
+ }
4023
+ return void 0;
4024
+ }
4025
+ function resolveInheritedMethods(cls) {
4026
+ const expr = cls.getExtends()?.getExpression();
4027
+ if (!expr || !Node7.isCallExpression(expr)) return void 0;
4028
+ const callee = expr.getExpression();
4029
+ if (!Node7.isIdentifier(callee)) return void 0;
4030
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isFunctionDeclaration(n));
4031
+ if (!factoryDecl) return void 0;
4032
+ const returnedClass = resolveReturnedClass(factoryDecl);
4033
+ if (!returnedClass) return void 0;
4034
+ const classArgs = [];
4035
+ for (const arg of expr.getArguments()) {
4036
+ if (!Node7.isIdentifier(arg)) continue;
4037
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isClassDeclaration(n));
4038
+ if (decl) {
4039
+ classArgs.push({
4040
+ name: decl.getName() ?? arg.getText(),
4041
+ filePath: decl.getSourceFile().getFilePath()
4042
+ });
4043
+ }
4044
+ }
4045
+ return {
4046
+ methods: returnedClass.getMethods(),
4047
+ factoryName: callee.getText(),
4048
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4049
+ classArgs
4050
+ };
4051
+ }
4052
+ var mixinTypeProject;
4053
+ function getMixinTypeProject() {
4054
+ mixinTypeProject ??= new Project3({
4055
+ skipAddingFilesFromTsConfig: true,
4056
+ compilerOptions: { strict: true }
4057
+ });
4058
+ return mixinTypeProject;
4059
+ }
4060
+ function clearMixinTypeProject() {
4061
+ mixinTypeProject = void 0;
4062
+ }
4063
+ function resolveInstantiatedReturnType(cls, methodName) {
4064
+ const filePath = cls.getSourceFile().getFilePath();
4065
+ const className = cls.getName();
4066
+ if (!className) return void 0;
4067
+ const project = getMixinTypeProject();
4068
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4069
+ const typedCls = sourceFile?.getClass(className);
4070
+ if (!typedCls) return void 0;
4071
+ const prop = typedCls.getType().getProperty(methodName);
4072
+ if (!prop) return void 0;
4073
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4074
+ if (!returnType) return void 0;
4075
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4076
+ return unwrapped.getText(typedCls);
4077
+ }
4078
+
3965
4079
  // src/discovery/zod-ast-to-ts.ts
3966
- import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
4080
+ import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
3967
4081
  function zodAstToTs(node) {
3968
- if (!Node7.isCallExpression(node)) return "unknown";
4082
+ if (!Node8.isCallExpression(node)) return "unknown";
3969
4083
  const expr = node.getExpression();
3970
- if (Node7.isPropertyAccessExpression(expr)) {
4084
+ if (Node8.isPropertyAccessExpression(expr)) {
3971
4085
  const methodName = expr.getName();
3972
4086
  const receiver = expr.getExpression();
3973
4087
  if (methodName === "optional") {
@@ -3991,17 +4105,17 @@ function zodAstToTs(node) {
3991
4105
  case "literal": {
3992
4106
  const lit = args[0];
3993
4107
  if (!lit) return "unknown";
3994
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
3995
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4108
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4109
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
3996
4110
  if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
3997
4111
  if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
3998
4112
  return "unknown";
3999
4113
  }
4000
4114
  case "enum": {
4001
4115
  const arrArg = args[0];
4002
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4116
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4003
4117
  const members = arrArg.getElements().map(
4004
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4118
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4005
4119
  );
4006
4120
  return members.join(" | ");
4007
4121
  }
@@ -4012,10 +4126,10 @@ function zodAstToTs(node) {
4012
4126
  }
4013
4127
  case "object": {
4014
4128
  const objArg = args[0];
4015
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
4129
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
4016
4130
  const lines = [];
4017
4131
  for (const prop of objArg.getProperties()) {
4018
- if (!Node7.isPropertyAssignment(prop)) continue;
4132
+ if (!Node8.isPropertyAssignment(prop)) continue;
4019
4133
  const key = prop.getName();
4020
4134
  const valNode = prop.getInitializer();
4021
4135
  if (!valNode) continue;
@@ -4027,7 +4141,7 @@ function zodAstToTs(node) {
4027
4141
  }
4028
4142
  case "union": {
4029
4143
  const arrArg = args[0];
4030
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4144
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4031
4145
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4032
4146
  }
4033
4147
  case "record": {
@@ -4037,7 +4151,7 @@ function zodAstToTs(node) {
4037
4151
  }
4038
4152
  case "tuple": {
4039
4153
  const arrArg = args[0];
4040
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4154
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4041
4155
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4042
4156
  }
4043
4157
  default:
@@ -4047,18 +4161,18 @@ function zodAstToTs(node) {
4047
4161
  return "unknown";
4048
4162
  }
4049
4163
  function isOptionalChain(node) {
4050
- if (!Node7.isCallExpression(node)) return false;
4164
+ if (!Node8.isCallExpression(node)) return false;
4051
4165
  const expr = node.getExpression();
4052
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4166
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4053
4167
  }
4054
4168
  function parseDefineContractCall(callExpr) {
4055
- if (!Node7.isCallExpression(callExpr)) return null;
4169
+ if (!Node8.isCallExpression(callExpr)) return null;
4056
4170
  const callee = callExpr.getExpression();
4057
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
4171
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
4058
4172
  if (calleeName !== "defineContract") return null;
4059
4173
  const args = callExpr.getArguments();
4060
4174
  const optsArg = args[0];
4061
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
4175
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
4062
4176
  let query = null;
4063
4177
  let body = null;
4064
4178
  let response = "unknown";
@@ -4066,7 +4180,7 @@ function parseDefineContractCall(callExpr) {
4066
4180
  let bodyZodText = null;
4067
4181
  let queryZodText = null;
4068
4182
  for (const prop of optsArg.getProperties()) {
4069
- if (!Node7.isPropertyAssignment(prop)) continue;
4183
+ if (!Node8.isPropertyAssignment(prop)) continue;
4070
4184
  const propName = prop.getName();
4071
4185
  const val = prop.getInitializer();
4072
4186
  if (!val) continue;
@@ -4087,18 +4201,18 @@ function parseDefineContractCall(callExpr) {
4087
4201
 
4088
4202
  // src/discovery/contracts-fast.ts
4089
4203
  function resolveTsconfigPath(cwd, tsconfig) {
4090
- return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
4204
+ return tsconfig ? resolve4(tsconfig) : join13(cwd, "tsconfig.json");
4091
4205
  }
4092
4206
  function createDiscoveryProject(tsconfigPath) {
4093
4207
  try {
4094
- return new Project3({
4208
+ return new Project4({
4095
4209
  tsConfigFilePath: tsconfigPath,
4096
4210
  skipAddingFilesFromTsConfig: true,
4097
4211
  skipLoadingLibFiles: true,
4098
4212
  skipFileDependencyResolution: true
4099
4213
  });
4100
4214
  } catch {
4101
- return new Project3({
4215
+ return new Project4({
4102
4216
  skipAddingFilesFromTsConfig: true,
4103
4217
  skipLoadingLibFiles: true,
4104
4218
  skipFileDependencyResolution: true,
@@ -4166,7 +4280,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4166
4280
  async rediscover(changedPaths) {
4167
4281
  if (changedPaths) {
4168
4282
  for (const p of changedPaths) {
4169
- const abs = resolve3(p);
4283
+ const abs = resolve4(p);
4170
4284
  const sf = this.project.getSourceFile(abs);
4171
4285
  if (sf) {
4172
4286
  await sf.refreshFromFileSystem();
@@ -4198,15 +4312,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4198
4312
  runExtraction() {
4199
4313
  clearTypeResolutionCaches(this.project);
4200
4314
  clearEnumCache(this.project);
4315
+ clearMixinTypeProject();
4201
4316
  return extractRoutesFrom(this.project, this.controllerPaths);
4202
4317
  }
4203
4318
  };
4204
4319
  function decoratorStringArg(decoratorExpr) {
4205
4320
  if (!decoratorExpr) return void 0;
4206
- if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4207
- if (Node8.isArrayLiteralExpression(decoratorExpr)) {
4321
+ if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4322
+ if (Node9.isArrayLiteralExpression(decoratorExpr)) {
4208
4323
  const first = decoratorExpr.getElements()[0];
4209
- if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
4324
+ if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
4210
4325
  }
4211
4326
  return void 0;
4212
4327
  }
@@ -4228,7 +4343,8 @@ function joinPaths(prefix, suffix) {
4228
4343
  if (!prefix && !suffix) return "/";
4229
4344
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4230
4345
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4231
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4346
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4347
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4232
4348
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4233
4349
  const combined = p + s;
4234
4350
  return combined === "" ? "/" : combined;
@@ -4282,7 +4398,8 @@ function buildRoute(args) {
4282
4398
  methodAs,
4283
4399
  sourceFile,
4284
4400
  seenNames,
4285
- contractSource
4401
+ contractSource,
4402
+ mixin
4286
4403
  } = args;
4287
4404
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4288
4405
  const qualifiedRef = `${className}.${methodName}`;
@@ -4298,7 +4415,12 @@ function buildRoute(args) {
4298
4415
  path: combinedPath,
4299
4416
  name: routeName,
4300
4417
  params: extractParams(combinedPath),
4301
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4418
+ controllerRef: {
4419
+ className,
4420
+ methodName,
4421
+ filePath: sourceFile.getFilePath(),
4422
+ ...mixin ? { mixin } : {}
4423
+ },
4302
4424
  contract: { contractSource }
4303
4425
  };
4304
4426
  }
@@ -4312,16 +4434,17 @@ function extractContractRoute(args) {
4312
4434
  className,
4313
4435
  sourceFile,
4314
4436
  project,
4315
- seenNames
4437
+ seenNames,
4438
+ mixin
4316
4439
  } = args;
4317
4440
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4318
4441
  if (!firstDecoratorArg) return null;
4319
4442
  let contractDef = null;
4320
4443
  let bodyZodRef = null;
4321
4444
  let queryZodRef = null;
4322
- if (Node8.isCallExpression(firstDecoratorArg)) {
4445
+ if (Node9.isCallExpression(firstDecoratorArg)) {
4323
4446
  contractDef = parseDefineContractCall(firstDecoratorArg);
4324
- } else if (Node8.isIdentifier(firstDecoratorArg)) {
4447
+ } else if (Node9.isIdentifier(firstDecoratorArg)) {
4325
4448
  const identName = firstDecoratorArg.getText();
4326
4449
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4327
4450
  if (!resolvedVar) {
@@ -4364,6 +4487,7 @@ function extractContractRoute(args) {
4364
4487
  methodAs,
4365
4488
  sourceFile,
4366
4489
  seenNames,
4490
+ mixin,
4367
4491
  contractSource: {
4368
4492
  query: contractDef.query,
4369
4493
  body: contractDef.body,
@@ -4380,13 +4504,14 @@ function extractContractRoute(args) {
4380
4504
  });
4381
4505
  }
4382
4506
  function extractDtoRoute(args) {
4383
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4507
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4384
4508
  if (!verb) return null;
4385
4509
  const combined = joinPaths(prefix, verb.handlerPath);
4386
4510
  const methodName = method.getName();
4387
4511
  const classAs = readAsDecorator(cls, `class ${className}`);
4388
4512
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4389
- const dtoContract = extractDtoContract(method, sourceFile, project);
4513
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4514
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4390
4515
  return buildRoute({
4391
4516
  className,
4392
4517
  methodName,
@@ -4396,10 +4521,11 @@ function extractDtoRoute(args) {
4396
4521
  methodAs,
4397
4522
  sourceFile,
4398
4523
  seenNames,
4524
+ mixin,
4399
4525
  contractSource: {
4400
4526
  query: dtoContract?.query ?? null,
4401
4527
  body: dtoContract?.body ?? null,
4402
- response: dtoContract?.response ?? "unknown",
4528
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4403
4529
  error: dtoContract?.error ?? null,
4404
4530
  queryRef: dtoContract?.queryRef ?? null,
4405
4531
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4428,7 +4554,17 @@ function extractFromSourceFile(sourceFile, project) {
4428
4554
  const firstArg2 = controllerDecorator.getArguments()[0];
4429
4555
  const prefix = decoratorStringArg(firstArg2) ?? "";
4430
4556
  const className = cls.getName() ?? "Unknown";
4431
- for (const method of cls.getMethods()) {
4557
+ const inherited = resolveInheritedMethods(cls);
4558
+ const mixinBinding = inherited ? {
4559
+ factoryName: inherited.factoryName,
4560
+ factoryFilePath: inherited.factoryFilePath,
4561
+ classArgs: inherited.classArgs
4562
+ } : void 0;
4563
+ const methods = [
4564
+ ...cls.getMethods().map((method) => ({ method })),
4565
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4566
+ ];
4567
+ for (const { method, mixin } of methods) {
4432
4568
  const verb = resolveVerb(method);
4433
4569
  const applyContractDecorator = method.getDecorator("ApplyContract");
4434
4570
  const route = applyContractDecorator ? extractContractRoute({
@@ -4440,7 +4576,8 @@ function extractFromSourceFile(sourceFile, project) {
4440
4576
  className,
4441
4577
  sourceFile,
4442
4578
  project,
4443
- seenNames
4579
+ seenNames,
4580
+ mixin
4444
4581
  }) : extractDtoRoute({
4445
4582
  cls,
4446
4583
  method,
@@ -4449,7 +4586,8 @@ function extractFromSourceFile(sourceFile, project) {
4449
4586
  className,
4450
4587
  sourceFile,
4451
4588
  project,
4452
- seenNames
4589
+ seenNames,
4590
+ mixin
4453
4591
  });
4454
4592
  if (route) routes.push(route);
4455
4593
  }
@@ -4650,7 +4788,7 @@ async function watch(config, onChange, options = {}) {
4650
4788
  }
4651
4789
 
4652
4790
  // src/index.ts
4653
- var VERSION = "0.15.0";
4791
+ var VERSION = "0.17.0";
4654
4792
 
4655
4793
  // src/generate-manifest.ts
4656
4794
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -4674,6 +4812,9 @@ function isManifestShape(value) {
4674
4812
  if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
4675
4813
  return false;
4676
4814
  }
4815
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
4816
+ return false;
4817
+ }
4677
4818
  if (!Array.isArray(candidate.files)) return false;
4678
4819
  return candidate.files.every((entry) => typeof entry === "string");
4679
4820
  }
@@ -4713,21 +4854,30 @@ async function discoverInputFiles(config) {
4713
4854
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4714
4855
  return [...new Set(matched)].sort();
4715
4856
  }
4716
- async function computeInputsHash(config) {
4857
+ async function computeInputsHash(config, extraInputs = []) {
4717
4858
  const hash = createHash("sha256");
4718
4859
  hash.update(`version:${VERSION}
4719
4860
  `);
4720
4861
  hash.update(`config:${serializeConfig(config)}
4721
4862
  `);
4722
- const inputFiles = await discoverInputFiles(config);
4723
4863
  const cwd = config.codegen.cwd;
4724
- for (const file of inputFiles) {
4864
+ const globbed = await discoverInputFiles(config);
4865
+ const globbedRelative = new Set(globbed.map((file) => relative7(cwd, file)));
4866
+ const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
4867
+ for (const file of globbed) {
4725
4868
  const contents = await readFile4(file, "utf8");
4726
- hash.update(`file:${relative6(cwd, file)}
4869
+ hash.update(`file:${relative7(cwd, file)}
4727
4870
  `);
4728
4871
  hash.update(contents);
4729
4872
  hash.update("\n");
4730
4873
  }
4874
+ for (const file of extra) {
4875
+ const contents = await readFile4(join16(cwd, file), "utf8").catch(() => null);
4876
+ hash.update(`extra:${file}
4877
+ `);
4878
+ hash.update(contents ?? "\0missing");
4879
+ hash.update("\n");
4880
+ }
4731
4881
  return hash.digest("hex");
4732
4882
  }
4733
4883
  async function readManifest(outDir) {
@@ -4741,6 +4891,7 @@ async function readManifest(outDir) {
4741
4891
  ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
4742
4892
  ...parsed.configHash ? { configHash: parsed.configHash } : {},
4743
4893
  ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
4894
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
4744
4895
  files: parsed.files
4745
4896
  };
4746
4897
  } catch {
@@ -4763,7 +4914,7 @@ async function listOutputFiles(outDir) {
4763
4914
  if (entry.isDirectory()) {
4764
4915
  await walk(abs);
4765
4916
  } else if (entry.isFile()) {
4766
- const rel = relative6(outDir, abs);
4917
+ const rel = relative7(outDir, abs);
4767
4918
  if (rel === MANIFEST_FILE || rel === LOCK_FILE2) continue;
4768
4919
  found.push(rel);
4769
4920
  }