@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.cjs CHANGED
@@ -243,7 +243,7 @@ async function emitApi(routes, outDir) {
243
243
  await (0, import_promises3.mkdir)(outDir, {
244
244
  recursive: true
245
245
  });
246
- const content = buildApiFile(routes);
246
+ const content = buildApiFile(routes, outDir);
247
247
  await (0, import_promises3.writeFile)((0, import_node_path3.join)(outDir, "api.ts"), content, "utf8");
248
248
  }
249
249
  __name(emitApi, "emitApi");
@@ -308,7 +308,20 @@ function insertIntoTree(tree, segments, leaf, fullName) {
308
308
  }
309
309
  }
310
310
  __name(insertIntoTree, "insertIntoTree");
311
- function emitRouterTypeBlock(tree, indent) {
311
+ function buildResponseType(c, outDir) {
312
+ if (c.controllerRef) {
313
+ let relPath = (0, import_node_path3.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
314
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
315
+ return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
316
+ }
317
+ const respRef = c.contractSource.responseRef;
318
+ if (respRef) {
319
+ return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
320
+ }
321
+ return c.contractSource.response;
322
+ }
323
+ __name(buildResponseType, "buildResponseType");
324
+ function emitRouterTypeBlock(tree, indent, outDir) {
312
325
  const pad = " ".repeat(indent);
313
326
  const lines = [];
314
327
  for (const [key, node] of tree) {
@@ -316,15 +329,17 @@ function emitRouterTypeBlock(tree, indent) {
316
329
  if (node.kind === "leaf") {
317
330
  const c = node;
318
331
  const method = c.method.toUpperCase();
319
- const query = c.contractSource.query ?? "never";
320
- const body = method === "GET" ? "never" : c.contractSource.body ?? "never";
321
- const response = c.contractSource.response;
332
+ const queryRef = c.contractSource.queryRef;
333
+ const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : c.contractSource.query ?? "never";
334
+ const bodyRef = c.contractSource.bodyRef;
335
+ const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
336
+ const response = buildResponseType(c, outDir);
322
337
  const safeMethod = JSON.stringify(method);
323
338
  const safeUrl = JSON.stringify(c.path);
324
339
  lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; query: ${query}; body: ${body}; response: ${response} };`);
325
340
  } else {
326
341
  lines.push(`${pad}${objKey}: {`);
327
- lines.push(...emitRouterTypeBlock(node.children, indent + 2));
342
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
328
343
  lines.push(`${pad}};`);
329
344
  }
330
345
  }
@@ -345,18 +360,21 @@ function emitApiObjectBlock(tree, indent) {
345
360
  if (method === "GET") {
346
361
  const typeAccess = buildRouterTypeAccess(c.name);
347
362
  lines.push(`${pad}${objKey}: {`);
363
+ lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
348
364
  lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
349
- lines.push(`${pad} queryOptions({`);
350
- lines.push(`${pad} queryKey: [${flatName}, query],`);
365
+ lines.push(`${pad} _queryOptions({`);
366
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
351
367
  lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
352
368
  lines.push(`${pad} }),`);
353
369
  lines.push(`${pad}},`);
354
370
  } else {
355
371
  const typeAccess = buildRouterTypeAccess(c.name);
356
372
  lines.push(`${pad}${objKey}: {`);
357
- lines.push(`${pad} mutationOptions: () => ({`);
358
- lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
359
- lines.push(`${pad} }),`);
373
+ lines.push(`${pad} queryKey: () => [${flatName}] as const,`);
374
+ lines.push(`${pad} mutationOptions: () =>`);
375
+ lines.push(`${pad} _mutationOptions({`);
376
+ lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
377
+ lines.push(`${pad} }),`);
360
378
  lines.push(`${pad}},`);
361
379
  }
362
380
  } else {
@@ -373,18 +391,55 @@ function buildRouterTypeAccess(name) {
373
391
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
374
392
  }
375
393
  __name(buildRouterTypeAccess, "buildRouterTypeAccess");
376
- function buildApiFile(routes) {
394
+ function buildApiFile(routes, outDir) {
377
395
  const contracted = routes.filter((r) => r.contract);
396
+ const importsByFile = /* @__PURE__ */ new Map();
397
+ for (const r of contracted) {
398
+ const cs = r.contract?.contractSource;
399
+ if (!cs) continue;
400
+ const refs = r.controllerRef ? [
401
+ cs.queryRef,
402
+ cs.bodyRef
403
+ ] : [
404
+ cs.queryRef,
405
+ cs.bodyRef,
406
+ cs.responseRef
407
+ ];
408
+ for (const ref of refs) {
409
+ if (!ref) continue;
410
+ let names = importsByFile.get(ref.filePath);
411
+ if (!names) {
412
+ names = /* @__PURE__ */ new Set();
413
+ importsByFile.set(ref.filePath, names);
414
+ }
415
+ names.add(ref.name);
416
+ }
417
+ }
378
418
  const hasGetRoutes = contracted.some((r) => r.method === "GET");
419
+ const hasMutationRoutes = contracted.some((r) => r.method !== "GET");
379
420
  const lines = [
380
421
  "// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.",
381
422
  ""
382
423
  ];
383
- if (hasGetRoutes) {
384
- lines.push("import { queryOptions } from '@tanstack/query-core';");
424
+ const tqImports = [];
425
+ if (hasGetRoutes) tqImports.push("queryOptions as _queryOptions");
426
+ if (hasMutationRoutes) tqImports.push("mutationOptions as _mutationOptions");
427
+ if (tqImports.length > 0) {
428
+ lines.push(`import { ${tqImports.join(", ")} } from '@tanstack/react-query';`);
385
429
  }
386
430
  lines.push("import { route } from './routes.js';");
387
431
  lines.push("import { createFetcher } from '@dudousxd/nestjs-inertia-client';");
432
+ if (importsByFile.size > 0 && outDir) {
433
+ lines.push("");
434
+ for (const [filePath, names] of importsByFile) {
435
+ let relPath = (0, import_node_path3.relative)(outDir, filePath).replace(/\.ts$/, "");
436
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
437
+ const sortedNames = [
438
+ ...names
439
+ ].sort();
440
+ lines.push(`import type { ${sortedNames.join(", ")} } from '${relPath}';`);
441
+ }
442
+ }
388
443
  lines.push("");
389
444
  lines.push("export const fetcher = createFetcher();");
390
445
  lines.push("");
@@ -425,13 +480,14 @@ function buildApiFile(routes) {
425
480
  method: r.method,
426
481
  name,
427
482
  path: r.path,
483
+ controllerRef: r.controllerRef,
428
484
  contractSource: c.contractSource
429
485
  };
430
486
  insertIntoTree(tree, segments, leaf, name);
431
487
  }
432
488
  void detectCollisions;
433
489
  lines.push("export type ApiRouter = {");
434
- lines.push(...emitRouterTypeBlock(tree, 2));
490
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
435
491
  lines.push("};");
436
492
  lines.push("");
437
493
  lines.push("export const api = {");
@@ -701,9 +757,28 @@ var import_node_path10 = require("path");
701
757
  var import_chokidar = __toESM(require("chokidar"), 1);
702
758
 
703
759
  // src/discovery/contracts-fast.ts
760
+ var import_node_fs = require("fs");
704
761
  var import_node_path8 = require("path");
705
762
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
706
763
  var import_ts_morph = require("ts-morph");
764
+ var _projectRoot = "";
765
+ var _tsconfigPaths = null;
766
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
767
+ function dbg(...args) {
768
+ if (_debug) console.log("[codegen:debug]", ...args);
769
+ }
770
+ __name(dbg, "dbg");
771
+ function loadTsconfigPaths(tsconfigPath) {
772
+ try {
773
+ const raw = (0, import_node_fs.readFileSync)(tsconfigPath, "utf8");
774
+ const stripped = raw.replace(/\/\/.*$/gm, "");
775
+ const parsed = JSON.parse(stripped);
776
+ return parsed.compilerOptions?.paths ?? null;
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ __name(loadTsconfigPaths, "loadTsconfigPaths");
707
782
  async function discoverContractsFast(opts) {
708
783
  const { cwd, glob, tsconfig } = opts;
709
784
  const tsconfigPath = tsconfig ? (0, import_node_path8.resolve)(tsconfig) : (0, import_node_path8.join)(cwd, "tsconfig.json");
@@ -736,6 +811,8 @@ async function discoverContractsFast(opts) {
736
811
  project.addSourceFileAtPath(f);
737
812
  }
738
813
  const routes = [];
814
+ _projectRoot = cwd;
815
+ _tsconfigPaths = loadTsconfigPaths(tsconfigPath);
739
816
  for (const sourceFile of project.getSourceFiles()) {
740
817
  routes.push(...extractFromSourceFile(sourceFile, project));
741
818
  }
@@ -937,17 +1014,41 @@ function findTypeInFile(name, file) {
937
1014
  return null;
938
1015
  }
939
1016
  __name(findTypeInFile, "findTypeInFile");
1017
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1018
+ if (moduleSpecifier.startsWith(".")) {
1019
+ const dir = (0, import_node_path8.dirname)(sourceFile.getFilePath());
1020
+ return [
1021
+ (0, import_node_path8.resolve)(dir, `${moduleSpecifier}.ts`),
1022
+ (0, import_node_path8.resolve)(dir, moduleSpecifier, "index.ts")
1023
+ ];
1024
+ }
1025
+ const baseUrl = _projectRoot;
1026
+ dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(_tsconfigPaths), "baseUrl:", baseUrl);
1027
+ if (_tsconfigPaths) {
1028
+ for (const [pattern, mappings] of Object.entries(_tsconfigPaths)) {
1029
+ const prefix = pattern.replace("*", "");
1030
+ if (moduleSpecifier.startsWith(prefix)) {
1031
+ const rest = moduleSpecifier.slice(prefix.length);
1032
+ const candidates = [];
1033
+ for (const mapping of mappings) {
1034
+ const resolved = (0, import_node_path8.resolve)(baseUrl, mapping.replace("*", rest));
1035
+ candidates.push(`${resolved}.ts`, (0, import_node_path8.resolve)(resolved, "index.ts"));
1036
+ }
1037
+ dbg(" resolved candidates:", candidates);
1038
+ return candidates;
1039
+ }
1040
+ }
1041
+ }
1042
+ return [];
1043
+ }
1044
+ __name(resolveModuleSpecifier, "resolveModuleSpecifier");
940
1045
  function resolveImportedType(name, sourceFile, project) {
941
1046
  for (const importDecl of sourceFile.getImportDeclarations()) {
942
1047
  const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
943
1048
  if (!namedImport) continue;
944
1049
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
945
- if (!moduleSpecifier.startsWith(".")) return null;
946
- const dir = (0, import_node_path8.dirname)(sourceFile.getFilePath());
947
- const candidates = [
948
- (0, import_node_path8.resolve)(dir, `${moduleSpecifier}.ts`),
949
- (0, import_node_path8.resolve)(dir, moduleSpecifier, "index.ts")
950
- ];
1050
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
1051
+ if (candidates.length === 0) continue;
951
1052
  for (const candidate of candidates) {
952
1053
  let importedFile = project.getSourceFile(candidate);
953
1054
  if (!importedFile) {
@@ -981,7 +1082,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
981
1082
  const name = import_ts_morph.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
982
1083
  if (name === "string" || name === "number" || name === "boolean") return name;
983
1084
  if (name === "Date") return "string";
984
- if (name === "unknown" || name === "any") return "unknown";
1085
+ if (name === "unknown" || name === "any" || name === "void") return "unknown";
1086
+ if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream") return "unknown";
985
1087
  if (name === "Array") {
986
1088
  const typeArgs = typeNode.getTypeArguments();
987
1089
  const firstTypeArg = typeArgs[0];
@@ -990,6 +1092,18 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
990
1092
  }
991
1093
  return "Array<unknown>";
992
1094
  }
1095
+ if ([
1096
+ "Record",
1097
+ "Omit",
1098
+ "Pick",
1099
+ "Partial",
1100
+ "Required",
1101
+ "Readonly",
1102
+ "Map",
1103
+ "Set"
1104
+ ].includes(name)) {
1105
+ return typeNode.getText();
1106
+ }
993
1107
  if (name === "Promise") {
994
1108
  const typeArgs = typeNode.getTypeArguments();
995
1109
  const firstTypeArg = typeArgs[0];
@@ -1002,7 +1116,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1002
1116
  if (resolved) {
1003
1117
  return expandTypeDecl(resolved, project, depth - 1);
1004
1118
  }
1005
- return name;
1119
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1120
+ return "unknown";
1006
1121
  }
1007
1122
  const kind = typeNode.getKind();
1008
1123
  if (kind === import_ts_morph.SyntaxKind.StringKeyword) return "string";
@@ -1129,6 +1244,68 @@ function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1129
1244
  return name;
1130
1245
  }
1131
1246
  __name(resolveIdentifierToClassType, "resolveIdentifierToClassType");
1247
+ function tryResolveTypeRef(typeNode, sourceFile, project) {
1248
+ if (import_ts_morph.Node.isTypeReference(typeNode)) {
1249
+ const typeName = typeNode.getTypeName();
1250
+ const name = import_ts_morph.Node.isIdentifier(typeName) ? typeName.getText() : null;
1251
+ if (!name) return null;
1252
+ if (name === "Promise") {
1253
+ const typeArgs = typeNode.getTypeArguments();
1254
+ const first = typeArgs[0];
1255
+ if (first) return tryResolveTypeRef(first, sourceFile, project);
1256
+ return null;
1257
+ }
1258
+ if (name === "Array") {
1259
+ const typeArgs = typeNode.getTypeArguments();
1260
+ const first = typeArgs[0];
1261
+ if (first) {
1262
+ const inner = tryResolveTypeRef(first, sourceFile, project);
1263
+ if (inner) return {
1264
+ ...inner,
1265
+ isArray: true
1266
+ };
1267
+ }
1268
+ return null;
1269
+ }
1270
+ if ([
1271
+ "string",
1272
+ "number",
1273
+ "boolean",
1274
+ "void",
1275
+ "unknown",
1276
+ "any",
1277
+ "Date"
1278
+ ].includes(name)) {
1279
+ return null;
1280
+ }
1281
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1282
+ if (localDecl?.isExported()) {
1283
+ return {
1284
+ name,
1285
+ filePath: sourceFile.getFilePath()
1286
+ };
1287
+ }
1288
+ const resolved = resolveImportedType(name, sourceFile, project);
1289
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface")) {
1290
+ const decl = resolved.decl;
1291
+ if (decl.isExported()) {
1292
+ return {
1293
+ name,
1294
+ filePath: resolved.file.getFilePath()
1295
+ };
1296
+ }
1297
+ }
1298
+ }
1299
+ if (import_ts_morph.Node.isArrayTypeNode(typeNode)) {
1300
+ const inner = tryResolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project);
1301
+ if (inner) return {
1302
+ ...inner,
1303
+ isArray: true
1304
+ };
1305
+ }
1306
+ return null;
1307
+ }
1308
+ __name(tryResolveTypeRef, "tryResolveTypeRef");
1132
1309
  function extractDtoContract(method, sourceFile, project) {
1133
1310
  const body = extractBodyType(method, sourceFile, project);
1134
1311
  const query = extractQueryType(method, sourceFile, project);
@@ -1137,11 +1314,61 @@ function extractDtoContract(method, sourceFile, project) {
1137
1314
  if (body === null && query === null && paramsType === null && response === "unknown") {
1138
1315
  return null;
1139
1316
  }
1317
+ let bodyRef = null;
1318
+ let queryRef = null;
1319
+ let responseRef = null;
1320
+ for (const param of method.getParameters()) {
1321
+ if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode()) {
1322
+ bodyRef = tryResolveTypeRef(param.getTypeNode(), sourceFile, project);
1323
+ }
1324
+ if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode()) {
1325
+ queryRef = tryResolveTypeRef(param.getTypeNode(), sourceFile, project);
1326
+ }
1327
+ }
1328
+ const returnTypeNode = method.getReturnTypeNode();
1329
+ if (returnTypeNode) {
1330
+ responseRef = tryResolveTypeRef(returnTypeNode, sourceFile, project);
1331
+ }
1332
+ if (!responseRef) {
1333
+ const apiResp = method.getDecorator("ApiResponse");
1334
+ if (apiResp) {
1335
+ const args = apiResp.getArguments();
1336
+ const optsArg = args[0];
1337
+ if (optsArg && import_ts_morph.Node.isObjectLiteralExpression(optsArg)) {
1338
+ for (const prop of optsArg.getProperties()) {
1339
+ if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
1340
+ const val = prop.getInitializer();
1341
+ if (val && import_ts_morph.Node.isIdentifier(val)) {
1342
+ const name = val.getText();
1343
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1344
+ if (localDecl?.isExported()) {
1345
+ responseRef = {
1346
+ name,
1347
+ filePath: sourceFile.getFilePath()
1348
+ };
1349
+ } else {
1350
+ const resolved = resolveImportedType(name, sourceFile, project);
1351
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
1352
+ responseRef = {
1353
+ name,
1354
+ filePath: resolved.file.getFilePath()
1355
+ };
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+ }
1140
1364
  return {
1141
1365
  query,
1142
1366
  body,
1143
1367
  response,
1144
- params: paramsType
1368
+ params: paramsType,
1369
+ queryRef,
1370
+ bodyRef,
1371
+ responseRef
1145
1372
  };
1146
1373
  }
1147
1374
  __name(extractDtoContract, "extractDtoContract");
@@ -1240,6 +1467,11 @@ function extractFromSourceFile(sourceFile, project) {
1240
1467
  path: combined,
1241
1468
  name: routeName,
1242
1469
  params,
1470
+ controllerRef: {
1471
+ className,
1472
+ methodName,
1473
+ filePath: sourceFile.getFilePath()
1474
+ },
1243
1475
  contract: {
1244
1476
  contractSource: {
1245
1477
  query: contractDef.query,
@@ -1274,13 +1506,20 @@ function extractFromSourceFile(sourceFile, project) {
1274
1506
  path: combined,
1275
1507
  name: routeName,
1276
1508
  params,
1277
- // Attach contract if DTO extraction produced useful type info
1509
+ controllerRef: {
1510
+ className,
1511
+ methodName,
1512
+ filePath: sourceFile.getFilePath()
1513
+ },
1278
1514
  ...dtoContract ? {
1279
1515
  contract: {
1280
1516
  contractSource: {
1281
1517
  query: dtoContract.query,
1282
1518
  body: dtoContract.body,
1283
- response: dtoContract.response
1519
+ response: dtoContract.response,
1520
+ queryRef: dtoContract.queryRef,
1521
+ bodyRef: dtoContract.bodyRef,
1522
+ responseRef: dtoContract.responseRef
1284
1523
  }
1285
1524
  }
1286
1525
  } : {}
@@ -1453,7 +1692,7 @@ async function watch(config, onChange) {
1453
1692
  __name(watch, "watch");
1454
1693
 
1455
1694
  // src/index.ts
1456
- var VERSION = "1.0.7";
1695
+ var VERSION = "1.3.0";
1457
1696
  // Annotate the CommonJS export names for ESM import in node:
1458
1697
  0 && (module.exports = {
1459
1698
  CodegenError,