@dudousxd/nestjs-inertia-codegen 1.3.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
@@ -127,6 +127,9 @@ declare function watch(config: ResolvedConfig, onChange?: () => void): Promise<W
127
127
  /**
128
128
  * Try to acquire an exclusive lock for a watcher in `outDir`.
129
129
  *
130
+ * Uses O_CREAT | O_EXCL (via 'wx' flag) for atomic file creation to prevent
131
+ * TOCTOU race conditions between concurrent processes.
132
+ *
130
133
  * Returns `{ release }` on success.
131
134
  * Returns `null` if another live process already holds the lock.
132
135
  */
@@ -134,6 +137,6 @@ declare function acquireLock(outDir: string): Promise<{
134
137
  release: () => Promise<void>;
135
138
  } | null>;
136
139
 
137
- declare const VERSION = "1.3.0";
140
+ declare const VERSION = "1.4.0";
138
141
 
139
142
  export { CodegenError, ConfigError, type ResolvedConfig, type ScopeConfig, type UserConfig, VERSION, type Watcher, acquireLock, defineConfig, generate, loadConfig, watch };
package/dist/index.d.ts CHANGED
@@ -127,6 +127,9 @@ declare function watch(config: ResolvedConfig, onChange?: () => void): Promise<W
127
127
  /**
128
128
  * Try to acquire an exclusive lock for a watcher in `outDir`.
129
129
  *
130
+ * Uses O_CREAT | O_EXCL (via 'wx' flag) for atomic file creation to prevent
131
+ * TOCTOU race conditions between concurrent processes.
132
+ *
130
133
  * Returns `{ release }` on success.
131
134
  * Returns `null` if another live process already holds the lock.
132
135
  */
@@ -134,6 +137,6 @@ declare function acquireLock(outDir: string): Promise<{
134
137
  release: () => Promise<void>;
135
138
  } | null>;
136
139
 
137
- declare const VERSION = "1.3.0";
140
+ declare const VERSION = "1.4.0";
138
141
 
139
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,6 +257,16 @@ function insertIntoTree(tree, segments, leaf, fullName) {
267
257
  }
268
258
  }
269
259
  __name(insertIntoTree, "insertIntoTree");
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
270
  function buildResponseType(c, outDir) {
271
271
  if (c.controllerRef) {
272
272
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
@@ -293,9 +293,10 @@ function emitRouterTypeBlock(tree, indent, outDir) {
293
293
  const bodyRef = c.contractSource.bodyRef;
294
294
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
295
295
  const response = buildResponseType(c, outDir);
296
+ const params = buildParamsType(c.params);
296
297
  const safeMethod = JSON.stringify(method);
297
298
  const safeUrl = JSON.stringify(c.path);
298
- 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} };`);
299
300
  } else {
300
301
  lines.push(`${pad}${objKey}: {`);
301
302
  lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
@@ -318,21 +319,60 @@ function emitApiObjectBlock(tree, indent) {
318
319
  const fetcherMethod = method.toLowerCase();
319
320
  if (method === "GET") {
320
321
  const typeAccess = buildRouterTypeAccess(c.name);
322
+ const withParams = hasPathParams(c.params);
321
323
  lines.push(`${pad}${objKey}: {`);
322
- lines.push(`${pad} queryKey: (query?: ${typeAccess}['query']) => query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
323
- lines.push(`${pad} queryOptions: (query?: ${typeAccess}['query']) =>`);
324
- lines.push(`${pad} _queryOptions({`);
325
- lines.push(`${pad} queryKey: query !== undefined ? [${flatName}, query] as const : [${flatName}] as const,`);
326
- lines.push(`${pad} queryFn: () => fetcher.get<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { query }),`);
327
- 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
+ }
328
363
  lines.push(`${pad}},`);
329
364
  } else {
330
365
  const typeAccess = buildRouterTypeAccess(c.name);
366
+ const withParams = hasPathParams(c.params);
331
367
  lines.push(`${pad}${objKey}: {`);
332
368
  lines.push(`${pad} queryKey: () => [${flatName}] as const,`);
333
369
  lines.push(`${pad} mutationOptions: () =>`);
334
370
  lines.push(`${pad} _mutationOptions({`);
335
- lines.push(`${pad} mutationFn: (body: ${typeAccess}['body']) => fetcher.${fetcherMethod}<${typeAccess}['response']>(route(${flatName} as never) || ${safePath}, { body }),`);
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
+ }
336
376
  lines.push(`${pad} }),`);
337
377
  lines.push(`${pad}},`);
338
378
  }
@@ -439,12 +479,12 @@ function buildApiFile(routes, outDir) {
439
479
  method: r.method,
440
480
  name,
441
481
  path: r.path,
482
+ params: r.params,
442
483
  controllerRef: r.controllerRef,
443
484
  contractSource: c.contractSource
444
485
  };
445
486
  insertIntoTree(tree, segments, leaf, name);
446
487
  }
447
- void detectCollisions;
448
488
  lines.push("export type ApiRouter = {");
449
489
  lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
450
490
  lines.push("};");
@@ -542,8 +582,9 @@ __name(emitIndex, "emitIndex");
542
582
 
543
583
  // src/emit/emit-pages.ts
544
584
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
545
- import { join as join5 } from "path";
546
- 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";
547
588
  await mkdir4(outDir, {
548
589
  recursive: true
549
590
  });
@@ -552,14 +593,40 @@ async function emitPages(pages, outDir) {
552
593
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
553
594
  return ` ${key}: ${propType};`;
554
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";
555
603
  const content = `// Generated by @dudousxd/nestjs-inertia-codegen. Do not edit.
556
604
  export interface InertiaPages {
557
605
  ${body}
558
606
  }
607
+
608
+ export type InertiaPageName = ${pageNameUnion};
609
+ ` + propsHelper + `
610
+ declare module '@dudousxd/nestjs-inertia' {
611
+ interface InertiaPages {
612
+ ${augBody}
613
+ }
614
+ }
559
615
  `;
560
616
  await writeFile4(join5(outDir, "pages.d.ts"), content, "utf8");
561
617
  }
562
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");
563
630
  function needsQuotes(name) {
564
631
  return !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
565
632
  }
@@ -696,7 +763,9 @@ async function generate(config, routes = []) {
696
763
  propsExport: config.pages.propsExport,
697
764
  componentNameStrategy: config.pages.componentNameStrategy
698
765
  });
699
- await emitPages(pages, config.codegen.outDir);
766
+ await emitPages(pages, config.codegen.outDir, {
767
+ propsExport: config.pages.propsExport
768
+ });
700
769
  await emitCache(pages, config.codegen.outDir);
701
770
  const hasRoutes = routes.length > 0;
702
771
  const hasContracts = routes.some((r) => r.contract);
@@ -720,8 +789,18 @@ import { readFileSync } from "fs";
720
789
  import { dirname, join as join7, resolve as resolve2 } from "path";
721
790
  import fg2 from "fast-glob";
722
791
  import { Node, Project, SyntaxKind } from "ts-morph";
723
- var _projectRoot = "";
724
- var _tsconfigPaths = null;
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");
725
804
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
726
805
  function dbg(...args) {
727
806
  if (_debug) console.log("[codegen:debug]", ...args);
@@ -770,10 +849,17 @@ async function discoverContractsFast(opts) {
770
849
  project.addSourceFileAtPath(f);
771
850
  }
772
851
  const routes = [];
773
- _projectRoot = cwd;
774
- _tsconfigPaths = loadTsconfigPaths(tsconfigPath);
775
- for (const sourceFile of project.getSourceFiles()) {
776
- 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;
777
863
  }
778
864
  return routes;
779
865
  }
@@ -981,10 +1067,11 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
981
1067
  resolve2(dir, moduleSpecifier, "index.ts")
982
1068
  ];
983
1069
  }
984
- const baseUrl = _projectRoot;
985
- dbg("resolveModuleSpecifier", moduleSpecifier, "paths:", JSON.stringify(_tsconfigPaths), "baseUrl:", baseUrl);
986
- if (_tsconfigPaths) {
987
- for (const [pattern, mappings] of Object.entries(_tsconfigPaths)) {
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)) {
988
1075
  const prefix = pattern.replace("*", "");
989
1076
  if (moduleSpecifier.startsWith(prefix)) {
990
1077
  const rest = moduleSpecifier.slice(prefix.length);
@@ -1470,18 +1557,16 @@ function extractFromSourceFile(sourceFile, project) {
1470
1557
  methodName,
1471
1558
  filePath: sourceFile.getFilePath()
1472
1559
  },
1473
- ...dtoContract ? {
1474
- contract: {
1475
- contractSource: {
1476
- query: dtoContract.query,
1477
- body: dtoContract.body,
1478
- response: dtoContract.response,
1479
- queryRef: dtoContract.queryRef,
1480
- bodyRef: dtoContract.bodyRef,
1481
- responseRef: dtoContract.responseRef
1482
- }
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
1483
1568
  }
1484
- } : {}
1569
+ }
1485
1570
  });
1486
1571
  }
1487
1572
  }
@@ -1491,7 +1576,8 @@ function extractFromSourceFile(sourceFile, project) {
1491
1576
  __name(extractFromSourceFile, "extractFromSourceFile");
1492
1577
 
1493
1578
  // src/watch/lock-file.ts
1494
- 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";
1495
1581
  import { join as join8 } from "path";
1496
1582
  var LOCK_FILE = ".watcher.lock";
1497
1583
  function isProcessAlive(pid) {
@@ -1508,20 +1594,29 @@ async function acquireLock(outDir) {
1508
1594
  recursive: true
1509
1595
  });
1510
1596
  const lockPath = join8(outDir, LOCK_FILE);
1511
- try {
1512
- const raw = await readFile2(lockPath, "utf8");
1513
- const existing = JSON.parse(raw);
1514
- if (isProcessAlive(existing.pid)) {
1515
- return null;
1516
- }
1517
- } catch {
1518
- }
1519
1597
  const lockData = {
1520
1598
  pid: process.pid,
1521
1599
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1522
1600
  };
1523
- 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)}
1524
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
+ }
1525
1620
  return {
1526
1621
  release: /* @__PURE__ */ __name(async () => {
1527
1622
  try {
@@ -1585,7 +1680,8 @@ async function watch(config, onChange) {
1585
1680
  pagesDebounceTimer = void 0;
1586
1681
  try {
1587
1682
  await generate(config);
1588
- } catch {
1683
+ } catch (err) {
1684
+ console.error("[nestjs-inertia-codegen] Pages generation failed:", err instanceof Error ? err.message : err);
1589
1685
  }
1590
1686
  onChange?.();
1591
1687
  }, PAGES_DEBOUNCE_MS);
@@ -1623,7 +1719,8 @@ async function watch(config, onChange) {
1623
1719
  if (hasContracts) {
1624
1720
  await emitApi(routes, config.codegen.outDir);
1625
1721
  }
1626
- } catch {
1722
+ } catch (err) {
1723
+ console.error("[nestjs-inertia-codegen] Contracts generation failed:", err instanceof Error ? err.message : err);
1627
1724
  }
1628
1725
  onChange?.();
1629
1726
  }, config.contracts.debounceMs);
@@ -1651,7 +1748,7 @@ async function watch(config, onChange) {
1651
1748
  __name(watch, "watch");
1652
1749
 
1653
1750
  // src/index.ts
1654
- var VERSION = "1.3.0";
1751
+ var VERSION = "1.4.0";
1655
1752
  export {
1656
1753
  CodegenError,
1657
1754
  ConfigError,