@dudousxd/nestjs-inertia-codegen 1.2.0 → 1.4.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
@@ -78,6 +78,11 @@ interface ContractSource {
78
78
  interface ContractDescriptor {
79
79
  contractSource: ContractSource;
80
80
  }
81
+ interface ControllerRef {
82
+ className: string;
83
+ methodName: string;
84
+ filePath: string;
85
+ }
81
86
  interface RouteDescriptor {
82
87
  method: string;
83
88
  path: string;
@@ -87,6 +92,7 @@ interface RouteDescriptor {
87
92
  source: 'path' | 'query' | 'body' | 'header';
88
93
  }>;
89
94
  contract?: ContractDescriptor;
95
+ controllerRef?: ControllerRef;
90
96
  }
91
97
 
92
98
  /**
@@ -121,6 +127,9 @@ declare function watch(config: ResolvedConfig, onChange?: () => void): Promise<W
121
127
  /**
122
128
  * Try to acquire an exclusive lock for a watcher in `outDir`.
123
129
  *
130
+ * Uses O_CREAT | O_EXCL (via 'wx' flag) for atomic file creation to prevent
131
+ * TOCTOU race conditions between concurrent processes.
132
+ *
124
133
  * Returns `{ release }` on success.
125
134
  * Returns `null` if another live process already holds the lock.
126
135
  */
@@ -128,6 +137,6 @@ declare function acquireLock(outDir: string): Promise<{
128
137
  release: () => Promise<void>;
129
138
  } | null>;
130
139
 
131
- declare const VERSION = "1.2.0";
140
+ declare const VERSION = "1.4.0";
132
141
 
133
142
  export { CodegenError, ConfigError, type ResolvedConfig, type ScopeConfig, type UserConfig, VERSION, type Watcher, acquireLock, defineConfig, generate, loadConfig, watch };
package/dist/index.d.ts CHANGED
@@ -78,6 +78,11 @@ interface ContractSource {
78
78
  interface ContractDescriptor {
79
79
  contractSource: ContractSource;
80
80
  }
81
+ interface ControllerRef {
82
+ className: string;
83
+ methodName: string;
84
+ filePath: string;
85
+ }
81
86
  interface RouteDescriptor {
82
87
  method: string;
83
88
  path: string;
@@ -87,6 +92,7 @@ interface RouteDescriptor {
87
92
  source: 'path' | 'query' | 'body' | 'header';
88
93
  }>;
89
94
  contract?: ContractDescriptor;
95
+ controllerRef?: ControllerRef;
90
96
  }
91
97
 
92
98
  /**
@@ -121,6 +127,9 @@ declare function watch(config: ResolvedConfig, onChange?: () => void): Promise<W
121
127
  /**
122
128
  * Try to acquire an exclusive lock for a watcher in `outDir`.
123
129
  *
130
+ * Uses O_CREAT | O_EXCL (via 'wx' flag) for atomic file creation to prevent
131
+ * TOCTOU race conditions between concurrent processes.
132
+ *
124
133
  * Returns `{ release }` on success.
125
134
  * Returns `null` if another live process already holds the lock.
126
135
  */
@@ -128,6 +137,6 @@ declare function acquireLock(outDir: string): Promise<{
128
137
  release: () => Promise<void>;
129
138
  } | null>;
130
139
 
131
- declare const VERSION = "1.2.0";
140
+ declare const VERSION = "1.4.0";
132
141
 
133
142
  export { CodegenError, ConfigError, type ResolvedConfig, type ScopeConfig, type UserConfig, VERSION, type Watcher, acquireLock, defineConfig, generate, loadConfig, watch };
package/dist/index.js CHANGED
@@ -228,16 +228,6 @@ function validateNameSegment(seg, fullName) {
228
228
  }
229
229
  }
230
230
  __name(validateNameSegment, "validateNameSegment");
231
- function detectCollisions(tree, name) {
232
- for (const [key, node] of tree) {
233
- if (node.kind === "leaf") {
234
- } else {
235
- void key;
236
- }
237
- }
238
- void name;
239
- }
240
- __name(detectCollisions, "detectCollisions");
241
231
  function insertIntoTree(tree, segments, leaf, fullName) {
242
232
  const head = segments[0];
243
233
  const rest = segments.slice(1);
@@ -267,7 +257,30 @@ function insertIntoTree(tree, segments, leaf, fullName) {
267
257
  }
268
258
  }
269
259
  __name(insertIntoTree, "insertIntoTree");
270
- function emitRouterTypeBlock(tree, indent) {
260
+ function buildParamsType(params) {
261
+ const pathParams = params.filter((p) => p.source === "path");
262
+ if (pathParams.length === 0) return "never";
263
+ return `{ ${pathParams.map((p) => `${p.name}: string`).join("; ")} }`;
264
+ }
265
+ __name(buildParamsType, "buildParamsType");
266
+ function hasPathParams(params) {
267
+ return params.some((p) => p.source === "path");
268
+ }
269
+ __name(hasPathParams, "hasPathParams");
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) {
@@ -279,14 +292,14 @@ function emitRouterTypeBlock(tree, indent) {
279
292
  const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : c.contractSource.query ?? "never";
280
293
  const bodyRef = c.contractSource.bodyRef;
281
294
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
282
- const respRef = c.contractSource.responseRef;
283
- const response = respRef ? respRef.isArray ? `Array<${respRef.name}>` : respRef.name : c.contractSource.response;
295
+ const response = buildResponseType(c, outDir);
296
+ const params = buildParamsType(c.params);
284
297
  const safeMethod = JSON.stringify(method);
285
298
  const safeUrl = JSON.stringify(c.path);
286
- lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; query: ${query}; body: ${body}; response: ${response} };`);
299
+ lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response} };`);
287
300
  } else {
288
301
  lines.push(`${pad}${objKey}: {`);
289
- lines.push(...emitRouterTypeBlock(node.children, indent + 2));
302
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
290
303
  lines.push(`${pad}};`);
291
304
  }
292
305
  }
@@ -306,20 +319,61 @@ function emitApiObjectBlock(tree, indent) {
306
319
  const fetcherMethod = method.toLowerCase();
307
320
  if (method === "GET") {
308
321
  const typeAccess = buildRouterTypeAccess(c.name);
322
+ const withParams = hasPathParams(c.params);
309
323
  lines.push(`${pad}${objKey}: {`);
310
- lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
311
- lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) => ({`);
312
- lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
313
- lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
314
- lines.push(`${pad} }),`);
324
+ if (withParams) {
325
+ lines.push(`${pad} queryKey: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
326
+ lines.push(`${pad} queryOptions: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) =>`);
327
+ lines.push(`${pad} _queryOptions({`);
328
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
329
+ lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never, params as never) || ${safePath}, { query }),`);
330
+ lines.push(`${pad} }),`);
331
+ lines.push(`${pad} infiniteQueryOptions: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) => ({`);
332
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
333
+ lines.push(`${pad} queryFn: ({ pageParam }: { pageParam: number }) => fetcher.get<${typeAccess}['response']>(route(${flatName} as never, params as never) || ${safePath}, { query: { ...query, page: pageParam } }),`);
334
+ lines.push(`${pad} initialPageParam: 1,`);
335
+ lines.push(`${pad} getNextPageParam: (lastPage: ${typeAccess}['response']) => {`);
336
+ lines.push(`${pad} const meta = (lastPage as any)?.meta;`);
337
+ lines.push(`${pad} if (meta?.page != null && meta?.lastPage != null) {`);
338
+ lines.push(`${pad} return meta.page < meta.lastPage ? meta.page + 1 : undefined;`);
339
+ lines.push(`${pad} }`);
340
+ lines.push(`${pad} return undefined;`);
341
+ lines.push(`${pad} },`);
342
+ lines.push(`${pad} }),`);
343
+ } else {
344
+ lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
345
+ lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
346
+ lines.push(`${pad} _queryOptions({`);
347
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
348
+ lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
349
+ lines.push(`${pad} }),`);
350
+ lines.push(`${pad} infiniteQueryOptions: (query?: ${typeAccess}['query']) => ({`);
351
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
352
+ lines.push(`${pad} queryFn: ({ pageParam }: { pageParam: number }) => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query: { ...query, page: pageParam } }),`);
353
+ lines.push(`${pad} initialPageParam: 1,`);
354
+ lines.push(`${pad} getNextPageParam: (lastPage: ${typeAccess}['response']) => {`);
355
+ lines.push(`${pad} const meta = (lastPage as any)?.meta;`);
356
+ lines.push(`${pad} if (meta?.page != null && meta?.lastPage != null) {`);
357
+ lines.push(`${pad} return meta.page < meta.lastPage ? meta.page + 1 : undefined;`);
358
+ lines.push(`${pad} }`);
359
+ lines.push(`${pad} return undefined;`);
360
+ lines.push(`${pad} },`);
361
+ lines.push(`${pad} }),`);
362
+ }
315
363
  lines.push(`${pad}},`);
316
364
  } else {
317
365
  const typeAccess = buildRouterTypeAccess(c.name);
366
+ const withParams = hasPathParams(c.params);
318
367
  lines.push(`${pad}${objKey}: {`);
319
368
  lines.push(`${pad} queryKey: () => [${flatName}] as const,`);
320
- lines.push(`${pad} mutationOptions: () => ({`);
321
- lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
322
- lines.push(`${pad} }),`);
369
+ lines.push(`${pad} mutationOptions: () =>`);
370
+ lines.push(`${pad} _mutationOptions({`);
371
+ if (withParams) {
372
+ lines.push(`${pad} mutationFn: (input: { params: ${typeAccess}['params']; body: ${typeAccess}['body'] }) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never, input.params as never) || ${safePath}, { body: input.body }),`);
373
+ } else {
374
+ lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
375
+ }
376
+ lines.push(`${pad} }),`);
323
377
  lines.push(`${pad}},`);
324
378
  }
325
379
  } else {
@@ -342,11 +396,15 @@ function buildApiFile(routes, outDir) {
342
396
  for (const r of contracted) {
343
397
  const cs = r.contract?.contractSource;
344
398
  if (!cs) continue;
345
- for (const ref of [
399
+ const refs = r.controllerRef ? [
400
+ cs.queryRef,
401
+ cs.bodyRef
402
+ ] : [
346
403
  cs.queryRef,
347
404
  cs.bodyRef,
348
405
  cs.responseRef
349
- ]) {
406
+ ];
407
+ for (const ref of refs) {
350
408
  if (!ref) continue;
351
409
  let names = importsByFile.get(ref.filePath);
352
410
  if (!names) {
@@ -356,10 +414,18 @@ function buildApiFile(routes, outDir) {
356
414
  names.add(ref.name);
357
415
  }
358
416
  }
417
+ const hasGetRoutes = contracted.some((r) => r.method === "GET");
418
+ const hasMutationRoutes = contracted.some((r) => r.method !== "GET");
359
419
  const lines = [
360
420
  "// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.",
361
421
  ""
362
422
  ];
423
+ const tqImports = [];
424
+ if (hasGetRoutes) tqImports.push("queryOptions as _queryOptions");
425
+ if (hasMutationRoutes) tqImports.push("mutationOptions as _mutationOptions");
426
+ if (tqImports.length > 0) {
427
+ lines.push(`import { ${tqImports.join(", ")} } from '@tanstack/react-query';`);
428
+ }
363
429
  lines.push("import { route } from './routes.js';");
364
430
  lines.push("import { createFetcher } from '@dudousxd/nestjs-inertia-client';");
365
431
  if (importsByFile.size > 0 && outDir) {
@@ -413,13 +479,14 @@ function buildApiFile(routes, outDir) {
413
479
  method: r.method,
414
480
  name,
415
481
  path: r.path,
482
+ params: r.params,
483
+ controllerRef: r.controllerRef,
416
484
  contractSource: c.contractSource
417
485
  };
418
486
  insertIntoTree(tree, segments, leaf, name);
419
487
  }
420
- void detectCollisions;
421
488
  lines.push("export type ApiRouter = {");
422
- lines.push(...emitRouterTypeBlock(tree, 2));
489
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
423
490
  lines.push("};");
424
491
  lines.push("");
425
492
  lines.push("export const api = {");
@@ -515,8 +582,9 @@ __name(emitIndex, "emitIndex");
515
582
 
516
583
  // src/emit/emit-pages.ts
517
584
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
518
- import { join as join5 } from "path";
519
- async function emitPages(pages, outDir) {
585
+ import { join as join5, relative as relative4 } from "path";
586
+ async function emitPages(pages, outDir, options = {}) {
587
+ const propsExport = options.propsExport ?? "ComponentProps";
520
588
  await mkdir4(outDir, {
521
589
  recursive: true
522
590
  });
@@ -525,14 +593,40 @@ async function emitPages(pages, outDir) {
525
593
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
526
594
  return ` ${key}: ${propType};`;
527
595
  }).join("\n");
596
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
597
+ const augBody = pages.map((p) => {
598
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
599
+ const valueType = buildAugmentationType(p, outDir, propsExport);
600
+ return ` ${key}: ${valueType};`;
601
+ }).join("\n");
602
+ const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
528
603
  const content = `// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.
529
604
  export interface InertiaPages {
530
605
  ${body}
531
606
  }
607
+
608
+ export type InertiaPageName = ${pageNameUnion};
609
+ ` + propsHelper + `
610
+ declare module '@dudousxd/nestjs-inertia' {
611
+ interface InertiaPages {
612
+ ${augBody}
613
+ }
614
+ }
532
615
  `;
533
616
  await writeFile4(join5(outDir, "pages.d.ts"), content, "utf8");
534
617
  }
535
618
  __name(emitPages, "emitPages");
619
+ function buildAugmentationType(page, outDir, propsExport) {
620
+ if (!page.propsSource) {
621
+ return "Record<string, unknown>";
622
+ }
623
+ let importPath = relative4(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
624
+ if (!importPath.startsWith(".")) {
625
+ importPath = `./${importPath}`;
626
+ }
627
+ return `import('${importPath}').${propsExport}`;
628
+ }
629
+ __name(buildAugmentationType, "buildAugmentationType");
536
630
  function needsQuotes(name) {
537
631
  return !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
538
632
  }
@@ -669,7 +763,9 @@ async function generate(config, routes = []) {
669
763
  propsExport: config.pages.propsExport,
670
764
  componentNameStrategy: config.pages.componentNameStrategy
671
765
  });
672
- await emitPages(pages, config.codegen.outDir);
766
+ await emitPages(pages, config.codegen.outDir, {
767
+ propsExport: config.pages.propsExport
768
+ });
673
769
  await emitCache(pages, config.codegen.outDir);
674
770
  const hasRoutes = routes.length > 0;
675
771
  const hasContracts = routes.some((r) => r.contract);
@@ -689,9 +785,38 @@ import { join as join9 } from "path";
689
785
  import chokidar from "chokidar";
690
786
 
691
787
  // src/discovery/contracts-fast.ts
788
+ import { readFileSync } from "fs";
692
789
  import { dirname, join as join7, resolve as resolve2 } from "path";
693
790
  import fg2 from "fast-glob";
694
791
  import { Node, Project, SyntaxKind } from "ts-morph";
792
+ var _ctx = {
793
+ projectRoot: "",
794
+ tsconfigPaths: null
795
+ };
796
+ function _projectRoot() {
797
+ return _ctx.projectRoot;
798
+ }
799
+ __name(_projectRoot, "_projectRoot");
800
+ function _tsconfigPaths() {
801
+ return _ctx.tsconfigPaths;
802
+ }
803
+ __name(_tsconfigPaths, "_tsconfigPaths");
804
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
805
+ function dbg(...args) {
806
+ if (_debug) console.log("[codegen:debug]", ...args);
807
+ }
808
+ __name(dbg, "dbg");
809
+ function loadTsconfigPaths(tsconfigPath) {
810
+ try {
811
+ const raw = readFileSync(tsconfigPath, "utf8");
812
+ const stripped = raw.replace(/\/\/.*$/gm, "");
813
+ const parsed = JSON.parse(stripped);
814
+ return parsed.compilerOptions?.paths ?? null;
815
+ } catch {
816
+ return null;
817
+ }
818
+ }
819
+ __name(loadTsconfigPaths, "loadTsconfigPaths");
695
820
  async function discoverContractsFast(opts) {
696
821
  const { cwd, glob, tsconfig } = opts;
697
822
  const tsconfigPath = tsconfig ? resolve2(tsconfig) : join7(cwd, "tsconfig.json");
@@ -724,8 +849,17 @@ async function discoverContractsFast(opts) {
724
849
  project.addSourceFileAtPath(f);
725
850
  }
726
851
  const routes = [];
727
- for (const sourceFile of project.getSourceFiles()) {
728
- routes.push(...extractFromSourceFile(sourceFile, project));
852
+ const prevCtx = _ctx;
853
+ _ctx = {
854
+ projectRoot: cwd,
855
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
856
+ };
857
+ try {
858
+ for (const sourceFile of project.getSourceFiles()) {
859
+ routes.push(...extractFromSourceFile(sourceFile, project));
860
+ }
861
+ } finally {
862
+ _ctx = prevCtx;
729
863
  }
730
864
  return routes;
731
865
  }
@@ -925,17 +1059,42 @@ function findTypeInFile(name, file) {
925
1059
  return null;
926
1060
  }
927
1061
  __name(findTypeInFile, "findTypeInFile");
1062
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1063
+ if (moduleSpecifier.startsWith(".")) {
1064
+ const dir = dirname(sourceFile.getFilePath());
1065
+ return [
1066
+ resolve2(dir, `${moduleSpecifier}.ts`),
1067
+ resolve2(dir, moduleSpecifier, "index.ts")
1068
+ ];
1069
+ }
1070
+ const baseUrl = _projectRoot();
1071
+ const tsconfigPaths = _tsconfigPaths();
1072
+ dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(tsconfigPaths), "baseUrl:", baseUrl);
1073
+ if (tsconfigPaths) {
1074
+ for (const [pattern, mappings] of Object.entries(tsconfigPaths)) {
1075
+ const prefix = pattern.replace("*", "");
1076
+ if (moduleSpecifier.startsWith(prefix)) {
1077
+ const rest = moduleSpecifier.slice(prefix.length);
1078
+ const candidates = [];
1079
+ for (const mapping of mappings) {
1080
+ const resolved = resolve2(baseUrl, mapping.replace("*", rest));
1081
+ candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
1082
+ }
1083
+ dbg(" resolved candidates:", candidates);
1084
+ return candidates;
1085
+ }
1086
+ }
1087
+ }
1088
+ return [];
1089
+ }
1090
+ __name(resolveModuleSpecifier, "resolveModuleSpecifier");
928
1091
  function resolveImportedType(name, sourceFile, project) {
929
1092
  for (const importDecl of sourceFile.getImportDeclarations()) {
930
1093
  const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
931
1094
  if (!namedImport) continue;
932
1095
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
933
- if (!moduleSpecifier.startsWith(".")) return null;
934
- const dir = dirname(sourceFile.getFilePath());
935
- const candidates = [
936
- resolve2(dir, `${moduleSpecifier}.ts`),
937
- resolve2(dir, moduleSpecifier, "index.ts")
938
- ];
1096
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
1097
+ if (candidates.length === 0) continue;
939
1098
  for (const candidate of candidates) {
940
1099
  let importedFile = project.getSourceFile(candidate);
941
1100
  if (!importedFile) {
@@ -979,6 +1138,18 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
979
1138
  }
980
1139
  return "Array<unknown>";
981
1140
  }
1141
+ if ([
1142
+ "Record",
1143
+ "Omit",
1144
+ "Pick",
1145
+ "Partial",
1146
+ "Required",
1147
+ "Readonly",
1148
+ "Map",
1149
+ "Set"
1150
+ ].includes(name)) {
1151
+ return typeNode.getText();
1152
+ }
982
1153
  if (name === "Promise") {
983
1154
  const typeArgs = typeNode.getTypeArguments();
984
1155
  const firstTypeArg = typeArgs[0];
@@ -991,7 +1162,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
991
1162
  if (resolved) {
992
1163
  return expandTypeDecl(resolved, project, depth - 1);
993
1164
  }
994
- return name;
1165
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1166
+ return "unknown";
995
1167
  }
996
1168
  const kind = typeNode.getKind();
997
1169
  if (kind === SyntaxKind.StringKeyword) return "string";
@@ -1153,7 +1325,7 @@ function tryResolveTypeRef(typeNode, sourceFile, project) {
1153
1325
  return null;
1154
1326
  }
1155
1327
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1156
- if (localDecl && localDecl.isExported()) {
1328
+ if (localDecl?.isExported()) {
1157
1329
  return {
1158
1330
  name,
1159
1331
  filePath: sourceFile.getFilePath()
@@ -1215,7 +1387,7 @@ function extractDtoContract(method, sourceFile, project) {
1215
1387
  if (val && Node.isIdentifier(val)) {
1216
1388
  const name = val.getText();
1217
1389
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1218
- if (localDecl && localDecl.isExported()) {
1390
+ if (localDecl?.isExported()) {
1219
1391
  responseRef = {
1220
1392
  name,
1221
1393
  filePath: sourceFile.getFilePath()
@@ -1341,6 +1513,11 @@ function extractFromSourceFile(sourceFile, project) {
1341
1513
  path: combined,
1342
1514
  name: routeName,
1343
1515
  params,
1516
+ controllerRef: {
1517
+ className,
1518
+ methodName,
1519
+ filePath: sourceFile.getFilePath()
1520
+ },
1344
1521
  contract: {
1345
1522
  contractSource: {
1346
1523
  query: contractDef.query,
@@ -1375,19 +1552,21 @@ function extractFromSourceFile(sourceFile, project) {
1375
1552
  path: combined,
1376
1553
  name: routeName,
1377
1554
  params,
1378
- // Attach contract if DTO extraction produced useful type info
1379
- ...dtoContract ? {
1380
- contract: {
1381
- contractSource: {
1382
- query: dtoContract.query,
1383
- body: dtoContract.body,
1384
- response: dtoContract.response,
1385
- queryRef: dtoContract.queryRef,
1386
- bodyRef: dtoContract.bodyRef,
1387
- responseRef: dtoContract.responseRef
1388
- }
1555
+ controllerRef: {
1556
+ className,
1557
+ methodName,
1558
+ filePath: sourceFile.getFilePath()
1559
+ },
1560
+ contract: {
1561
+ contractSource: {
1562
+ query: dtoContract?.query ?? null,
1563
+ body: dtoContract?.body ?? null,
1564
+ response: dtoContract?.response ?? "unknown",
1565
+ queryRef: dtoContract?.queryRef,
1566
+ bodyRef: dtoContract?.bodyRef,
1567
+ responseRef: dtoContract?.responseRef
1389
1568
  }
1390
- } : {}
1569
+ }
1391
1570
  });
1392
1571
  }
1393
1572
  }
@@ -1397,7 +1576,8 @@ function extractFromSourceFile(sourceFile, project) {
1397
1576
  __name(extractFromSourceFile, "extractFromSourceFile");
1398
1577
 
1399
1578
  // src/watch/lock-file.ts
1400
- import { mkdir as mkdir6, readFile as readFile2, unlink, writeFile as writeFile6 } from "fs/promises";
1579
+ import { open } from "fs/promises";
1580
+ import { mkdir as mkdir6, readFile as readFile2, unlink } from "fs/promises";
1401
1581
  import { join as join8 } from "path";
1402
1582
  var LOCK_FILE = ".watcher.lock";
1403
1583
  function isProcessAlive(pid) {
@@ -1414,20 +1594,29 @@ async function acquireLock(outDir) {
1414
1594
  recursive: true
1415
1595
  });
1416
1596
  const lockPath = join8(outDir, LOCK_FILE);
1417
- try {
1418
- const raw = await readFile2(lockPath, "utf8");
1419
- const existing = JSON.parse(raw);
1420
- if (isProcessAlive(existing.pid)) {
1421
- return null;
1422
- }
1423
- } catch {
1424
- }
1425
1597
  const lockData = {
1426
1598
  pid: process.pid,
1427
1599
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1428
1600
  };
1429
- await writeFile6(lockPath, `${JSON.stringify(lockData, null, 2)}
1601
+ try {
1602
+ const fd = await open(lockPath, "wx");
1603
+ await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
1430
1604
  `, "utf8");
1605
+ await fd.close();
1606
+ } catch (err) {
1607
+ if (err.code === "EEXIST") {
1608
+ try {
1609
+ const raw = await readFile2(lockPath, "utf8");
1610
+ const existing = JSON.parse(raw);
1611
+ if (isProcessAlive(existing.pid)) return null;
1612
+ await unlink(lockPath);
1613
+ return acquireLock(outDir);
1614
+ } catch {
1615
+ return null;
1616
+ }
1617
+ }
1618
+ return null;
1619
+ }
1431
1620
  return {
1432
1621
  release: /* @__PURE__ */ __name(async () => {
1433
1622
  try {
@@ -1491,7 +1680,8 @@ async function watch(config, onChange) {
1491
1680
  pagesDebounceTimer = void 0;
1492
1681
  try {
1493
1682
  await generate(config);
1494
- } catch {
1683
+ } catch (err) {
1684
+ console.error("[nestjs-inertia-codegen] Pages generation failed:", err instanceof Error ? err.message : err);
1495
1685
  }
1496
1686
  onChange?.();
1497
1687
  }, PAGES_DEBOUNCE_MS);
@@ -1529,7 +1719,8 @@ async function watch(config, onChange) {
1529
1719
  if (hasContracts) {
1530
1720
  await emitApi(routes, config.codegen.outDir);
1531
1721
  }
1532
- } catch {
1722
+ } catch (err) {
1723
+ console.error("[nestjs-inertia-codegen] Contracts generation failed:", err instanceof Error ? err.message : err);
1533
1724
  }
1534
1725
  onChange?.();
1535
1726
  }, config.contracts.debounceMs);
@@ -1557,7 +1748,7 @@ async function watch(config, onChange) {
1557
1748
  __name(watch, "watch");
1558
1749
 
1559
1750
  // src/index.ts
1560
- var VERSION = "1.2.0";
1751
+ var VERSION = "1.4.0";
1561
1752
  export {
1562
1753
  CodegenError,
1563
1754
  ConfigError,