@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/cli/main.js CHANGED
@@ -216,16 +216,6 @@ function validateNameSegment(seg, fullName) {
216
216
  }
217
217
  }
218
218
  __name(validateNameSegment, "validateNameSegment");
219
- function detectCollisions(tree, name) {
220
- for (const [key, node] of tree) {
221
- if (node.kind === "leaf") {
222
- } else {
223
- void key;
224
- }
225
- }
226
- void name;
227
- }
228
- __name(detectCollisions, "detectCollisions");
229
219
  function insertIntoTree(tree, segments, leaf, fullName) {
230
220
  const head = segments[0];
231
221
  const rest = segments.slice(1);
@@ -255,7 +245,30 @@ function insertIntoTree(tree, segments, leaf, fullName) {
255
245
  }
256
246
  }
257
247
  __name(insertIntoTree, "insertIntoTree");
258
- function emitRouterTypeBlock(tree, indent) {
248
+ function buildParamsType(params) {
249
+ const pathParams = params.filter((p) => p.source === "path");
250
+ if (pathParams.length === 0) return "never";
251
+ return `{ ${pathParams.map((p) => `${p.name}: string`).join("; ")} }`;
252
+ }
253
+ __name(buildParamsType, "buildParamsType");
254
+ function hasPathParams(params) {
255
+ return params.some((p) => p.source === "path");
256
+ }
257
+ __name(hasPathParams, "hasPathParams");
258
+ function buildResponseType(c, outDir) {
259
+ if (c.controllerRef) {
260
+ let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
261
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
262
+ return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
263
+ }
264
+ const respRef = c.contractSource.responseRef;
265
+ if (respRef) {
266
+ return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
267
+ }
268
+ return c.contractSource.response;
269
+ }
270
+ __name(buildResponseType, "buildResponseType");
271
+ function emitRouterTypeBlock(tree, indent, outDir) {
259
272
  const pad = " ".repeat(indent);
260
273
  const lines = [];
261
274
  for (const [key, node] of tree) {
@@ -267,14 +280,14 @@ function emitRouterTypeBlock(tree, indent) {
267
280
  const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : c.contractSource.query ?? "never";
268
281
  const bodyRef = c.contractSource.bodyRef;
269
282
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
270
- const respRef = c.contractSource.responseRef;
271
- const response = respRef ? respRef.isArray ? `Array<${respRef.name}>` : respRef.name : c.contractSource.response;
283
+ const response = buildResponseType(c, outDir);
284
+ const params = buildParamsType(c.params);
272
285
  const safeMethod = JSON.stringify(method);
273
286
  const safeUrl = JSON.stringify(c.path);
274
- lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; query: ${query}; body: ${body}; response: ${response} };`);
287
+ lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response} };`);
275
288
  } else {
276
289
  lines.push(`${pad}${objKey}: {`);
277
- lines.push(...emitRouterTypeBlock(node.children, indent + 2));
290
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
278
291
  lines.push(`${pad}};`);
279
292
  }
280
293
  }
@@ -294,20 +307,61 @@ function emitApiObjectBlock(tree, indent) {
294
307
  const fetcherMethod = method.toLowerCase();
295
308
  if (method === "GET") {
296
309
  const typeAccess = buildRouterTypeAccess(c.name);
310
+ const withParams = hasPathParams(c.params);
297
311
  lines.push(`${pad}${objKey}: {`);
298
- lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
299
- lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) => ({`);
300
- lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
301
- lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
302
- lines.push(`${pad} }),`);
312
+ if (withParams) {
313
+ lines.push(`${pad} queryKey: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
314
+ lines.push(`${pad} queryOptions: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) =>`);
315
+ lines.push(`${pad} _queryOptions({`);
316
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
317
+ lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never, params as never) || ${safePath}, { query }),`);
318
+ lines.push(`${pad} }),`);
319
+ lines.push(`${pad} infiniteQueryOptions: (params: ${typeAccess}['params'], query?: ${typeAccess}['query']) => ({`);
320
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, params, query] as const : [${flatName}, params] as const,`);
321
+ lines.push(`${pad} queryFn: ({ pageParam }: { pageParam: number }) => fetcher.get<${typeAccess}['response']>(route(${flatName} as never, params as never) || ${safePath}, { query: { ...query, page: pageParam } }),`);
322
+ lines.push(`${pad} initialPageParam: 1,`);
323
+ lines.push(`${pad} getNextPageParam: (lastPage: ${typeAccess}['response']) => {`);
324
+ lines.push(`${pad} const meta = (lastPage as any)?.meta;`);
325
+ lines.push(`${pad} if (meta?.page != null && meta?.lastPage != null) {`);
326
+ lines.push(`${pad} return meta.page < meta.lastPage ? meta.page + 1 : undefined;`);
327
+ lines.push(`${pad} }`);
328
+ lines.push(`${pad} return undefined;`);
329
+ lines.push(`${pad} },`);
330
+ lines.push(`${pad} }),`);
331
+ } else {
332
+ lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
333
+ lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
334
+ lines.push(`${pad} _queryOptions({`);
335
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
336
+ lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
337
+ lines.push(`${pad} }),`);
338
+ lines.push(`${pad} infiniteQueryOptions: (query?: ${typeAccess}['query']) => ({`);
339
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
340
+ lines.push(`${pad} queryFn: ({ pageParam }: { pageParam: number }) => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query: { ...query, page: pageParam } }),`);
341
+ lines.push(`${pad} initialPageParam: 1,`);
342
+ lines.push(`${pad} getNextPageParam: (lastPage: ${typeAccess}['response']) => {`);
343
+ lines.push(`${pad} const meta = (lastPage as any)?.meta;`);
344
+ lines.push(`${pad} if (meta?.page != null && meta?.lastPage != null) {`);
345
+ lines.push(`${pad} return meta.page < meta.lastPage ? meta.page + 1 : undefined;`);
346
+ lines.push(`${pad} }`);
347
+ lines.push(`${pad} return undefined;`);
348
+ lines.push(`${pad} },`);
349
+ lines.push(`${pad} }),`);
350
+ }
303
351
  lines.push(`${pad}},`);
304
352
  } else {
305
353
  const typeAccess = buildRouterTypeAccess(c.name);
354
+ const withParams = hasPathParams(c.params);
306
355
  lines.push(`${pad}${objKey}: {`);
307
356
  lines.push(`${pad} queryKey: () => [${flatName}] as const,`);
308
- lines.push(`${pad} mutationOptions: () => ({`);
309
- lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
310
- lines.push(`${pad} }),`);
357
+ lines.push(`${pad} mutationOptions: () =>`);
358
+ lines.push(`${pad} _mutationOptions({`);
359
+ if (withParams) {
360
+ 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 }),`);
361
+ } else {
362
+ lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
363
+ }
364
+ lines.push(`${pad} }),`);
311
365
  lines.push(`${pad}},`);
312
366
  }
313
367
  } else {
@@ -330,11 +384,15 @@ function buildApiFile(routes, outDir) {
330
384
  for (const r of contracted) {
331
385
  const cs = r.contract?.contractSource;
332
386
  if (!cs) continue;
333
- for (const ref of [
387
+ const refs = r.controllerRef ? [
388
+ cs.queryRef,
389
+ cs.bodyRef
390
+ ] : [
334
391
  cs.queryRef,
335
392
  cs.bodyRef,
336
393
  cs.responseRef
337
- ]) {
394
+ ];
395
+ for (const ref of refs) {
338
396
  if (!ref) continue;
339
397
  let names = importsByFile.get(ref.filePath);
340
398
  if (!names) {
@@ -344,10 +402,18 @@ function buildApiFile(routes, outDir) {
344
402
  names.add(ref.name);
345
403
  }
346
404
  }
405
+ const hasGetRoutes = contracted.some((r) => r.method === "GET");
406
+ const hasMutationRoutes = contracted.some((r) => r.method !== "GET");
347
407
  const lines = [
348
408
  "// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.",
349
409
  ""
350
410
  ];
411
+ const tqImports = [];
412
+ if (hasGetRoutes) tqImports.push("queryOptions as _queryOptions");
413
+ if (hasMutationRoutes) tqImports.push("mutationOptions as _mutationOptions");
414
+ if (tqImports.length > 0) {
415
+ lines.push(`import { ${tqImports.join(", ")} } from '@tanstack/react-query';`);
416
+ }
351
417
  lines.push("import { route } from './routes.js';");
352
418
  lines.push("import { createFetcher } from '@dudousxd/nestjs-inertia-client';");
353
419
  if (importsByFile.size > 0 && outDir) {
@@ -401,13 +467,14 @@ function buildApiFile(routes, outDir) {
401
467
  method: r.method,
402
468
  name,
403
469
  path: r.path,
470
+ params: r.params,
471
+ controllerRef: r.controllerRef,
404
472
  contractSource: c.contractSource
405
473
  };
406
474
  insertIntoTree(tree, segments, leaf, name);
407
475
  }
408
- void detectCollisions;
409
476
  lines.push("export type ApiRouter = {");
410
- lines.push(...emitRouterTypeBlock(tree, 2));
477
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
411
478
  lines.push("};");
412
479
  lines.push("");
413
480
  lines.push("export const api = {");
@@ -503,8 +570,9 @@ __name(emitIndex, "emitIndex");
503
570
 
504
571
  // src/emit/emit-pages.ts
505
572
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
506
- import { join as join5 } from "path";
507
- async function emitPages(pages, outDir) {
573
+ import { join as join5, relative as relative4 } from "path";
574
+ async function emitPages(pages, outDir, options = {}) {
575
+ const propsExport = options.propsExport ?? "ComponentProps";
508
576
  await mkdir4(outDir, {
509
577
  recursive: true
510
578
  });
@@ -513,14 +581,40 @@ async function emitPages(pages, outDir) {
513
581
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
514
582
  return ` ${key}: ${propType};`;
515
583
  }).join("\n");
584
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
585
+ const augBody = pages.map((p) => {
586
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
587
+ const valueType = buildAugmentationType(p, outDir, propsExport);
588
+ return ` ${key}: ${valueType};`;
589
+ }).join("\n");
590
+ const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
516
591
  const content = `// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.
517
592
  export interface InertiaPages {
518
593
  ${body}
519
594
  }
595
+
596
+ export type InertiaPageName = ${pageNameUnion};
597
+ ` + propsHelper + `
598
+ declare module '@dudousxd/nestjs-inertia' {
599
+ interface InertiaPages {
600
+ ${augBody}
601
+ }
602
+ }
520
603
  `;
521
604
  await writeFile4(join5(outDir, "pages.d.ts"), content, "utf8");
522
605
  }
523
606
  __name(emitPages, "emitPages");
607
+ function buildAugmentationType(page, outDir, propsExport) {
608
+ if (!page.propsSource) {
609
+ return "Record<string, unknown>";
610
+ }
611
+ let importPath = relative4(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
612
+ if (!importPath.startsWith(".")) {
613
+ importPath = `./${importPath}`;
614
+ }
615
+ return `import('${importPath}').${propsExport}`;
616
+ }
617
+ __name(buildAugmentationType, "buildAugmentationType");
524
618
  function needsQuotes(name) {
525
619
  return !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
526
620
  }
@@ -657,7 +751,9 @@ async function generate(config, routes = []) {
657
751
  propsExport: config.pages.propsExport,
658
752
  componentNameStrategy: config.pages.componentNameStrategy
659
753
  });
660
- await emitPages(pages, config.codegen.outDir);
754
+ await emitPages(pages, config.codegen.outDir, {
755
+ propsExport: config.pages.propsExport
756
+ });
661
757
  await emitCache(pages, config.codegen.outDir);
662
758
  const hasRoutes = routes.length > 0;
663
759
  const hasContracts = routes.some((r) => r.contract);
@@ -677,9 +773,38 @@ import { join as join9 } from "path";
677
773
  import chokidar from "chokidar";
678
774
 
679
775
  // src/discovery/contracts-fast.ts
776
+ import { readFileSync } from "fs";
680
777
  import { dirname, join as join7, resolve as resolve2 } from "path";
681
778
  import fg2 from "fast-glob";
682
779
  import { Node, Project, SyntaxKind } from "ts-morph";
780
+ var _ctx = {
781
+ projectRoot: "",
782
+ tsconfigPaths: null
783
+ };
784
+ function _projectRoot() {
785
+ return _ctx.projectRoot;
786
+ }
787
+ __name(_projectRoot, "_projectRoot");
788
+ function _tsconfigPaths() {
789
+ return _ctx.tsconfigPaths;
790
+ }
791
+ __name(_tsconfigPaths, "_tsconfigPaths");
792
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
793
+ function dbg(...args) {
794
+ if (_debug) console.log("[codegen:debug]", ...args);
795
+ }
796
+ __name(dbg, "dbg");
797
+ function loadTsconfigPaths(tsconfigPath) {
798
+ try {
799
+ const raw = readFileSync(tsconfigPath, "utf8");
800
+ const stripped = raw.replace(/\/\/.*$/gm, "");
801
+ const parsed = JSON.parse(stripped);
802
+ return parsed.compilerOptions?.paths ?? null;
803
+ } catch {
804
+ return null;
805
+ }
806
+ }
807
+ __name(loadTsconfigPaths, "loadTsconfigPaths");
683
808
  async function discoverContractsFast(opts) {
684
809
  const { cwd, glob, tsconfig } = opts;
685
810
  const tsconfigPath = tsconfig ? resolve2(tsconfig) : join7(cwd, "tsconfig.json");
@@ -712,8 +837,17 @@ async function discoverContractsFast(opts) {
712
837
  project.addSourceFileAtPath(f);
713
838
  }
714
839
  const routes = [];
715
- for (const sourceFile of project.getSourceFiles()) {
716
- routes.push(...extractFromSourceFile(sourceFile, project));
840
+ const prevCtx = _ctx;
841
+ _ctx = {
842
+ projectRoot: cwd,
843
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
844
+ };
845
+ try {
846
+ for (const sourceFile of project.getSourceFiles()) {
847
+ routes.push(...extractFromSourceFile(sourceFile, project));
848
+ }
849
+ } finally {
850
+ _ctx = prevCtx;
717
851
  }
718
852
  return routes;
719
853
  }
@@ -913,17 +1047,42 @@ function findTypeInFile(name, file) {
913
1047
  return null;
914
1048
  }
915
1049
  __name(findTypeInFile, "findTypeInFile");
1050
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1051
+ if (moduleSpecifier.startsWith(".")) {
1052
+ const dir = dirname(sourceFile.getFilePath());
1053
+ return [
1054
+ resolve2(dir, `${moduleSpecifier}.ts`),
1055
+ resolve2(dir, moduleSpecifier, "index.ts")
1056
+ ];
1057
+ }
1058
+ const baseUrl = _projectRoot();
1059
+ const tsconfigPaths = _tsconfigPaths();
1060
+ dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(tsconfigPaths), "baseUrl:", baseUrl);
1061
+ if (tsconfigPaths) {
1062
+ for (const [pattern, mappings] of Object.entries(tsconfigPaths)) {
1063
+ const prefix = pattern.replace("*", "");
1064
+ if (moduleSpecifier.startsWith(prefix)) {
1065
+ const rest = moduleSpecifier.slice(prefix.length);
1066
+ const candidates = [];
1067
+ for (const mapping of mappings) {
1068
+ const resolved = resolve2(baseUrl, mapping.replace("*", rest));
1069
+ candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
1070
+ }
1071
+ dbg(" resolved candidates:", candidates);
1072
+ return candidates;
1073
+ }
1074
+ }
1075
+ }
1076
+ return [];
1077
+ }
1078
+ __name(resolveModuleSpecifier, "resolveModuleSpecifier");
916
1079
  function resolveImportedType(name, sourceFile, project) {
917
1080
  for (const importDecl of sourceFile.getImportDeclarations()) {
918
1081
  const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
919
1082
  if (!namedImport) continue;
920
1083
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
921
- if (!moduleSpecifier.startsWith(".")) return null;
922
- const dir = dirname(sourceFile.getFilePath());
923
- const candidates = [
924
- resolve2(dir, `${moduleSpecifier}.ts`),
925
- resolve2(dir, moduleSpecifier, "index.ts")
926
- ];
1084
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
1085
+ if (candidates.length === 0) continue;
927
1086
  for (const candidate of candidates) {
928
1087
  let importedFile = project.getSourceFile(candidate);
929
1088
  if (!importedFile) {
@@ -967,6 +1126,18 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
967
1126
  }
968
1127
  return "Array<unknown>";
969
1128
  }
1129
+ if ([
1130
+ "Record",
1131
+ "Omit",
1132
+ "Pick",
1133
+ "Partial",
1134
+ "Required",
1135
+ "Readonly",
1136
+ "Map",
1137
+ "Set"
1138
+ ].includes(name)) {
1139
+ return typeNode.getText();
1140
+ }
970
1141
  if (name === "Promise") {
971
1142
  const typeArgs = typeNode.getTypeArguments();
972
1143
  const firstTypeArg = typeArgs[0];
@@ -979,7 +1150,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
979
1150
  if (resolved) {
980
1151
  return expandTypeDecl(resolved, project, depth - 1);
981
1152
  }
982
- return name;
1153
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1154
+ return "unknown";
983
1155
  }
984
1156
  const kind = typeNode.getKind();
985
1157
  if (kind === SyntaxKind.StringKeyword) return "string";
@@ -1141,7 +1313,7 @@ function tryResolveTypeRef(typeNode, sourceFile, project) {
1141
1313
  return null;
1142
1314
  }
1143
1315
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1144
- if (localDecl && localDecl.isExported()) {
1316
+ if (localDecl?.isExported()) {
1145
1317
  return {
1146
1318
  name,
1147
1319
  filePath: sourceFile.getFilePath()
@@ -1203,7 +1375,7 @@ function extractDtoContract(method, sourceFile, project) {
1203
1375
  if (val && Node.isIdentifier(val)) {
1204
1376
  const name = val.getText();
1205
1377
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1206
- if (localDecl && localDecl.isExported()) {
1378
+ if (localDecl?.isExported()) {
1207
1379
  responseRef = {
1208
1380
  name,
1209
1381
  filePath: sourceFile.getFilePath()
@@ -1329,6 +1501,11 @@ function extractFromSourceFile(sourceFile, project) {
1329
1501
  path: combined,
1330
1502
  name: routeName,
1331
1503
  params,
1504
+ controllerRef: {
1505
+ className,
1506
+ methodName,
1507
+ filePath: sourceFile.getFilePath()
1508
+ },
1332
1509
  contract: {
1333
1510
  contractSource: {
1334
1511
  query: contractDef.query,
@@ -1363,19 +1540,21 @@ function extractFromSourceFile(sourceFile, project) {
1363
1540
  path: combined,
1364
1541
  name: routeName,
1365
1542
  params,
1366
- // Attach contract if DTO extraction produced useful type info
1367
- ...dtoContract ? {
1368
- contract: {
1369
- contractSource: {
1370
- query: dtoContract.query,
1371
- body: dtoContract.body,
1372
- response: dtoContract.response,
1373
- queryRef: dtoContract.queryRef,
1374
- bodyRef: dtoContract.bodyRef,
1375
- responseRef: dtoContract.responseRef
1376
- }
1543
+ controllerRef: {
1544
+ className,
1545
+ methodName,
1546
+ filePath: sourceFile.getFilePath()
1547
+ },
1548
+ contract: {
1549
+ contractSource: {
1550
+ query: dtoContract?.query ?? null,
1551
+ body: dtoContract?.body ?? null,
1552
+ response: dtoContract?.response ?? "unknown",
1553
+ queryRef: dtoContract?.queryRef,
1554
+ bodyRef: dtoContract?.bodyRef,
1555
+ responseRef: dtoContract?.responseRef
1377
1556
  }
1378
- } : {}
1557
+ }
1379
1558
  });
1380
1559
  }
1381
1560
  }
@@ -1385,7 +1564,8 @@ function extractFromSourceFile(sourceFile, project) {
1385
1564
  __name(extractFromSourceFile, "extractFromSourceFile");
1386
1565
 
1387
1566
  // src/watch/lock-file.ts
1388
- import { mkdir as mkdir6, readFile as readFile2, unlink, writeFile as writeFile6 } from "fs/promises";
1567
+ import { open } from "fs/promises";
1568
+ import { mkdir as mkdir6, readFile as readFile2, unlink } from "fs/promises";
1389
1569
  import { join as join8 } from "path";
1390
1570
  var LOCK_FILE = ".watcher.lock";
1391
1571
  function isProcessAlive(pid) {
@@ -1402,20 +1582,29 @@ async function acquireLock(outDir) {
1402
1582
  recursive: true
1403
1583
  });
1404
1584
  const lockPath = join8(outDir, LOCK_FILE);
1405
- try {
1406
- const raw = await readFile2(lockPath, "utf8");
1407
- const existing = JSON.parse(raw);
1408
- if (isProcessAlive(existing.pid)) {
1409
- return null;
1410
- }
1411
- } catch {
1412
- }
1413
1585
  const lockData = {
1414
1586
  pid: process.pid,
1415
1587
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1416
1588
  };
1417
- await writeFile6(lockPath, `${JSON.stringify(lockData, null, 2)}
1589
+ try {
1590
+ const fd = await open(lockPath, "wx");
1591
+ await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
1418
1592
  `, "utf8");
1593
+ await fd.close();
1594
+ } catch (err) {
1595
+ if (err.code === "EEXIST") {
1596
+ try {
1597
+ const raw = await readFile2(lockPath, "utf8");
1598
+ const existing = JSON.parse(raw);
1599
+ if (isProcessAlive(existing.pid)) return null;
1600
+ await unlink(lockPath);
1601
+ return acquireLock(outDir);
1602
+ } catch {
1603
+ return null;
1604
+ }
1605
+ }
1606
+ return null;
1607
+ }
1419
1608
  return {
1420
1609
  release: /* @__PURE__ */ __name(async () => {
1421
1610
  try {
@@ -1479,7 +1668,8 @@ async function watch(config, onChange) {
1479
1668
  pagesDebounceTimer = void 0;
1480
1669
  try {
1481
1670
  await generate(config);
1482
- } catch {
1671
+ } catch (err) {
1672
+ console.error("[nestjs-inertia-codegen] Pages generation failed:", err instanceof Error ? err.message : err);
1483
1673
  }
1484
1674
  onChange?.();
1485
1675
  }, PAGES_DEBOUNCE_MS);
@@ -1517,7 +1707,8 @@ async function watch(config, onChange) {
1517
1707
  if (hasContracts) {
1518
1708
  await emitApi(routes, config.codegen.outDir);
1519
1709
  }
1520
- } catch {
1710
+ } catch (err) {
1711
+ console.error("[nestjs-inertia-codegen] Contracts generation failed:", err instanceof Error ? err.message : err);
1521
1712
  }
1522
1713
  onChange?.();
1523
1714
  }, config.contracts.debounceMs);
@@ -1545,7 +1736,7 @@ async function watch(config, onChange) {
1545
1736
  __name(watch, "watch");
1546
1737
 
1547
1738
  // src/index.ts
1548
- var VERSION = "1.2.0";
1739
+ var VERSION = "1.4.0";
1549
1740
 
1550
1741
  // src/cli/codegen.ts
1551
1742
  async function runCodegen(opts = {}) {
@@ -1575,11 +1766,163 @@ async function runCodegen(opts = {}) {
1575
1766
  }
1576
1767
  __name(runCodegen, "runCodegen");
1577
1768
 
1578
- // src/cli/init.ts
1579
- import { execSync } from "child_process";
1580
- import { readFileSync, writeFileSync } from "fs";
1581
- import { access as access2, mkdir as mkdir7, readFile as readFile4, writeFile as writeFile7 } from "fs/promises";
1769
+ // src/cli/doctor.ts
1770
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1582
1771
  import { join as join10 } from "path";
1772
+ function checkFileExists(cwd, file) {
1773
+ return existsSync(join10(cwd, file));
1774
+ }
1775
+ __name(checkFileExists, "checkFileExists");
1776
+ function readJson(path) {
1777
+ try {
1778
+ const raw = readFileSync2(path, "utf8").replace(/\/\/.*$/gm, "");
1779
+ return JSON.parse(raw);
1780
+ } catch {
1781
+ return null;
1782
+ }
1783
+ }
1784
+ __name(readJson, "readJson");
1785
+ function getPackageVersion(cwd, pkg) {
1786
+ try {
1787
+ const pkgJson = readJson(join10(cwd, "node_modules", pkg, "package.json"));
1788
+ return pkgJson?.version ?? null;
1789
+ } catch {
1790
+ return null;
1791
+ }
1792
+ }
1793
+ __name(getPackageVersion, "getPackageVersion");
1794
+ async function runDoctor(opts) {
1795
+ const { cwd } = opts;
1796
+ const checks = [];
1797
+ checks.push({
1798
+ name: "nestjs-inertia.config.ts exists",
1799
+ pass: checkFileExists(cwd, "nestjs-inertia.config.ts"),
1800
+ fix: "Run: pnpm exec nestjs-inertia init"
1801
+ });
1802
+ const hasApi = checkFileExists(cwd, ".nestjs-inertia/api.ts");
1803
+ const hasRoutes = checkFileExists(cwd, ".nestjs-inertia/routes.ts");
1804
+ const hasPages = checkFileExists(cwd, ".nestjs-inertia/pages.d.ts");
1805
+ checks.push({
1806
+ name: ".nestjs-inertia/ codegen output exists",
1807
+ pass: hasApi && hasRoutes && hasPages,
1808
+ fix: "Run: pnpm exec nestjs-inertia codegen"
1809
+ });
1810
+ const tsconfig = readJson(join10(cwd, "tsconfig.json"));
1811
+ const paths = tsconfig?.compilerOptions?.paths;
1812
+ checks.push({
1813
+ name: "tsconfig.json has @/* path alias",
1814
+ pass: !!paths?.["@/*"],
1815
+ fix: 'Add to tsconfig.json compilerOptions.paths: { "@/*": ["./src/*"] }'
1816
+ });
1817
+ const inertiaTsconfig = readJson(join10(cwd, "tsconfig.inertia.json"));
1818
+ if (inertiaTsconfig) {
1819
+ const inertiaPaths = inertiaTsconfig.compilerOptions?.paths;
1820
+ checks.push({
1821
+ name: "tsconfig.inertia.json has ~/* and ~codegen/* aliases",
1822
+ pass: !!inertiaPaths?.["~/*"] && !!inertiaPaths?.["~codegen/*"],
1823
+ fix: 'Add paths: { "~/*": ["inertia/*"], "~codegen/*": [".nestjs-inertia/*"] }'
1824
+ });
1825
+ }
1826
+ if (checkFileExists(cwd, "vite.config.ts")) {
1827
+ const viteContent = readFileSync2(join10(cwd, "vite.config.ts"), "utf8");
1828
+ checks.push({
1829
+ name: "vite.config.ts has resolve.alias",
1830
+ pass: viteContent.includes("resolve") && viteContent.includes("alias"),
1831
+ fix: "Add resolve.alias with @\u2192src, ~\u2192inertia, ~codegen\u2192.nestjs-inertia"
1832
+ });
1833
+ checks.push({
1834
+ name: "vite.config.ts references nestjs-inertia",
1835
+ pass: viteContent.includes("nestInertia") || viteContent.includes("nestjs-inertia") || viteContent.includes("setupInertiaVite"),
1836
+ fix: "Add: import nestInertia from '@dudousxd/nestjs-inertia-vite/plugin'"
1837
+ });
1838
+ }
1839
+ const libPackages = [
1840
+ "@dudousxd/nestjs-inertia",
1841
+ "@dudousxd/nestjs-inertia-codegen",
1842
+ "@dudousxd/nestjs-inertia-client",
1843
+ "@dudousxd/nestjs-inertia-vite",
1844
+ "@dudousxd/nestjs-inertia-testing"
1845
+ ];
1846
+ const versions = libPackages.map((pkg) => ({
1847
+ pkg,
1848
+ version: getPackageVersion(cwd, pkg)
1849
+ }));
1850
+ const installed = versions.filter((v) => v.version !== null);
1851
+ const uniqueVersions = new Set(installed.map((v) => v.version));
1852
+ const requiredPkgs = [
1853
+ "@dudousxd/nestjs-inertia",
1854
+ "@dudousxd/nestjs-inertia-codegen",
1855
+ "@dudousxd/nestjs-inertia-client"
1856
+ ];
1857
+ const missingRequired = requiredPkgs.filter((pkg) => !getPackageVersion(cwd, pkg));
1858
+ checks.push({
1859
+ name: "Core packages installed (core + codegen + client)",
1860
+ pass: missingRequired.length === 0,
1861
+ fix: missingRequired.length > 0 ? `Missing: ${missingRequired.join(", ")}` : void 0
1862
+ });
1863
+ if (installed.length > 1) {
1864
+ checks.push({
1865
+ name: "All packages on same version",
1866
+ pass: uniqueVersions.size === 1,
1867
+ fix: `Versions: ${installed.map((v) => `${v.pkg.replace("@dudousxd/", "")}@${v.version}`).join(", ")}`
1868
+ });
1869
+ }
1870
+ const inertiaReact = getPackageVersion(cwd, "@inertiajs/react");
1871
+ const inertiaVue = getPackageVersion(cwd, "@inertiajs/vue3");
1872
+ const inertiaSvelte = getPackageVersion(cwd, "@inertiajs/svelte");
1873
+ const inertiaVersion = inertiaReact ?? inertiaVue ?? inertiaSvelte;
1874
+ const inertiaFramework = inertiaReact ? "react" : inertiaVue ? "vue" : inertiaSvelte ? "svelte" : null;
1875
+ if (inertiaVersion) {
1876
+ const majorVersion = Number.parseInt(inertiaVersion.split(".")[0] ?? "0", 10);
1877
+ checks.push({
1878
+ name: `@inertiajs/${inertiaFramework} is v3+`,
1879
+ pass: majorVersion >= 3,
1880
+ fix: `Current: v${inertiaVersion}. Run: pnpm add @inertiajs/${inertiaFramework}@^3.0.0`
1881
+ });
1882
+ }
1883
+ if (checkFileExists(cwd, ".gitignore")) {
1884
+ const gitignore = readFileSync2(join10(cwd, ".gitignore"), "utf8");
1885
+ checks.push({
1886
+ name: ".gitignore includes .nestjs-inertia/",
1887
+ pass: gitignore.includes(".nestjs-inertia"),
1888
+ fix: "Add .nestjs-inertia/ to .gitignore"
1889
+ });
1890
+ }
1891
+ const pkgJson = readJson(join10(cwd, "package.json"));
1892
+ const scripts = pkgJson?.scripts ?? {};
1893
+ checks.push({
1894
+ name: "package.json has build:client script",
1895
+ pass: !!scripts["build:client"],
1896
+ fix: 'Add: "build:client": "vite build"'
1897
+ });
1898
+ console.log("");
1899
+ console.log("\x1B[1mnestjs-inertia doctor\x1B[0m");
1900
+ console.log("");
1901
+ let hasFailures = false;
1902
+ for (const check of checks) {
1903
+ const icon = check.pass ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
1904
+ console.log(` ${icon} ${check.name}`);
1905
+ if (!check.pass && check.fix) {
1906
+ console.log(` \x1B[2m${check.fix}\x1B[0m`);
1907
+ hasFailures = true;
1908
+ }
1909
+ }
1910
+ console.log("");
1911
+ if (hasFailures) {
1912
+ console.log(`\x1B[33m${checks.filter((c) => !c.pass).length} issue(s) found\x1B[0m`);
1913
+ } else {
1914
+ console.log("\x1B[32mAll checks passed!\x1B[0m");
1915
+ }
1916
+ console.log("");
1917
+ return hasFailures ? 1 : 0;
1918
+ }
1919
+ __name(runDoctor, "runDoctor");
1920
+
1921
+ // src/cli/init.ts
1922
+ import { execFileSync } from "child_process";
1923
+ import { readFileSync as readFileSync3, writeFileSync } from "fs";
1924
+ import { access as access2, mkdir as mkdir7, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
1925
+ import { join as join11 } from "path";
1583
1926
  import { createInterface } from "readline";
1584
1927
  var GITIGNORE_ENTRY = ".nestjs-inertia/";
1585
1928
  var green = /* @__PURE__ */ __name((s) => `\x1B[32m${s}\x1B[0m`, "green");
@@ -1610,7 +1953,7 @@ ${bold(title)}`);
1610
1953
  __name(logSection, "logSection");
1611
1954
  async function readPackageJson(cwd) {
1612
1955
  try {
1613
- const raw = await readFile4(join10(cwd, "package.json"), "utf8");
1956
+ const raw = await readFile4(join11(cwd, "package.json"), "utf8");
1614
1957
  return JSON.parse(raw);
1615
1958
  } catch {
1616
1959
  return {};
@@ -1648,7 +1991,7 @@ __name(detectTemplateEngine, "detectTemplateEngine");
1648
1991
  async function detectPackageManager(cwd) {
1649
1992
  async function exists(file) {
1650
1993
  try {
1651
- await access2(join10(cwd, file));
1994
+ await access2(join11(cwd, file));
1652
1995
  return true;
1653
1996
  } catch {
1654
1997
  return false;
@@ -1697,12 +2040,12 @@ async function writeIfNotExists(filePath, content, label) {
1697
2040
  recursive: true
1698
2041
  });
1699
2042
  }
1700
- await writeFile7(filePath, content, "utf8");
2043
+ await writeFile6(filePath, content, "utf8");
1701
2044
  logCreated(label);
1702
2045
  }
1703
2046
  __name(writeIfNotExists, "writeIfNotExists");
1704
2047
  async function handleViteConfig(cwd, framework) {
1705
- const filePath = join10(cwd, "vite.config.ts");
2048
+ const filePath = join11(cwd, "vite.config.ts");
1706
2049
  if (await fileExists2(filePath)) {
1707
2050
  const existing = await readFile4(filePath, "utf8");
1708
2051
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -1722,7 +2065,7 @@ async function handleViteConfig(cwd, framework) {
1722
2065
  recursive: true
1723
2066
  });
1724
2067
  }
1725
- await writeFile7(filePath, viteConfigTemplate(framework), "utf8");
2068
+ await writeFile6(filePath, viteConfigTemplate(framework), "utf8");
1726
2069
  logCreated("vite.config.ts");
1727
2070
  }
1728
2071
  __name(handleViteConfig, "handleViteConfig");
@@ -1739,27 +2082,33 @@ async function patchGitignore(gitignorePath) {
1739
2082
  ` : `${existing}
1740
2083
  ${GITIGNORE_ENTRY}
1741
2084
  `;
1742
- await writeFile7(gitignorePath, newContent, "utf8");
2085
+ await writeFile6(gitignorePath, newContent, "utf8");
1743
2086
  logPatched(".gitignore", "added .nestjs-inertia/");
1744
2087
  }
1745
2088
  __name(patchGitignore, "patchGitignore");
1746
2089
  function installDeps(pkgManager, deps, dev) {
1747
2090
  if (deps.length === 0) return;
1748
- const flag = dev ? pkgManager === "npm" ? "--save-dev" : "-D" : "";
1749
- const cmd = pkgManager === "npm" ? `npm install ${flag} ${deps.join(" ")}` : pkgManager === "yarn" ? `yarn add ${flag} ${deps.join(" ")}` : `pnpm add ${flag} ${deps.join(" ")}`;
2091
+ const args = [];
2092
+ if (pkgManager === "npm") {
2093
+ args.push("install");
2094
+ if (dev) args.push("--save-dev");
2095
+ } else {
2096
+ args.push("add");
2097
+ if (dev) args.push("-D");
2098
+ }
2099
+ args.push(...deps);
1750
2100
  logPatched(deps.join(", "), "installed");
1751
2101
  try {
1752
- execSync(cmd, {
2102
+ execFileSync(pkgManager, args, {
1753
2103
  stdio: "inherit"
1754
2104
  });
1755
2105
  } catch {
1756
- logWarning(`Failed to install deps. Run manually:
1757
- ${cmd}`);
2106
+ logWarning(`Failed to install: ${deps.join(", ")}`);
1758
2107
  }
1759
2108
  }
1760
2109
  __name(installDeps, "installDeps");
1761
2110
  async function patchPackageJsonScripts(cwd, scripts) {
1762
- const pkgPath = join10(cwd, "package.json");
2111
+ const pkgPath = join11(cwd, "package.json");
1763
2112
  let pkg = {};
1764
2113
  try {
1765
2114
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -1781,7 +2130,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
1781
2130
  return;
1782
2131
  }
1783
2132
  pkg.scripts = existing;
1784
- await writeFile7(pkgPath, `${JSON.stringify(pkg, null, 2)}
2133
+ await writeFile6(pkgPath, `${JSON.stringify(pkg, null, 2)}
1785
2134
  `, "utf8");
1786
2135
  }
1787
2136
  __name(patchPackageJsonScripts, "patchPackageJsonScripts");
@@ -1801,7 +2150,7 @@ __name(findAfterLastImport, "findAfterLastImport");
1801
2150
  function patchAppModule(filePath, rootView) {
1802
2151
  let content;
1803
2152
  try {
1804
- content = readFileSync(filePath, "utf8");
2153
+ content = readFileSync3(filePath, "utf8");
1805
2154
  } catch {
1806
2155
  return "skipped";
1807
2156
  }
@@ -1846,7 +2195,7 @@ __name(patchAppModule, "patchAppModule");
1846
2195
  function patchMainTs(filePath) {
1847
2196
  let content;
1848
2197
  try {
1849
- content = readFileSync(filePath, "utf8");
2198
+ content = readFileSync3(filePath, "utf8");
1850
2199
  } catch {
1851
2200
  return "skipped";
1852
2201
  }
@@ -1919,11 +2268,19 @@ function htmlShellTemplate(framework, _engine) {
1919
2268
  __name(htmlShellTemplate, "htmlShellTemplate");
1920
2269
  function viteConfigTemplate(framework) {
1921
2270
  const pluginOption = `{ ${framework}: true }`;
1922
- return `import { defineConfig } from 'vite';
2271
+ return `import { resolve } from 'node:path';
2272
+ import { defineConfig } from 'vite';
1923
2273
  import nestInertia from '@dudousxd/nestjs-inertia-vite/plugin';
1924
2274
 
1925
2275
  export default defineConfig({
1926
2276
  plugins: [nestInertia(${pluginOption})],
2277
+ resolve: {
2278
+ alias: {
2279
+ '@': resolve(__dirname, 'src'),
2280
+ '~': resolve(__dirname, 'inertia'),
2281
+ '~codegen': resolve(__dirname, '.nestjs-inertia'),
2282
+ },
2283
+ },
1927
2284
  });
1928
2285
  `;
1929
2286
  }
@@ -2043,16 +2400,16 @@ ${bold("nestjs-inertia init")}`);
2043
2400
  const entryExt = framework === "react" ? "tsx" : "ts";
2044
2401
  const pageExt = framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte";
2045
2402
  logSection("Scaffold files");
2046
- await writeIfNotExists(join10(cwd, "nestjs-inertia.config.ts"), configTemplate(framework), "nestjs-inertia.config.ts");
2047
- await writeIfNotExists(join10(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
2048
- await writeIfNotExists(join10(cwd, "inertia", shellFileName), htmlShellTemplate(framework, engine), `inertia/${shellFileName}`);
2403
+ await writeIfNotExists(join11(cwd, "nestjs-inertia.config.ts"), configTemplate(framework), "nestjs-inertia.config.ts");
2404
+ await writeIfNotExists(join11(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
2405
+ await writeIfNotExists(join11(cwd, "inertia", shellFileName), htmlShellTemplate(framework, engine), `inertia/${shellFileName}`);
2049
2406
  await handleViteConfig(cwd, framework);
2050
- await writeIfNotExists(join10(cwd, "inertia", `app.${entryExt}`), entryPointTemplate(framework), `inertia/app.${entryExt}`);
2051
- await writeIfNotExists(join10(cwd, "inertia", "pages", `Home.${pageExt}`), samplePageTemplate(framework), `inertia/pages/Home.${pageExt}`);
2052
- await writeIfNotExists(join10(cwd, "src", "home.controller.ts"), SAMPLE_CONTROLLER, "src/home.controller.ts");
2407
+ await writeIfNotExists(join11(cwd, "inertia", `app.${entryExt}`), entryPointTemplate(framework), `inertia/app.${entryExt}`);
2408
+ await writeIfNotExists(join11(cwd, "inertia", "pages", `Home.${pageExt}`), samplePageTemplate(framework), `inertia/pages/Home.${pageExt}`);
2409
+ await writeIfNotExists(join11(cwd, "src", "home.controller.ts"), SAMPLE_CONTROLLER, "src/home.controller.ts");
2053
2410
  logSection("Patch existing files");
2054
2411
  const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
2055
- const appModulePath = join10(cwd, "src", "app.module.ts");
2412
+ const appModulePath = join11(cwd, "src", "app.module.ts");
2056
2413
  const appModuleResult = patchAppModule(appModulePath, rootView);
2057
2414
  if (appModuleResult === "patched") {
2058
2415
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -2062,7 +2419,7 @@ ${bold("nestjs-inertia init")}`);
2062
2419
  } else {
2063
2420
  logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
2064
2421
  }
2065
- const mainTsPath = join10(cwd, "src", "main.ts");
2422
+ const mainTsPath = join11(cwd, "src", "main.ts");
2066
2423
  const mainTsResult = patchMainTs(mainTsPath);
2067
2424
  if (mainTsResult === "patched") {
2068
2425
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -2071,7 +2428,7 @@ ${bold("nestjs-inertia init")}`);
2071
2428
  } else {
2072
2429
  logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
2073
2430
  }
2074
- await patchGitignore(join10(cwd, ".gitignore"));
2431
+ await patchGitignore(join11(cwd, ".gitignore"));
2075
2432
  await patchPackageJsonScripts(cwd, {
2076
2433
  "build:client": "vite build",
2077
2434
  "build:ssr": "VITE_SSR=1 vite build --ssr"
@@ -2148,6 +2505,12 @@ async function run(argv) {
2148
2505
  cwd: process.cwd()
2149
2506
  });
2150
2507
  });
2508
+ cli.command("doctor", "Diagnose your nestjs-inertia setup").action(async () => {
2509
+ const code = await runDoctor({
2510
+ cwd: process.cwd()
2511
+ });
2512
+ process.exitCode = code;
2513
+ });
2151
2514
  cli.help();
2152
2515
  cli.version(VERSION);
2153
2516
  try {