@dudousxd/nestjs-inertia-codegen 1.2.0 → 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/cli/main.js CHANGED
@@ -255,7 +255,20 @@ function insertIntoTree(tree, segments, leaf, fullName) {
255
255
  }
256
256
  }
257
257
  __name(insertIntoTree, "insertIntoTree");
258
- function emitRouterTypeBlock(tree, indent) {
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,13 @@ 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);
272
284
  const safeMethod = JSON.stringify(method);
273
285
  const safeUrl = JSON.stringify(c.path);
274
286
  lines.push(`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; query: ${query}; body: ${body}; response: ${response} };`);
275
287
  } else {
276
288
  lines.push(`${pad}${objKey}: {`);
277
- lines.push(...emitRouterTypeBlock(node.children, indent + 2));
289
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
278
290
  lines.push(`${pad}};`);
279
291
  }
280
292
  }
@@ -296,18 +308,20 @@ function emitApiObjectBlock(tree, indent) {
296
308
  const typeAccess = buildRouterTypeAccess(c.name);
297
309
  lines.push(`${pad}${objKey}: {`);
298
310
  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} }),`);
311
+ lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
312
+ lines.push(`${pad} _queryOptions({`);
313
+ lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
314
+ lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
315
+ lines.push(`${pad} }),`);
303
316
  lines.push(`${pad}},`);
304
317
  } else {
305
318
  const typeAccess = buildRouterTypeAccess(c.name);
306
319
  lines.push(`${pad}${objKey}: {`);
307
320
  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} }),`);
321
+ lines.push(`${pad} mutationOptions: () =>`);
322
+ lines.push(`${pad} _mutationOptions({`);
323
+ lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
324
+ lines.push(`${pad} }),`);
311
325
  lines.push(`${pad}},`);
312
326
  }
313
327
  } else {
@@ -330,11 +344,15 @@ function buildApiFile(routes, outDir) {
330
344
  for (const r of contracted) {
331
345
  const cs = r.contract?.contractSource;
332
346
  if (!cs) continue;
333
- for (const ref of [
347
+ const refs = r.controllerRef ? [
348
+ cs.queryRef,
349
+ cs.bodyRef
350
+ ] : [
334
351
  cs.queryRef,
335
352
  cs.bodyRef,
336
353
  cs.responseRef
337
- ]) {
354
+ ];
355
+ for (const ref of refs) {
338
356
  if (!ref) continue;
339
357
  let names = importsByFile.get(ref.filePath);
340
358
  if (!names) {
@@ -344,10 +362,18 @@ function buildApiFile(routes, outDir) {
344
362
  names.add(ref.name);
345
363
  }
346
364
  }
365
+ const hasGetRoutes = contracted.some((r) => r.method === "GET");
366
+ const hasMutationRoutes = contracted.some((r) => r.method !== "GET");
347
367
  const lines = [
348
368
  "// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.",
349
369
  ""
350
370
  ];
371
+ const tqImports = [];
372
+ if (hasGetRoutes) tqImports.push("queryOptions as _queryOptions");
373
+ if (hasMutationRoutes) tqImports.push("mutationOptions as _mutationOptions");
374
+ if (tqImports.length > 0) {
375
+ lines.push(`import { ${tqImports.join(", ")} } from '@tanstack/react-query';`);
376
+ }
351
377
  lines.push("import { route } from './routes.js';");
352
378
  lines.push("import { createFetcher } from '@dudousxd/nestjs-inertia-client';");
353
379
  if (importsByFile.size > 0 && outDir) {
@@ -401,13 +427,14 @@ function buildApiFile(routes, outDir) {
401
427
  method: r.method,
402
428
  name,
403
429
  path: r.path,
430
+ controllerRef: r.controllerRef,
404
431
  contractSource: c.contractSource
405
432
  };
406
433
  insertIntoTree(tree, segments, leaf, name);
407
434
  }
408
435
  void detectCollisions;
409
436
  lines.push("export type ApiRouter = {");
410
- lines.push(...emitRouterTypeBlock(tree, 2));
437
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
411
438
  lines.push("};");
412
439
  lines.push("");
413
440
  lines.push("export const api = {");
@@ -677,9 +704,28 @@ import { join as join9 } from "path";
677
704
  import chokidar from "chokidar";
678
705
 
679
706
  // src/discovery/contracts-fast.ts
707
+ import { readFileSync } from "fs";
680
708
  import { dirname, join as join7, resolve as resolve2 } from "path";
681
709
  import fg2 from "fast-glob";
682
710
  import { Node, Project, SyntaxKind } from "ts-morph";
711
+ var _projectRoot = "";
712
+ var _tsconfigPaths = null;
713
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
714
+ function dbg(...args) {
715
+ if (_debug) console.log("[codegen:debug]", ...args);
716
+ }
717
+ __name(dbg, "dbg");
718
+ function loadTsconfigPaths(tsconfigPath) {
719
+ try {
720
+ const raw = readFileSync(tsconfigPath, "utf8");
721
+ const stripped = raw.replace(/\/\/.*$/gm, "");
722
+ const parsed = JSON.parse(stripped);
723
+ return parsed.compilerOptions?.paths ?? null;
724
+ } catch {
725
+ return null;
726
+ }
727
+ }
728
+ __name(loadTsconfigPaths, "loadTsconfigPaths");
683
729
  async function discoverContractsFast(opts) {
684
730
  const { cwd, glob, tsconfig } = opts;
685
731
  const tsconfigPath = tsconfig ? resolve2(tsconfig) : join7(cwd, "tsconfig.json");
@@ -712,6 +758,8 @@ async function discoverContractsFast(opts) {
712
758
  project.addSourceFileAtPath(f);
713
759
  }
714
760
  const routes = [];
761
+ _projectRoot = cwd;
762
+ _tsconfigPaths = loadTsconfigPaths(tsconfigPath);
715
763
  for (const sourceFile of project.getSourceFiles()) {
716
764
  routes.push(...extractFromSourceFile(sourceFile, project));
717
765
  }
@@ -913,17 +961,41 @@ function findTypeInFile(name, file) {
913
961
  return null;
914
962
  }
915
963
  __name(findTypeInFile, "findTypeInFile");
964
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
965
+ if (moduleSpecifier.startsWith(".")) {
966
+ const dir = dirname(sourceFile.getFilePath());
967
+ return [
968
+ resolve2(dir, `${moduleSpecifier}.ts`),
969
+ resolve2(dir, moduleSpecifier, "index.ts")
970
+ ];
971
+ }
972
+ const baseUrl = _projectRoot;
973
+ dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(_tsconfigPaths), "baseUrl:", baseUrl);
974
+ if (_tsconfigPaths) {
975
+ for (const [pattern, mappings] of Object.entries(_tsconfigPaths)) {
976
+ const prefix = pattern.replace("*", "");
977
+ if (moduleSpecifier.startsWith(prefix)) {
978
+ const rest = moduleSpecifier.slice(prefix.length);
979
+ const candidates = [];
980
+ for (const mapping of mappings) {
981
+ const resolved = resolve2(baseUrl, mapping.replace("*", rest));
982
+ candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
983
+ }
984
+ dbg(" resolved candidates:", candidates);
985
+ return candidates;
986
+ }
987
+ }
988
+ }
989
+ return [];
990
+ }
991
+ __name(resolveModuleSpecifier, "resolveModuleSpecifier");
916
992
  function resolveImportedType(name, sourceFile, project) {
917
993
  for (const importDecl of sourceFile.getImportDeclarations()) {
918
994
  const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
919
995
  if (!namedImport) continue;
920
996
  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
- ];
997
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
998
+ if (candidates.length === 0) continue;
927
999
  for (const candidate of candidates) {
928
1000
  let importedFile = project.getSourceFile(candidate);
929
1001
  if (!importedFile) {
@@ -967,6 +1039,18 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
967
1039
  }
968
1040
  return "Array<unknown>";
969
1041
  }
1042
+ if ([
1043
+ "Record",
1044
+ "Omit",
1045
+ "Pick",
1046
+ "Partial",
1047
+ "Required",
1048
+ "Readonly",
1049
+ "Map",
1050
+ "Set"
1051
+ ].includes(name)) {
1052
+ return typeNode.getText();
1053
+ }
970
1054
  if (name === "Promise") {
971
1055
  const typeArgs = typeNode.getTypeArguments();
972
1056
  const firstTypeArg = typeArgs[0];
@@ -979,7 +1063,8 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
979
1063
  if (resolved) {
980
1064
  return expandTypeDecl(resolved, project, depth - 1);
981
1065
  }
982
- return name;
1066
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1067
+ return "unknown";
983
1068
  }
984
1069
  const kind = typeNode.getKind();
985
1070
  if (kind === SyntaxKind.StringKeyword) return "string";
@@ -1141,7 +1226,7 @@ function tryResolveTypeRef(typeNode, sourceFile, project) {
1141
1226
  return null;
1142
1227
  }
1143
1228
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1144
- if (localDecl && localDecl.isExported()) {
1229
+ if (localDecl?.isExported()) {
1145
1230
  return {
1146
1231
  name,
1147
1232
  filePath: sourceFile.getFilePath()
@@ -1203,7 +1288,7 @@ function extractDtoContract(method, sourceFile, project) {
1203
1288
  if (val && Node.isIdentifier(val)) {
1204
1289
  const name = val.getText();
1205
1290
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1206
- if (localDecl && localDecl.isExported()) {
1291
+ if (localDecl?.isExported()) {
1207
1292
  responseRef = {
1208
1293
  name,
1209
1294
  filePath: sourceFile.getFilePath()
@@ -1329,6 +1414,11 @@ function extractFromSourceFile(sourceFile, project) {
1329
1414
  path: combined,
1330
1415
  name: routeName,
1331
1416
  params,
1417
+ controllerRef: {
1418
+ className,
1419
+ methodName,
1420
+ filePath: sourceFile.getFilePath()
1421
+ },
1332
1422
  contract: {
1333
1423
  contractSource: {
1334
1424
  query: contractDef.query,
@@ -1363,7 +1453,11 @@ function extractFromSourceFile(sourceFile, project) {
1363
1453
  path: combined,
1364
1454
  name: routeName,
1365
1455
  params,
1366
- // Attach contract if DTO extraction produced useful type info
1456
+ controllerRef: {
1457
+ className,
1458
+ methodName,
1459
+ filePath: sourceFile.getFilePath()
1460
+ },
1367
1461
  ...dtoContract ? {
1368
1462
  contract: {
1369
1463
  contractSource: {
@@ -1545,7 +1639,7 @@ async function watch(config, onChange) {
1545
1639
  __name(watch, "watch");
1546
1640
 
1547
1641
  // src/index.ts
1548
- var VERSION = "1.2.0";
1642
+ var VERSION = "1.3.0";
1549
1643
 
1550
1644
  // src/cli/codegen.ts
1551
1645
  async function runCodegen(opts = {}) {
@@ -1577,7 +1671,7 @@ __name(runCodegen, "runCodegen");
1577
1671
 
1578
1672
  // src/cli/init.ts
1579
1673
  import { execSync } from "child_process";
1580
- import { readFileSync, writeFileSync } from "fs";
1674
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
1581
1675
  import { access as access2, mkdir as mkdir7, readFile as readFile4, writeFile as writeFile7 } from "fs/promises";
1582
1676
  import { join as join10 } from "path";
1583
1677
  import { createInterface } from "readline";
@@ -1801,7 +1895,7 @@ __name(findAfterLastImport, "findAfterLastImport");
1801
1895
  function patchAppModule(filePath, rootView) {
1802
1896
  let content;
1803
1897
  try {
1804
- content = readFileSync(filePath, "utf8");
1898
+ content = readFileSync2(filePath, "utf8");
1805
1899
  } catch {
1806
1900
  return "skipped";
1807
1901
  }
@@ -1846,7 +1940,7 @@ __name(patchAppModule, "patchAppModule");
1846
1940
  function patchMainTs(filePath) {
1847
1941
  let content;
1848
1942
  try {
1849
- content = readFileSync(filePath, "utf8");
1943
+ content = readFileSync2(filePath, "utf8");
1850
1944
  } catch {
1851
1945
  return "skipped";
1852
1946
  }
@@ -1919,11 +2013,19 @@ function htmlShellTemplate(framework, _engine) {
1919
2013
  __name(htmlShellTemplate, "htmlShellTemplate");
1920
2014
  function viteConfigTemplate(framework) {
1921
2015
  const pluginOption = `{ ${framework}: true }`;
1922
- return `import { defineConfig } from 'vite';
2016
+ return `import { resolve } from 'node:path';
2017
+ import { defineConfig } from 'vite';
1923
2018
  import nestInertia from '@dudousxd/nestjs-inertia-vite/plugin';
1924
2019
 
1925
2020
  export default defineConfig({
1926
2021
  plugins: [nestInertia(${pluginOption})],
2022
+ resolve: {
2023
+ alias: {
2024
+ '@': resolve(__dirname, 'src'),
2025
+ '~': resolve(__dirname, 'inertia'),
2026
+ '~codegen': resolve(__dirname, '.nestjs-inertia'),
2027
+ },
2028
+ },
1927
2029
  });
1928
2030
  `;
1929
2031
  }