@dudousxd/nestjs-inertia-codegen 1.0.7 → 1.3.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.d.cts CHANGED
@@ -62,14 +62,27 @@ declare class CodegenError extends Error {
62
62
  constructor(message: string, options?: ErrorOptions);
63
63
  }
64
64
 
65
+ interface TypeRef {
66
+ name: string;
67
+ filePath: string;
68
+ isArray?: boolean;
69
+ }
65
70
  interface ContractSource {
66
71
  query: string | null;
67
72
  body: string | null;
68
73
  response: string;
74
+ queryRef?: TypeRef | null;
75
+ bodyRef?: TypeRef | null;
76
+ responseRef?: TypeRef | null;
69
77
  }
70
78
  interface ContractDescriptor {
71
79
  contractSource: ContractSource;
72
80
  }
81
+ interface ControllerRef {
82
+ className: string;
83
+ methodName: string;
84
+ filePath: string;
85
+ }
73
86
  interface RouteDescriptor {
74
87
  method: string;
75
88
  path: string;
@@ -79,6 +92,7 @@ interface RouteDescriptor {
79
92
  source: 'path' | 'query' | 'body' | 'header';
80
93
  }>;
81
94
  contract?: ContractDescriptor;
95
+ controllerRef?: ControllerRef;
82
96
  }
83
97
 
84
98
  /**
@@ -120,6 +134,6 @@ declare function acquireLock(outDir: string): Promise<{
120
134
  release: () => Promise<void>;
121
135
  } | null>;
122
136
 
123
- declare const VERSION = "1.0.7";
137
+ declare const VERSION = "1.3.0";
124
138
 
125
139
  export { CodegenError, ConfigError, type ResolvedConfig, type ScopeConfig, type UserConfig, VERSION, type Watcher, acquireLock, defineConfig, generate, loadConfig, watch };
package/dist/index.d.ts CHANGED
@@ -62,14 +62,27 @@ declare class CodegenError extends Error {
62
62
  constructor(message: string, options?: ErrorOptions);
63
63
  }
64
64
 
65
+ interface TypeRef {
66
+ name: string;
67
+ filePath: string;
68
+ isArray?: boolean;
69
+ }
65
70
  interface ContractSource {
66
71
  query: string | null;
67
72
  body: string | null;
68
73
  response: string;
74
+ queryRef?: TypeRef | null;
75
+ bodyRef?: TypeRef | null;
76
+ responseRef?: TypeRef | null;
69
77
  }
70
78
  interface ContractDescriptor {
71
79
  contractSource: ContractSource;
72
80
  }
81
+ interface ControllerRef {
82
+ className: string;
83
+ methodName: string;
84
+ filePath: string;
85
+ }
73
86
  interface RouteDescriptor {
74
87
  method: string;
75
88
  path: string;
@@ -79,6 +92,7 @@ interface RouteDescriptor {
79
92
  source: 'path' | 'query' | 'body' | 'header';
80
93
  }>;
81
94
  contract?: ContractDescriptor;
95
+ controllerRef?: ControllerRef;
82
96
  }
83
97
 
84
98
  /**
@@ -120,6 +134,6 @@ declare function acquireLock(outDir: string): Promise<{
120
134
  release: () => Promise<void>;
121
135
  } | null>;
122
136
 
123
- declare const VERSION = "1.0.7";
137
+ declare const VERSION = "1.3.0";
124
138
 
125
139
  export { CodegenError, ConfigError, type ResolvedConfig, type ScopeConfig, type UserConfig, VERSION, type Watcher, acquireLock, defineConfig, generate, loadConfig, watch };
package/dist/index.js CHANGED
@@ -197,12 +197,12 @@ __name(extractPropsSource, "extractPropsSource");
197
197
 
198
198
  // src/emit/emit-api.ts
199
199
  import { mkdir, writeFile } from "fs/promises";
200
- import { join as join2 } from "path";
200
+ import { join as join2, relative as relative3 } from "path";
201
201
  async function emitApi(routes, outDir) {
202
202
  await mkdir(outDir, {
203
203
  recursive: true
204
204
  });
205
- const content = buildApiFile(routes);
205
+ const content = buildApiFile(routes, outDir);
206
206
  await writeFile(join2(outDir, "api.ts"), content, "utf8");
207
207
  }
208
208
  __name(emitApi, "emitApi");
@@ -267,7 +267,20 @@ function insertIntoTree(tree, segments, leaf, fullName) {
267
267
  }
268
268
  }
269
269
  __name(insertIntoTree, "insertIntoTree");
270
- function emitRouterTypeBlock(tree, indent) {
270
+ function buildResponseType(c, outDir) {
271
+ if (c.controllerRef) {
272
+ let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
273
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
274
+ return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
275
+ }
276
+ const respRef = c.contractSource.responseRef;
277
+ if (respRef) {
278
+ return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
279
+ }
280
+ return c.contractSource.response;
281
+ }
282
+ __name(buildResponseType, "buildResponseType");
283
+ function emitRouterTypeBlock(tree, indent, outDir) {
271
284
  const pad = " ".repeat(indent);
272
285
  const lines = [];
273
286
  for (const [key, node] of tree) {
@@ -275,15 +288,17 @@ function emitRouterTypeBlock(tree, indent) {
275
288
  if (node.kind === "leaf") {
276
289
  const c = node;
277
290
  const method = c.method.toUpperCase();
278
- const query = c.contractSource.query ?? "never";
279
- const body = method === "GET" ? "never" : c.contractSource.body ?? "never";
280
- const response = c.contractSource.response;
291
+ const queryRef = c.contractSource.queryRef;
292
+ const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : c.contractSource.query ?? "never";
293
+ const bodyRef = c.contractSource.bodyRef;
294
+ const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
295
+ const response = buildResponseType(c, outDir);
281
296
  const safeMethod = JSON.stringify(method);
282
297
  const safeUrl = JSON.stringify(c.path);
283
298
  lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; query: ${query}; body: ${body}; response: ${response} };`);
284
299
  } else {
285
300
  lines.push(`${pad}${objKey}: {`);
286
- lines.push(...emitRouterTypeBlock(node.children, indent + 2));
301
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
287
302
  lines.push(`${pad}};`);
288
303
  }
289
304
  }
@@ -304,18 +319,21 @@ function emitApiObjectBlock(tree, indent) {
304
319
  if (method === "GET") {
305
320
  const typeAccess = buildRouterTypeAccess(c.name);
306
321
  lines.push(`${pad}${objKey}: {`);
322
+ lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
307
323
  lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
308
- lines.push(`${pad} queryOptions({`);
309
- lines.push(`${pad} queryKey: [${flatName}, query],`);
324
+ lines.push(`${pad} _queryOptions({`);
325
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
310
326
  lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
311
327
  lines.push(`${pad} }),`);
312
328
  lines.push(`${pad}},`);
313
329
  } else {
314
330
  const typeAccess = buildRouterTypeAccess(c.name);
315
331
  lines.push(`${pad}${objKey}: {`);
316
- lines.push(`${pad} mutationOptions: () => ({`);
317
- lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
318
- lines.push(`${pad} }),`);
332
+ lines.push(`${pad} queryKey: () => [${flatName}] as const,`);
333
+ lines.push(`${pad} mutationOptions: () =>`);
334
+ lines.push(`${pad} _mutationOptions({`);
335
+ lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
336
+ lines.push(`${pad} }),`);
319
337
  lines.push(`${pad}},`);
320
338
  }
321
339
  } else {
@@ -332,18 +350,55 @@ function buildRouterTypeAccess(name) {
332
350
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
333
351
  }
334
352
  __name(buildRouterTypeAccess, "buildRouterTypeAccess");
335
- function buildApiFile(routes) {
353
+ function buildApiFile(routes, outDir) {
336
354
  const contracted = routes.filter((r) => r.contract);
355
+ const importsByFile = /* @__PURE__ */ new Map();
356
+ for (const r of contracted) {
357
+ const cs = r.contract?.contractSource;
358
+ if (!cs) continue;
359
+ const refs = r.controllerRef ? [
360
+ cs.queryRef,
361
+ cs.bodyRef
362
+ ] : [
363
+ cs.queryRef,
364
+ cs.bodyRef,
365
+ cs.responseRef
366
+ ];
367
+ for (const ref of refs) {
368
+ if (!ref) continue;
369
+ let names = importsByFile.get(ref.filePath);
370
+ if (!names) {
371
+ names = /* @__PURE__ */ new Set();
372
+ importsByFile.set(ref.filePath, names);
373
+ }
374
+ names.add(ref.name);
375
+ }
376
+ }
337
377
  const hasGetRoutes = contracted.some((r) => r.method === "GET");
378
+ const hasMutationRoutes = contracted.some((r) => r.method !== "GET");
338
379
  const lines = [
339
380
  "// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.",
340
381
  ""
341
382
  ];
342
- if (hasGetRoutes) {
343
- lines.push("import { queryOptions } from '@tanstack/query-core';");
383
+ const tqImports = [];
384
+ if (hasGetRoutes) tqImports.push("queryOptions as _queryOptions");
385
+ if (hasMutationRoutes) tqImports.push("mutationOptions as _mutationOptions");
386
+ if (tqImports.length > 0) {
387
+ lines.push(`import { ${tqImports.join(", ")} } from '@tanstack/react-query';`);
344
388
  }
345
389
  lines.push("import { route } from './routes.js';");
346
390
  lines.push("import { createFetcher } from '@dudousxd/nestjs-inertia-client';");
391
+ if (importsByFile.size > 0 && outDir) {
392
+ lines.push("");
393
+ for (const [filePath, names] of importsByFile) {
394
+ let relPath = relative3(outDir, filePath).replace(/\.ts$/, "");
395
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
396
+ const sortedNames = [
397
+ ...names
398
+ ].sort();
399
+ lines.push(`import type { ${sortedNames.join(", ")} } from '${relPath}';`);
400
+ }
401
+ }
347
402
  lines.push("");
348
403
  lines.push("export const fetcher = createFetcher();");
349
404
  lines.push("");
@@ -384,13 +439,14 @@ function buildApiFile(routes) {
384
439
  method: r.method,
385
440
  name,
386
441
  path: r.path,
442
+ controllerRef: r.controllerRef,
387
443
  contractSource: c.contractSource
388
444
  };
389
445
  insertIntoTree(tree, segments, leaf, name);
390
446
  }
391
447
  void detectCollisions;
392
448
  lines.push("export type ApiRouter = {");
393
- lines.push(...emitRouterTypeBlock(tree, 2));
449
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
394
450
  lines.push("};");
395
451
  lines.push("");
396
452
  lines.push("export const api = {");
@@ -660,9 +716,28 @@ import { join as join9 } from "path";
660
716
  import chokidar from "chokidar";
661
717
 
662
718
  // src/discovery/contracts-fast.ts
719
+ import { readFileSync } from "fs";
663
720
  import { dirname, join as join7, resolve as resolve2 } from "path";
664
721
  import fg2 from "fast-glob";
665
722
  import { Node, Project, SyntaxKind } from "ts-morph";
723
+ var _projectRoot = "";
724
+ var _tsconfigPaths = null;
725
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
726
+ function dbg(...args) {
727
+ if (_debug) console.log("[codegen:debug]", ...args);
728
+ }
729
+ __name(dbg, "dbg");
730
+ function loadTsconfigPaths(tsconfigPath) {
731
+ try {
732
+ const raw = readFileSync(tsconfigPath, "utf8");
733
+ const stripped = raw.replace(/\/\/.*$/gm, "");
734
+ const parsed = JSON.parse(stripped);
735
+ return parsed.compilerOptions?.paths ?? null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ }
740
+ __name(loadTsconfigPaths, "loadTsconfigPaths");
666
741
  async function discoverContractsFast(opts) {
667
742
  const { cwd, glob, tsconfig } = opts;
668
743
  const tsconfigPath = tsconfig ? resolve2(tsconfig) : join7(cwd, "tsconfig.json");
@@ -695,6 +770,8 @@ async function discoverContractsFast(opts) {
695
770
  project.addSourceFileAtPath(f);
696
771
  }
697
772
  const routes = [];
773
+ _projectRoot = cwd;
774
+ _tsconfigPaths = loadTsconfigPaths(tsconfigPath);
698
775
  for (const sourceFile of project.getSourceFiles()) {
699
776
  routes.push(...extractFromSourceFile(sourceFile, project));
700
777
  }
@@ -896,17 +973,41 @@ function findTypeInFile(name, file) {
896
973
  return null;
897
974
  }
898
975
  __name(findTypeInFile, "findTypeInFile");
976
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
977
+ if (moduleSpecifier.startsWith(".")) {
978
+ const dir = dirname(sourceFile.getFilePath());
979
+ return [
980
+ resolve2(dir, `${moduleSpecifier}.ts`),
981
+ resolve2(dir, moduleSpecifier, "index.ts")
982
+ ];
983
+ }
984
+ const baseUrl = _projectRoot;
985
+ dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(_tsconfigPaths), "baseUrl:", baseUrl);
986
+ if (_tsconfigPaths) {
987
+ for (const [pattern, mappings] of Object.entries(_tsconfigPaths)) {
988
+ const prefix = pattern.replace("*", "");
989
+ if (moduleSpecifier.startsWith(prefix)) {
990
+ const rest = moduleSpecifier.slice(prefix.length);
991
+ const candidates = [];
992
+ for (const mapping of mappings) {
993
+ const resolved = resolve2(baseUrl, mapping.replace("*", rest));
994
+ candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
995
+ }
996
+ dbg(" resolved candidates:", candidates);
997
+ return candidates;
998
+ }
999
+ }
1000
+ }
1001
+ return [];
1002
+ }
1003
+ __name(resolveModuleSpecifier, "resolveModuleSpecifier");
899
1004
  function resolveImportedType(name, sourceFile, project) {
900
1005
  for (const importDecl of sourceFile.getImportDeclarations()) {
901
1006
  const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
902
1007
  if (!namedImport) continue;
903
1008
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
904
- if (!moduleSpecifier.startsWith(".")) return null;
905
- const dir = dirname(sourceFile.getFilePath());
906
- const candidates = [
907
- resolve2(dir, `${moduleSpecifier}.ts`),
908
- resolve2(dir, moduleSpecifier, "index.ts")
909
- ];
1009
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
1010
+ if (candidates.length === 0) continue;
910
1011
  for (const candidate of candidates) {
911
1012
  let importedFile = project.getSourceFile(candidate);
912
1013
  if (!importedFile) {
@@ -940,7 +1041,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
940
1041
  const name = Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
941
1042
  if (name === "string" || name === "number" || name === "boolean") return name;
942
1043
  if (name === "Date") return "string";
943
- if (name === "unknown" || name === "any") return "unknown";
1044
+ if (name === "unknown" || name === "any" || name === "void") return "unknown";
1045
+ if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream") return "unknown";
944
1046
  if (name === "Array") {
945
1047
  const typeArgs = typeNode.getTypeArguments();
946
1048
  const firstTypeArg = typeArgs[0];
@@ -949,6 +1051,18 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
949
1051
  }
950
1052
  return "Array<unknown>";
951
1053
  }
1054
+ if ([
1055
+ "Record",
1056
+ "Omit",
1057
+ "Pick",
1058
+ "Partial",
1059
+ "Required",
1060
+ "Readonly",
1061
+ "Map",
1062
+ "Set"
1063
+ ].includes(name)) {
1064
+ return typeNode.getText();
1065
+ }
952
1066
  if (name === "Promise") {
953
1067
  const typeArgs = typeNode.getTypeArguments();
954
1068
  const firstTypeArg = typeArgs[0];
@@ -961,7 +1075,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
961
1075
  if (resolved) {
962
1076
  return expandTypeDecl(resolved, project, depth - 1);
963
1077
  }
964
- return name;
1078
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1079
+ return "unknown";
965
1080
  }
966
1081
  const kind = typeNode.getKind();
967
1082
  if (kind === SyntaxKind.StringKeyword) return "string";
@@ -1088,6 +1203,68 @@ function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1088
1203
  return name;
1089
1204
  }
1090
1205
  __name(resolveIdentifierToClassType, "resolveIdentifierToClassType");
1206
+ function tryResolveTypeRef(typeNode, sourceFile, project) {
1207
+ if (Node.isTypeReference(typeNode)) {
1208
+ const typeName = typeNode.getTypeName();
1209
+ const name = Node.isIdentifier(typeName) ? typeName.getText() : null;
1210
+ if (!name) return null;
1211
+ if (name === "Promise") {
1212
+ const typeArgs = typeNode.getTypeArguments();
1213
+ const first = typeArgs[0];
1214
+ if (first) return tryResolveTypeRef(first, sourceFile, project);
1215
+ return null;
1216
+ }
1217
+ if (name === "Array") {
1218
+ const typeArgs = typeNode.getTypeArguments();
1219
+ const first = typeArgs[0];
1220
+ if (first) {
1221
+ const inner = tryResolveTypeRef(first, sourceFile, project);
1222
+ if (inner) return {
1223
+ ...inner,
1224
+ isArray: true
1225
+ };
1226
+ }
1227
+ return null;
1228
+ }
1229
+ if ([
1230
+ "string",
1231
+ "number",
1232
+ "boolean",
1233
+ "void",
1234
+ "unknown",
1235
+ "any",
1236
+ "Date"
1237
+ ].includes(name)) {
1238
+ return null;
1239
+ }
1240
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1241
+ if (localDecl?.isExported()) {
1242
+ return {
1243
+ name,
1244
+ filePath: sourceFile.getFilePath()
1245
+ };
1246
+ }
1247
+ const resolved = resolveImportedType(name, sourceFile, project);
1248
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface")) {
1249
+ const decl = resolved.decl;
1250
+ if (decl.isExported()) {
1251
+ return {
1252
+ name,
1253
+ filePath: resolved.file.getFilePath()
1254
+ };
1255
+ }
1256
+ }
1257
+ }
1258
+ if (Node.isArrayTypeNode(typeNode)) {
1259
+ const inner = tryResolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project);
1260
+ if (inner) return {
1261
+ ...inner,
1262
+ isArray: true
1263
+ };
1264
+ }
1265
+ return null;
1266
+ }
1267
+ __name(tryResolveTypeRef, "tryResolveTypeRef");
1091
1268
  function extractDtoContract(method, sourceFile, project) {
1092
1269
  const body = extractBodyType(method, sourceFile, project);
1093
1270
  const query = extractQueryType(method, sourceFile, project);
@@ -1096,11 +1273,61 @@ function extractDtoContract(method, sourceFile, project) {
1096
1273
  if (body === null && query === null && paramsType === null && response === "unknown") {
1097
1274
  return null;
1098
1275
  }
1276
+ let bodyRef = null;
1277
+ let queryRef = null;
1278
+ let responseRef = null;
1279
+ for (const param of method.getParameters()) {
1280
+ if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode()) {
1281
+ bodyRef = tryResolveTypeRef(param.getTypeNode(), sourceFile, project);
1282
+ }
1283
+ if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode()) {
1284
+ queryRef = tryResolveTypeRef(param.getTypeNode(), sourceFile, project);
1285
+ }
1286
+ }
1287
+ const returnTypeNode = method.getReturnTypeNode();
1288
+ if (returnTypeNode) {
1289
+ responseRef = tryResolveTypeRef(returnTypeNode, sourceFile, project);
1290
+ }
1291
+ if (!responseRef) {
1292
+ const apiResp = method.getDecorator("ApiResponse");
1293
+ if (apiResp) {
1294
+ const args = apiResp.getArguments();
1295
+ const optsArg = args[0];
1296
+ if (optsArg && Node.isObjectLiteralExpression(optsArg)) {
1297
+ for (const prop of optsArg.getProperties()) {
1298
+ if (Node.isPropertyAssignment(prop) && prop.getName() === "type") {
1299
+ const val = prop.getInitializer();
1300
+ if (val && Node.isIdentifier(val)) {
1301
+ const name = val.getText();
1302
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1303
+ if (localDecl?.isExported()) {
1304
+ responseRef = {
1305
+ name,
1306
+ filePath: sourceFile.getFilePath()
1307
+ };
1308
+ } else {
1309
+ const resolved = resolveImportedType(name, sourceFile, project);
1310
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
1311
+ responseRef = {
1312
+ name,
1313
+ filePath: resolved.file.getFilePath()
1314
+ };
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1099
1323
  return {
1100
1324
  query,
1101
1325
  body,
1102
1326
  response,
1103
- params: paramsType
1327
+ params: paramsType,
1328
+ queryRef,
1329
+ bodyRef,
1330
+ responseRef
1104
1331
  };
1105
1332
  }
1106
1333
  __name(extractDtoContract, "extractDtoContract");
@@ -1199,6 +1426,11 @@ function extractFromSourceFile(sourceFile, project) {
1199
1426
  path: combined,
1200
1427
  name: routeName,
1201
1428
  params,
1429
+ controllerRef: {
1430
+ className,
1431
+ methodName,
1432
+ filePath: sourceFile.getFilePath()
1433
+ },
1202
1434
  contract: {
1203
1435
  contractSource: {
1204
1436
  query: contractDef.query,
@@ -1233,13 +1465,20 @@ function extractFromSourceFile(sourceFile, project) {
1233
1465
  path: combined,
1234
1466
  name: routeName,
1235
1467
  params,
1236
- // Attach contract if DTO extraction produced useful type info
1468
+ controllerRef: {
1469
+ className,
1470
+ methodName,
1471
+ filePath: sourceFile.getFilePath()
1472
+ },
1237
1473
  ...dtoContract ? {
1238
1474
  contract: {
1239
1475
  contractSource: {
1240
1476
  query: dtoContract.query,
1241
1477
  body: dtoContract.body,
1242
- response: dtoContract.response
1478
+ response: dtoContract.response,
1479
+ queryRef: dtoContract.queryRef,
1480
+ bodyRef: dtoContract.bodyRef,
1481
+ responseRef: dtoContract.responseRef
1243
1482
  }
1244
1483
  }
1245
1484
  } : {}
@@ -1412,7 +1651,7 @@ async function watch(config, onChange) {
1412
1651
  __name(watch, "watch");
1413
1652
 
1414
1653
  // src/index.ts
1415
- var VERSION = "1.0.7";
1654
+ var VERSION = "1.3.0";
1416
1655
  export {
1417
1656
  CodegenError,
1418
1657
  ConfigError,